Browse Source

Extract some components from animation form

Define separate components and modules for reducing side of form source code.
master
TheoryOfNekomata 1 year ago
parent
commit
54e0ad6da4
5 changed files with 104 additions and 59 deletions
  1. +1
    -1
      tools/animation-workbench/app/components/Button/index.tsx
  2. +52
    -0
      tools/animation-workbench/app/components/ComboBox/index.tsx
  3. +2
    -2
      tools/animation-workbench/app/components/FileButton/index.tsx
  4. +20
    -0
      tools/animation-workbench/app/modules/file.ts
  5. +29
    -56
      tools/animation-workbench/app/routes/index.tsx

+ 1
- 1
tools/animation-workbench/app/components/Button/index.tsx View File

@@ -5,7 +5,7 @@ export interface ButtonProps extends Omit<HTMLProps<HTMLButtonElement>, 'type'>
} }


export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
className,
className = '',
...etcProps ...etcProps
}, ref) => ( }, ref) => (
<button <button


+ 52
- 0
tools/animation-workbench/app/components/ComboBox/index.tsx View File

@@ -0,0 +1,52 @@
import {FormEventHandler, forwardRef, HTMLProps, ReactNode, useId} from 'react';

export interface ComboBoxProps extends Omit<HTMLProps<HTMLInputElement>, 'list'> {
onOptionSelect?: FormEventHandler<HTMLInputElement>;
onCustomValueInput?: FormEventHandler<HTMLInputElement>;
children?: ReactNode;
}

export const ComboBox = forwardRef<HTMLInputElement, ComboBoxProps>(({
onOptionSelect,
children,
className = '',
onInput,
onCustomValueInput,
...etcProps
}, ref) => {
const id = useId();
const handleInput: FormEventHandler<HTMLInputElement> = (e) => {
onInput?.(e);
const nativeEvent = e.nativeEvent as unknown as { inputType?: string };
switch (nativeEvent.inputType) {
case 'deleteContentBackward':
case 'deleteContentForward':
case 'insertText':
// we type into the input
onCustomValueInput?.(e);
return;
default:
break;
// we select from the datalist

}
onOptionSelect?.(e);
}

return (
<>
<datalist
id={id}
>
{children}
</datalist>
<input
{...etcProps}
ref={ref}
list={id}
className={`w-full block h-12 px-4 ${className}`}
onInput={handleInput}
/>
</>
)
})

+ 2
- 2
tools/animation-workbench/app/components/FileButton/index.tsx View File

@@ -1,11 +1,11 @@
import {forwardRef, HTMLProps, ReactNode} from 'react'; import {forwardRef, HTMLProps, ReactNode} from 'react';


export interface FileButtonProps extends Omit<HTMLProps<HTMLInputElement>, 'type'> { export interface FileButtonProps extends Omit<HTMLProps<HTMLInputElement>, 'type'> {
children: ReactNode;
children?: ReactNode;
} }


export const FileButton = forwardRef<HTMLInputElement, FileButtonProps>(({ export const FileButton = forwardRef<HTMLInputElement, FileButtonProps>(({
className,
className = '',
children, children,
...etcProps ...etcProps
}, ref) => ( }, ref) => (


+ 20
- 0
tools/animation-workbench/app/modules/file.ts View File

@@ -0,0 +1,20 @@
export const readFile = async (file: File) => {
const fileReader = new FileReader()

return new Promise<string>((resolve, reject) => {
fileReader.addEventListener('load', (e) => {
if (!e.target) {
reject(new Error('Invalid target'))
return;
}

resolve(e.target.result as string);
})

fileReader.addEventListener('error', () => {
reject(new Error('Error reading file'))
});

fileReader.readAsText(file);
});
}

+ 29
- 56
tools/animation-workbench/app/routes/index.tsx View File

@@ -9,7 +9,9 @@ import {
} from 'react'; } from 'react';
import { getFormValues, setFormValues } from '@theoryofnekomata/formxtra'; import { getFormValues, setFormValues } from '@theoryofnekomata/formxtra';
import { Button } from '~/components/Button'; import { Button } from '~/components/Button';
import {FileButton} from '~/components/FileButton';
import { FileButton } from '~/components/FileButton';
import * as FileModule from '~/modules/file';
import {ComboBox} from '~/components/ComboBox';


type SvgGroup = { type SvgGroup = {
id: string; id: string;
@@ -39,30 +41,8 @@ const useAnimationForm = () => {
const [savedSvgKeyframes, setSavedSvgKeyframes] = useState<Keyframe[]>(); const [savedSvgKeyframes, setSavedSvgKeyframes] = useState<Keyframe[]>();


const svgAnimationIdsListId = useId(); const svgAnimationIdsListId = useId();

const svgPreviewRef = useRef<HTMLDivElement>(null); const svgPreviewRef = useRef<HTMLDivElement>(null);


const readFile = async (file: File) => {
const fileReader = new FileReader()

return new Promise<string>((resolve, reject) => {
fileReader.addEventListener('load', (e) => {
if (!e.target) {
reject(new Error('Invalid target'))
return;
}

resolve(e.target.result as string);
})

fileReader.addEventListener('error', () => {
reject(new Error('Error reading file'))
});

fileReader.readAsText(file);
});
}

const loadSvgFile: ChangeEventHandler<HTMLInputElement> = async (e) => { const loadSvgFile: ChangeEventHandler<HTMLInputElement> = async (e) => {
const filePicker = e.currentTarget; const filePicker = e.currentTarget;
if (!filePicker.files) { if (!filePicker.files) {
@@ -74,29 +54,22 @@ const useAnimationForm = () => {
} }
setLoadedFileName(file.name); setLoadedFileName(file.name);
setIsNewAnimationId(false); setIsNewAnimationId(false);
const result = await readFile(file);
const result = await FileModule.readFile(file);
setLoadedFileContents(result); setLoadedFileContents(result);
filePicker.value = '' filePicker.value = ''
} }


const handleAnimationIdInput: FormEventHandler<HTMLInputElement> = (e) => {
const nativeEvent = e.nativeEvent as unknown as { inputType?: string };
switch (nativeEvent.inputType) {
case 'deleteContentBackward':
case 'deleteContentForward':
case 'insertText':
// we type into the input
setIsNewAnimationId(true);
break;
default:
// we select from the datalist
const target = e.currentTarget;
setIsNewAnimationId(false);
setCurrentAnimationId(e.currentTarget.value);
setTimeout(() => {
target.blur();
});
}
const selectNewAnimationId: FormEventHandler<HTMLInputElement> = (e) => {
const target = e.currentTarget;
setIsNewAnimationId(false);
setCurrentAnimationId(e.currentTarget.value);
setTimeout(() => {
target.blur();
});
}

const proposeNewAnimationId: FormEventHandler<HTMLInputElement> = (e) => {
setIsNewAnimationId(true);
} }


const cancelAnimationIdChange: MouseEventHandler<HTMLButtonElement> = (e) => { const cancelAnimationIdChange: MouseEventHandler<HTMLButtonElement> = (e) => {
@@ -325,6 +298,8 @@ const useAnimationForm = () => {
default: default:
break; break;
} }

console.log(values);
} }


const updateTime: ChangeEventHandler<HTMLInputElement> = (e) => { const updateTime: ChangeEventHandler<HTMLInputElement> = (e) => {
@@ -364,7 +339,8 @@ const useAnimationForm = () => {
svgPreviewRef, svgPreviewRef,
svgGroups, svgGroups,
svgAnimationIdsListId, svgAnimationIdsListId,
handleAnimationIdInput,
selectNewAnimationId,
proposeNewAnimationId,
cancelAnimationIdChange, cancelAnimationIdChange,
focusAnimationIdInput, focusAnimationIdInput,
blurAnimationIdInput, blurAnimationIdInput,
@@ -389,7 +365,8 @@ const IndexPage = () => {
svgGroups, svgGroups,
svgAnimations, svgAnimations,
svgAnimationIdsListId, svgAnimationIdsListId,
handleAnimationIdInput,
selectNewAnimationId,
proposeNewAnimationId,
isNewAnimationId, isNewAnimationId,
cancelAnimationIdChange, cancelAnimationIdChange,
focusAnimationIdInput, focusAnimationIdInput,
@@ -464,27 +441,23 @@ const IndexPage = () => {
&& ( && (
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
<div className="flex gap-4"> <div className="flex gap-4">
<datalist
id={svgAnimationIdsListId}
>
{svgAnimations?.map(s => (
<option key={s}>{s}</option>
))}
</datalist>
<label className="block w-full"> <label className="block w-full">
<span className="sr-only"> <span className="sr-only">
Animation ID Animation ID
</span> </span>
<input
list={svgAnimationIdsListId}
<ComboBox
name="animationId" name="animationId"
className="w-full block h-full px-4 h-12"
defaultValue={svgAnimations?.[0] ?? ''} defaultValue={svgAnimations?.[0] ?? ''}
onInput={handleAnimationIdInput}
onOptionSelect={selectNewAnimationId}
onCustomValueInput={proposeNewAnimationId}
onFocus={focusAnimationIdInput} onFocus={focusAnimationIdInput}
onBlur={blurAnimationIdInput} onBlur={blurAnimationIdInput}
autoComplete="off" autoComplete="off"
/>
>
{svgAnimations?.map(s => (
<option key={s}>{s}</option>
))}
</ComboBox>
</label> </label>
{ {
isNewAnimationId && ( isNewAnimationId && (


Loading…
Cancel
Save