import { useEffect, useRef, RefObject } from 'react'; type Typing = ( node: T, speed: number, ...args: TypingSteps ) => Promise; type Editor = (node: T) => number; type GeneratorEditor = Generator, void, Editor>; type TypingSteps = Array any) | Typing>; interface TypingOptions { steps: TypingSteps; loop?: number; speed?: number; } export default function useTypical({ steps, loop, speed = 60, }: TypingOptions): RefObject { const ref = useRef(null); async function typing( node: T, speed: number, ...args: TypingSteps ): Promise { for (const arg of args) { switch (typeof arg) { case 'string': await edit(node, speed, arg); break; case 'number': await wait(arg); break; case 'function': await arg(node, speed, ...args); break; default: await arg; } } } async function edit( node: T, speed: number, text: string, ): Promise { const textContent = node.textContent || ''; const overlap = getOverlap(textContent, text); await perform(node, speed, [ ...(deleter(textContent, overlap) as Iterable), ...(writer(text, overlap) as Iterable), ]); } async function wait(ms: number): Promise { await new Promise(resolve => setTimeout(resolve, ms)); } async function perform( node: T, speed: number, edits: Iterable, ): Promise { for (const op of editor(edits) as Iterable>) { op(node); await wait(speed + speed * (Math.random() - 0.5)); } } function* editor( edits: Iterable, ): GeneratorEditor { for (const edit of edits) { yield (node: T) => requestAnimationFrame(() => (node.textContent = edit)); } } function* writer( [...text]: Iterable, startIndex = 0, endIndex = text.length, ): Generator { while (startIndex < endIndex) { yield text.slice(0, ++startIndex).join(''); } } function* deleter( [...text]: Iterable, startIndex = 0, endIndex = text.length, ): Generator { while (endIndex > startIndex) { yield text.slice(0, --endIndex).join(''); } } function getOverlap(start: string, [...end]: string): number { return [...start, NaN].findIndex((char, i) => end[i] !== char); } const loopedType = typing; useEffect(() => { if (ref.current != null) { if (loop === Infinity) { typing(ref.current, speed, ...steps, loopedType); } else if (typeof loop === 'number') { typing( ref.current, speed, ...Array(loop) .fill(steps) .flat(), ); } else { typing(ref.current, speed, ...steps); } } }, []); return ref; }