UNPKG

vevet

Version:

Vevet is a JavaScript library for creative development that simplifies crafting rich interactions like split text animations, carousels, marquees, preloading, and more.

265 lines (219 loc) 6.15 kB
import { onResize } from '@/utils/listeners/onResize'; import { splitBase } from './utils/splitBase'; import { wrapLines } from './utils/wrapLines'; import { ISplitTextCallbacksMap, ISplitTextLetterMeta, ISplitTextLineMeta, ISplitTextMutableProps, ISplitTextStaticProps, ISplitTextWordMeta, } from './types'; import { Module } from '@/base'; import { TRequiredProps } from '@/internal/requiredProps'; import { initVevet } from '@/global/initVevet'; import { saveInitialNodes } from './utils/saveInitialNodes'; export * from './types'; /** * `SplitText` splits text within a container into individual lines, words, and letters. * * Features: * - Supports resizing, HTML content, and special symbols like emojis. * - Handles multi-line breaks and non-breaking spaces. * - Saves initial nodes and listeners for easy restoration. * - Allows splitting into lines, words, or letters as needed. * * **Note**: Apply `fontKerning: none` to prevent layout shifts. * * [Documentation](https://antonbobrov.github.io/vevet/docs/components/SplitText) * * @group Components */ export class SplitText< CallbacksMap extends ISplitTextCallbacksMap = ISplitTextCallbacksMap, StaticProps extends ISplitTextStaticProps = ISplitTextStaticProps, MutableProps extends ISplitTextMutableProps = ISplitTextMutableProps, > extends Module<CallbacksMap, StaticProps, MutableProps> { /** * Retrieves the default static properties. */ public _getStatic(): TRequiredProps<StaticProps> { return { ...super._getStatic(), letters: false, lines: false, linesWrapper: false, letterTag: 'span', wordTag: 'span', lineTag: 'span', resizeDebounce: 0, } as TRequiredProps<StaticProps>; } /** * Retrieves the default mutable properties. */ public _getMutable(): TRequiredProps<MutableProps> { return { ...super._getMutable() } as TRequiredProps<MutableProps>; } /** * Classname prefix for styling elements. */ get prefix() { return `${initVevet().prefix}split-text`; } /** * Saved initial HTML nodes of the container. */ protected _savedNodes: ReturnType<typeof saveInitialNodes>; /** * Tracks whether the text is already split into base elements: words and letters. */ protected _isBaseSplit = false; /** * List of letters metadata. */ protected _lettersMeta: ISplitTextLetterMeta[] = []; /** * Retrieves an array of letters metadata. */ get lettersMeta() { return this._lettersMeta; } /** * Retrieves an array of letter elements. */ get letters() { return this._lettersMeta.map((letter) => letter.element); } /** * List of words metadata. */ protected _wordsMeta: ISplitTextWordMeta[] = []; /** * Retrieves an array of words metadata. */ get wordsMeta() { return this._wordsMeta; } /** * Retrieves an array of word elements. */ get words() { return this._wordsMeta.map((word) => word.element); } /** * List of lines metadata. */ protected _linesMeta: ISplitTextLineMeta[] = []; /** * Retrieves an array of lines metadata. */ get linesMeta() { return this._linesMeta; } /** * Retrieves an array of line elements. */ get lines() { return this._linesMeta.map((line) => line.element); } /** * Utility for wrapping words into line containers. */ protected _lineSplitWrapper?: ReturnType<typeof wrapLines>; /** * Initializes the SplitText instance and saves the initial state. */ constructor(props?: StaticProps & MutableProps) { super(props); const { container } = this.props; container.style.fontKerning = 'none'; this._addTempClassName(container, this._cn('')); container.translate = false; this._savedNodes = saveInitialNodes(container); this._setup(); } /** * Sets up event listeners and handles initial splitting. */ protected _setup() { const { container, resizeDebounce } = this.props; if (!this.props.lines) { this.split(); return; } const resizeHandler = onResize({ callback: () => this.split(), element: container, viewportTarget: 'width', resizeDebounce, name: this.name, }); resizeHandler.resize(); this.onDestroy(() => resizeHandler.remove()); } /** * Splits the text into letters, words, and optionally lines based on configuration. */ public split() { this.callbacks.emit('beforeSplit', undefined); this._splitBase(); if (this.props.lines) { this._splitLines(); } this.callbacks.emit('split', undefined); } /** * Splits text into base elements: letters and words. */ protected _splitBase() { if (this._isBaseSplit) { return; } const { container, letterTag, wordTag } = this.props; this._isBaseSplit = true; const { wordsMeta, lettersMeta } = splitBase({ container, letterClassName: this._cn('__letter'), wordClassName: this._cn('__word'), hasLetters: this.props.letters, letterTag, wordTag, }); this._wordsMeta = wordsMeta; this._lettersMeta = lettersMeta; } /** * Wraps words into line containers. */ protected _splitLines() { const { wordsMeta } = this; const { container, lineTag } = this.props; const isHidden = container.offsetParent === null; if (isHidden) { return; } this._lineSplitWrapper?.destroy(); this._lineSplitWrapper = wrapLines({ container, hasLinesWrapper: this.props.linesWrapper, wordsMeta, lineClassName: this._cn('__line'), lineWrapperClassName: this._cn('__line-wrapper'), tagName: lineTag, }); this._linesMeta = this._lineSplitWrapper.linesMeta; } /** * Destroys the component. * This method does not restore the initial nodes. For this purpose, use `restore()`. */ protected _destroy() { super._destroy(); const isSuccess = this._lineSplitWrapper?.destroy(); this._lineSplitWrapper = undefined; if (isSuccess) { this._savedNodes.restore(); } } }