feat(vue): expand @robonen/vue composable collection
Composables, tests, category barrels, and README for @robonen/vue.
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { effectScope, nextTick, ref } from 'vue';
|
||||
import type { FileSystemFileHandle, UseFileSystemAccessReturn } from '.';
|
||||
import { useFileSystemAccess } from '.';
|
||||
|
||||
interface WritableSpy {
|
||||
write: ReturnType<typeof vi.fn>;
|
||||
close: ReturnType<typeof vi.fn>;
|
||||
}
|
||||
|
||||
function makeFile(contents = 'hello world', name = 'demo.txt', type = 'text/plain'): File {
|
||||
const file = {
|
||||
name,
|
||||
type,
|
||||
size: contents.length,
|
||||
lastModified: 1234,
|
||||
text: vi.fn(async () => contents),
|
||||
arrayBuffer: vi.fn(async () => new ArrayBuffer(contents.length)),
|
||||
};
|
||||
return file as unknown as File;
|
||||
}
|
||||
|
||||
function makeHandle(file: File): { handle: FileSystemFileHandle; writable: WritableSpy } {
|
||||
const writable: WritableSpy = {
|
||||
write: vi.fn(async () => {}),
|
||||
close: vi.fn(async () => {}),
|
||||
};
|
||||
const handle = {
|
||||
getFile: vi.fn(async () => file),
|
||||
createWritable: vi.fn(async () => writable),
|
||||
} as unknown as FileSystemFileHandle;
|
||||
return { handle, writable };
|
||||
}
|
||||
|
||||
function stubWindow(handle?: FileSystemFileHandle) {
|
||||
const showOpenFilePicker = vi.fn(async () => (handle ? [handle] : []));
|
||||
const showSaveFilePicker = vi.fn(async () => handle);
|
||||
const window = {
|
||||
showOpenFilePicker,
|
||||
showSaveFilePicker,
|
||||
} as unknown as Window;
|
||||
return { window, showOpenFilePicker, showSaveFilePicker };
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe(useFileSystemAccess, () => {
|
||||
it('reports support when the picker APIs exist', () => {
|
||||
const { window } = stubWindow();
|
||||
const scope = effectScope();
|
||||
let fsa: UseFileSystemAccessReturn;
|
||||
scope.run(() => {
|
||||
fsa = useFileSystemAccess({ window });
|
||||
});
|
||||
expect(fsa!.isSupported.value).toBeTruthy();
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('is not supported without the picker APIs', () => {
|
||||
const window = {} as unknown as Window;
|
||||
const scope = effectScope();
|
||||
let fsa: UseFileSystemAccessReturn;
|
||||
scope.run(() => {
|
||||
fsa = useFileSystemAccess({ window });
|
||||
});
|
||||
expect(fsa!.isSupported.value).toBeFalsy();
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('is not supported and is a no-op under SSR (no window)', async () => {
|
||||
const scope = effectScope();
|
||||
let fsa: UseFileSystemAccessReturn;
|
||||
scope.run(() => {
|
||||
fsa = useFileSystemAccess({ window: undefined });
|
||||
});
|
||||
expect(fsa!.isSupported.value).toBeFalsy();
|
||||
await expect(fsa!.open()).resolves.toBeUndefined();
|
||||
await expect(fsa!.save()).resolves.toBeUndefined();
|
||||
expect(fsa!.fileName.value).toBe('');
|
||||
expect(fsa!.fileSize.value).toBe(0);
|
||||
expect(fsa!.data.value).toBeUndefined();
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('opens a file and reads it as text by default', async () => {
|
||||
const file = makeFile('content');
|
||||
const { handle } = makeHandle(file);
|
||||
const { window, showOpenFilePicker } = stubWindow(handle);
|
||||
const scope = effectScope();
|
||||
let fsa: UseFileSystemAccessReturn<string>;
|
||||
scope.run(() => {
|
||||
fsa = useFileSystemAccess({ window, dataType: 'Text' });
|
||||
});
|
||||
|
||||
await fsa!.open();
|
||||
expect(showOpenFilePicker).toHaveBeenCalledOnce();
|
||||
expect(fsa!.data.value).toBe('content');
|
||||
expect(fsa!.file.value).toBe(file);
|
||||
expect(fsa!.fileName.value).toBe('demo.txt');
|
||||
expect(fsa!.fileMIME.value).toBe('text/plain');
|
||||
expect(fsa!.fileSize.value).toBe(7);
|
||||
expect(fsa!.fileLastModified.value).toBe(1234);
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('reads as ArrayBuffer when dataType is ArrayBuffer', async () => {
|
||||
const file = makeFile('abc');
|
||||
const { handle } = makeHandle(file);
|
||||
const { window } = stubWindow(handle);
|
||||
const scope = effectScope();
|
||||
let fsa: UseFileSystemAccessReturn<ArrayBuffer>;
|
||||
scope.run(() => {
|
||||
fsa = useFileSystemAccess({ window, dataType: 'ArrayBuffer' });
|
||||
});
|
||||
|
||||
await fsa!.open();
|
||||
expect(fsa!.data.value).toBeInstanceOf(ArrayBuffer);
|
||||
expect(file.arrayBuffer).toHaveBeenCalled();
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('exposes the File itself when dataType is Blob', async () => {
|
||||
const file = makeFile('abc');
|
||||
const { handle } = makeHandle(file);
|
||||
const { window } = stubWindow(handle);
|
||||
const scope = effectScope();
|
||||
let fsa: UseFileSystemAccessReturn<Blob>;
|
||||
scope.run(() => {
|
||||
fsa = useFileSystemAccess({ window, dataType: 'Blob' });
|
||||
});
|
||||
|
||||
await fsa!.open();
|
||||
expect(fsa!.data.value).toBe(file);
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('passes types and excludeAcceptAllOption to the open picker', async () => {
|
||||
const file = makeFile();
|
||||
const { handle } = makeHandle(file);
|
||||
const { window, showOpenFilePicker } = stubWindow(handle);
|
||||
const types = [{ description: 'text', accept: { 'text/plain': ['.txt'] } }];
|
||||
const scope = effectScope();
|
||||
let fsa: UseFileSystemAccessReturn;
|
||||
scope.run(() => {
|
||||
fsa = useFileSystemAccess({ window, types, excludeAcceptAllOption: true });
|
||||
});
|
||||
|
||||
await fsa!.open();
|
||||
expect(showOpenFilePicker).toHaveBeenCalledWith({ types, excludeAcceptAllOption: true });
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('lets per-call open options override the defaults', async () => {
|
||||
const file = makeFile();
|
||||
const { handle } = makeHandle(file);
|
||||
const { window, showOpenFilePicker } = stubWindow(handle);
|
||||
const scope = effectScope();
|
||||
let fsa: UseFileSystemAccessReturn;
|
||||
scope.run(() => {
|
||||
fsa = useFileSystemAccess({ window, excludeAcceptAllOption: false });
|
||||
});
|
||||
|
||||
await fsa!.open({ excludeAcceptAllOption: true });
|
||||
expect(showOpenFilePicker).toHaveBeenCalledWith({ types: undefined, excludeAcceptAllOption: true });
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('creates a new empty handle and clears prior data', async () => {
|
||||
const file = makeFile('');
|
||||
const { handle } = makeHandle(file);
|
||||
const { window, showSaveFilePicker } = stubWindow(handle);
|
||||
const scope = effectScope();
|
||||
let fsa: UseFileSystemAccessReturn;
|
||||
scope.run(() => {
|
||||
fsa = useFileSystemAccess({ window });
|
||||
});
|
||||
|
||||
await fsa!.create({ suggestedName: 'new.txt' });
|
||||
expect(showSaveFilePicker).toHaveBeenCalledWith({ types: undefined, excludeAcceptAllOption: undefined, suggestedName: 'new.txt' });
|
||||
expect(fsa!.file.value).toBe(file);
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('save writes current data to the existing handle', async () => {
|
||||
const file = makeFile('original');
|
||||
const { handle, writable } = makeHandle(file);
|
||||
const { window } = stubWindow(handle);
|
||||
const scope = effectScope();
|
||||
let fsa: UseFileSystemAccessReturn<string>;
|
||||
scope.run(() => {
|
||||
fsa = useFileSystemAccess({ window, dataType: 'Text' });
|
||||
});
|
||||
|
||||
await fsa!.open();
|
||||
fsa!.data.value = 'edited';
|
||||
await fsa!.save();
|
||||
|
||||
expect(writable.write).toHaveBeenCalledWith('edited');
|
||||
expect(writable.close).toHaveBeenCalledOnce();
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('save falls back to saveAs when there is no handle', async () => {
|
||||
const file = makeFile('');
|
||||
const { handle, writable } = makeHandle(file);
|
||||
const { window, showSaveFilePicker, showOpenFilePicker } = stubWindow(handle);
|
||||
const scope = effectScope();
|
||||
let fsa: UseFileSystemAccessReturn<string>;
|
||||
scope.run(() => {
|
||||
fsa = useFileSystemAccess({ window, dataType: 'Text' });
|
||||
});
|
||||
|
||||
fsa!.data.value = 'fresh';
|
||||
await fsa!.save();
|
||||
|
||||
expect(showOpenFilePicker).not.toHaveBeenCalled();
|
||||
expect(showSaveFilePicker).toHaveBeenCalledOnce();
|
||||
expect(writable.write).toHaveBeenCalledWith('fresh');
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('saveAs requests a new handle and writes data', async () => {
|
||||
const file = makeFile('');
|
||||
const { handle, writable } = makeHandle(file);
|
||||
const { window, showSaveFilePicker } = stubWindow(handle);
|
||||
const scope = effectScope();
|
||||
let fsa: UseFileSystemAccessReturn<string>;
|
||||
scope.run(() => {
|
||||
fsa = useFileSystemAccess({ window, dataType: 'Text' });
|
||||
});
|
||||
|
||||
fsa!.data.value = 'payload';
|
||||
await fsa!.saveAs({ suggestedName: 'out.txt' });
|
||||
|
||||
expect(showSaveFilePicker).toHaveBeenCalledWith({ types: undefined, excludeAcceptAllOption: undefined, suggestedName: 'out.txt' });
|
||||
expect(writable.write).toHaveBeenCalledWith('payload');
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('does not write when there is no data', async () => {
|
||||
const file = makeFile('');
|
||||
const { handle, writable } = makeHandle(file);
|
||||
const { window } = stubWindow(handle);
|
||||
const scope = effectScope();
|
||||
let fsa: UseFileSystemAccessReturn<string>;
|
||||
scope.run(() => {
|
||||
fsa = useFileSystemAccess({ window, dataType: 'Text' });
|
||||
});
|
||||
|
||||
await fsa!.saveAs();
|
||||
expect(writable.write).not.toHaveBeenCalled();
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('updateData re-reads the current file', async () => {
|
||||
const file = makeFile('v1');
|
||||
const { handle } = makeHandle(file);
|
||||
const { window } = stubWindow(handle);
|
||||
const scope = effectScope();
|
||||
let fsa: UseFileSystemAccessReturn<string>;
|
||||
scope.run(() => {
|
||||
fsa = useFileSystemAccess({ window, dataType: 'Text' });
|
||||
});
|
||||
|
||||
await fsa!.open();
|
||||
expect(fsa!.data.value).toBe('v1');
|
||||
|
||||
(file.text as ReturnType<typeof vi.fn>).mockResolvedValueOnce('v2');
|
||||
await fsa!.updateData();
|
||||
expect(fsa!.data.value).toBe('v2');
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('re-reads data when a reactive dataType changes', async () => {
|
||||
const file = makeFile('reactive');
|
||||
const { handle } = makeHandle(file);
|
||||
const { window } = stubWindow(handle);
|
||||
const dataType = ref<'Text' | 'Blob'>('Text');
|
||||
const scope = effectScope();
|
||||
let fsa: UseFileSystemAccessReturn;
|
||||
scope.run(() => {
|
||||
fsa = useFileSystemAccess({ window, dataType });
|
||||
});
|
||||
|
||||
await fsa!.open();
|
||||
expect(fsa!.data.value).toBe('reactive');
|
||||
|
||||
dataType.value = 'Blob';
|
||||
await nextTick();
|
||||
await Promise.resolve();
|
||||
expect(fsa!.data.value).toBe(file);
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('routes picker errors to onError instead of throwing', async () => {
|
||||
const abort = new DOMException('cancelled', 'AbortError');
|
||||
const window = {
|
||||
showOpenFilePicker: vi.fn(async () => { throw abort; }),
|
||||
showSaveFilePicker: vi.fn(async () => { throw abort; }),
|
||||
} as unknown as Window;
|
||||
const onError = vi.fn();
|
||||
const scope = effectScope();
|
||||
let fsa: UseFileSystemAccessReturn;
|
||||
scope.run(() => {
|
||||
fsa = useFileSystemAccess({ window, onError });
|
||||
});
|
||||
|
||||
await expect(fsa!.open()).resolves.toBeUndefined();
|
||||
expect(onError).toHaveBeenCalledWith(abort);
|
||||
scope.stop();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,332 @@
|
||||
import { computed, shallowRef, toValue, watch } from 'vue';
|
||||
import type { ComputedRef, MaybeRefOrGetter, ShallowRef } from 'vue';
|
||||
import { noop } from '@robonen/stdlib';
|
||||
import { defaultWindow } from '@/types';
|
||||
import type { ConfigurableWindow } from '@/types';
|
||||
import { useSupported } from '@/composables/utilities/useSupported';
|
||||
|
||||
/**
|
||||
* `window.showOpenFilePicker` parameters.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/showOpenFilePicker#parameters
|
||||
*/
|
||||
export interface FileSystemAccessShowOpenFileOptions {
|
||||
multiple?: boolean;
|
||||
types?: Array<{
|
||||
description?: string;
|
||||
accept: Record<string, string[]>;
|
||||
}>;
|
||||
excludeAcceptAllOption?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* `window.showSaveFilePicker` parameters.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/showSaveFilePicker#parameters
|
||||
*/
|
||||
export interface FileSystemAccessShowSaveFileOptions {
|
||||
suggestedName?: string;
|
||||
types?: Array<{
|
||||
description?: string;
|
||||
accept: Record<string, string[]>;
|
||||
}>;
|
||||
excludeAcceptAllOption?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemWritableFileStream/write
|
||||
*/
|
||||
export interface FileSystemWritableFileStreamWrite {
|
||||
(data: string | BufferSource | Blob): Promise<void>;
|
||||
(options: { type: 'write'; position: number; data: string | BufferSource | Blob }): Promise<void>;
|
||||
(options: { type: 'seek'; position: number }): Promise<void>;
|
||||
(options: { type: 'truncate'; size: number }): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemWritableFileStream
|
||||
*/
|
||||
export interface FileSystemWritableFileStream extends WritableStream {
|
||||
write: FileSystemWritableFileStreamWrite;
|
||||
seek: (position: number) => Promise<void>;
|
||||
truncate: (size: number) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle
|
||||
*/
|
||||
export interface FileSystemFileHandle {
|
||||
getFile: () => Promise<File>;
|
||||
createWritable: () => Promise<FileSystemWritableFileStream>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A `window` augmented with the File System Access API entry points.
|
||||
*/
|
||||
export type FileSystemAccessWindow
|
||||
= Window & {
|
||||
showSaveFilePicker: (options: FileSystemAccessShowSaveFileOptions) => Promise<FileSystemFileHandle>;
|
||||
showOpenFilePicker: (options: FileSystemAccessShowOpenFileOptions) => Promise<FileSystemFileHandle[]>;
|
||||
};
|
||||
|
||||
/**
|
||||
* The supported file data types.
|
||||
*/
|
||||
export type UseFileSystemAccessDataType = 'Text' | 'ArrayBuffer' | 'Blob';
|
||||
|
||||
/**
|
||||
* Picker options shared between open/create/save operations.
|
||||
*/
|
||||
export type UseFileSystemAccessCommonOptions
|
||||
= Pick<FileSystemAccessShowOpenFileOptions, 'types' | 'excludeAcceptAllOption'>;
|
||||
|
||||
/**
|
||||
* Picker options accepted by save-style operations.
|
||||
*/
|
||||
export type UseFileSystemAccessShowSaveFileOptions
|
||||
= Pick<FileSystemAccessShowSaveFileOptions, 'suggestedName'> & UseFileSystemAccessCommonOptions;
|
||||
|
||||
export type UseFileSystemAccessOptions
|
||||
= ConfigurableWindow & UseFileSystemAccessCommonOptions & {
|
||||
/**
|
||||
* How the file contents are read into `data`.
|
||||
*
|
||||
* @default 'Text'
|
||||
*/
|
||||
dataType?: MaybeRefOrGetter<UseFileSystemAccessDataType>;
|
||||
|
||||
/**
|
||||
* Called when a picker or file operation fails.
|
||||
*
|
||||
* User-cancelled pickers reject with an `AbortError`; route them here to
|
||||
* keep them out of the global unhandled-rejection channel.
|
||||
*
|
||||
* @default noop
|
||||
*/
|
||||
onError?: (error: unknown) => void;
|
||||
};
|
||||
|
||||
export interface UseFileSystemAccessReturn<T = string | ArrayBuffer | Blob> {
|
||||
/**
|
||||
* Whether the File System Access API is available.
|
||||
*/
|
||||
isSupported: ComputedRef<boolean>;
|
||||
|
||||
/**
|
||||
* The current file contents, read according to `dataType`.
|
||||
*/
|
||||
data: ShallowRef<T | undefined>;
|
||||
|
||||
/**
|
||||
* The currently bound `File`, or `undefined` when no file is open.
|
||||
*/
|
||||
file: ShallowRef<File | undefined>;
|
||||
|
||||
/**
|
||||
* The current file name (empty string when no file is open).
|
||||
*/
|
||||
fileName: ComputedRef<string>;
|
||||
|
||||
/**
|
||||
* The current file MIME type (empty string when no file is open).
|
||||
*/
|
||||
fileMIME: ComputedRef<string>;
|
||||
|
||||
/**
|
||||
* The current file size in bytes (`0` when no file is open).
|
||||
*/
|
||||
fileSize: ComputedRef<number>;
|
||||
|
||||
/**
|
||||
* The current file's last-modified timestamp (`0` when no file is open).
|
||||
*/
|
||||
fileLastModified: ComputedRef<number>;
|
||||
|
||||
/**
|
||||
* Show the open-file picker and load the chosen file.
|
||||
*/
|
||||
open: (options?: UseFileSystemAccessCommonOptions) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Show the save-file picker to create a new, empty file handle.
|
||||
*/
|
||||
create: (options?: UseFileSystemAccessShowSaveFileOptions) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Write `data` back to the current handle (falls back to `saveAs` when none).
|
||||
*/
|
||||
save: (options?: UseFileSystemAccessShowSaveFileOptions) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Show the save-file picker, then write `data` to the chosen handle.
|
||||
*/
|
||||
saveAs: (options?: UseFileSystemAccessShowSaveFileOptions) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Re-read `data` (and metadata) from the current handle.
|
||||
*/
|
||||
updateData: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name useFileSystemAccess
|
||||
* @category Browser
|
||||
* @description Create, read, and write local files via the File System Access API.
|
||||
*
|
||||
* @param {UseFileSystemAccessOptions} [options={}] Options including `dataType`, `types`, `excludeAcceptAllOption`, and `onError`
|
||||
* @returns {UseFileSystemAccessReturn} `isSupported`, `data`, `file`, `fileName`, `fileMIME`, `fileSize`, `fileLastModified`, `open`, `create`, `save`, `saveAs`, `updateData`
|
||||
*
|
||||
* @example
|
||||
* const { isSupported, data, open, save } = useFileSystemAccess({ dataType: 'Text' });
|
||||
* await open();
|
||||
* data.value += '\nappended';
|
||||
* await save();
|
||||
*
|
||||
* @example
|
||||
* // Read raw bytes
|
||||
* const { data } = useFileSystemAccess({ dataType: 'ArrayBuffer' });
|
||||
*
|
||||
* @since 0.0.15
|
||||
*/
|
||||
export function useFileSystemAccess(): UseFileSystemAccessReturn<string | ArrayBuffer | Blob>;
|
||||
export function useFileSystemAccess(options: UseFileSystemAccessOptions & { dataType: 'Text' }): UseFileSystemAccessReturn<string>;
|
||||
export function useFileSystemAccess(options: UseFileSystemAccessOptions & { dataType: 'ArrayBuffer' }): UseFileSystemAccessReturn<ArrayBuffer>;
|
||||
export function useFileSystemAccess(options: UseFileSystemAccessOptions & { dataType: 'Blob' }): UseFileSystemAccessReturn<Blob>;
|
||||
export function useFileSystemAccess(options: UseFileSystemAccessOptions): UseFileSystemAccessReturn<string | ArrayBuffer | Blob>;
|
||||
export function useFileSystemAccess(
|
||||
options: UseFileSystemAccessOptions = {},
|
||||
): UseFileSystemAccessReturn<string | ArrayBuffer | Blob> {
|
||||
const {
|
||||
window: win = defaultWindow,
|
||||
dataType = 'Text',
|
||||
types,
|
||||
excludeAcceptAllOption,
|
||||
onError = noop,
|
||||
} = options;
|
||||
|
||||
const fsWindow = win as FileSystemAccessWindow | undefined;
|
||||
const isSupported = useSupported(() => fsWindow && 'showSaveFilePicker' in fsWindow && 'showOpenFilePicker' in fsWindow);
|
||||
|
||||
const fileHandle = shallowRef<FileSystemFileHandle>();
|
||||
const data = shallowRef<string | ArrayBuffer | Blob>();
|
||||
const file = shallowRef<File>();
|
||||
|
||||
const fileName = computed(() => file.value?.name ?? '');
|
||||
const fileMIME = computed(() => file.value?.type ?? '');
|
||||
const fileSize = computed(() => file.value?.size ?? 0);
|
||||
const fileLastModified = computed(() => file.value?.lastModified ?? 0);
|
||||
|
||||
// Resolve the picker defaults once per call rather than spreading the full
|
||||
// options bag (which carries `window`/`onError`) into the native picker.
|
||||
function pickerDefaults(): UseFileSystemAccessCommonOptions {
|
||||
return { types, excludeAcceptAllOption };
|
||||
}
|
||||
|
||||
async function updateFile(): Promise<void> {
|
||||
file.value = await fileHandle.value?.getFile();
|
||||
}
|
||||
|
||||
async function updateData(): Promise<void> {
|
||||
await updateFile();
|
||||
|
||||
const type = toValue(dataType);
|
||||
|
||||
if (type === 'Text')
|
||||
data.value = await file.value?.text();
|
||||
else if (type === 'ArrayBuffer')
|
||||
data.value = await file.value?.arrayBuffer();
|
||||
else if (type === 'Blob')
|
||||
data.value = file.value;
|
||||
}
|
||||
|
||||
async function writeData(): Promise<void> {
|
||||
if (!fileHandle.value || data.value === undefined || data.value === null)
|
||||
return;
|
||||
|
||||
const stream = await fileHandle.value.createWritable();
|
||||
await stream.write(data.value);
|
||||
await stream.close();
|
||||
}
|
||||
|
||||
async function open(_options: UseFileSystemAccessCommonOptions = {}): Promise<void> {
|
||||
if (!isSupported.value || !fsWindow)
|
||||
return;
|
||||
|
||||
try {
|
||||
const [handle] = await fsWindow.showOpenFilePicker({ ...pickerDefaults(), ..._options });
|
||||
if (!handle)
|
||||
return;
|
||||
fileHandle.value = handle;
|
||||
await updateData();
|
||||
}
|
||||
catch (error) {
|
||||
onError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function create(_options: UseFileSystemAccessShowSaveFileOptions = {}): Promise<void> {
|
||||
if (!isSupported.value || !fsWindow)
|
||||
return;
|
||||
|
||||
try {
|
||||
fileHandle.value = await fsWindow.showSaveFilePicker({ ...pickerDefaults(), ..._options });
|
||||
data.value = undefined;
|
||||
await updateData();
|
||||
}
|
||||
catch (error) {
|
||||
onError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAs(_options: UseFileSystemAccessShowSaveFileOptions = {}): Promise<void> {
|
||||
if (!isSupported.value || !fsWindow)
|
||||
return;
|
||||
|
||||
try {
|
||||
fileHandle.value = await fsWindow.showSaveFilePicker({ ...pickerDefaults(), ..._options });
|
||||
await writeData();
|
||||
await updateFile();
|
||||
}
|
||||
catch (error) {
|
||||
onError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function save(_options: UseFileSystemAccessShowSaveFileOptions = {}): Promise<void> {
|
||||
if (!isSupported.value)
|
||||
return;
|
||||
|
||||
if (!fileHandle.value)
|
||||
return saveAs(_options);
|
||||
|
||||
try {
|
||||
await writeData();
|
||||
await updateFile();
|
||||
}
|
||||
catch (error) {
|
||||
onError(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-read with the new strategy whenever `dataType` changes; skip the redundant
|
||||
// initial run since nothing is open yet.
|
||||
watch(() => toValue(dataType), () => {
|
||||
if (fileHandle.value)
|
||||
void updateData();
|
||||
});
|
||||
|
||||
return {
|
||||
isSupported,
|
||||
data,
|
||||
file,
|
||||
fileName,
|
||||
fileMIME,
|
||||
fileSize,
|
||||
fileLastModified,
|
||||
open,
|
||||
create,
|
||||
save,
|
||||
saveAs,
|
||||
updateData,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user