110 lines
2.7 KiB
JavaScript
110 lines
2.7 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
|
|
export default function TypingEffect({
|
|
steps = [],
|
|
loop = true,
|
|
typingSpeed = [40, 120],
|
|
deleteSpeed = [20, 60],
|
|
}) {
|
|
const [displayed, setDisplayed] = useState('');
|
|
const [showCursor, setShowCursor] = useState(true);
|
|
const [cursorBlink, setCursorBlink] = useState(true);
|
|
|
|
const configKey = JSON.stringify({ steps, loop, typingSpeed, deleteSpeed });
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
let timerId = null;
|
|
|
|
const sleep = (ms) =>
|
|
new Promise((resolve) => {
|
|
timerId = setTimeout(resolve, ms);
|
|
});
|
|
|
|
const rand = ([min, max]) =>
|
|
Math.floor(min + Math.random() * (max - min + 1));
|
|
|
|
const run = async () => {
|
|
do {
|
|
let chars = [];
|
|
setDisplayed('');
|
|
|
|
for (const step of steps) {
|
|
if (cancelled) return;
|
|
|
|
if ('text' in step) {
|
|
const speed = step.speed || typingSpeed;
|
|
for (const c of Array.from(step.text)) {
|
|
if (cancelled) return;
|
|
chars.push(c);
|
|
setDisplayed(chars.join(''));
|
|
await sleep(rand(speed));
|
|
}
|
|
} else if ('pause' in step) {
|
|
await sleep(step.pause);
|
|
} else if ('delete' in step) {
|
|
const speed = step.speed || deleteSpeed;
|
|
const count =
|
|
step.delete === 'all'
|
|
? chars.length
|
|
: Math.min(step.delete, chars.length);
|
|
for (let i = 0; i < count; i++) {
|
|
if (cancelled) return;
|
|
chars.pop();
|
|
setDisplayed(chars.join(''));
|
|
await sleep(rand(speed));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!loop) {
|
|
setShowCursor(false);
|
|
return;
|
|
}
|
|
|
|
await sleep(500);
|
|
} while (!cancelled);
|
|
};
|
|
|
|
run();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
if (timerId != null) clearTimeout(timerId);
|
|
};
|
|
}, [configKey]);
|
|
|
|
useEffect(() => {
|
|
if (!showCursor) return;
|
|
const id = setInterval(() => setCursorBlink((v) => !v), 530);
|
|
return () => clearInterval(id);
|
|
}, [showCursor]);
|
|
|
|
const parts = displayed.split('\n');
|
|
|
|
return (
|
|
<span>
|
|
{parts.map((part, i) => (
|
|
<React.Fragment key={i}>
|
|
{i > 0 && <br />}
|
|
{part}
|
|
</React.Fragment>
|
|
))}
|
|
{showCursor && (
|
|
<span
|
|
aria-hidden="true"
|
|
style={{
|
|
display: 'inline-block',
|
|
width: '2px',
|
|
height: '1em',
|
|
backgroundColor: 'currentColor',
|
|
marginLeft: '1px',
|
|
verticalAlign: 'text-bottom',
|
|
opacity: cursorBlink ? 1 : 0,
|
|
}}
|
|
/>
|
|
)}
|
|
</span>
|
|
);
|
|
}
|