UNPKG

@lobehub/ui

Version:

Lobe UI is an open-source UI component library for building AIGC web apps

1 lines 13.4 kB
{"version":3,"file":"TypewriterEffect.mjs","names":["timeout: ReturnType<typeof setTimeout>"],"sources":["../../../src/awesome/TypewriterEffect/TypewriterEffect.tsx"],"sourcesContent":["'use client';\n\nimport { cx } from 'antd-style';\nimport { createElement, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport { useMotionComponent } from '@/MotionProvider';\n\nimport { styles } from './style';\nimport type { TypewriterEffectProps } from './type';\n\nconst TypewriterEffect = memo<TypewriterEffectProps>(\n ({\n sentences,\n as: Component = 'div',\n typingSpeed = 100,\n initialDelay = 0,\n pauseDuration = 2000,\n deletingSpeed = 50,\n deletePauseDuration = 0,\n loop = true,\n className = '',\n color,\n showCursor = true,\n hideCursorWhileTyping = false,\n cursorCharacter,\n cursorClassName = '',\n cursorColor,\n cursorBlinkDuration = 0.8,\n cursorFade = true,\n cursorStyle = 'pipe',\n textColors = [],\n variableSpeed,\n onSentenceComplete,\n startOnVisible = false,\n reverseMode = false,\n segmentMode = 'grapheme',\n ...props\n }: TypewriterEffectProps) => {\n const Motion = useMotionComponent();\n const cxStyles = cx;\n const [displayedText, setDisplayedText] = useState('');\n const [currentCharIndex, setCurrentCharIndex] = useState(0);\n const [isDeleting, setIsDeleting] = useState(false);\n const [currentTextIndex, setCurrentTextIndex] = useState(0);\n const [isVisible, setIsVisible] = useState(!startOnVisible);\n const [isDeletePausing, setIsDeletePausing] = useState(false);\n const containerRef = useRef<HTMLElement>(null);\n\n const textArray = useMemo(\n () => (Array.isArray(sentences) ? sentences : [sentences]),\n [sentences],\n );\n\n // Helper function to split text based on segment mode\n const splitText = useCallback(\n (text: string): string[] => {\n // Use Intl.Segmenter if available\n if (typeof Intl !== 'undefined' && 'Segmenter' in Intl) {\n const segmenter = new Intl.Segmenter(undefined, { granularity: segmentMode });\n return Array.from(segmenter.segment(text), (segment) => segment.segment);\n }\n\n // Fallback when Intl.Segmenter is not available\n if (segmentMode === 'word') {\n // Simple word splitting fallback\n return text.split(/(\\s+)/).filter(Boolean);\n }\n\n // Grapheme fallback\n return Array.from(text);\n },\n [segmentMode],\n );\n\n const getRandomSpeed = useCallback(() => {\n if (!variableSpeed) return typingSpeed;\n const { min, max } = variableSpeed;\n return Math.random() * (max - min) + min;\n }, [variableSpeed, typingSpeed]);\n\n const getCurrentTextColor = () => {\n if (textColors.length > 0) {\n return textColors[currentTextIndex % textColors.length];\n }\n return color;\n };\n\n const getCurrentCursorColor = () => {\n return cursorColor || color;\n };\n\n useEffect(() => {\n if (!startOnVisible || !containerRef.current) return;\n\n const observer = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting) {\n setIsVisible(true);\n }\n });\n },\n { threshold: 0.1 },\n );\n\n observer.observe(containerRef.current);\n\n return () => observer.disconnect();\n }, [startOnVisible]);\n\n useEffect(() => {\n if (!isVisible) return;\n\n let timeout: ReturnType<typeof setTimeout>;\n\n const currentText = textArray[currentTextIndex];\n // Split text based on segment mode\n const textSegments = splitText(currentText);\n const processedText = reverseMode ? textSegments.reverse().join('') : currentText;\n\n // Handle delete pause state\n if (isDeletePausing) {\n timeout = setTimeout(() => {\n setIsDeletePausing(false);\n }, deletePauseDuration);\n return () => clearTimeout(timeout);\n }\n\n const executeTypingAnimation = () => {\n if (isDeleting) {\n if (displayedText === '') {\n setIsDeleting(false);\n if (currentTextIndex === textArray.length - 1 && !loop) {\n return;\n }\n if (onSentenceComplete) {\n onSentenceComplete(textArray[currentTextIndex], currentTextIndex);\n }\n setCurrentTextIndex((prev) => (prev + 1) % textArray.length);\n setCurrentCharIndex(0);\n\n if (deletePauseDuration > 0) {\n setIsDeletePausing(true);\n return;\n }\n } else {\n timeout = setTimeout(() => {\n setDisplayedText((prev) => {\n const segments = splitText(prev);\n return segments.slice(0, -1).join('');\n });\n }, deletingSpeed);\n }\n } else {\n const processedSegments = splitText(processedText);\n if (currentCharIndex < processedSegments.length) {\n timeout = setTimeout(\n () => {\n setDisplayedText((prev) => prev + processedSegments[currentCharIndex]);\n setCurrentCharIndex((prev) => prev + 1);\n },\n variableSpeed ? getRandomSpeed() : typingSpeed,\n );\n } else if (textArray.length >= 1) {\n if (!loop && currentTextIndex === textArray.length - 1) return;\n\n timeout = setTimeout(() => {\n setIsDeleting(true);\n }, pauseDuration);\n }\n }\n };\n\n if (currentCharIndex === 0 && !isDeleting && displayedText === '') {\n timeout = setTimeout(executeTypingAnimation, initialDelay);\n } else {\n executeTypingAnimation();\n }\n\n return () => clearTimeout(timeout);\n }, [\n currentCharIndex,\n displayedText,\n isDeleting,\n isDeletePausing,\n typingSpeed,\n deletingSpeed,\n deletePauseDuration,\n pauseDuration,\n textArray,\n currentTextIndex,\n loop,\n initialDelay,\n isVisible,\n reverseMode,\n variableSpeed,\n onSentenceComplete,\n getRandomSpeed,\n splitText,\n ]);\n\n const getCursorStyle = () => {\n if (cursorCharacter) return styles.cursorCustom;\n\n switch (cursorStyle) {\n case 'block': {\n return styles.cursorBlock;\n }\n case 'dot': {\n return styles.cursorDot;\n }\n case 'underscore': {\n return styles.cursorUnderscore;\n }\n case 'pipe': {\n return styles.cursor;\n }\n }\n };\n\n const currentTextLength = splitText(textArray[currentTextIndex]).length;\n const isTyping = currentCharIndex < currentTextLength && !isDeleting;\n const isAfterTyping = currentCharIndex === currentTextLength && !isDeleting;\n\n const shouldHideCursor = (() => {\n if (hideCursorWhileTyping === true) return true; // 完全隐藏\n if (hideCursorWhileTyping === 'typing') return isTyping || isDeleting; // 打字和删除时隐藏\n if (hideCursorWhileTyping === 'afterTyping') return isAfterTyping; // 打字完成后隐藏\n return false;\n })();\n\n const textColor = getCurrentTextColor();\n const finalCursorColor = getCurrentCursorColor();\n\n // Split displayed text for animation\n const characters = splitText(displayedText);\n\n return createElement(\n Component,\n {\n className: cxStyles(styles.container, className),\n ref: containerRef,\n ...props,\n },\n <>\n <span className={styles.text} style={textColor ? { color: textColor } : undefined}>\n {characters.map((char, index) => (\n <Motion.span\n animate={{ opacity: 1 }}\n initial={{ opacity: 0 }}\n key={`${currentTextIndex}-${index}`}\n style={{ display: 'inline-block' }}\n transition={{\n duration: typingSpeed / 500,\n ease: 'easeInOut',\n }}\n >\n {char === ' ' ? '\\u00A0' : char}\n </Motion.span>\n ))}\n </span>\n {showCursor &&\n (cursorFade ? (\n <Motion.span\n animate={{ opacity: shouldHideCursor ? 0 : 1 }}\n className={cxStyles(getCursorStyle(), cursorClassName)}\n initial={{ opacity: 0 }}\n style={finalCursorColor ? { backgroundColor: finalCursorColor } : undefined}\n transition={{\n duration: shouldHideCursor ? 0.2 : cursorBlinkDuration,\n ease: 'easeInOut',\n repeat: shouldHideCursor ? 0 : Number.POSITIVE_INFINITY,\n repeatType: 'reverse',\n }}\n >\n {cursorCharacter}\n </Motion.span>\n ) : (\n <span\n className={cxStyles(getCursorStyle(), cursorClassName)}\n style={{\n backgroundColor: finalCursorColor,\n opacity: shouldHideCursor ? 0 : 1,\n }}\n >\n {cursorCharacter}\n </span>\n ))}\n </>,\n );\n },\n);\n\nTypewriterEffect.displayName = 'TypewriterEffect';\n\nexport default TypewriterEffect;\n"],"mappings":"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"}