elements
lifecycle
browser
- useAudio
- useBattery
- useBluetooth
- useBreakpoints
- useBroadcastChannel
- useBrowserLocation
- useClipboard
- useCopy
- useCssVar
- useDisplayMedia
- useDocumentEvent
- useDocumentTitle
- useDocumentVisibility
- useEventListener
- useEventSource
- useEyeDropper
- useFavicon
- useFileSystemAccess
- useFps
- useFullscreen
- useGamepad
- useGeolocation
- useMeasure
- useMediaControls
- useMediaQuery
- useMemory
- useNetwork
- useObjectUrl
- useOnline
- useOtpCredential
- usePermission
- usePictureInPicture
- usePointerLock
- usePostMessage
- useRaf
- useShare
- useSpeechRecognition
- useSpeechSynthesis
- useSticky
utilities
state
user
sensors
API not supported, make sure to check for compatibility with different browsers when using this API
import type { MouseEvent } from 'react';
import {
useBoolean,
useClickOutside,
useField,
useFileSystemAccess
} from '@siberiacancode/reactuse';
import { FileTextIcon, ReplaceIcon, XIcon } from 'lucide-react';
import { useState } from 'react';
const Demo = () => {
const fileSystemAccess = useFileSystemAccess({
dataType: 'Text',
types: [
{
description: 'Text',
accept: { 'text/plain': ['.txt'] }
},
{
description: 'Markdown',
accept: { 'text/markdown': ['.md', '.markdown'] }
}
]
});
const findField = useField('');
const replaceField = useField('');
const [findOpen, toggleFindOpen] = useBoolean(false);
const [content, setContent] = useState('');
const findPanelRef = useClickOutside<HTMLDivElement>(() => {
if (findOpen) toggleFindOpen(false);
});
if (!fileSystemAccess.supported)
return (
<p>
API not supported, make sure to check for compatibility with different browsers when using
this{' '}
<a
href='https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API'
rel='noreferrer'
target='_blank'
>
API
</a>
</p>
);
const find = findField.watch();
const replace = replaceField.watch();
const matches = find ? content.split(find).length - 1 : 0;
const dirty = !!fileSystemAccess.file && content !== fileSystemAccess.data;
const onSave = async (event: MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
await fileSystemAccess.save();
setContent(fileSystemAccess.data ?? '');
};
const onOpen = async (event: MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
const data = await fileSystemAccess.open();
setContent(data);
};
const onReplaceAll = (event: MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
if (!find || !matches) return;
const updated = content.split(find).join(replace);
fileSystemAccess.set(updated);
findField.setValue('');
replaceField.setValue('');
};
return (
<section className='flex w-full max-w-2xl flex-col p-4'>
<div className='border-border bg-card relative flex h-[280px] flex-col overflow-hidden rounded-xl border shadow-sm'>
{!fileSystemAccess.file && (
<div className='flex size-full flex-col items-center justify-center gap-3 p-6'>
<div className='bg-muted text-muted-foreground flex size-12 items-center justify-center rounded-full'>
<FileTextIcon className='size-6' />
</div>
<div className='flex flex-col items-center gap-1 text-center'>
<p className='text-foreground text-sm font-medium'>No file opened</p>
<p className='text-muted-foreground text-xs'>
Open a .txt or .md file to start editing
</p>
</div>
<button data-size='sm' type='button' onClick={onOpen}>
Open file
</button>
</div>
)}
{fileSystemAccess.file && (
<>
<div className='border-border bg-muted/40 flex shrink-0 items-center gap-2 border-b px-3 py-2'>
<div className='bg-card flex size-6 shrink-0 items-center justify-center'>
<FileTextIcon className='text-muted-foreground size-3' />
</div>
<span className='text-foreground min-w-0 flex-1 truncate text-xs font-medium'>
{fileSystemAccess.name}
</span>
<div className='flex items-center gap-1'>
<button
aria-label='Find and replace'
data-size='icon-sm'
data-variant='ghost'
type='button'
onClick={() => toggleFindOpen()}
>
<ReplaceIcon className='size-3.5' />
</button>
<button data-size='sm' disabled={!dirty} type='button' onClick={onSave}>
Save
</button>
</div>
</div>
<textarea
className='no-scrollbar text-foreground flex-1 resize-none rounded-none! border-none! bg-transparent p-3 font-mono text-xs shadow-none! ring-0! outline-none!'
value={fileSystemAccess.data}
onChange={(event) => fileSystemAccess.set(event.target.value)}
/>
{findOpen && (
<div
ref={findPanelRef}
className='border-border bg-card absolute top-12 right-3 z-20 flex w-[240px] flex-col gap-3 rounded-xl border p-3 shadow-lg'
>
<div className='flex items-center justify-between'>
<span className='text-foreground text-[11px] font-medium'>Find and replace</span>
<button
aria-label='Close'
data-size='icon'
data-variant='ghost'
type='button'
onClick={() => toggleFindOpen()}
>
<XIcon className='size-3' />
</button>
</div>
<div className='flex flex-col gap-2'>
<input
autoFocus
className='border-border bg-background text-foreground rounded-md border px-2.5 py-1.5 text-[11px] outline-none'
placeholder='Find'
{...findField.register()}
/>
<div className='relative'>
<input
className='border-border bg-background text-foreground w-full rounded-md border px-2.5 py-1.5 pr-8 text-[11px] outline-none'
placeholder='Replace with'
{...replaceField.register()}
/>
<button
aria-label='Replace all'
className='absolute top-1/2 right-1 -translate-y-1/2'
data-size='icon-xs'
data-variant='ghost'
disabled={!find || !matches}
type='button'
onClick={onReplaceAll}
>
<ReplaceIcon className='size-3' />
</button>
</div>
</div>
{!!find && (
<span className='text-muted-foreground font-mono text-[10px] tabular-nums'>
{matches} {matches === 1 ? 'match' : 'matches'}
</span>
)}
</div>
)}
</>
)}
</div>
</section>
);
};
export default Demo;
This hook uses File browser api to provide enhanced functionality. Make sure to check for compatibility with different browsers when using this api
Installation
pnpm add @siberiacancode/reactuseUsage
const fileSystemAccess = useFileSystemAccess({ dataType: 'Text' });Type Declarations
export interface FileSystemAccessShowOpenFileOptions {
excludeAcceptAllOption?: boolean;
multiple?: boolean;
types?: Array<{
description?: string;
accept: Record<string, string[]>;
}>;
}
export interface FileSystemAccessShowSaveFileOptions {
excludeAcceptAllOption?: boolean;
suggestedName?: string;
types?: Array<{
description?: string;
accept: Record<string, string[]>;
}>;
}
export interface FileSystemFileHandle {
createWritable: () => Promise<FileSystemWritableFileStream>;
getFile: () => Promise<File>;
}
export interface FileSystemWritableFileStream extends WritableStream {
write: FileSystemWritableFileStreamWrite;
seek: (position: number) => Promise<void>;
truncate: (size: number) => Promise<void>;
}
export interface FileSystemWritableFileStreamWrite {
(data: string | Blob | BufferSource): Promise<void>;
(options: { type: 'write'; position: number; data: string | Blob | BufferSource }): Promise<void>;
(options: { type: 'seek'; position: number }): Promise<void>;
(options: { type: 'truncate'; size: number }): Promise<void>;
}
interface Window {
readonly showOpenFilePicker: (
options?: FileSystemAccessShowOpenFileOptions
) => Promise<FileSystemFileHandle[]>;
readonly showSaveFilePicker: (
options?: FileSystemAccessShowSaveFileOptions
) => Promise<FileSystemFileHandle>;
}
export type UseFileSystemAccessCommonOptions = Pick<
FileSystemAccessShowOpenFileOptions,
'excludeAcceptAllOption' | 'types'
>;
export type UseFileSystemAccessShowSaveOptions = Pick<
FileSystemAccessShowSaveFileOptions,
'suggestedName'
>;
export type UseFileSystemAccessOptions = UseFileSystemAccessCommonOptions & {
dataType?: 'ArrayBuffer' | 'Blob' | 'Text';
};
export interface UseFileSystemAccessReturn<Data = string | ArrayBuffer | Blob> {
/** Last read data */
data?: Data;
/** Current file */
file?: File;
/** Last modified timestamp */
lastModified: number;
/** File base name */
name: string;
/** Size in bytes */
size: number;
/** Whether the File System Access API is available */
supported: boolean;
/** MIME type */
type: string;
/** Create a new file via save picker */
create: (createOptions?: UseFileSystemAccessShowSaveOptions) => Promise<Data>;
/** Open an existing file */
open: (openOptions?: UseFileSystemAccessCommonOptions) => Promise<Data>;
/** Save to the current handle, or prompt with {@link saveAs} if none */
save: (saveOptions?: UseFileSystemAccessShowSaveOptions) => Promise<Data>;
/** Always prompt for a file path then save */
saveAs: (saveOptions?: UseFileSystemAccessShowSaveOptions) => Promise<Data>;
/** Set the data */
set: (data: Data) => void;
/** Re-read data from the current handle using `dataType` */
update: () => Promise<Data>;
}
export interface UseFileSystemAccess {
(): UseFileSystemAccessReturn<string | ArrayBuffer | Blob>;
(
options: UseFileSystemAccessOptions & { dataType: 'ArrayBuffer' }
): UseFileSystemAccessReturn<ArrayBuffer>;
(options: UseFileSystemAccessOptions & { dataType: 'Blob' }): UseFileSystemAccessReturn<Blob>;
(options: UseFileSystemAccessOptions & { dataType: 'Text' }): UseFileSystemAccessReturn<string>;
(options?: UseFileSystemAccessOptions): UseFileSystemAccessReturn<string | ArrayBuffer | Blob>;
}API
Returns
UseFileSystemAccessReturn<string | ArrayBuffer | Blob>Parameters
| Name | Type | Default | Note |
|---|---|---|---|
| options | UseFileSystemAccessOptions | - |
Returns
UseFileSystemAccessReturnContributors
ddebabin
Last updated on