React 打字机效果

597 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 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

图片.png

判断当前是否是 <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;
}

Aug-08-2022 16-48-03.gif

解决 <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>
  );
}

Aug-08-2022 16-52-17.gif

回退

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 {
          // ...
        }
        // ...
      });
  });
}

Aug-08-2022 17-16-28.gif