UNPKG

react-typing-hook

Version:

Animated typing in ~400 bytes 🐡 of React Hook.

108 lines (85 loc) 3.12 kB
import { useEffect, useRef, RefObject } from "react"; import './index.css' type Typing<T> = (node: T, speed: number, ...args: TypingSteps<T>) => Promise<void> type Editor<T> = (node: T) => number type GeneratorEditor<T> = Generator<Editor<T>, void, Editor<T>> type TypingSteps<T> = Array<string | number | (() => any) | Typing<T>> interface TypingOptions<T> { steps: TypingSteps<T>, loop?: number, speed?: number } export default function useTyping<T extends HTMLElement>({ steps, loop, speed = 60 } : TypingOptions<T>) : RefObject<T> { const ref = useRef<T>(null); async function typing<T extends HTMLElement>(node: T, speed: number, ...args: TypingSteps<T>): Promise<void> { 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<T extends HTMLElement>(node: T, speed: number, text: string): Promise<void> { const textContent = node.textContent || ''; const overlap = getOverlap(textContent, text); await perform<T>(node, speed, [ ...deleter(textContent, overlap) as Iterable<string>, ...writer(text, overlap) as Iterable<string>, ]); } async function wait(ms: number): Promise<void> { await new Promise((resolve) => setTimeout(resolve, ms)); } async function perform<T>(node: T, speed: number, edits: Iterable<string>): Promise<void> { for (const op of editor(edits) as Iterable<Editor<T>>) { op(node); await wait(speed + speed * (Math.random() - 0.5)); } } function* editor<T extends HTMLElement>(edits: Iterable<string>): GeneratorEditor<T> { for (const edit of edits) { yield (node: T) => requestAnimationFrame(() => (node.textContent = edit)); } } function* writer([...text]: Iterable<string>, startIndex = 0, endIndex = text.length): Generator<string, void, string> { while (startIndex < endIndex) { yield text.slice(0, ++startIndex).join(""); } } function* deleter([...text]: Iterable<string>, startIndex = 0, endIndex = text.length): Generator<string, void, string> { 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 }