text-slicer 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -40
- package/dist/index.cjs +200 -82
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -4
- package/dist/index.d.ts +17 -4
- package/dist/index.js +200 -82
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["export type SplitMode = 'words' | 'chars' | 'both';\n\nexport interface TextSlicerOptions {\n container?: HTMLElement | string;\n splitMode?: SplitMode;\n cssVariables?: boolean;\n dataAttributes?: boolean;\n keepWhitespaceNodes?: boolean;\n containerHeightVar?: boolean;\n}\n\nexport interface TextSlicerMetrics {\n wordTotal: number;\n charTotal: number;\n renderedAt: number;\n}\n\nexport interface TextSlicerCallbacks {\n onAfterRender?: (metrics: TextSlicerMetrics) => void;\n}\n\ntype RuntimeOptions = Required<Omit<TextSlicerOptions, 'container'>>;\n\nconst DEFAULT_OPTIONS: RuntimeOptions = {\n splitMode: 'both',\n cssVariables: false,\n dataAttributes: false,\n keepWhitespaceNodes: true,\n containerHeightVar: false,\n};\n\nexport const CLASSNAMES = Object.freeze({\n word: 'ts-word',\n char: 'ts-char',\n whitespace: 'ts-whitespace',\n});\n\nconst SPACE = ' ';\nconst CSS_VAR_WORD_TOTAL = '--word-total';\nconst CSS_VAR_CHAR_TOTAL = '--char-total';\nconst CSS_VAR_WORD_INDEX = '--word-index';\nconst CSS_VAR_CHAR_INDEX = '--char-index';\nconst CSS_VAR_CONTAINER_HEIGHT = '--container-height';\nconst MEASURING_CLASS = 'ts-measuring';\n\nconst canUseDOM = (): boolean => typeof window !== 'undefined' && typeof document !== 'undefined';\n\nconst resolveContainer = (container?: HTMLElement | string): HTMLElement | null => {\n if (!canUseDOM()) return null;\n if (!container) return null;\n\n return typeof container === 'string' ? document.querySelector(container) : container;\n};\n\ntype IntlWithSegmenter = typeof Intl & {\n Segmenter?: new (locales?: string | string[], options?: Intl.SegmenterOptions) => Intl.Segmenter;\n};\n\nconst splitIntoGraphemes = (text: string): string[] => {\n const Seg = (Intl as IntlWithSegmenter).Segmenter;\n\n if (typeof Seg === 'function') {\n const segmenter = new Seg('en', { granularity: 'grapheme' });\n return Array.from(segmenter.segment(text), (s: Intl.SegmentData) => s.segment);\n }\n\n return Array.from(text);\n};\n\nconst splitIntoWords = (text: string): string[] => text.split(SPACE);\n\nconst emptyElement = (el: HTMLElement): void => {\n el.replaceChildren();\n};\n\nconst isHTMLElement = (el: unknown): el is HTMLElement =>\n !!el && typeof HTMLElement !== 'undefined' && el instanceof HTMLElement;\n\ntype NonUndefined<T> = T extends undefined ? never : T;\ntype OmitUndefined<T extends Record<string, unknown>> = {\n [K in keyof T as T[K] extends undefined ? never : K]: NonUndefined<T[K]>;\n};\n\nconst omitUndefined = <T extends Record<string, unknown>>(obj: T): OmitUndefined<T> => {\n const out = {} as OmitUndefined<T>;\n\n (Object.keys(obj) as Array<keyof T>).forEach((key) => {\n const val = obj[key];\n\n if (val !== undefined) {\n (out as Record<string, unknown>)[key as string] = val as unknown;\n }\n });\n\n return out;\n};\n\nexport class TextSlicer {\n private readonly el: HTMLElement | null;\n private original: string;\n private opts: RuntimeOptions;\n private callbacks: TextSlicerCallbacks | undefined;\n private charIndex: number;\n private mounted: boolean;\n private resizeObserver?: ResizeObserver;\n\n constructor(options: TextSlicerOptions = {}, callbacks?: TextSlicerCallbacks) {\n const el = resolveContainer(options.container);\n this.el = el;\n this.original = isHTMLElement(el) ? (el.textContent?.toString() ?? '') : '';\n this.opts = {\n ...DEFAULT_OPTIONS,\n ...omitUndefined<Omit<TextSlicerOptions, 'container'>>(options),\n } as RuntimeOptions;\n\n this.callbacks = callbacks;\n this.charIndex = 0;\n this.mounted = false;\n }\n\n get metrics(): TextSlicerMetrics {\n const text = this.original;\n\n return {\n wordTotal: text.length ? splitIntoWords(text).length : 0,\n charTotal: text.length,\n renderedAt: Date.now(),\n };\n }\n\n init(): void {\n if (!this.el) return;\n\n this.mounted = true;\n this.split();\n\n if (this.opts.containerHeightVar) {\n this.initHeightObserver();\n }\n }\n\n reinit(newText?: string, next?: Partial<TextSlicerOptions>): void {\n if (!this.el) return;\n if (typeof newText === 'string') this.original = newText;\n if (next) this.opts = { ...this.opts, ...omitUndefined(next) } as RuntimeOptions;\n\n this.split();\n }\n\n clear(): void {\n if (!this.el) return;\n\n emptyElement(this.el);\n }\n\n split(): void {\n if (!this.el) return;\n\n this.clear();\n this.charIndex = 0;\n\n const text = this.original;\n const fragment = document.createDocumentFragment();\n const words = splitIntoWords(text);\n\n if (this.opts.splitMode === 'chars') {\n this.appendChars(fragment, text);\n } else {\n this.appendWords(fragment, words);\n }\n\n this.el.appendChild(fragment);\n\n if (this.opts.cssVariables) {\n this.el.style.setProperty(CSS_VAR_WORD_TOTAL, String(words.length));\n this.el.style.setProperty(CSS_VAR_CHAR_TOTAL, String(text.length));\n }\n\n this.callbacks?.onAfterRender?.(this.metrics);\n }\n\n destroy(): void {\n if (!this.el) return;\n\n this.clear();\n this.unlockHeight();\n\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = undefined;\n }\n\n if (this.opts.containerHeightVar) {\n this.el.style.removeProperty(CSS_VAR_CONTAINER_HEIGHT);\n }\n\n this.mounted = false;\n }\n\n updateOptions(next: Partial<TextSlicerOptions>): void {\n this.opts = { ...this.opts, ...omitUndefined(next) } as RuntimeOptions;\n\n if (this.mounted) this.split();\n }\n\n lockHeight(): void {\n if (!this.el) return;\n\n const h = this.measureHeight();\n\n if (h > 0) {\n this.el.style.height = `${h}px`;\n }\n }\n\n unlockHeight(): void {\n if (!this.el) return;\n\n this.el.style.removeProperty('height');\n }\n\n private appendWords(fragment: DocumentFragment, words: string[]): void {\n words.forEach((word, wordIndex) => {\n if (this.opts.splitMode === 'both') {\n const wordSpan = this.createWordSpan(wordIndex, word);\n\n for (const ch of splitIntoGraphemes(word)) {\n const charSpan = this.createCharSpan(ch);\n\n wordSpan.append(charSpan);\n }\n\n fragment.append(wordSpan);\n } else {\n const wordSpan = this.createWordSpan(wordIndex);\n\n wordSpan.append(document.createTextNode(word));\n fragment.append(wordSpan);\n }\n\n if (wordIndex < words.length - 1) {\n fragment.append(this.createSpaceSpan());\n }\n });\n }\n\n private appendChars(fragment: DocumentFragment, text: string): void {\n for (const ch of splitIntoGraphemes(text)) {\n const span = this.createCharSpan(ch);\n\n fragment.append(span);\n }\n }\n\n private createWordSpan(index: number, word: string = ''): HTMLSpanElement {\n const span = document.createElement('span');\n\n span.classList.add(CLASSNAMES.word);\n\n if (this.opts.dataAttributes && word) {\n span.setAttribute('data-word', word);\n }\n\n if (this.opts.cssVariables) {\n span.style.setProperty(CSS_VAR_WORD_INDEX, String(index));\n }\n\n return span;\n }\n\n private createCharSpan(ch: string): HTMLSpanElement {\n const span = document.createElement('span');\n\n span.textContent = ch;\n\n if (this.opts.dataAttributes) span.setAttribute('data-char', ch);\n\n if (ch === SPACE) {\n span.classList.add(CLASSNAMES.whitespace);\n\n if (!this.opts.keepWhitespaceNodes) span.textContent = SPACE;\n } else {\n span.classList.add(CLASSNAMES.char);\n\n if (this.opts.cssVariables) {\n span.style.setProperty(CSS_VAR_CHAR_INDEX, String(this.charIndex));\n }\n\n this.charIndex += 1;\n }\n\n return span;\n }\n\n private createSpaceSpan(): HTMLSpanElement {\n const span = document.createElement('span');\n\n span.classList.add(CLASSNAMES.whitespace);\n span.textContent = SPACE;\n\n return span;\n }\n\n private measureHeight(): number {\n if (!this.el) return 0;\n\n this.el.classList.add(MEASURING_CLASS);\n void this.el.offsetHeight;\n\n let h = this.el.offsetHeight || this.el.clientHeight || 0;\n\n if (!h) {\n h = Math.round(this.el.getBoundingClientRect().height);\n }\n\n this.el.classList.remove(MEASURING_CLASS);\n\n return Math.max(0, Math.ceil(h));\n }\n\n private initHeightObserver(): void {\n if (!this.el) return;\n\n this.resizeObserver = new ResizeObserver(() => {\n const h = this.measureHeight();\n\n if (h > 0) {\n this.el?.style.setProperty(CSS_VAR_CONTAINER_HEIGHT, `${h}px`);\n }\n });\n\n this.resizeObserver.observe(this.el);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["export type SplitMode = 'words' | 'chars' | 'both';\n\nexport interface TextSlicerOptions {\n container?: HTMLElement | string;\n splitMode?: SplitMode;\n cssVariables?: boolean;\n dataAttributes?: boolean;\n keepWhitespaceNodes?: boolean;\n containerHeightVar?: boolean;\n locale?: string | string[];\n}\n\nexport interface TextSlicerMetrics {\n wordTotal: number;\n charTotal: number;\n renderedAt: number;\n}\n\nexport interface TextSlicerCallbacks {\n onAfterRender?: (metrics: TextSlicerMetrics) => void;\n}\n\ntype RuntimeOptions = Required<Omit<TextSlicerOptions, 'container' | 'locale'>> &\n Pick<TextSlicerOptions, 'locale'>;\n\nconst DEFAULT_OPTIONS: RuntimeOptions = {\n splitMode: 'both',\n cssVariables: false,\n dataAttributes: false,\n keepWhitespaceNodes: true,\n containerHeightVar: false,\n locale: undefined,\n};\n\nexport const CLASSNAMES = Object.freeze({\n word: 'ts-word',\n char: 'ts-char',\n whitespace: 'ts-whitespace',\n});\n\nconst CSS_VAR_WORD_TOTAL = '--word-total';\nconst CSS_VAR_CHAR_TOTAL = '--char-total';\nconst CSS_VAR_WORD_INDEX = '--word-index';\nconst CSS_VAR_CHAR_INDEX = '--char-index';\nconst CSS_VAR_CONTAINER_HEIGHT = '--container-height';\nconst MEASURING_CLASS = 'ts-measuring';\n\nconst canUseDOM = (): boolean => typeof window !== 'undefined' && typeof document !== 'undefined';\n\nconst resolveContainer = (container?: HTMLElement | string): HTMLElement | null => {\n if (!canUseDOM()) return null;\n if (!container) return null;\n\n return typeof container === 'string' ? document.querySelector(container) : container;\n};\n\ntype IntlWithSegmenter = typeof Intl & {\n Segmenter?: new (locales?: string | string[], options?: Intl.SegmenterOptions) => Intl.Segmenter;\n};\n\nconst localeKey = (locale: string | string[] | undefined): string =>\n locale === undefined ? '' : Array.isArray(locale) ? locale.join('\\0') : locale;\n\nconst segmenterCache = new Map<string, Intl.Segmenter>();\n\nconst getSegmenter = (\n locale: string | string[] | undefined,\n granularity: 'grapheme' | 'word',\n): Intl.Segmenter | null => {\n if (typeof Intl === 'undefined') return null;\n\n const Seg = (Intl as IntlWithSegmenter).Segmenter;\n\n if (typeof Seg !== 'function') return null;\n\n const key = `${granularity}:${localeKey(locale)}`;\n let segmenter = segmenterCache.get(key);\n\n if (!segmenter) {\n segmenter = new Seg(locale, { granularity });\n segmenterCache.set(key, segmenter);\n }\n\n return segmenter;\n};\n\nconst splitIntoGraphemes = (text: string, locale?: string | string[]): string[] => {\n const segmenter = getSegmenter(locale, 'grapheme');\n\n if (segmenter) {\n return Array.from(segmenter.segment(text), (s: Intl.SegmentData) => s.segment);\n }\n\n // Fallback: Array.from does not preserve all ZWJ/emoji grapheme clusters.\n // Remove when the browser baseline requires Intl.Segmenter.\n return Array.from(text);\n};\n\nconst isWhitespaceGrapheme = (ch: string): boolean => /^\\s+$/u.test(ch);\n\nconst countCharGraphemes = (text: string, locale?: string | string[]): number =>\n splitIntoGraphemes(text, locale).filter((ch) => !isWhitespaceGrapheme(ch)).length;\n\ntype WordSegment = Intl.SegmentData;\n\nconst segmentWords = (text: string, locale?: string | string[]): WordSegment[] => {\n const segmenter = getSegmenter(locale, 'word');\n\n if (segmenter) {\n return Array.from(segmenter.segment(text));\n }\n\n // Fallback: whitespace splitting is not locale-aware and keeps punctuation attached to tokens.\n // Remove when the browser baseline requires Intl.Segmenter.\n const words = text.split(/\\s+/u).filter(Boolean);\n const segments: WordSegment[] = [];\n\n let cursor = 0;\n\n for (const word of words) {\n const start = text.indexOf(word, cursor);\n\n if (start > cursor) {\n segments.push({\n segment: text.slice(cursor, start),\n index: cursor,\n input: text,\n isWordLike: false,\n });\n }\n\n segments.push({ segment: word, index: start, input: text, isWordLike: true });\n cursor = start + word.length;\n }\n\n if (cursor < text.length) {\n segments.push({ segment: text.slice(cursor), index: cursor, input: text, isWordLike: false });\n }\n\n return segments;\n};\n\nconst countWords = (text: string, locale?: string | string[]): number =>\n segmentWords(text, locale).filter((s) => s.isWordLike).length;\n\nconst emptyElement = (el: HTMLElement): void => {\n el.replaceChildren();\n};\n\nconst isHTMLElement = (el: unknown): el is HTMLElement =>\n !!el && typeof HTMLElement !== 'undefined' && el instanceof HTMLElement;\n\ntype NonUndefined<T> = T extends undefined ? never : T;\ntype OmitUndefined<T extends Record<string, unknown>> = {\n [K in keyof T as T[K] extends undefined ? never : K]: NonUndefined<T[K]>;\n};\n\nconst omitUndefined = <T extends Record<string, unknown>>(obj: T): OmitUndefined<T> => {\n const out = {} as OmitUndefined<T>;\n\n (Object.keys(obj) as Array<keyof T>).forEach((key) => {\n const val = obj[key];\n\n if (val !== undefined) {\n (out as Record<string, unknown>)[key as string] = val as unknown;\n }\n });\n\n return out;\n};\n\ntype RuntimeOptionPatch = Partial<Omit<TextSlicerOptions, 'container'>>;\n\nconst mergeRuntimeOptions = (base: RuntimeOptions, patch: RuntimeOptionPatch): RuntimeOptions => {\n const { container: _container, ...runtimeOptions } = patch as TextSlicerOptions;\n\n return {\n ...base,\n ...omitUndefined(runtimeOptions as Record<string, unknown>),\n };\n};\n\ntype ManagedStyle = { value: string; priority: string };\n\nexport class TextSlicer {\n private readonly el: HTMLElement | null;\n private original: string;\n private opts: RuntimeOptions;\n private callbacks: TextSlicerCallbacks | undefined;\n private charIndex: number;\n private mounted: boolean;\n private resizeObserver?: ResizeObserver;\n private managedStyles = new Map<string, ManagedStyle>();\n private managedAttributes = new Map<string, string | null>();\n\n constructor(options: TextSlicerOptions = {}, callbacks?: TextSlicerCallbacks) {\n const { container, ...runtimeOptions } = options;\n const el = resolveContainer(container);\n this.el = el;\n // Plain-text mode intentionally discards nested markup. Upgrade path:\n // walk text nodes and preserve element boundaries in a DOM-preserving mode.\n this.original = isHTMLElement(el) ? (el.textContent?.toString() ?? '') : '';\n this.opts = mergeRuntimeOptions(DEFAULT_OPTIONS, runtimeOptions);\n\n this.callbacks = callbacks;\n this.charIndex = 0;\n this.mounted = false;\n }\n\n get metrics(): TextSlicerMetrics {\n const text = this.original;\n const { locale } = this.opts;\n\n return {\n wordTotal: text.length ? countWords(text, locale) : 0,\n charTotal: text.length ? countCharGraphemes(text, locale) : 0,\n renderedAt: Date.now(),\n };\n }\n\n init(): void {\n if (!this.el) return;\n\n this.mounted = true;\n this.split();\n this.syncHeightObserver();\n }\n\n reinit(newText?: string, next?: RuntimeOptionPatch): void {\n if (!this.el) return;\n if (typeof newText === 'string') this.original = newText;\n if (next) this.opts = mergeRuntimeOptions(this.opts, next);\n\n this.mounted = true;\n this.split();\n this.syncHeightObserver();\n }\n\n clear(): void {\n if (!this.el) return;\n\n emptyElement(this.el);\n this.restoreManagedAttribute('aria-label');\n }\n\n split(): void {\n if (!this.el) return;\n\n emptyElement(this.el);\n this.charIndex = 0;\n\n const text = this.original;\n const fragment = document.createDocumentFragment();\n\n if (this.opts.splitMode === 'chars') {\n this.appendChars(fragment, text);\n } else {\n this.appendWords(fragment, text);\n }\n\n this.el.appendChild(fragment);\n this.syncAccessibleLabel(text);\n\n const metrics = this.metrics;\n\n if (this.opts.cssVariables) {\n this.setManagedStyle(CSS_VAR_WORD_TOTAL, String(metrics.wordTotal));\n this.setManagedStyle(CSS_VAR_CHAR_TOTAL, String(metrics.charTotal));\n } else {\n this.restoreManagedStyle(CSS_VAR_WORD_TOTAL);\n this.restoreManagedStyle(CSS_VAR_CHAR_TOTAL);\n }\n\n this.callbacks?.onAfterRender?.(metrics);\n }\n\n destroy(): void {\n if (!this.el) return;\n\n this.syncHeightObserver(false);\n this.restoreManagedStyles();\n this.restoreManagedAttribute('aria-label');\n this.el.textContent = this.original;\n this.mounted = false;\n }\n\n updateOptions(next: RuntimeOptionPatch): void {\n this.opts = mergeRuntimeOptions(this.opts, next);\n\n if (this.mounted) {\n this.split();\n this.syncHeightObserver();\n }\n }\n\n lockHeight(): void {\n if (!this.el) return;\n\n const h = this.measureHeight();\n\n if (h > 0) {\n this.setManagedStyle('height', `${h}px`);\n }\n }\n\n unlockHeight(): void {\n this.restoreManagedStyle('height');\n }\n\n private appendWords(fragment: DocumentFragment, text: string): void {\n const segments = segmentWords(text, this.opts.locale);\n let wordIndex = 0;\n\n for (const { segment, isWordLike } of segments) {\n if (isWordLike) {\n const wordSpan = this.createWordSpan(wordIndex, segment);\n\n if (this.opts.splitMode === 'both') {\n this.appendGraphemes(wordSpan, segment);\n } else {\n wordSpan.append(document.createTextNode(segment));\n }\n\n fragment.append(wordSpan);\n wordIndex += 1;\n } else if (isWhitespaceGrapheme(segment)) {\n if (this.opts.splitMode === 'both') {\n this.appendGraphemes(fragment, segment);\n } else {\n fragment.append(this.createWhitespaceSpan(segment));\n }\n } else if (segment.length > 0) {\n if (this.opts.splitMode === 'both') {\n this.appendGraphemes(fragment, segment);\n } else {\n fragment.append(this.createHiddenTextSpan(segment));\n }\n }\n }\n }\n\n private appendChars(fragment: DocumentFragment, text: string): void {\n for (const ch of splitIntoGraphemes(text, this.opts.locale)) {\n this.appendGrapheme(fragment, ch);\n }\n }\n\n private appendGraphemes(parent: DocumentFragment | HTMLElement, text: string): void {\n for (const ch of splitIntoGraphemes(text, this.opts.locale)) {\n this.appendGrapheme(parent, ch);\n }\n }\n\n private appendGrapheme(parent: DocumentFragment | HTMLElement, ch: string): void {\n if (isWhitespaceGrapheme(ch)) {\n if (this.opts.keepWhitespaceNodes) {\n parent.append(this.createWhitespaceSpan(ch));\n } else {\n parent.append(document.createTextNode(ch));\n }\n\n return;\n }\n\n parent.append(this.createCharSpan(ch));\n }\n\n private createWordSpan(index: number, word: string): HTMLSpanElement {\n const span = document.createElement('span');\n\n span.classList.add(CLASSNAMES.word);\n span.setAttribute('aria-hidden', 'true');\n\n if (this.opts.dataAttributes) {\n span.setAttribute('data-word', word);\n }\n\n if (this.opts.cssVariables) {\n span.style.setProperty(CSS_VAR_WORD_INDEX, String(index));\n }\n\n return span;\n }\n\n private createCharSpan(ch: string): HTMLSpanElement {\n const span = document.createElement('span');\n\n span.textContent = ch;\n span.setAttribute('aria-hidden', 'true');\n span.classList.add(CLASSNAMES.char);\n\n if (this.opts.dataAttributes) span.setAttribute('data-char', ch);\n\n if (this.opts.cssVariables) {\n span.style.setProperty(CSS_VAR_CHAR_INDEX, String(this.charIndex));\n }\n\n this.charIndex += 1;\n\n return span;\n }\n\n private createWhitespaceSpan(text: string): HTMLSpanElement {\n const span = document.createElement('span');\n\n span.classList.add(CLASSNAMES.whitespace);\n span.textContent = text;\n span.setAttribute('aria-hidden', 'true');\n\n return span;\n }\n\n private createHiddenTextSpan(text: string): HTMLSpanElement {\n const span = document.createElement('span');\n\n span.textContent = text;\n span.setAttribute('aria-hidden', 'true');\n\n return span;\n }\n\n private syncAccessibleLabel(text: string): void {\n if (!this.el) return;\n\n if (this.managedAttributes.has('aria-label')) {\n this.setManagedAttribute('aria-label', text);\n return;\n }\n\n if (this.el.hasAttribute('aria-label') || this.el.hasAttribute('aria-labelledby')) {\n return;\n }\n\n this.setManagedAttribute('aria-label', text);\n }\n\n private measureHeight(): number {\n if (!this.el) return 0;\n\n this.el.classList.add(MEASURING_CLASS);\n\n try {\n void this.el.offsetHeight;\n\n let h = this.el.offsetHeight || this.el.clientHeight || 0;\n\n if (!h) {\n h = Math.round(this.el.getBoundingClientRect().height);\n }\n\n return Math.max(0, Math.ceil(h));\n } finally {\n this.el.classList.remove(MEASURING_CLASS);\n }\n }\n\n private syncHeightObserver(enable: boolean = this.opts.containerHeightVar): void {\n if (!this.el) return;\n\n if (enable) {\n if (typeof ResizeObserver !== 'function') {\n this.restoreManagedStyle(CSS_VAR_CONTAINER_HEIGHT);\n return;\n }\n\n if (!this.resizeObserver) {\n this.resizeObserver = new ResizeObserver((entries) => {\n const entry = entries[0];\n\n if (!entry || !this.el) return;\n\n const boxSize = entry.borderBoxSize?.[0]?.blockSize;\n const h = Math.max(0, Math.ceil(boxSize ?? entry.contentRect.height));\n\n if (h > 0) {\n this.setManagedStyle(CSS_VAR_CONTAINER_HEIGHT, `${h}px`);\n }\n });\n\n this.resizeObserver.observe(this.el);\n }\n } else if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = undefined;\n this.restoreManagedStyle(CSS_VAR_CONTAINER_HEIGHT);\n }\n }\n\n private setManagedStyle(property: string, value: string): void {\n if (!this.el) return;\n\n if (!this.managedStyles.has(property)) {\n this.managedStyles.set(property, {\n value: this.el.style.getPropertyValue(property),\n priority: this.el.style.getPropertyPriority(property),\n });\n }\n\n this.el.style.setProperty(property, value);\n }\n\n private restoreManagedStyle(property: string): void {\n if (!this.el) return;\n\n const previous = this.managedStyles.get(property);\n\n if (previous === undefined) return;\n\n if (previous.value) {\n this.el.style.setProperty(property, previous.value, previous.priority);\n } else {\n this.el.style.removeProperty(property);\n }\n\n this.managedStyles.delete(property);\n }\n\n private restoreManagedStyles(): void {\n for (const property of [...this.managedStyles.keys()]) {\n this.restoreManagedStyle(property);\n }\n }\n\n private setManagedAttribute(name: string, value: string): void {\n if (!this.el) return;\n\n if (!this.managedAttributes.has(name)) {\n this.managedAttributes.set(name, this.el.getAttribute(name));\n }\n\n this.el.setAttribute(name, value);\n }\n\n private restoreManagedAttribute(name: string): void {\n if (!this.el || !this.managedAttributes.has(name)) return;\n\n const previous = this.managedAttributes.get(name);\n\n if (previous === null) {\n this.el.removeAttribute(name);\n } else if (previous !== undefined) {\n this.el.setAttribute(name, previous);\n }\n\n this.managedAttributes.delete(name);\n }\n}\n"],"mappings":";;;AAyBA,MAAM,kBAAkC;CACtC,WAAW;CACX,cAAc;CACd,gBAAgB;CAChB,qBAAqB;CACrB,oBAAoB;CACpB,QAAQ;AACV;AAEA,MAAa,aAAa,OAAO,OAAO;CACtC,MAAM;CACN,MAAM;CACN,YAAY;AACd,CAAC;AAED,MAAM,qBAAqB;AAC3B,MAAM,qBAAqB;AAC3B,MAAM,qBAAqB;AAC3B,MAAM,qBAAqB;AAC3B,MAAM,2BAA2B;AACjC,MAAM,kBAAkB;AAExB,MAAM,kBAA2B,OAAO,WAAW,eAAe,OAAO,aAAa;AAEtF,MAAM,oBAAoB,cAAyD;CACjF,IAAI,CAAC,UAAU,GAAG,OAAO;CACzB,IAAI,CAAC,WAAW,OAAO;CAEvB,OAAO,OAAO,cAAc,WAAW,SAAS,cAAc,SAAS,IAAI;AAC7E;AAMA,MAAM,aAAa,WACjB,WAAW,SAAY,KAAK,MAAM,QAAQ,MAAM,IAAI,OAAO,KAAK,IAAI,IAAI;AAE1E,MAAM,iCAAiB,IAAI,IAA4B;AAEvD,MAAM,gBACJ,QACA,gBAC0B;CAC1B,IAAI,OAAO,SAAS,aAAa,OAAO;CAExC,MAAM,MAAO,KAA2B;CAExC,IAAI,OAAO,QAAQ,YAAY,OAAO;CAEtC,MAAM,MAAM,GAAG,YAAY,GAAG,UAAU,MAAM;CAC9C,IAAI,YAAY,eAAe,IAAI,GAAG;CAEtC,IAAI,CAAC,WAAW;EACd,YAAY,IAAI,IAAI,QAAQ,EAAE,YAAY,CAAC;EAC3C,eAAe,IAAI,KAAK,SAAS;CACnC;CAEA,OAAO;AACT;AAEA,MAAM,sBAAsB,MAAc,WAAyC;CACjF,MAAM,YAAY,aAAa,QAAQ,UAAU;CAEjD,IAAI,WACF,OAAO,MAAM,KAAK,UAAU,QAAQ,IAAI,IAAI,MAAwB,EAAE,OAAO;CAK/E,OAAO,MAAM,KAAK,IAAI;AACxB;AAEA,MAAM,wBAAwB,OAAwB,SAAS,KAAK,EAAE;AAEtE,MAAM,sBAAsB,MAAc,WACxC,mBAAmB,MAAM,MAAM,CAAC,CAAC,QAAQ,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAAC,CAAC;AAI7E,MAAM,gBAAgB,MAAc,WAA8C;CAChF,MAAM,YAAY,aAAa,QAAQ,MAAM;CAE7C,IAAI,WACF,OAAO,MAAM,KAAK,UAAU,QAAQ,IAAI,CAAC;CAK3C,MAAM,QAAQ,KAAK,MAAM,MAAM,CAAC,CAAC,OAAO,OAAO;CAC/C,MAAM,WAA0B,CAAC;CAEjC,IAAI,SAAS;CAEb,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,QAAQ,KAAK,QAAQ,MAAM,MAAM;EAEvC,IAAI,QAAQ,QACV,SAAS,KAAK;GACZ,SAAS,KAAK,MAAM,QAAQ,KAAK;GACjC,OAAO;GACP,OAAO;GACP,YAAY;EACd,CAAC;EAGH,SAAS,KAAK;GAAE,SAAS;GAAM,OAAO;GAAO,OAAO;GAAM,YAAY;EAAK,CAAC;EAC5E,SAAS,QAAQ,KAAK;CACxB;CAEA,IAAI,SAAS,KAAK,QAChB,SAAS,KAAK;EAAE,SAAS,KAAK,MAAM,MAAM;EAAG,OAAO;EAAQ,OAAO;EAAM,YAAY;CAAM,CAAC;CAG9F,OAAO;AACT;AAEA,MAAM,cAAc,MAAc,WAChC,aAAa,MAAM,MAAM,CAAC,CAAC,QAAQ,MAAM,EAAE,UAAU,CAAC,CAAC;AAEzD,MAAM,gBAAgB,OAA0B;CAC9C,GAAG,gBAAgB;AACrB;AAEA,MAAM,iBAAiB,OACrB,CAAC,CAAC,MAAM,OAAO,gBAAgB,eAAe,cAAc;AAO9D,MAAM,iBAAoD,QAA6B;CACrF,MAAM,MAAM,CAAC;CAEb,AAAC,OAAO,KAAK,GAAG,CAAC,CAAoB,SAAS,QAAQ;EACpD,MAAM,MAAM,IAAI;EAEhB,IAAI,QAAQ,QACV,AAAC,IAAgC,OAAiB;CAEtD,CAAC;CAED,OAAO;AACT;AAIA,MAAM,uBAAuB,MAAsB,UAA8C;CAC/F,MAAM,EAAE,WAAW,YAAY,GAAG,mBAAmB;CAErD,OAAO;EACL,GAAG;EACH,GAAG,cAAc,cAAyC;CAC5D;AACF;AAIA,IAAa,aAAb,MAAwB;CACtB,AAAiB;CACjB,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ,gCAAgB,IAAI,IAA0B;CACtD,AAAQ,oCAAoB,IAAI,IAA2B;CAE3D,YAAY,UAA6B,CAAC,GAAG,WAAiC;EAC5E,MAAM,EAAE,WAAW,GAAG,mBAAmB;EACzC,MAAM,KAAK,iBAAiB,SAAS;EACrC,KAAK,KAAK;EAGV,KAAK,WAAW,cAAc,EAAE,IAAK,GAAG,aAAa,SAAS,KAAK,KAAM;EACzE,KAAK,OAAO,oBAAoB,iBAAiB,cAAc;EAE/D,KAAK,YAAY;EACjB,KAAK,YAAY;EACjB,KAAK,UAAU;CACjB;CAEA,IAAI,UAA6B;EAC/B,MAAM,OAAO,KAAK;EAClB,MAAM,EAAE,WAAW,KAAK;EAExB,OAAO;GACL,WAAW,KAAK,SAAS,WAAW,MAAM,MAAM,IAAI;GACpD,WAAW,KAAK,SAAS,mBAAmB,MAAM,MAAM,IAAI;GAC5D,YAAY,KAAK,IAAI;EACvB;CACF;CAEA,OAAa;EACX,IAAI,CAAC,KAAK,IAAI;EAEd,KAAK,UAAU;EACf,KAAK,MAAM;EACX,KAAK,mBAAmB;CAC1B;CAEA,OAAO,SAAkB,MAAiC;EACxD,IAAI,CAAC,KAAK,IAAI;EACd,IAAI,OAAO,YAAY,UAAU,KAAK,WAAW;EACjD,IAAI,MAAM,KAAK,OAAO,oBAAoB,KAAK,MAAM,IAAI;EAEzD,KAAK,UAAU;EACf,KAAK,MAAM;EACX,KAAK,mBAAmB;CAC1B;CAEA,QAAc;EACZ,IAAI,CAAC,KAAK,IAAI;EAEd,aAAa,KAAK,EAAE;EACpB,KAAK,wBAAwB,YAAY;CAC3C;CAEA,QAAc;EACZ,IAAI,CAAC,KAAK,IAAI;EAEd,aAAa,KAAK,EAAE;EACpB,KAAK,YAAY;EAEjB,MAAM,OAAO,KAAK;EAClB,MAAM,WAAW,SAAS,uBAAuB;EAEjD,IAAI,KAAK,KAAK,cAAc,SAC1B,KAAK,YAAY,UAAU,IAAI;OAE/B,KAAK,YAAY,UAAU,IAAI;EAGjC,KAAK,GAAG,YAAY,QAAQ;EAC5B,KAAK,oBAAoB,IAAI;EAE7B,MAAM,UAAU,KAAK;EAErB,IAAI,KAAK,KAAK,cAAc;GAC1B,KAAK,gBAAgB,oBAAoB,OAAO,QAAQ,SAAS,CAAC;GAClE,KAAK,gBAAgB,oBAAoB,OAAO,QAAQ,SAAS,CAAC;EACpE,OAAO;GACL,KAAK,oBAAoB,kBAAkB;GAC3C,KAAK,oBAAoB,kBAAkB;EAC7C;EAEA,KAAK,WAAW,gBAAgB,OAAO;CACzC;CAEA,UAAgB;EACd,IAAI,CAAC,KAAK,IAAI;EAEd,KAAK,mBAAmB,KAAK;EAC7B,KAAK,qBAAqB;EAC1B,KAAK,wBAAwB,YAAY;EACzC,KAAK,GAAG,cAAc,KAAK;EAC3B,KAAK,UAAU;CACjB;CAEA,cAAc,MAAgC;EAC5C,KAAK,OAAO,oBAAoB,KAAK,MAAM,IAAI;EAE/C,IAAI,KAAK,SAAS;GAChB,KAAK,MAAM;GACX,KAAK,mBAAmB;EAC1B;CACF;CAEA,aAAmB;EACjB,IAAI,CAAC,KAAK,IAAI;EAEd,MAAM,IAAI,KAAK,cAAc;EAE7B,IAAI,IAAI,GACN,KAAK,gBAAgB,UAAU,GAAG,EAAE,GAAG;CAE3C;CAEA,eAAqB;EACnB,KAAK,oBAAoB,QAAQ;CACnC;CAEA,AAAQ,YAAY,UAA4B,MAAoB;EAClE,MAAM,WAAW,aAAa,MAAM,KAAK,KAAK,MAAM;EACpD,IAAI,YAAY;EAEhB,KAAK,MAAM,EAAE,SAAS,gBAAgB,UACpC,IAAI,YAAY;GACd,MAAM,WAAW,KAAK,eAAe,WAAW,OAAO;GAEvD,IAAI,KAAK,KAAK,cAAc,QAC1B,KAAK,gBAAgB,UAAU,OAAO;QAEtC,SAAS,OAAO,SAAS,eAAe,OAAO,CAAC;GAGlD,SAAS,OAAO,QAAQ;GACxB,aAAa;EACf,OAAO,IAAI,qBAAqB,OAAO,GACrC,IAAI,KAAK,KAAK,cAAc,QAC1B,KAAK,gBAAgB,UAAU,OAAO;OAEtC,SAAS,OAAO,KAAK,qBAAqB,OAAO,CAAC;OAE/C,IAAI,QAAQ,SAAS,GAC1B,IAAI,KAAK,KAAK,cAAc,QAC1B,KAAK,gBAAgB,UAAU,OAAO;OAEtC,SAAS,OAAO,KAAK,qBAAqB,OAAO,CAAC;CAI1D;CAEA,AAAQ,YAAY,UAA4B,MAAoB;EAClE,KAAK,MAAM,MAAM,mBAAmB,MAAM,KAAK,KAAK,MAAM,GACxD,KAAK,eAAe,UAAU,EAAE;CAEpC;CAEA,AAAQ,gBAAgB,QAAwC,MAAoB;EAClF,KAAK,MAAM,MAAM,mBAAmB,MAAM,KAAK,KAAK,MAAM,GACxD,KAAK,eAAe,QAAQ,EAAE;CAElC;CAEA,AAAQ,eAAe,QAAwC,IAAkB;EAC/E,IAAI,qBAAqB,EAAE,GAAG;GAC5B,IAAI,KAAK,KAAK,qBACZ,OAAO,OAAO,KAAK,qBAAqB,EAAE,CAAC;QAE3C,OAAO,OAAO,SAAS,eAAe,EAAE,CAAC;GAG3C;EACF;EAEA,OAAO,OAAO,KAAK,eAAe,EAAE,CAAC;CACvC;CAEA,AAAQ,eAAe,OAAe,MAA+B;EACnE,MAAM,OAAO,SAAS,cAAc,MAAM;EAE1C,KAAK,UAAU,IAAI,WAAW,IAAI;EAClC,KAAK,aAAa,eAAe,MAAM;EAEvC,IAAI,KAAK,KAAK,gBACZ,KAAK,aAAa,aAAa,IAAI;EAGrC,IAAI,KAAK,KAAK,cACZ,KAAK,MAAM,YAAY,oBAAoB,OAAO,KAAK,CAAC;EAG1D,OAAO;CACT;CAEA,AAAQ,eAAe,IAA6B;EAClD,MAAM,OAAO,SAAS,cAAc,MAAM;EAE1C,KAAK,cAAc;EACnB,KAAK,aAAa,eAAe,MAAM;EACvC,KAAK,UAAU,IAAI,WAAW,IAAI;EAElC,IAAI,KAAK,KAAK,gBAAgB,KAAK,aAAa,aAAa,EAAE;EAE/D,IAAI,KAAK,KAAK,cACZ,KAAK,MAAM,YAAY,oBAAoB,OAAO,KAAK,SAAS,CAAC;EAGnE,KAAK,aAAa;EAElB,OAAO;CACT;CAEA,AAAQ,qBAAqB,MAA+B;EAC1D,MAAM,OAAO,SAAS,cAAc,MAAM;EAE1C,KAAK,UAAU,IAAI,WAAW,UAAU;EACxC,KAAK,cAAc;EACnB,KAAK,aAAa,eAAe,MAAM;EAEvC,OAAO;CACT;CAEA,AAAQ,qBAAqB,MAA+B;EAC1D,MAAM,OAAO,SAAS,cAAc,MAAM;EAE1C,KAAK,cAAc;EACnB,KAAK,aAAa,eAAe,MAAM;EAEvC,OAAO;CACT;CAEA,AAAQ,oBAAoB,MAAoB;EAC9C,IAAI,CAAC,KAAK,IAAI;EAEd,IAAI,KAAK,kBAAkB,IAAI,YAAY,GAAG;GAC5C,KAAK,oBAAoB,cAAc,IAAI;GAC3C;EACF;EAEA,IAAI,KAAK,GAAG,aAAa,YAAY,KAAK,KAAK,GAAG,aAAa,iBAAiB,GAC9E;EAGF,KAAK,oBAAoB,cAAc,IAAI;CAC7C;CAEA,AAAQ,gBAAwB;EAC9B,IAAI,CAAC,KAAK,IAAI,OAAO;EAErB,KAAK,GAAG,UAAU,IAAI,eAAe;EAErC,IAAI;GACF,AAAK,KAAK,GAAG;GAEb,IAAI,IAAI,KAAK,GAAG,gBAAgB,KAAK,GAAG,gBAAgB;GAExD,IAAI,CAAC,GACH,IAAI,KAAK,MAAM,KAAK,GAAG,sBAAsB,CAAC,CAAC,MAAM;GAGvD,OAAO,KAAK,IAAI,GAAG,KAAK,KAAK,CAAC,CAAC;EACjC,UAAU;GACR,KAAK,GAAG,UAAU,OAAO,eAAe;EAC1C;CACF;CAEA,AAAQ,mBAAmB,SAAkB,KAAK,KAAK,oBAA0B;EAC/E,IAAI,CAAC,KAAK,IAAI;EAEd,IAAI,QAAQ;GACV,IAAI,OAAO,mBAAmB,YAAY;IACxC,KAAK,oBAAoB,wBAAwB;IACjD;GACF;GAEA,IAAI,CAAC,KAAK,gBAAgB;IACxB,KAAK,iBAAiB,IAAI,gBAAgB,YAAY;KACpD,MAAM,QAAQ,QAAQ;KAEtB,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI;KAExB,MAAM,UAAU,MAAM,gBAAgB,EAAE,EAAE;KAC1C,MAAM,IAAI,KAAK,IAAI,GAAG,KAAK,KAAK,WAAW,MAAM,YAAY,MAAM,CAAC;KAEpE,IAAI,IAAI,GACN,KAAK,gBAAgB,0BAA0B,GAAG,EAAE,GAAG;IAE3D,CAAC;IAED,KAAK,eAAe,QAAQ,KAAK,EAAE;GACrC;EACF,OAAO,IAAI,KAAK,gBAAgB;GAC9B,KAAK,eAAe,WAAW;GAC/B,KAAK,iBAAiB;GACtB,KAAK,oBAAoB,wBAAwB;EACnD;CACF;CAEA,AAAQ,gBAAgB,UAAkB,OAAqB;EAC7D,IAAI,CAAC,KAAK,IAAI;EAEd,IAAI,CAAC,KAAK,cAAc,IAAI,QAAQ,GAClC,KAAK,cAAc,IAAI,UAAU;GAC/B,OAAO,KAAK,GAAG,MAAM,iBAAiB,QAAQ;GAC9C,UAAU,KAAK,GAAG,MAAM,oBAAoB,QAAQ;EACtD,CAAC;EAGH,KAAK,GAAG,MAAM,YAAY,UAAU,KAAK;CAC3C;CAEA,AAAQ,oBAAoB,UAAwB;EAClD,IAAI,CAAC,KAAK,IAAI;EAEd,MAAM,WAAW,KAAK,cAAc,IAAI,QAAQ;EAEhD,IAAI,aAAa,QAAW;EAE5B,IAAI,SAAS,OACX,KAAK,GAAG,MAAM,YAAY,UAAU,SAAS,OAAO,SAAS,QAAQ;OAErE,KAAK,GAAG,MAAM,eAAe,QAAQ;EAGvC,KAAK,cAAc,OAAO,QAAQ;CACpC;CAEA,AAAQ,uBAA6B;EACnC,KAAK,MAAM,YAAY,CAAC,GAAG,KAAK,cAAc,KAAK,CAAC,GAClD,KAAK,oBAAoB,QAAQ;CAErC;CAEA,AAAQ,oBAAoB,MAAc,OAAqB;EAC7D,IAAI,CAAC,KAAK,IAAI;EAEd,IAAI,CAAC,KAAK,kBAAkB,IAAI,IAAI,GAClC,KAAK,kBAAkB,IAAI,MAAM,KAAK,GAAG,aAAa,IAAI,CAAC;EAG7D,KAAK,GAAG,aAAa,MAAM,KAAK;CAClC;CAEA,AAAQ,wBAAwB,MAAoB;EAClD,IAAI,CAAC,KAAK,MAAM,CAAC,KAAK,kBAAkB,IAAI,IAAI,GAAG;EAEnD,MAAM,WAAW,KAAK,kBAAkB,IAAI,IAAI;EAEhD,IAAI,aAAa,MACf,KAAK,GAAG,gBAAgB,IAAI;OACvB,IAAI,aAAa,QACtB,KAAK,GAAG,aAAa,MAAM,QAAQ;EAGrC,KAAK,kBAAkB,OAAO,IAAI;CACpC;AACF"}
|
package/dist/index.d.cts
CHANGED
|
@@ -7,6 +7,7 @@ interface TextSlicerOptions {
|
|
|
7
7
|
dataAttributes?: boolean;
|
|
8
8
|
keepWhitespaceNodes?: boolean;
|
|
9
9
|
containerHeightVar?: boolean;
|
|
10
|
+
locale?: string | string[];
|
|
10
11
|
}
|
|
11
12
|
interface TextSlicerMetrics {
|
|
12
13
|
wordTotal: number;
|
|
@@ -21,6 +22,7 @@ declare const CLASSNAMES: Readonly<{
|
|
|
21
22
|
char: "ts-char";
|
|
22
23
|
whitespace: "ts-whitespace";
|
|
23
24
|
}>;
|
|
25
|
+
type RuntimeOptionPatch = Partial<Omit<TextSlicerOptions, 'container'>>;
|
|
24
26
|
declare class TextSlicer {
|
|
25
27
|
private readonly el;
|
|
26
28
|
private original;
|
|
@@ -29,23 +31,34 @@ declare class TextSlicer {
|
|
|
29
31
|
private charIndex;
|
|
30
32
|
private mounted;
|
|
31
33
|
private resizeObserver?;
|
|
34
|
+
private managedStyles;
|
|
35
|
+
private managedAttributes;
|
|
32
36
|
constructor(options?: TextSlicerOptions, callbacks?: TextSlicerCallbacks);
|
|
33
37
|
get metrics(): TextSlicerMetrics;
|
|
34
38
|
init(): void;
|
|
35
|
-
reinit(newText?: string, next?:
|
|
39
|
+
reinit(newText?: string, next?: RuntimeOptionPatch): void;
|
|
36
40
|
clear(): void;
|
|
37
41
|
split(): void;
|
|
38
42
|
destroy(): void;
|
|
39
|
-
updateOptions(next:
|
|
43
|
+
updateOptions(next: RuntimeOptionPatch): void;
|
|
40
44
|
lockHeight(): void;
|
|
41
45
|
unlockHeight(): void;
|
|
42
46
|
private appendWords;
|
|
43
47
|
private appendChars;
|
|
48
|
+
private appendGraphemes;
|
|
49
|
+
private appendGrapheme;
|
|
44
50
|
private createWordSpan;
|
|
45
51
|
private createCharSpan;
|
|
46
|
-
private
|
|
52
|
+
private createWhitespaceSpan;
|
|
53
|
+
private createHiddenTextSpan;
|
|
54
|
+
private syncAccessibleLabel;
|
|
47
55
|
private measureHeight;
|
|
48
|
-
private
|
|
56
|
+
private syncHeightObserver;
|
|
57
|
+
private setManagedStyle;
|
|
58
|
+
private restoreManagedStyle;
|
|
59
|
+
private restoreManagedStyles;
|
|
60
|
+
private setManagedAttribute;
|
|
61
|
+
private restoreManagedAttribute;
|
|
49
62
|
}
|
|
50
63
|
//#endregion
|
|
51
64
|
export { CLASSNAMES, SplitMode, TextSlicer, TextSlicerCallbacks, TextSlicerMetrics, TextSlicerOptions };
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ interface TextSlicerOptions {
|
|
|
7
7
|
dataAttributes?: boolean;
|
|
8
8
|
keepWhitespaceNodes?: boolean;
|
|
9
9
|
containerHeightVar?: boolean;
|
|
10
|
+
locale?: string | string[];
|
|
10
11
|
}
|
|
11
12
|
interface TextSlicerMetrics {
|
|
12
13
|
wordTotal: number;
|
|
@@ -21,6 +22,7 @@ declare const CLASSNAMES: Readonly<{
|
|
|
21
22
|
char: "ts-char";
|
|
22
23
|
whitespace: "ts-whitespace";
|
|
23
24
|
}>;
|
|
25
|
+
type RuntimeOptionPatch = Partial<Omit<TextSlicerOptions, 'container'>>;
|
|
24
26
|
declare class TextSlicer {
|
|
25
27
|
private readonly el;
|
|
26
28
|
private original;
|
|
@@ -29,23 +31,34 @@ declare class TextSlicer {
|
|
|
29
31
|
private charIndex;
|
|
30
32
|
private mounted;
|
|
31
33
|
private resizeObserver?;
|
|
34
|
+
private managedStyles;
|
|
35
|
+
private managedAttributes;
|
|
32
36
|
constructor(options?: TextSlicerOptions, callbacks?: TextSlicerCallbacks);
|
|
33
37
|
get metrics(): TextSlicerMetrics;
|
|
34
38
|
init(): void;
|
|
35
|
-
reinit(newText?: string, next?:
|
|
39
|
+
reinit(newText?: string, next?: RuntimeOptionPatch): void;
|
|
36
40
|
clear(): void;
|
|
37
41
|
split(): void;
|
|
38
42
|
destroy(): void;
|
|
39
|
-
updateOptions(next:
|
|
43
|
+
updateOptions(next: RuntimeOptionPatch): void;
|
|
40
44
|
lockHeight(): void;
|
|
41
45
|
unlockHeight(): void;
|
|
42
46
|
private appendWords;
|
|
43
47
|
private appendChars;
|
|
48
|
+
private appendGraphemes;
|
|
49
|
+
private appendGrapheme;
|
|
44
50
|
private createWordSpan;
|
|
45
51
|
private createCharSpan;
|
|
46
|
-
private
|
|
52
|
+
private createWhitespaceSpan;
|
|
53
|
+
private createHiddenTextSpan;
|
|
54
|
+
private syncAccessibleLabel;
|
|
47
55
|
private measureHeight;
|
|
48
|
-
private
|
|
56
|
+
private syncHeightObserver;
|
|
57
|
+
private setManagedStyle;
|
|
58
|
+
private restoreManagedStyle;
|
|
59
|
+
private restoreManagedStyles;
|
|
60
|
+
private setManagedAttribute;
|
|
61
|
+
private restoreManagedAttribute;
|
|
49
62
|
}
|
|
50
63
|
//#endregion
|
|
51
64
|
export { CLASSNAMES, SplitMode, TextSlicer, TextSlicerCallbacks, TextSlicerMetrics, TextSlicerOptions };
|
package/dist/index.js
CHANGED
|
@@ -4,14 +4,14 @@ const DEFAULT_OPTIONS = {
|
|
|
4
4
|
cssVariables: false,
|
|
5
5
|
dataAttributes: false,
|
|
6
6
|
keepWhitespaceNodes: true,
|
|
7
|
-
containerHeightVar: false
|
|
7
|
+
containerHeightVar: false,
|
|
8
|
+
locale: void 0
|
|
8
9
|
};
|
|
9
10
|
const CLASSNAMES = Object.freeze({
|
|
10
11
|
word: "ts-word",
|
|
11
12
|
char: "ts-char",
|
|
12
13
|
whitespace: "ts-whitespace"
|
|
13
14
|
});
|
|
14
|
-
const SPACE = " ";
|
|
15
15
|
const CSS_VAR_WORD_TOTAL = "--word-total";
|
|
16
16
|
const CSS_VAR_CHAR_TOTAL = "--char-total";
|
|
17
17
|
const CSS_VAR_WORD_INDEX = "--word-index";
|
|
@@ -24,15 +24,58 @@ const resolveContainer = (container) => {
|
|
|
24
24
|
if (!container) return null;
|
|
25
25
|
return typeof container === "string" ? document.querySelector(container) : container;
|
|
26
26
|
};
|
|
27
|
-
const
|
|
27
|
+
const localeKey = (locale) => locale === void 0 ? "" : Array.isArray(locale) ? locale.join("\0") : locale;
|
|
28
|
+
const segmenterCache = /* @__PURE__ */ new Map();
|
|
29
|
+
const getSegmenter = (locale, granularity) => {
|
|
30
|
+
if (typeof Intl === "undefined") return null;
|
|
28
31
|
const Seg = Intl.Segmenter;
|
|
29
|
-
if (typeof Seg
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
if (typeof Seg !== "function") return null;
|
|
33
|
+
const key = `${granularity}:${localeKey(locale)}`;
|
|
34
|
+
let segmenter = segmenterCache.get(key);
|
|
35
|
+
if (!segmenter) {
|
|
36
|
+
segmenter = new Seg(locale, { granularity });
|
|
37
|
+
segmenterCache.set(key, segmenter);
|
|
32
38
|
}
|
|
39
|
+
return segmenter;
|
|
40
|
+
};
|
|
41
|
+
const splitIntoGraphemes = (text, locale) => {
|
|
42
|
+
const segmenter = getSegmenter(locale, "grapheme");
|
|
43
|
+
if (segmenter) return Array.from(segmenter.segment(text), (s) => s.segment);
|
|
33
44
|
return Array.from(text);
|
|
34
45
|
};
|
|
35
|
-
const
|
|
46
|
+
const isWhitespaceGrapheme = (ch) => /^\s+$/u.test(ch);
|
|
47
|
+
const countCharGraphemes = (text, locale) => splitIntoGraphemes(text, locale).filter((ch) => !isWhitespaceGrapheme(ch)).length;
|
|
48
|
+
const segmentWords = (text, locale) => {
|
|
49
|
+
const segmenter = getSegmenter(locale, "word");
|
|
50
|
+
if (segmenter) return Array.from(segmenter.segment(text));
|
|
51
|
+
const words = text.split(/\s+/u).filter(Boolean);
|
|
52
|
+
const segments = [];
|
|
53
|
+
let cursor = 0;
|
|
54
|
+
for (const word of words) {
|
|
55
|
+
const start = text.indexOf(word, cursor);
|
|
56
|
+
if (start > cursor) segments.push({
|
|
57
|
+
segment: text.slice(cursor, start),
|
|
58
|
+
index: cursor,
|
|
59
|
+
input: text,
|
|
60
|
+
isWordLike: false
|
|
61
|
+
});
|
|
62
|
+
segments.push({
|
|
63
|
+
segment: word,
|
|
64
|
+
index: start,
|
|
65
|
+
input: text,
|
|
66
|
+
isWordLike: true
|
|
67
|
+
});
|
|
68
|
+
cursor = start + word.length;
|
|
69
|
+
}
|
|
70
|
+
if (cursor < text.length) segments.push({
|
|
71
|
+
segment: text.slice(cursor),
|
|
72
|
+
index: cursor,
|
|
73
|
+
input: text,
|
|
74
|
+
isWordLike: false
|
|
75
|
+
});
|
|
76
|
+
return segments;
|
|
77
|
+
};
|
|
78
|
+
const countWords = (text, locale) => segmentWords(text, locale).filter((s) => s.isWordLike).length;
|
|
36
79
|
const emptyElement = (el) => {
|
|
37
80
|
el.replaceChildren();
|
|
38
81
|
};
|
|
@@ -45,6 +88,13 @@ const omitUndefined = (obj) => {
|
|
|
45
88
|
});
|
|
46
89
|
return out;
|
|
47
90
|
};
|
|
91
|
+
const mergeRuntimeOptions = (base, patch) => {
|
|
92
|
+
const { container: _container, ...runtimeOptions } = patch;
|
|
93
|
+
return {
|
|
94
|
+
...base,
|
|
95
|
+
...omitUndefined(runtimeOptions)
|
|
96
|
+
};
|
|
97
|
+
};
|
|
48
98
|
var TextSlicer = class {
|
|
49
99
|
el;
|
|
50
100
|
original;
|
|
@@ -53,23 +103,24 @@ var TextSlicer = class {
|
|
|
53
103
|
charIndex;
|
|
54
104
|
mounted;
|
|
55
105
|
resizeObserver;
|
|
106
|
+
managedStyles = /* @__PURE__ */ new Map();
|
|
107
|
+
managedAttributes = /* @__PURE__ */ new Map();
|
|
56
108
|
constructor(options = {}, callbacks) {
|
|
57
|
-
const
|
|
109
|
+
const { container, ...runtimeOptions } = options;
|
|
110
|
+
const el = resolveContainer(container);
|
|
58
111
|
this.el = el;
|
|
59
112
|
this.original = isHTMLElement(el) ? el.textContent?.toString() ?? "" : "";
|
|
60
|
-
this.opts =
|
|
61
|
-
...DEFAULT_OPTIONS,
|
|
62
|
-
...omitUndefined(options)
|
|
63
|
-
};
|
|
113
|
+
this.opts = mergeRuntimeOptions(DEFAULT_OPTIONS, runtimeOptions);
|
|
64
114
|
this.callbacks = callbacks;
|
|
65
115
|
this.charIndex = 0;
|
|
66
116
|
this.mounted = false;
|
|
67
117
|
}
|
|
68
118
|
get metrics() {
|
|
69
119
|
const text = this.original;
|
|
120
|
+
const { locale } = this.opts;
|
|
70
121
|
return {
|
|
71
|
-
wordTotal: text.length ?
|
|
72
|
-
charTotal: text.length,
|
|
122
|
+
wordTotal: text.length ? countWords(text, locale) : 0,
|
|
123
|
+
charTotal: text.length ? countCharGraphemes(text, locale) : 0,
|
|
73
124
|
renderedAt: Date.now()
|
|
74
125
|
};
|
|
75
126
|
}
|
|
@@ -77,130 +128,197 @@ var TextSlicer = class {
|
|
|
77
128
|
if (!this.el) return;
|
|
78
129
|
this.mounted = true;
|
|
79
130
|
this.split();
|
|
80
|
-
|
|
131
|
+
this.syncHeightObserver();
|
|
81
132
|
}
|
|
82
133
|
reinit(newText, next) {
|
|
83
134
|
if (!this.el) return;
|
|
84
135
|
if (typeof newText === "string") this.original = newText;
|
|
85
|
-
if (next) this.opts =
|
|
86
|
-
|
|
87
|
-
...omitUndefined(next)
|
|
88
|
-
};
|
|
136
|
+
if (next) this.opts = mergeRuntimeOptions(this.opts, next);
|
|
137
|
+
this.mounted = true;
|
|
89
138
|
this.split();
|
|
139
|
+
this.syncHeightObserver();
|
|
90
140
|
}
|
|
91
141
|
clear() {
|
|
92
142
|
if (!this.el) return;
|
|
93
143
|
emptyElement(this.el);
|
|
144
|
+
this.restoreManagedAttribute("aria-label");
|
|
94
145
|
}
|
|
95
146
|
split() {
|
|
96
147
|
if (!this.el) return;
|
|
97
|
-
this.
|
|
148
|
+
emptyElement(this.el);
|
|
98
149
|
this.charIndex = 0;
|
|
99
150
|
const text = this.original;
|
|
100
151
|
const fragment = document.createDocumentFragment();
|
|
101
|
-
const words = splitIntoWords(text);
|
|
102
152
|
if (this.opts.splitMode === "chars") this.appendChars(fragment, text);
|
|
103
|
-
else this.appendWords(fragment,
|
|
153
|
+
else this.appendWords(fragment, text);
|
|
104
154
|
this.el.appendChild(fragment);
|
|
155
|
+
this.syncAccessibleLabel(text);
|
|
156
|
+
const metrics = this.metrics;
|
|
105
157
|
if (this.opts.cssVariables) {
|
|
106
|
-
this.
|
|
107
|
-
this.
|
|
158
|
+
this.setManagedStyle(CSS_VAR_WORD_TOTAL, String(metrics.wordTotal));
|
|
159
|
+
this.setManagedStyle(CSS_VAR_CHAR_TOTAL, String(metrics.charTotal));
|
|
160
|
+
} else {
|
|
161
|
+
this.restoreManagedStyle(CSS_VAR_WORD_TOTAL);
|
|
162
|
+
this.restoreManagedStyle(CSS_VAR_CHAR_TOTAL);
|
|
108
163
|
}
|
|
109
|
-
this.callbacks?.onAfterRender?.(
|
|
164
|
+
this.callbacks?.onAfterRender?.(metrics);
|
|
110
165
|
}
|
|
111
166
|
destroy() {
|
|
112
167
|
if (!this.el) return;
|
|
113
|
-
this.
|
|
114
|
-
this.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
this.resizeObserver = void 0;
|
|
118
|
-
}
|
|
119
|
-
if (this.opts.containerHeightVar) this.el.style.removeProperty(CSS_VAR_CONTAINER_HEIGHT);
|
|
168
|
+
this.syncHeightObserver(false);
|
|
169
|
+
this.restoreManagedStyles();
|
|
170
|
+
this.restoreManagedAttribute("aria-label");
|
|
171
|
+
this.el.textContent = this.original;
|
|
120
172
|
this.mounted = false;
|
|
121
173
|
}
|
|
122
174
|
updateOptions(next) {
|
|
123
|
-
this.opts =
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
175
|
+
this.opts = mergeRuntimeOptions(this.opts, next);
|
|
176
|
+
if (this.mounted) {
|
|
177
|
+
this.split();
|
|
178
|
+
this.syncHeightObserver();
|
|
179
|
+
}
|
|
128
180
|
}
|
|
129
181
|
lockHeight() {
|
|
130
182
|
if (!this.el) return;
|
|
131
183
|
const h = this.measureHeight();
|
|
132
|
-
if (h > 0) this.
|
|
184
|
+
if (h > 0) this.setManagedStyle("height", `${h}px`);
|
|
133
185
|
}
|
|
134
186
|
unlockHeight() {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
fragment.append(wordSpan);
|
|
151
|
-
}
|
|
152
|
-
if (wordIndex < words.length - 1) fragment.append(this.createSpaceSpan());
|
|
153
|
-
});
|
|
187
|
+
this.restoreManagedStyle("height");
|
|
188
|
+
}
|
|
189
|
+
appendWords(fragment, text) {
|
|
190
|
+
const segments = segmentWords(text, this.opts.locale);
|
|
191
|
+
let wordIndex = 0;
|
|
192
|
+
for (const { segment, isWordLike } of segments) if (isWordLike) {
|
|
193
|
+
const wordSpan = this.createWordSpan(wordIndex, segment);
|
|
194
|
+
if (this.opts.splitMode === "both") this.appendGraphemes(wordSpan, segment);
|
|
195
|
+
else wordSpan.append(document.createTextNode(segment));
|
|
196
|
+
fragment.append(wordSpan);
|
|
197
|
+
wordIndex += 1;
|
|
198
|
+
} else if (isWhitespaceGrapheme(segment)) if (this.opts.splitMode === "both") this.appendGraphemes(fragment, segment);
|
|
199
|
+
else fragment.append(this.createWhitespaceSpan(segment));
|
|
200
|
+
else if (segment.length > 0) if (this.opts.splitMode === "both") this.appendGraphemes(fragment, segment);
|
|
201
|
+
else fragment.append(this.createHiddenTextSpan(segment));
|
|
154
202
|
}
|
|
155
203
|
appendChars(fragment, text) {
|
|
156
|
-
for (const ch of splitIntoGraphemes(text))
|
|
157
|
-
|
|
158
|
-
|
|
204
|
+
for (const ch of splitIntoGraphemes(text, this.opts.locale)) this.appendGrapheme(fragment, ch);
|
|
205
|
+
}
|
|
206
|
+
appendGraphemes(parent, text) {
|
|
207
|
+
for (const ch of splitIntoGraphemes(text, this.opts.locale)) this.appendGrapheme(parent, ch);
|
|
208
|
+
}
|
|
209
|
+
appendGrapheme(parent, ch) {
|
|
210
|
+
if (isWhitespaceGrapheme(ch)) {
|
|
211
|
+
if (this.opts.keepWhitespaceNodes) parent.append(this.createWhitespaceSpan(ch));
|
|
212
|
+
else parent.append(document.createTextNode(ch));
|
|
213
|
+
return;
|
|
159
214
|
}
|
|
215
|
+
parent.append(this.createCharSpan(ch));
|
|
160
216
|
}
|
|
161
|
-
createWordSpan(index, word
|
|
217
|
+
createWordSpan(index, word) {
|
|
162
218
|
const span = document.createElement("span");
|
|
163
219
|
span.classList.add(CLASSNAMES.word);
|
|
164
|
-
|
|
220
|
+
span.setAttribute("aria-hidden", "true");
|
|
221
|
+
if (this.opts.dataAttributes) span.setAttribute("data-word", word);
|
|
165
222
|
if (this.opts.cssVariables) span.style.setProperty(CSS_VAR_WORD_INDEX, String(index));
|
|
166
223
|
return span;
|
|
167
224
|
}
|
|
168
225
|
createCharSpan(ch) {
|
|
169
226
|
const span = document.createElement("span");
|
|
170
227
|
span.textContent = ch;
|
|
228
|
+
span.setAttribute("aria-hidden", "true");
|
|
229
|
+
span.classList.add(CLASSNAMES.char);
|
|
171
230
|
if (this.opts.dataAttributes) span.setAttribute("data-char", ch);
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
if (!this.opts.keepWhitespaceNodes) span.textContent = SPACE;
|
|
175
|
-
} else {
|
|
176
|
-
span.classList.add(CLASSNAMES.char);
|
|
177
|
-
if (this.opts.cssVariables) span.style.setProperty(CSS_VAR_CHAR_INDEX, String(this.charIndex));
|
|
178
|
-
this.charIndex += 1;
|
|
179
|
-
}
|
|
231
|
+
if (this.opts.cssVariables) span.style.setProperty(CSS_VAR_CHAR_INDEX, String(this.charIndex));
|
|
232
|
+
this.charIndex += 1;
|
|
180
233
|
return span;
|
|
181
234
|
}
|
|
182
|
-
|
|
235
|
+
createWhitespaceSpan(text) {
|
|
183
236
|
const span = document.createElement("span");
|
|
184
237
|
span.classList.add(CLASSNAMES.whitespace);
|
|
185
|
-
span.textContent =
|
|
238
|
+
span.textContent = text;
|
|
239
|
+
span.setAttribute("aria-hidden", "true");
|
|
240
|
+
return span;
|
|
241
|
+
}
|
|
242
|
+
createHiddenTextSpan(text) {
|
|
243
|
+
const span = document.createElement("span");
|
|
244
|
+
span.textContent = text;
|
|
245
|
+
span.setAttribute("aria-hidden", "true");
|
|
186
246
|
return span;
|
|
187
247
|
}
|
|
248
|
+
syncAccessibleLabel(text) {
|
|
249
|
+
if (!this.el) return;
|
|
250
|
+
if (this.managedAttributes.has("aria-label")) {
|
|
251
|
+
this.setManagedAttribute("aria-label", text);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (this.el.hasAttribute("aria-label") || this.el.hasAttribute("aria-labelledby")) return;
|
|
255
|
+
this.setManagedAttribute("aria-label", text);
|
|
256
|
+
}
|
|
188
257
|
measureHeight() {
|
|
189
258
|
if (!this.el) return 0;
|
|
190
259
|
this.el.classList.add(MEASURING_CLASS);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
260
|
+
try {
|
|
261
|
+
this.el.offsetHeight;
|
|
262
|
+
let h = this.el.offsetHeight || this.el.clientHeight || 0;
|
|
263
|
+
if (!h) h = Math.round(this.el.getBoundingClientRect().height);
|
|
264
|
+
return Math.max(0, Math.ceil(h));
|
|
265
|
+
} finally {
|
|
266
|
+
this.el.classList.remove(MEASURING_CLASS);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
syncHeightObserver(enable = this.opts.containerHeightVar) {
|
|
270
|
+
if (!this.el) return;
|
|
271
|
+
if (enable) {
|
|
272
|
+
if (typeof ResizeObserver !== "function") {
|
|
273
|
+
this.restoreManagedStyle(CSS_VAR_CONTAINER_HEIGHT);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (!this.resizeObserver) {
|
|
277
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
278
|
+
const entry = entries[0];
|
|
279
|
+
if (!entry || !this.el) return;
|
|
280
|
+
const boxSize = entry.borderBoxSize?.[0]?.blockSize;
|
|
281
|
+
const h = Math.max(0, Math.ceil(boxSize ?? entry.contentRect.height));
|
|
282
|
+
if (h > 0) this.setManagedStyle(CSS_VAR_CONTAINER_HEIGHT, `${h}px`);
|
|
283
|
+
});
|
|
284
|
+
this.resizeObserver.observe(this.el);
|
|
285
|
+
}
|
|
286
|
+
} else if (this.resizeObserver) {
|
|
287
|
+
this.resizeObserver.disconnect();
|
|
288
|
+
this.resizeObserver = void 0;
|
|
289
|
+
this.restoreManagedStyle(CSS_VAR_CONTAINER_HEIGHT);
|
|
290
|
+
}
|
|
196
291
|
}
|
|
197
|
-
|
|
292
|
+
setManagedStyle(property, value) {
|
|
198
293
|
if (!this.el) return;
|
|
199
|
-
this.
|
|
200
|
-
|
|
201
|
-
|
|
294
|
+
if (!this.managedStyles.has(property)) this.managedStyles.set(property, {
|
|
295
|
+
value: this.el.style.getPropertyValue(property),
|
|
296
|
+
priority: this.el.style.getPropertyPriority(property)
|
|
202
297
|
});
|
|
203
|
-
this.
|
|
298
|
+
this.el.style.setProperty(property, value);
|
|
299
|
+
}
|
|
300
|
+
restoreManagedStyle(property) {
|
|
301
|
+
if (!this.el) return;
|
|
302
|
+
const previous = this.managedStyles.get(property);
|
|
303
|
+
if (previous === void 0) return;
|
|
304
|
+
if (previous.value) this.el.style.setProperty(property, previous.value, previous.priority);
|
|
305
|
+
else this.el.style.removeProperty(property);
|
|
306
|
+
this.managedStyles.delete(property);
|
|
307
|
+
}
|
|
308
|
+
restoreManagedStyles() {
|
|
309
|
+
for (const property of [...this.managedStyles.keys()]) this.restoreManagedStyle(property);
|
|
310
|
+
}
|
|
311
|
+
setManagedAttribute(name, value) {
|
|
312
|
+
if (!this.el) return;
|
|
313
|
+
if (!this.managedAttributes.has(name)) this.managedAttributes.set(name, this.el.getAttribute(name));
|
|
314
|
+
this.el.setAttribute(name, value);
|
|
315
|
+
}
|
|
316
|
+
restoreManagedAttribute(name) {
|
|
317
|
+
if (!this.el || !this.managedAttributes.has(name)) return;
|
|
318
|
+
const previous = this.managedAttributes.get(name);
|
|
319
|
+
if (previous === null) this.el.removeAttribute(name);
|
|
320
|
+
else if (previous !== void 0) this.el.setAttribute(name, previous);
|
|
321
|
+
this.managedAttributes.delete(name);
|
|
204
322
|
}
|
|
205
323
|
};
|
|
206
324
|
|