携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第14天,点击查看活动详情
紧接上一篇 React 打字机效果
在实现了基本的功能之后,继续完善。
import { useState } from 'react';
import Typist from './Typist';
const TypistExample = () => {
return (
<div className="TypistExample">
<Typist
className="TypistExample-header"
>
生下红旗下
<Typist.Delay ms={1000} />
<br />
长在春分里
<Typist.Delay ms={1000} />
</Typist>
</div>
)
}
export default TypistExample
判断当前是否是 <Typist.Delay ms={1000} /> 元素,如果是,那就将传入的 delay 时间赋值。
Delay.jsx
import React from 'react';
import PropTypes from 'prop-types';
const Delay = () => <noscript />;
Delay.componentName = 'Delay';
Delay.propTypes = {
ms: PropTypes.number.isRequired,
};
export default Delay;
判断是否是 Delay 组件
utils.js
export function isElementType(element, component, name) {
const elementType = element && element.type;
if (!elementType) {
return false;
}
return (
elementType === component ||
elementType.componentName === name ||
elementType.displayName === name
);
}
export function isDelayElement(element) {
return isElementType(element, Delay, "Delay");
}
在 typeLine 函数中,判断当前的 line 是否是 isDelayElement。
constructor(props) {
super(props);
// ...
this.introducedDelay = null;
}
typeLine = (line, lineIdx) => {
let decoratedLine = line;
if (utils.isDelayElement(line)) {
this.introducedDelay = line.props.ms;
decoratedLine = '⏰';
}
return new Promise((resolve, reject) => {
this.setState({ textLines: this.state.textLines.concat(['']) }, () => {
utils.eachPromise(decoratedLine, this.typeCharacter, decoratedLine, lineIdx)
.then(resolve)
.catch(reject);
});
});
}
sleep 函数
utils.js
export const sleep = (val) => new Promise((resolve) => (
val != null ? setTimeout(resolve, val) : resolve()
));
在 typeCharacter 函数中,如果当前 introducedDelay 不为 null,即表明存在。接下来,判断当前的字符是否为特殊字符。如果是,直接 reslove(),并且将函数进行 return。
typeCharacter = (character, charIdx, line, lineIdx) => {
return new Promise((resolve) => {
const textLines = this.state.textLines.slice();
utils.sleep(this.introducedDelay)
.then(() => {
this.introducedDelay = null;
const isDelay = character === '⏰';
if (isDelay) {
resolve();
return;
}
textLines[lineIdx] += character;
this.setState({ textLines });
const delay = this.delayGeneratorComp();
setTimeout(resolve, delay);
});
});
}
对于一开始给 this.linesToType 赋值也需要做出一些修改。这里不再是简单的字符串,而是包含了 React.Component。
export function extractTextFromElement(element) {
const stack = element ? [element] : [];
const lines = [];
while (stack.length > 0) {
const current = stack.pop();
if (React.isValidElement(current)) {
if (isDelayElement(current)) {
lines.unshift(current);
} else {
React.Children.forEach(current.props.children, (child) => {
stack.push(child);
});
}
} else if (Array.isArray(current)) {
for (const el of current) {
stack.push(el);
}
} else {
lines.unshift(current);
}
}
return lines;
}
解决 <br /> 标签渲染
function cloneElementWithSpecifiedTextAtIndex(element, textLines, textIdx) {
if (textIdx >= textLines.length) {
return [null, textIdx];
}
let idx = textIdx;
const recurse = (el) => {
const [child, advIdx] = cloneElementWithSpecifiedTextAtIndex(
el,
textLines,
idx
);
idx = advIdx;
return child;
};
const isNonTypistElement = (
React.isValidElement(element) && !isDelayElement(element)
);
if (isNonTypistElement) {
const clonedChildren = React.Children.map(element.props.children, recurse) || [];
return [cloneElement(element, clonedChildren), idx];
}
if (Array.isArray(element)) {
const children = element.map(recurse);
return [children, idx];
}
return [textLines[idx], idx + 1];
}
export function cloneElementWithSpecifiedText({ element, textLines }) {
if (!element) {
return undefined;
}
return cloneElementWithSpecifiedTextAtIndex(element, textLines, 0)[0];
}
render() {
const { className, cursor, ...rest } = this.props;
const innerTree = utils.cloneElementWithSpecifiedText({
element: this.props.children,
textLines: this.state.textLines,
});
return (
<div className={`Typist ${className}`} {...rest}>
{innerTree}
<Cursor />
</div>
);
}
回退
import { useState } from 'react';
import Typist from './Typist';
const TypistExample = () => {
return (
<div className="TypistExample">
<Typist
className="TypistExample-header"
>
人民有信仰
<Typist.Delay ms={500} />
国家有力量
<Typist.Backspace count={5} delay={1000} />
</Typist>
</div>
)
}
export default TypistExample
typeLine 函数中判断当前字符是否是 isBackspaceElement。如果是,则将需要回退的字数赋值给 decoratedLine。如果还传入了 delay,则需要将值赋值给 introducedDelay。
typeLine = (line, lineIdx) => {
let decoratedLine = line;
if (utils.isDelayElement(line)) {
this.introducedDelay = line.props.ms;
decoratedLine = '⏰';
} else if (utils.isBackspaceElement(line)) {
if (line.props.delay > 0) {
this.introducedDelay = line.props.delay;
}
decoratedLine = String('🔙').repeat(line.props.count);
}
//....
}
const ACTION_CHARS = ['🔙', '⏰'];
typeCharacter = (character, charIdx, line, lineIdx) => {
return new Promise((resolve) => {
const textLines = this.state.textLines.slice();
utils.sleep(this.introducedDelay)
.then(() => {
this.introducedDelay = null;
const isBackspace = character === '🔙';
// ....
if (isBackspace && lineIdx > 0) {
// 获取前一行 index
let prevLineIdx = lineIdx - 1;
// 获取前一行
let prevLine = textLines[prevLineIdx];
for (let idx = prevLineIdx; idx >= 0; idx--) {
if (prevLine.length > 0 && !ACTION_CHARS.includes(prevLine[0])) {
break;
}
prevLineIdx = idx;
prevLine = textLines[prevLineIdx];
}
textLines[prevLineIdx] = prevLine.substr(0, prevLine.length - 1);
} else {
// ...
}
// ...
});
});
}