1. Method 1: Use the plug-in typed.js
typed.js website address, click me to open it
1.1. The core code is as follows:
//TypeWriteEffect/index.tsx component import React, {<!-- --> useEffect, useRef } from 'react'; import Typed from 'typed.js'; import {<!-- --> PropsType } from './index.d'; const TypeWriteEffect: React.FC<PropsType> = ({<!-- --> text = '', callback, seed = 20 }) => {<!-- --> const el = useRef(null); useEffect(() => {<!-- --> const typed = new Typed(el.current, {<!-- --> strings: [text], typeSpeed: seed, showCursor: true, onComplete(self) {<!-- --> callback?.(); self.cursor.style.display = 'none'; // Hide cursor }, }); return () => {<!-- --> typed.destroy(); }; }, []); return ( <div> <span ref={<!-- -->el}></span> </div> ); }; export default TypeWriteEffect;
// index.d.ts export type PropsType = {<!-- --> text: string; //Text content seed?: number; //Speed callback?: () => void; //Callback function after printing is completed };
1.2, use
/* * @Description: * @Author: muge * @LastEditors: muge */ import TypeWriteEffect from '@/components/TypeWriteEffect'; import React from 'react'; const Index = () => {<!-- --> const richText = '<code>2112.1</code>This is the response text of <span class="typing-text" style="color: red">Intelligent Question and Answer Assistant--</span>----very long of. <div style="color: pink; font-size: 20px">Genshin Impact* is launched! </div>---King* starts! <img src="//i2.wp.com/nimg.ws.126.net/?url=http://dingyue.ws.126.net/2022/0830/74168ba1j00rhf6m5002cd000u000jfp.jpg & amp;thumbnail=660x2147483647 & amp;quality=80 & amp;type=jpg" style="height: 150px"/>'; return <TypeWriteEffect text={<!-- -->richText} />; }; export default Index;
1.3, the effect is as shown in the figure
2. Method 2: Custom implementation
2.1, ideas
My idea is to cut the string into two arrays, one is the tag array of <>>, and the other is the array intercepted by characters and tags. The effect is as follows:
Then traverse chucksList to generate a new array, as shown below:
Then iterate through this array and use the timer to insert into the dom
2.2, core code
2.2.1, writeEffect.ts
// utils/writeEffect/index.ts import type {<!-- --> TypingEffectType } from './index.d'; import initData from './lib/tool'; import {<!-- --> createBlinkSpan } from './lib/createBlinkSpan'; import {<!-- --> textConversionArr } from './lib/textConversionArr'; import {<!-- --> getCursorClassName } from './lib/getCursorClassName'; import {<!-- --> removeCursor } from './lib/removeCursor'; /** * @description: Cursor printing effect * @param {HTMLElement} dom * @param {TypingEffectType} parameter * @author: muge */ export const typingEffect = (dom: HTMLElement, parameter: TypingEffectType) => {<!-- --> const {<!-- --> text, callback, cursorConfig = {<!-- -->}, seed = initData.seed } = parameter; const {<!-- --> cursor = false, dieTime = initData.dieTime, blinkSeed = initData.blinkSeed, } = cursorConfig as any; if (!dom || !text) return; const textArrs: string[] = textConversionArr(text); dom.innerHTML = ''; //Clear the content each time let blinkInterval: any = null; //Cursor timer //Add cursor effect cursor & amp; & amp; createBlinkSpan(dom, blinkInterval, blinkSeed); let startIndex = 0; const element = document.createElement('span'); //Text storage label const start = () => {<!-- --> startIndex + + ; if (startIndex >= textArrs.length) {<!-- --> cursor & amp; & amp; removeCursor(dom, blinkInterval, dieTime); callback?.(); return; } if (cursor) {<!-- --> element.innerHTML = textArrs[startIndex]; dom.insertBefore(element, getCursorClassName()); } else {<!-- --> dom.innerHTML = textArrs[startIndex]; } setTimeout(() => start(), seed); }; start(); }; //index.d.ts type cursorConfigType = {<!-- --> cursor?: boolean; //Whether to display the cursor seed?: number; //Default cursor speed =>default 250ms dieTime?: number; //The cursor disappears after typing =>Default 200ms blinkSeed?: number; //cursor blinking speed }; export type TypingEffectType = {<!-- --> text: string; //text seed?: number; //Default typing speed, default 250ms callback?: () => void; //The callback function when the typewriter ends cursorConfig?: cursorConfigType; //Cursor configuration item };
2.2.2, createBlinkSpan
import initData from './tool'; export const createBlinkSpan = ( dom: HTMLElement, intervalName: NodeJS.Timer, blinkSeed: number, ) => {<!-- --> const {<!-- --> cursorClassName } = initData; const blinkName = document.createElement('span'); blinkName.className = cursorClassName; blinkName.innerHTML = '|'; dom.appendChild(blinkName); //Set the blinking interval, for example, switch the cursor state every 500 milliseconds intervalName = setInterval(() => {<!-- --> blinkName.style.display = blinkName.style.display === 'none' ? 'inline' : 'none'; }, blinkSeed); };
2.2.3, textConversionArr
// Label cutting const labelCut = (str: string) => {<!-- --> const arrs = str.match(/<[^>] + >(?!\/>)/g); if (!arrs) return []; return arrs.filter((item) => !/<[^>] + \/>$/.test(item)); }; // Separate strings by <></> => array const splitStringToChunks = (str: string): string[] => {<!-- --> const chunks: string[] = []; let currentChunk = ''; let insideTag = false; for (let i = 0; i < str.length; i + + ) {<!-- --> const char = str[i]; if (char === '<') {<!-- --> insideTag = true; currentChunk + = char; } else if (char === '>') {<!-- --> insideTag = false; currentChunk + = char; } else {<!-- --> currentChunk + = char; } if (!insideTag || i === str.length - 1) {<!-- --> chunks.push(currentChunk); currentChunk = ''; } } return chunks; }; /** * @description: text conversion array * @param {string} str * @author: muge */ export const textConversionArr = (str: string): string[] => {<!-- --> const labelCutList = labelCut(str); const chucksList = splitStringToChunks(str); let startIndex: number = 0; const result: string[] = []; let lastStr = ''; // concatenated string const isCloseTagReg = /<\/[^>]*>/; //Whether it is a closed tag </img>=>true <>=>false <div/>=>false while (startIndex < chucksList?.length) {<!-- --> let currentIndex = startIndex; + + startIndex; const currentStr = chucksList[currentIndex]; const index = labelCutList.indexOf(currentStr); if (index === -1) {<!-- --> lastStr + = currentStr; result.push(lastStr); continue; } // start tag if (!/<\/[^>] + >/.test(currentStr)) {<!-- --> // Determine whether it is a self-closing tag, such as <img><br>This kind of irregular writing method const nextCloseTag: string | undefined = labelCutList[index + 1]; if (!nextCloseTag || !isCloseTagReg.test(nextCloseTag)) {<!-- --> lastStr + = currentStr; result.push(lastStr); continue; } // Find the index of the first closed tag const findArrs = chucksList.slice(currentIndex); const endTagIndex = findArrs.findIndex((item) => item === nextCloseTag); let curStr: string = ''; for (let i = 1; i < endTagIndex; i + + ) {<!-- --> curStr + = findArrs[i]; const res = labelCutList[index] + curStr + nextCloseTag; result.push(lastStr + res); if (endTagIndex - 1 === i) {<!-- --> lastStr + = res; } } startIndex = currentIndex + endTagIndex; //Reset subscript continue; } } return result; };
2.2.4, getCursorClassName
import initData from './tool'; /** * @description: //Get cursor dom * @author: muge */ export const getCursorClassName = () => {<!-- --> return document.querySelector(`.${<!-- -->initData.cursorClassName}`) as HTMLElement; };
2.2.5, removeCursor
import initData from './tool'; /** * @description: //Remove cursor label * @param {HTMLElement} dom //cursor label dom * @param {string} intervalName //Timer name * @param {number} cursorAway //cursor disappearance time * @author: muge */ export const removeCursor = (dom: HTMLElement, intervalName: NodeJS.Timer, cursorAway: number) => {<!-- --> setTimeout(() => {<!-- --> clearInterval(intervalName); dom.removeChild(document.querySelector(`.${<!-- -->initData.cursorClassName}`) as HTMLElement); }, cursorAway); };
2.2.6, initData
type initDataType = {<!-- --> cursorClassName: string; seed: number; blinkSeed: number; dieTime: number; }; const initData: initDataType = {<!-- --> cursorClassName: 'blink-class', seed: 100, dieTime: 500, blinkSeed: 350, }; export default initData;
2.3, use
import {<!-- --> typingEffect } from '@/utils/writeEffect'; import React, {<!-- --> useEffect, useRef } from 'react'; const Index = () => {<!-- --> const el = useRef<HTMLElement | any>(null); const richText = 'Genshin Impact · Start! <img src="//i2.wp.com/nimg.ws.126.net/?url=http://dingyue.ws.126.net/2022/0830/74168ba1j00rhf6m5002cd000u000jfp.jpg & amp;thumbnail=660x2147483647 & amp;quality=80 & amp;type=jpg" style="height: 150px"/><br/><br><div>Glory of Kings · Start! </div>'; useEffect(() => {<!-- --> typingEffect(el.current, {<!-- --> text: richText, callback: () => {<!-- --> console.log('Callback function executed after the printer ends!'); }, cursorConfig: {<!-- --> cursor: true, }, }); }, []); return <div ref={<!-- -->el}></div>; }; export default Index;
2.4, Effect
git project address, click me to open