text-slicer 1.5.0 → 2.0.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 CHANGED
@@ -1,58 +1,70 @@
1
- <p align="center"><strong>text-slicer</strong></p>
1
+ # text-slicer
2
2
 
3
- <div align="center">
3
+ Split text inside an HTML element into words and/or characters, wrapping each fragment in its own span.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/text-slicer.svg?colorB=brightgreen)](https://www.npmjs.com/package/text-slicer)
6
- [![GitHub package version](https://img.shields.io/github/package-json/v/ux-ui-pro/text-slicer.svg)](https://github.com/ux-ui-pro/text-slicer)
7
- [![NPM Downloads](https://img.shields.io/npm/dm/text-slicer.svg?style=flat)](https://www.npmjs.org/package/text-slicer)
6
+ [![NPM Downloads](https://img.shields.io/npm/dm/text-slicer.svg?style=flat)](https://www.npmjs.com/package/text-slicer)
8
7
 
9
- </div>
8
+ [Demo](https://codepen.io/ux-ui/full/vYMoGoG)
10
9
 
11
- <p align="center">TextSlicer splits text within an HTML element into words and/or characters, wrapping each in individual spans. It provides flexible options, CSS variable integration, lifecycle management, and callbacks for post-render handling.</p>
12
- <p align="center"><sup>1.5kB gzipped</sup></p>
13
- <p align="center"><a href="https://codepen.io/ux-ui/full/vYMoGoG">Demo</a></p>
14
- <br>
10
+ ---
15
11
 
16
- **Install**
12
+ - Split by words, characters, or both (`splitMode`).
13
+ - Optional CSS variables (`--word-index`, `--char-index`, `--word-total`, `--char-total`, `--container-height`).
14
+ - Optional `data-word` / `data-char` attributes for styling or scripting.
15
+ - Lifecycle helpers: `init`, `reinit`, `clear`, `split`, `destroy`, `updateOptions`, `lockHeight`, `unlockHeight`.
16
+ - `onAfterRender` callback with word/char totals and timestamp.
17
+ - ~1.5kB gzipped.
17
18
 
18
- ```console
19
- yarn add text-slicer
19
+ ---
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install text-slicer
20
25
  ```
21
- <br>
22
26
 
23
- **Import**
27
+ ## Quick Start
24
28
 
25
- ```javascript
26
- import { TextSlicer } from 'text-slicer';
29
+ HTML:
30
+
31
+ ```html
32
+ <p class="text-slicer">Hello world</p>
27
33
  ```
28
- <br>
29
34
 
30
- ➠ **Usage**
35
+ JavaScript:
31
36
 
32
- ```javascript
33
- const textSlicer = new TextSlicer();
37
+ ```ts
38
+ import { TextSlicer } from 'text-slicer';
39
+
40
+ const textSlicer = new TextSlicer({ container: '.text-slicer' });
34
41
 
35
42
  textSlicer.init();
36
43
  ```
37
44
 
38
- <sub>Initialization with specified parameters</sub>
39
- ```javascript
45
+ With options and a callback:
46
+
47
+ ```ts
40
48
  document.addEventListener('DOMContentLoaded', () => {
41
- const textSlicer = new TextSlicer({
42
- container: '.text-slicer',
43
- splitMode: 'both',
44
- cssVariables: true,
45
- dataAttributes: true,
46
- }, {
47
- onAfterRender: (metrics) => console.log(metrics)
48
- });
49
+ const textSlicer = new TextSlicer(
50
+ {
51
+ container: '.text-slicer',
52
+ splitMode: 'both',
53
+ cssVariables: true,
54
+ dataAttributes: true,
55
+ },
56
+ {
57
+ onAfterRender: (metrics) => console.log(metrics),
58
+ },
59
+ );
49
60
 
50
61
  textSlicer.init();
51
62
  });
52
63
  ```
53
64
 
54
- <sub>How to apply the TextSlicer class to all elements on a page</sub>
55
- ```javascript
65
+ Apply to every matching element on the page:
66
+
67
+ ```ts
56
68
  document.addEventListener('DOMContentLoaded', () => {
57
69
  document.querySelectorAll('.text-slicer').forEach((element) => {
58
70
  const textSlicer = new TextSlicer({ container: element });
@@ -61,45 +73,62 @@ document.addEventListener('DOMContentLoaded', () => {
61
73
  });
62
74
  });
63
75
  ```
64
- <br>
65
76
 
66
- **Options**
77
+ ## API
78
+
79
+ Named exports:
80
+
81
+ ```ts
82
+ import { TextSlicer, CLASSNAMES } from 'text-slicer';
83
+ ```
84
+
85
+ - `TextSlicer` — main class.
86
+ - `CLASSNAMES` — BEM-style class map (`word`, `char`, `whitespace`).
87
+ - Types: `SplitMode`, `TextSlicerOptions`, `TextSlicerMetrics`, `TextSlicerCallbacks`.
88
+
89
+ Constructor: `new TextSlicer(options?, callbacks?)`.
90
+
91
+ ## Options
92
+
93
+ | Option | Type | Default | Description |
94
+ |:----------------------|:------------------------|:-----------:|:----------------------------------------------------------------------------|
95
+ | `container` | `HTMLElement \| string` | `undefined` | Target element or selector for text splitting. |
96
+ | `splitMode` | `'words' \| 'chars' \| 'both'` | `both` | Split by words, characters, or both. |
97
+ | `cssVariables` | `boolean` | `false` | Sets `--word-index`, `--char-index`, `--word-total`, `--char-total` on spans. |
98
+ | `dataAttributes` | `boolean` | `false` | Adds `data-word` and `data-char` attributes. |
99
+ | `keepWhitespaceNodes` | `boolean` | `true` | When `false`, whitespace nodes are ignored in character mode. |
100
+ | `containerHeightVar` | `boolean` | `false` | Sets dynamic `--container-height` via `ResizeObserver`. |
67
101
 
68
- | Option | Type | Default | Description |
69
- |:--------------------:|:-----------------------:|:------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------|
70
- | `container` | `HTMLElement \| string` | `undefined` | The target element or selector for text splitting. |
71
- | `splitMode` | `'words' \| 'chars' \| 'both'` | `both` | Defines splitting mode: by words, characters, or both. |
72
- | `cssVariables` | `boolean` | `false` | Enables CSS variables like `--word-index` and `--char-index` for each span. |
73
- | `dataAttributes` | `boolean` | `false` | Adds `data-word` and `data-char` attributes for additional styling or scripting. |
74
- | `keepWhitespaceNodes`| `boolean` | `true` | If `false`, whitespace nodes will be ignored when splitting characters. |
75
- | `containerHeightVar` | `boolean` | `false` | If `true`, sets a dynamic CSS variable `--container-height` that updates on resize. |
102
+ ## Callbacks
76
103
 
77
- <br>
104
+ | Callback | Arguments | Description |
105
+ |-----------------|---------------------|--------------------------------------------------------------------------|
106
+ | `onAfterRender` | `TextSlicerMetrics` | Called after render; provides `wordTotal`, `charTotal`, `renderedAt`. |
78
107
 
79
- **Callbacks**
108
+ ## Methods
80
109
 
81
- | Callback | Arguments | Description |
82
- |-----------------|-----------------------|---------------------------------------------------------------------------------------------------------|
83
- | `onAfterRender` | `TextSlicerMetrics` | Invoked after rendering. Provides `wordTotal`, `charTotal`, and `renderedAt` timestamp. |
110
+ | Method | Description |
111
+ |--------------------------------|----------------------------------------------------------|
112
+ | `init()` | Initializes and renders text splitting. |
113
+ | `reinit(newText?, options?)` | Re-initializes with optional new text and updated options. |
114
+ | `clear()` | Clears all content inside the container. |
115
+ | `split()` | Manually triggers splitting and rendering. |
116
+ | `destroy()` | Cleans up instance, observers, and styles. |
117
+ | `updateOptions(options)` | Updates options at runtime; re-renders if mounted. |
118
+ | `lockHeight()` | Locks container height to its measured value. |
119
+ | `unlockHeight()` | Unlocks container height. |
120
+ | `metrics` (getter) | Returns `wordTotal`, `charTotal`, and `renderedAt`. |
84
121
 
85
- <br>
122
+ ## Styling
86
123
 
87
- **API Methods**
124
+ Exported `CLASSNAMES`:
88
125
 
89
- | Method | Description |
90
- |-------------------|--------------------------------------------------------------------------------------------------|
91
- | `init()` | Initializes and renders text splitting. |
92
- | `reinit(newText?, options?)` | Re-initializes with optional new text and updated options. |
93
- | `clear()` | Clears all content inside the container element. |
94
- | `split()` | Manually triggers splitting and rendering. |
95
- | `destroy()` | Cleans up instance, observers, and styles. |
96
- | `updateOptions()` | Updates options at runtime and re-renders if mounted. |
97
- | `lockHeight()` | Locks container height to its measured value. |
98
- | `unlockHeight()` | Unlocks container height. |
99
- | `metrics` (getter)| Returns current metrics: `wordTotal`, `charTotal`, and `renderedAt`. |
126
+ - `ts-word` — word wrapper (when splitting includes words).
127
+ - `ts-char` — character span.
128
+ - `ts-whitespace` whitespace span.
100
129
 
101
- <br>
130
+ When `cssVariables: true`, spans receive `--word-index`, `--char-index`; the container may receive `--word-total`, `--char-total`, and (with `containerHeightVar`) `--container-height`.
102
131
 
103
- **License**
132
+ ## License
104
133
 
105
- text-slicer is released under MIT license
134
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,212 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ //#region src/index.ts
4
+ const DEFAULT_OPTIONS = {
5
+ splitMode: "both",
6
+ cssVariables: false,
7
+ dataAttributes: false,
8
+ keepWhitespaceNodes: true,
9
+ containerHeightVar: false
10
+ };
11
+ const CLASSNAMES = Object.freeze({
12
+ word: "ts-word",
13
+ char: "ts-char",
14
+ whitespace: "ts-whitespace"
15
+ });
16
+ const SPACE = " ";
17
+ const CSS_VAR_WORD_TOTAL = "--word-total";
18
+ const CSS_VAR_CHAR_TOTAL = "--char-total";
19
+ const CSS_VAR_WORD_INDEX = "--word-index";
20
+ const CSS_VAR_CHAR_INDEX = "--char-index";
21
+ const CSS_VAR_CONTAINER_HEIGHT = "--container-height";
22
+ const MEASURING_CLASS = "ts-measuring";
23
+ const canUseDOM = () => typeof window !== "undefined" && typeof document !== "undefined";
24
+ const resolveContainer = (container) => {
25
+ if (!canUseDOM()) return null;
26
+ if (!container) return null;
27
+ return typeof container === "string" ? document.querySelector(container) : container;
28
+ };
29
+ const splitIntoGraphemes = (text) => {
30
+ const Seg = Intl.Segmenter;
31
+ if (typeof Seg === "function") {
32
+ const segmenter = new Seg("en", { granularity: "grapheme" });
33
+ return Array.from(segmenter.segment(text), (s) => s.segment);
34
+ }
35
+ return Array.from(text);
36
+ };
37
+ const splitIntoWords = (text) => text.split(SPACE);
38
+ const emptyElement = (el) => {
39
+ el.replaceChildren();
40
+ };
41
+ const isHTMLElement = (el) => !!el && typeof HTMLElement !== "undefined" && el instanceof HTMLElement;
42
+ const omitUndefined = (obj) => {
43
+ const out = {};
44
+ Object.keys(obj).forEach((key) => {
45
+ const val = obj[key];
46
+ if (val !== void 0) out[key] = val;
47
+ });
48
+ return out;
49
+ };
50
+ var TextSlicer = class {
51
+ el;
52
+ original;
53
+ opts;
54
+ callbacks;
55
+ charIndex;
56
+ mounted;
57
+ resizeObserver;
58
+ constructor(options = {}, callbacks) {
59
+ const el = resolveContainer(options.container);
60
+ this.el = el;
61
+ this.original = isHTMLElement(el) ? el.textContent?.toString() ?? "" : "";
62
+ this.opts = {
63
+ ...DEFAULT_OPTIONS,
64
+ ...omitUndefined(options)
65
+ };
66
+ this.callbacks = callbacks;
67
+ this.charIndex = 0;
68
+ this.mounted = false;
69
+ }
70
+ get metrics() {
71
+ const text = this.original;
72
+ return {
73
+ wordTotal: text.length ? splitIntoWords(text).length : 0,
74
+ charTotal: text.length,
75
+ renderedAt: Date.now()
76
+ };
77
+ }
78
+ init() {
79
+ if (!this.el) return;
80
+ this.mounted = true;
81
+ this.split();
82
+ if (this.opts.containerHeightVar) this.initHeightObserver();
83
+ }
84
+ reinit(newText, next) {
85
+ if (!this.el) return;
86
+ if (typeof newText === "string") this.original = newText;
87
+ if (next) this.opts = {
88
+ ...this.opts,
89
+ ...omitUndefined(next)
90
+ };
91
+ this.split();
92
+ }
93
+ clear() {
94
+ if (!this.el) return;
95
+ emptyElement(this.el);
96
+ }
97
+ split() {
98
+ if (!this.el) return;
99
+ this.clear();
100
+ this.charIndex = 0;
101
+ const text = this.original;
102
+ const fragment = document.createDocumentFragment();
103
+ const words = splitIntoWords(text);
104
+ if (this.opts.splitMode === "chars") this.appendChars(fragment, text);
105
+ else this.appendWords(fragment, words);
106
+ this.el.appendChild(fragment);
107
+ if (this.opts.cssVariables) {
108
+ this.el.style.setProperty(CSS_VAR_WORD_TOTAL, String(words.length));
109
+ this.el.style.setProperty(CSS_VAR_CHAR_TOTAL, String(text.length));
110
+ }
111
+ this.callbacks?.onAfterRender?.(this.metrics);
112
+ }
113
+ destroy() {
114
+ if (!this.el) return;
115
+ this.clear();
116
+ this.unlockHeight();
117
+ if (this.resizeObserver) {
118
+ this.resizeObserver.disconnect();
119
+ this.resizeObserver = void 0;
120
+ }
121
+ if (this.opts.containerHeightVar) this.el.style.removeProperty(CSS_VAR_CONTAINER_HEIGHT);
122
+ this.mounted = false;
123
+ }
124
+ updateOptions(next) {
125
+ this.opts = {
126
+ ...this.opts,
127
+ ...omitUndefined(next)
128
+ };
129
+ if (this.mounted) this.split();
130
+ }
131
+ lockHeight() {
132
+ if (!this.el) return;
133
+ const h = this.measureHeight();
134
+ if (h > 0) this.el.style.height = `${h}px`;
135
+ }
136
+ unlockHeight() {
137
+ if (!this.el) return;
138
+ this.el.style.removeProperty("height");
139
+ }
140
+ appendWords(fragment, words) {
141
+ words.forEach((word, wordIndex) => {
142
+ if (this.opts.splitMode === "both") {
143
+ const wordSpan = this.createWordSpan(wordIndex, word);
144
+ for (const ch of splitIntoGraphemes(word)) {
145
+ const charSpan = this.createCharSpan(ch);
146
+ wordSpan.append(charSpan);
147
+ }
148
+ fragment.append(wordSpan);
149
+ } else {
150
+ const wordSpan = this.createWordSpan(wordIndex);
151
+ wordSpan.append(document.createTextNode(word));
152
+ fragment.append(wordSpan);
153
+ }
154
+ if (wordIndex < words.length - 1) fragment.append(this.createSpaceSpan());
155
+ });
156
+ }
157
+ appendChars(fragment, text) {
158
+ for (const ch of splitIntoGraphemes(text)) {
159
+ const span = this.createCharSpan(ch);
160
+ fragment.append(span);
161
+ }
162
+ }
163
+ createWordSpan(index, word = "") {
164
+ const span = document.createElement("span");
165
+ span.classList.add(CLASSNAMES.word);
166
+ if (this.opts.dataAttributes && word) span.setAttribute("data-word", word);
167
+ if (this.opts.cssVariables) span.style.setProperty(CSS_VAR_WORD_INDEX, String(index));
168
+ return span;
169
+ }
170
+ createCharSpan(ch) {
171
+ const span = document.createElement("span");
172
+ span.textContent = ch;
173
+ if (this.opts.dataAttributes) span.setAttribute("data-char", ch);
174
+ if (ch === SPACE) {
175
+ span.classList.add(CLASSNAMES.whitespace);
176
+ if (!this.opts.keepWhitespaceNodes) span.textContent = SPACE;
177
+ } else {
178
+ span.classList.add(CLASSNAMES.char);
179
+ if (this.opts.cssVariables) span.style.setProperty(CSS_VAR_CHAR_INDEX, String(this.charIndex));
180
+ this.charIndex += 1;
181
+ }
182
+ return span;
183
+ }
184
+ createSpaceSpan() {
185
+ const span = document.createElement("span");
186
+ span.classList.add(CLASSNAMES.whitespace);
187
+ span.textContent = SPACE;
188
+ return span;
189
+ }
190
+ measureHeight() {
191
+ if (!this.el) return 0;
192
+ this.el.classList.add(MEASURING_CLASS);
193
+ this.el.offsetHeight;
194
+ let h = this.el.offsetHeight || this.el.clientHeight || 0;
195
+ if (!h) h = Math.round(this.el.getBoundingClientRect().height);
196
+ this.el.classList.remove(MEASURING_CLASS);
197
+ return Math.max(0, Math.ceil(h));
198
+ }
199
+ initHeightObserver() {
200
+ if (!this.el) return;
201
+ this.resizeObserver = new ResizeObserver(() => {
202
+ const h = this.measureHeight();
203
+ if (h > 0) this.el?.style.setProperty(CSS_VAR_CONTAINER_HEIGHT, `${h}px`);
204
+ });
205
+ this.resizeObserver.observe(this.el);
206
+ }
207
+ };
208
+
209
+ //#endregion
210
+ exports.CLASSNAMES = CLASSNAMES;
211
+ exports.TextSlicer = TextSlicer;
212
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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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"}
@@ -0,0 +1,52 @@
1
+ //#region src/index.d.ts
2
+ type SplitMode = 'words' | 'chars' | 'both';
3
+ interface TextSlicerOptions {
4
+ container?: HTMLElement | string;
5
+ splitMode?: SplitMode;
6
+ cssVariables?: boolean;
7
+ dataAttributes?: boolean;
8
+ keepWhitespaceNodes?: boolean;
9
+ containerHeightVar?: boolean;
10
+ }
11
+ interface TextSlicerMetrics {
12
+ wordTotal: number;
13
+ charTotal: number;
14
+ renderedAt: number;
15
+ }
16
+ interface TextSlicerCallbacks {
17
+ onAfterRender?: (metrics: TextSlicerMetrics) => void;
18
+ }
19
+ declare const CLASSNAMES: Readonly<{
20
+ word: "ts-word";
21
+ char: "ts-char";
22
+ whitespace: "ts-whitespace";
23
+ }>;
24
+ declare class TextSlicer {
25
+ private readonly el;
26
+ private original;
27
+ private opts;
28
+ private callbacks;
29
+ private charIndex;
30
+ private mounted;
31
+ private resizeObserver?;
32
+ constructor(options?: TextSlicerOptions, callbacks?: TextSlicerCallbacks);
33
+ get metrics(): TextSlicerMetrics;
34
+ init(): void;
35
+ reinit(newText?: string, next?: Partial<TextSlicerOptions>): void;
36
+ clear(): void;
37
+ split(): void;
38
+ destroy(): void;
39
+ updateOptions(next: Partial<TextSlicerOptions>): void;
40
+ lockHeight(): void;
41
+ unlockHeight(): void;
42
+ private appendWords;
43
+ private appendChars;
44
+ private createWordSpan;
45
+ private createCharSpan;
46
+ private createSpaceSpan;
47
+ private measureHeight;
48
+ private initHeightObserver;
49
+ }
50
+ //#endregion
51
+ export { CLASSNAMES, SplitMode, TextSlicer, TextSlicerCallbacks, TextSlicerMetrics, TextSlicerOptions };
52
+ //# sourceMappingURL=index.d.cts.map
package/dist/index.d.ts CHANGED
@@ -1,49 +1,52 @@
1
- export type SplitMode = 'words' | 'chars' | 'both';
2
- export interface TextSlicerOptions {
3
- container?: HTMLElement | string;
4
- splitMode?: SplitMode;
5
- cssVariables?: boolean;
6
- dataAttributes?: boolean;
7
- keepWhitespaceNodes?: boolean;
8
- containerHeightVar?: boolean;
1
+ //#region src/index.d.ts
2
+ type SplitMode = 'words' | 'chars' | 'both';
3
+ interface TextSlicerOptions {
4
+ container?: HTMLElement | string;
5
+ splitMode?: SplitMode;
6
+ cssVariables?: boolean;
7
+ dataAttributes?: boolean;
8
+ keepWhitespaceNodes?: boolean;
9
+ containerHeightVar?: boolean;
9
10
  }
10
- export interface TextSlicerMetrics {
11
- wordTotal: number;
12
- charTotal: number;
13
- renderedAt: number;
11
+ interface TextSlicerMetrics {
12
+ wordTotal: number;
13
+ charTotal: number;
14
+ renderedAt: number;
14
15
  }
15
- export interface TextSlicerCallbacks {
16
- onAfterRender?: (metrics: TextSlicerMetrics) => void;
16
+ interface TextSlicerCallbacks {
17
+ onAfterRender?: (metrics: TextSlicerMetrics) => void;
17
18
  }
18
- export declare const CLASSNAMES: Readonly<{
19
- word: "ts-word";
20
- char: "ts-char";
21
- whitespace: "ts-whitespace";
19
+ declare const CLASSNAMES: Readonly<{
20
+ word: "ts-word";
21
+ char: "ts-char";
22
+ whitespace: "ts-whitespace";
22
23
  }>;
23
- export declare class TextSlicer {
24
- private readonly el;
25
- private original;
26
- private opts;
27
- private callbacks;
28
- private charIndex;
29
- private mounted;
30
- private heightLocked;
31
- private resizeObserver?;
32
- constructor(options?: TextSlicerOptions, callbacks?: TextSlicerCallbacks);
33
- get metrics(): TextSlicerMetrics;
34
- init(): void;
35
- reinit(newText?: string, next?: Partial<TextSlicerOptions>): void;
36
- clear(): void;
37
- split(): void;
38
- destroy(): void;
39
- updateOptions(next: Partial<TextSlicerOptions>): void;
40
- lockHeight(): void;
41
- unlockHeight(): void;
42
- private appendWords;
43
- private appendChars;
44
- private createWordSpan;
45
- private createCharSpan;
46
- private createSpaceSpan;
47
- private measureHeight;
48
- private initHeightObserver;
24
+ declare class TextSlicer {
25
+ private readonly el;
26
+ private original;
27
+ private opts;
28
+ private callbacks;
29
+ private charIndex;
30
+ private mounted;
31
+ private resizeObserver?;
32
+ constructor(options?: TextSlicerOptions, callbacks?: TextSlicerCallbacks);
33
+ get metrics(): TextSlicerMetrics;
34
+ init(): void;
35
+ reinit(newText?: string, next?: Partial<TextSlicerOptions>): void;
36
+ clear(): void;
37
+ split(): void;
38
+ destroy(): void;
39
+ updateOptions(next: Partial<TextSlicerOptions>): void;
40
+ lockHeight(): void;
41
+ unlockHeight(): void;
42
+ private appendWords;
43
+ private appendChars;
44
+ private createWordSpan;
45
+ private createCharSpan;
46
+ private createSpaceSpan;
47
+ private measureHeight;
48
+ private initHeightObserver;
49
49
  }
50
+ //#endregion
51
+ export { CLASSNAMES, SplitMode, TextSlicer, TextSlicerCallbacks, TextSlicerMetrics, TextSlicerOptions };
52
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,209 @@
1
+ //#region src/index.ts
2
+ const DEFAULT_OPTIONS = {
3
+ splitMode: "both",
4
+ cssVariables: false,
5
+ dataAttributes: false,
6
+ keepWhitespaceNodes: true,
7
+ containerHeightVar: false
8
+ };
9
+ const CLASSNAMES = Object.freeze({
10
+ word: "ts-word",
11
+ char: "ts-char",
12
+ whitespace: "ts-whitespace"
13
+ });
14
+ const SPACE = " ";
15
+ const CSS_VAR_WORD_TOTAL = "--word-total";
16
+ const CSS_VAR_CHAR_TOTAL = "--char-total";
17
+ const CSS_VAR_WORD_INDEX = "--word-index";
18
+ const CSS_VAR_CHAR_INDEX = "--char-index";
19
+ const CSS_VAR_CONTAINER_HEIGHT = "--container-height";
20
+ const MEASURING_CLASS = "ts-measuring";
21
+ const canUseDOM = () => typeof window !== "undefined" && typeof document !== "undefined";
22
+ const resolveContainer = (container) => {
23
+ if (!canUseDOM()) return null;
24
+ if (!container) return null;
25
+ return typeof container === "string" ? document.querySelector(container) : container;
26
+ };
27
+ const splitIntoGraphemes = (text) => {
28
+ const Seg = Intl.Segmenter;
29
+ if (typeof Seg === "function") {
30
+ const segmenter = new Seg("en", { granularity: "grapheme" });
31
+ return Array.from(segmenter.segment(text), (s) => s.segment);
32
+ }
33
+ return Array.from(text);
34
+ };
35
+ const splitIntoWords = (text) => text.split(SPACE);
36
+ const emptyElement = (el) => {
37
+ el.replaceChildren();
38
+ };
39
+ const isHTMLElement = (el) => !!el && typeof HTMLElement !== "undefined" && el instanceof HTMLElement;
40
+ const omitUndefined = (obj) => {
41
+ const out = {};
42
+ Object.keys(obj).forEach((key) => {
43
+ const val = obj[key];
44
+ if (val !== void 0) out[key] = val;
45
+ });
46
+ return out;
47
+ };
48
+ var TextSlicer = class {
49
+ el;
50
+ original;
51
+ opts;
52
+ callbacks;
53
+ charIndex;
54
+ mounted;
55
+ resizeObserver;
56
+ constructor(options = {}, callbacks) {
57
+ const el = resolveContainer(options.container);
58
+ this.el = el;
59
+ this.original = isHTMLElement(el) ? el.textContent?.toString() ?? "" : "";
60
+ this.opts = {
61
+ ...DEFAULT_OPTIONS,
62
+ ...omitUndefined(options)
63
+ };
64
+ this.callbacks = callbacks;
65
+ this.charIndex = 0;
66
+ this.mounted = false;
67
+ }
68
+ get metrics() {
69
+ const text = this.original;
70
+ return {
71
+ wordTotal: text.length ? splitIntoWords(text).length : 0,
72
+ charTotal: text.length,
73
+ renderedAt: Date.now()
74
+ };
75
+ }
76
+ init() {
77
+ if (!this.el) return;
78
+ this.mounted = true;
79
+ this.split();
80
+ if (this.opts.containerHeightVar) this.initHeightObserver();
81
+ }
82
+ reinit(newText, next) {
83
+ if (!this.el) return;
84
+ if (typeof newText === "string") this.original = newText;
85
+ if (next) this.opts = {
86
+ ...this.opts,
87
+ ...omitUndefined(next)
88
+ };
89
+ this.split();
90
+ }
91
+ clear() {
92
+ if (!this.el) return;
93
+ emptyElement(this.el);
94
+ }
95
+ split() {
96
+ if (!this.el) return;
97
+ this.clear();
98
+ this.charIndex = 0;
99
+ const text = this.original;
100
+ const fragment = document.createDocumentFragment();
101
+ const words = splitIntoWords(text);
102
+ if (this.opts.splitMode === "chars") this.appendChars(fragment, text);
103
+ else this.appendWords(fragment, words);
104
+ this.el.appendChild(fragment);
105
+ if (this.opts.cssVariables) {
106
+ this.el.style.setProperty(CSS_VAR_WORD_TOTAL, String(words.length));
107
+ this.el.style.setProperty(CSS_VAR_CHAR_TOTAL, String(text.length));
108
+ }
109
+ this.callbacks?.onAfterRender?.(this.metrics);
110
+ }
111
+ destroy() {
112
+ if (!this.el) return;
113
+ this.clear();
114
+ this.unlockHeight();
115
+ if (this.resizeObserver) {
116
+ this.resizeObserver.disconnect();
117
+ this.resizeObserver = void 0;
118
+ }
119
+ if (this.opts.containerHeightVar) this.el.style.removeProperty(CSS_VAR_CONTAINER_HEIGHT);
120
+ this.mounted = false;
121
+ }
122
+ updateOptions(next) {
123
+ this.opts = {
124
+ ...this.opts,
125
+ ...omitUndefined(next)
126
+ };
127
+ if (this.mounted) this.split();
128
+ }
129
+ lockHeight() {
130
+ if (!this.el) return;
131
+ const h = this.measureHeight();
132
+ if (h > 0) this.el.style.height = `${h}px`;
133
+ }
134
+ unlockHeight() {
135
+ if (!this.el) return;
136
+ this.el.style.removeProperty("height");
137
+ }
138
+ appendWords(fragment, words) {
139
+ words.forEach((word, wordIndex) => {
140
+ if (this.opts.splitMode === "both") {
141
+ const wordSpan = this.createWordSpan(wordIndex, word);
142
+ for (const ch of splitIntoGraphemes(word)) {
143
+ const charSpan = this.createCharSpan(ch);
144
+ wordSpan.append(charSpan);
145
+ }
146
+ fragment.append(wordSpan);
147
+ } else {
148
+ const wordSpan = this.createWordSpan(wordIndex);
149
+ wordSpan.append(document.createTextNode(word));
150
+ fragment.append(wordSpan);
151
+ }
152
+ if (wordIndex < words.length - 1) fragment.append(this.createSpaceSpan());
153
+ });
154
+ }
155
+ appendChars(fragment, text) {
156
+ for (const ch of splitIntoGraphemes(text)) {
157
+ const span = this.createCharSpan(ch);
158
+ fragment.append(span);
159
+ }
160
+ }
161
+ createWordSpan(index, word = "") {
162
+ const span = document.createElement("span");
163
+ span.classList.add(CLASSNAMES.word);
164
+ if (this.opts.dataAttributes && word) span.setAttribute("data-word", word);
165
+ if (this.opts.cssVariables) span.style.setProperty(CSS_VAR_WORD_INDEX, String(index));
166
+ return span;
167
+ }
168
+ createCharSpan(ch) {
169
+ const span = document.createElement("span");
170
+ span.textContent = ch;
171
+ if (this.opts.dataAttributes) span.setAttribute("data-char", ch);
172
+ if (ch === SPACE) {
173
+ span.classList.add(CLASSNAMES.whitespace);
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
+ }
180
+ return span;
181
+ }
182
+ createSpaceSpan() {
183
+ const span = document.createElement("span");
184
+ span.classList.add(CLASSNAMES.whitespace);
185
+ span.textContent = SPACE;
186
+ return span;
187
+ }
188
+ measureHeight() {
189
+ if (!this.el) return 0;
190
+ this.el.classList.add(MEASURING_CLASS);
191
+ this.el.offsetHeight;
192
+ let h = this.el.offsetHeight || this.el.clientHeight || 0;
193
+ if (!h) h = Math.round(this.el.getBoundingClientRect().height);
194
+ this.el.classList.remove(MEASURING_CLASS);
195
+ return Math.max(0, Math.ceil(h));
196
+ }
197
+ initHeightObserver() {
198
+ if (!this.el) return;
199
+ this.resizeObserver = new ResizeObserver(() => {
200
+ const h = this.measureHeight();
201
+ if (h > 0) this.el?.style.setProperty(CSS_VAR_CONTAINER_HEIGHT, `${h}px`);
202
+ });
203
+ this.resizeObserver.observe(this.el);
204
+ }
205
+ };
206
+
207
+ //#endregion
208
+ export { CLASSNAMES, TextSlicer };
209
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "text-slicer",
3
- "version": "1.5.0",
3
+ "version": "2.0.0",
4
4
  "description": "TextSlicer is designed to split text within an HTML element into separate words and/or characters, wrapping each word and/or character in separate span elements.",
5
5
  "author": "ux-ui.pro",
6
6
  "license": "MIT",
@@ -13,46 +13,58 @@
13
13
  },
14
14
  "homepage": "https://github.com/ux-ui-pro/text-slicer",
15
15
  "sideEffects": false,
16
+ "type": "module",
17
+ "packageManager": "npm@10.9.3",
18
+ "engines": {
19
+ "node": ">=20.19.0"
20
+ },
16
21
  "scripts": {
17
22
  "clean": "rimraf dist",
18
- "build": "vite build",
19
- "lint:ts": "eslint src/**/*.ts",
20
- "lint:fix:ts": "eslint src/**/*.ts --fix",
21
- "format:ts": "prettier --write src/**/*.ts",
22
- "lint:fix": "yarn lint:fix:ts && yarn format:ts"
23
+ "build": "tsdown",
24
+ "verify": "npm run lint && npm run typecheck && npm run build && npm run test:smoke && npm run pack:check",
25
+ "lint": "biome check src tests tsdown.config.ts",
26
+ "lint:fix": "biome check --write src tests tsdown.config.ts",
27
+ "format": "biome format --write src tests tsdown.config.ts",
28
+ "typecheck": "tsc -p tsconfig.json --noEmit",
29
+ "test:smoke": "node --test \"tests/**/*.js\"",
30
+ "pack:check": "npm pack --dry-run && publint && attw --pack . --profile node16",
31
+ "prepublishOnly": "npm run clean && npm run verify"
23
32
  },
24
33
  "source": "src/index.ts",
25
- "main": "dist/index.cjs.js",
26
- "module": "dist/index.es.js",
27
- "browser": "./dist/index.umd.js",
28
- "types": "dist/index.d.ts",
34
+ "main": "./dist/index.cjs",
35
+ "module": "./dist/index.js",
36
+ "types": "./dist/index.d.ts",
29
37
  "exports": {
30
38
  ".": {
31
- "require": "./dist/index.cjs.js",
32
- "import": "./dist/index.es.js",
33
- "default": "./dist/index.umd.js"
34
- },
35
- "./dist/*": "./dist/*"
39
+ "import": {
40
+ "types": "./dist/index.d.ts",
41
+ "default": "./dist/index.js"
42
+ },
43
+ "require": {
44
+ "types": "./dist/index.d.cts",
45
+ "default": "./dist/index.cjs"
46
+ }
47
+ }
36
48
  },
37
49
  "files": [
38
- "dist/"
50
+ "dist",
51
+ "README.md",
52
+ "LICENSE"
39
53
  ],
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
40
57
  "devDependencies": {
41
- "@eslint/js": "9.34.0",
42
- "@rollup/plugin-terser": "0.4.4",
43
- "@types/node": "24.3.0",
44
- "@typescript-eslint/eslint-plugin": "8.40.0",
45
- "@typescript-eslint/parser": "8.40.0",
46
- "eslint": "9.34.0",
47
- "eslint-config-prettier": "10.1.8",
48
- "eslint-import-resolver-typescript": "4.4.4",
49
- "eslint-plugin-import": "2.32.0",
50
- "globals": "16.3.0",
51
- "prettier": "3.6.2",
52
- "rimraf": "6.0.1",
53
- "typescript": "5.9.2",
54
- "vite": "7.1.3",
55
- "vite-plugin-dts": "4.5.4"
58
+ "@arethetypeswrong/cli": "^0.18.3",
59
+ "@biomejs/biome": "2.4.16",
60
+ "@types/node": "25.9.2",
61
+ "@ux-ui/biome-config": "^0.1.0",
62
+ "@ux-ui/tsconfig-base": "^0.1.0",
63
+ "@ux-ui/tsdown-config": "^0.1.0",
64
+ "publint": "^0.3.21",
65
+ "rimraf": "6.1.3",
66
+ "tsdown": "0.22.3",
67
+ "typescript": "6.0.3"
56
68
  },
57
69
  "keywords": [
58
70
  "text",
package/dist/index.cjs.js DELETED
@@ -1 +0,0 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const u={splitMode:"both",cssVariables:!1,dataAttributes:!1,keepWhitespaceNodes:!0,containerHeightVar:!1},n=Object.freeze({word:"ts-word",char:"ts-char",whitespace:"ts-whitespace"}),h=" ",a="--container-height",c="ts-measuring",m=t=>typeof window>"u"||typeof document>"u"?null:t?typeof t=="string"?document.querySelector(t):t:null,l=t=>{const e=Intl.Segmenter;if(typeof e=="function"){const s=new e("en",{granularity:"grapheme"});return Array.from(s.segment(t),i=>i.segment)}return Array.from(t)},d=t=>t.split(h),o=t=>{const e={};return Object.keys(t).forEach(s=>{const i=t[s];i!==void 0&&(e[s]=i)}),e};exports.CLASSNAMES=n,exports.TextSlicer=class{el;original;opts;callbacks;charIndex;mounted;heightLocked;resizeObserver;constructor(t={},e){const s=m(t.container);this.el=s,this.original=(i=>!!i&&typeof HTMLElement<"u"&&i instanceof HTMLElement)(s)?s.textContent?.toString()??"":"",this.opts={...u,...o(t)},this.callbacks=e,this.charIndex=0,this.mounted=!1,this.heightLocked=!1}get metrics(){const t=this.original;return{wordTotal:t.length?d(t).length:0,charTotal:t.length,renderedAt:Date.now()}}init(){this.el&&(this.mounted=!0,this.split(),this.opts.containerHeightVar&&this.initHeightObserver())}reinit(t,e){this.el&&(typeof t=="string"&&(this.original=t),e&&(this.opts={...this.opts,...o(e)}),this.split())}clear(){this.el&&this.el.replaceChildren()}split(){if(!this.el)return;this.clear(),this.charIndex=0;const t=this.original,e=document.createDocumentFragment(),s=d(t);this.opts.splitMode==="chars"?this.appendChars(e,t):this.appendWords(e,s),this.el.appendChild(e),this.opts.cssVariables&&(this.el.style.setProperty("--word-total",String(s.length)),this.el.style.setProperty("--char-total",String(t.length))),this.callbacks?.onAfterRender?.(this.metrics)}destroy(){this.el&&(this.clear(),this.unlockHeight(),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=void 0),this.opts.containerHeightVar&&this.el.style.removeProperty(a),this.mounted=!1)}updateOptions(t){this.opts={...this.opts,...o(t)},this.mounted&&this.split()}lockHeight(){if(!this.el)return;const t=this.measureHeight();t>0&&(this.el.style.height=`${t}px`,this.heightLocked=!0)}unlockHeight(){this.el&&(this.el.style.removeProperty("height"),this.heightLocked=!1)}appendWords(t,e){e.forEach((s,i)=>{if(this.opts.splitMode==="both"){const r=this.createWordSpan(i,s);for(const p of l(s)){const g=this.createCharSpan(p);r.append(g)}t.append(r)}else{const r=this.createWordSpan(i);r.append(document.createTextNode(s)),t.append(r)}i<e.length-1&&t.append(this.createSpaceSpan())})}appendChars(t,e){for(const s of l(e)){const i=this.createCharSpan(s);t.append(i)}}createWordSpan(t,e=""){const s=document.createElement("span");return s.classList.add(n.word),this.opts.dataAttributes&&e&&s.setAttribute("data-word",e),this.opts.cssVariables&&s.style.setProperty("--word-index",String(t)),s}createCharSpan(t){const e=document.createElement("span");return e.textContent=t,this.opts.dataAttributes&&e.setAttribute("data-char",t),t===h?(e.classList.add(n.whitespace),this.opts.keepWhitespaceNodes||(e.textContent=h)):(e.classList.add(n.char),this.opts.cssVariables&&e.style.setProperty("--char-index",String(this.charIndex)),this.charIndex+=1),e}createSpaceSpan(){const t=document.createElement("span");return t.classList.add(n.whitespace),t.textContent=h,t}measureHeight(){if(!this.el)return 0;this.el.classList.add(c),this.el.offsetHeight;let t=this.el.offsetHeight||this.el.clientHeight||0;return t||(t=Math.round(this.el.getBoundingClientRect().height)),this.el.classList.remove(c),Math.max(0,Math.ceil(t))}initHeightObserver(){this.el&&(this.resizeObserver=new ResizeObserver(()=>{const t=this.measureHeight();t>0&&this.el.style.setProperty(a,`${t}px`)}),this.resizeObserver.observe(this.el))}};
package/dist/index.es.js DELETED
@@ -1,111 +0,0 @@
1
- const m = { splitMode: "both", cssVariables: !1, dataAttributes: !1, keepWhitespaceNodes: !0, containerHeightVar: !1 }, h = Object.freeze({ word: "ts-word", char: "ts-char", whitespace: "ts-whitespace" }), a = " ", c = "--container-height", l = "ts-measuring", f = (i) => typeof window > "u" || typeof document > "u" ? null : i ? typeof i == "string" ? document.querySelector(i) : i : null, p = (i) => {
2
- const t = Intl.Segmenter;
3
- if (typeof t == "function") {
4
- const e = new t("en", { granularity: "grapheme" });
5
- return Array.from(e.segment(i), (s) => s.segment);
6
- }
7
- return Array.from(i);
8
- }, d = (i) => i.split(a), o = (i) => {
9
- const t = {};
10
- return Object.keys(i).forEach((e) => {
11
- const s = i[e];
12
- s !== void 0 && (t[e] = s);
13
- }), t;
14
- };
15
- class y {
16
- el;
17
- original;
18
- opts;
19
- callbacks;
20
- charIndex;
21
- mounted;
22
- heightLocked;
23
- resizeObserver;
24
- constructor(t = {}, e) {
25
- const s = f(t.container);
26
- this.el = s, this.original = ((r) => !!r && typeof HTMLElement < "u" && r instanceof HTMLElement)(s) ? s.textContent?.toString() ?? "" : "", this.opts = { ...m, ...o(t) }, this.callbacks = e, this.charIndex = 0, this.mounted = !1, this.heightLocked = !1;
27
- }
28
- get metrics() {
29
- const t = this.original;
30
- return { wordTotal: t.length ? d(t).length : 0, charTotal: t.length, renderedAt: Date.now() };
31
- }
32
- init() {
33
- this.el && (this.mounted = !0, this.split(), this.opts.containerHeightVar && this.initHeightObserver());
34
- }
35
- reinit(t, e) {
36
- this.el && (typeof t == "string" && (this.original = t), e && (this.opts = { ...this.opts, ...o(e) }), this.split());
37
- }
38
- clear() {
39
- this.el && this.el.replaceChildren();
40
- }
41
- split() {
42
- if (!this.el) return;
43
- this.clear(), this.charIndex = 0;
44
- const t = this.original, e = document.createDocumentFragment(), s = d(t);
45
- this.opts.splitMode === "chars" ? this.appendChars(e, t) : this.appendWords(e, s), this.el.appendChild(e), this.opts.cssVariables && (this.el.style.setProperty("--word-total", String(s.length)), this.el.style.setProperty("--char-total", String(t.length))), this.callbacks?.onAfterRender?.(this.metrics);
46
- }
47
- destroy() {
48
- this.el && (this.clear(), this.unlockHeight(), this.resizeObserver && (this.resizeObserver.disconnect(), this.resizeObserver = void 0), this.opts.containerHeightVar && this.el.style.removeProperty(c), this.mounted = !1);
49
- }
50
- updateOptions(t) {
51
- this.opts = { ...this.opts, ...o(t) }, this.mounted && this.split();
52
- }
53
- lockHeight() {
54
- if (!this.el) return;
55
- const t = this.measureHeight();
56
- t > 0 && (this.el.style.height = `${t}px`, this.heightLocked = !0);
57
- }
58
- unlockHeight() {
59
- this.el && (this.el.style.removeProperty("height"), this.heightLocked = !1);
60
- }
61
- appendWords(t, e) {
62
- e.forEach((s, r) => {
63
- if (this.opts.splitMode === "both") {
64
- const n = this.createWordSpan(r, s);
65
- for (const g of p(s)) {
66
- const u = this.createCharSpan(g);
67
- n.append(u);
68
- }
69
- t.append(n);
70
- } else {
71
- const n = this.createWordSpan(r);
72
- n.append(document.createTextNode(s)), t.append(n);
73
- }
74
- r < e.length - 1 && t.append(this.createSpaceSpan());
75
- });
76
- }
77
- appendChars(t, e) {
78
- for (const s of p(e)) {
79
- const r = this.createCharSpan(s);
80
- t.append(r);
81
- }
82
- }
83
- createWordSpan(t, e = "") {
84
- const s = document.createElement("span");
85
- return s.classList.add(h.word), this.opts.dataAttributes && e && s.setAttribute("data-word", e), this.opts.cssVariables && s.style.setProperty("--word-index", String(t)), s;
86
- }
87
- createCharSpan(t) {
88
- const e = document.createElement("span");
89
- return e.textContent = t, this.opts.dataAttributes && e.setAttribute("data-char", t), t === a ? (e.classList.add(h.whitespace), this.opts.keepWhitespaceNodes || (e.textContent = a)) : (e.classList.add(h.char), this.opts.cssVariables && e.style.setProperty("--char-index", String(this.charIndex)), this.charIndex += 1), e;
90
- }
91
- createSpaceSpan() {
92
- const t = document.createElement("span");
93
- return t.classList.add(h.whitespace), t.textContent = a, t;
94
- }
95
- measureHeight() {
96
- if (!this.el) return 0;
97
- this.el.classList.add(l), this.el.offsetHeight;
98
- let t = this.el.offsetHeight || this.el.clientHeight || 0;
99
- return t || (t = Math.round(this.el.getBoundingClientRect().height)), this.el.classList.remove(l), Math.max(0, Math.ceil(t));
100
- }
101
- initHeightObserver() {
102
- this.el && (this.resizeObserver = new ResizeObserver(() => {
103
- const t = this.measureHeight();
104
- t > 0 && this.el.style.setProperty(c, `${t}px`);
105
- }), this.resizeObserver.observe(this.el));
106
- }
107
- }
108
- export {
109
- h as CLASSNAMES,
110
- y as TextSlicer
111
- };
package/dist/index.umd.js DELETED
@@ -1 +0,0 @@
1
- (function(r,n){typeof exports=="object"&&typeof module<"u"?n(exports):typeof define=="function"&&define.amd?define(["exports"],n):n((r=typeof globalThis<"u"?globalThis:r||self).TextSlicer={})})(this,function(r){"use strict";const n={splitMode:"both",cssVariables:!1,dataAttributes:!1,keepWhitespaceNodes:!0,containerHeightVar:!1},o=Object.freeze({word:"ts-word",char:"ts-char",whitespace:"ts-whitespace"}),a=" ",l="--container-height",d="ts-measuring",g=e=>typeof window>"u"||typeof document>"u"?null:e?typeof e=="string"?document.querySelector(e):e:null,p=e=>{const t=Intl.Segmenter;if(typeof t=="function"){const s=new t("en",{granularity:"grapheme"});return Array.from(s.segment(e),i=>i.segment)}return Array.from(e)},u=e=>e.split(a),c=e=>{const t={};return Object.keys(e).forEach(s=>{const i=e[s];i!==void 0&&(t[s]=i)}),t};r.CLASSNAMES=o,r.TextSlicer=class{el;original;opts;callbacks;charIndex;mounted;heightLocked;resizeObserver;constructor(e={},t){const s=g(e.container);this.el=s,this.original=(i=>!!i&&typeof HTMLElement<"u"&&i instanceof HTMLElement)(s)?s.textContent?.toString()??"":"",this.opts={...n,...c(e)},this.callbacks=t,this.charIndex=0,this.mounted=!1,this.heightLocked=!1}get metrics(){const e=this.original;return{wordTotal:e.length?u(e).length:0,charTotal:e.length,renderedAt:Date.now()}}init(){this.el&&(this.mounted=!0,this.split(),this.opts.containerHeightVar&&this.initHeightObserver())}reinit(e,t){this.el&&(typeof e=="string"&&(this.original=e),t&&(this.opts={...this.opts,...c(t)}),this.split())}clear(){this.el&&this.el.replaceChildren()}split(){if(!this.el)return;this.clear(),this.charIndex=0;const e=this.original,t=document.createDocumentFragment(),s=u(e);this.opts.splitMode==="chars"?this.appendChars(t,e):this.appendWords(t,s),this.el.appendChild(t),this.opts.cssVariables&&(this.el.style.setProperty("--word-total",String(s.length)),this.el.style.setProperty("--char-total",String(e.length))),this.callbacks?.onAfterRender?.(this.metrics)}destroy(){this.el&&(this.clear(),this.unlockHeight(),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=void 0),this.opts.containerHeightVar&&this.el.style.removeProperty(l),this.mounted=!1)}updateOptions(e){this.opts={...this.opts,...c(e)},this.mounted&&this.split()}lockHeight(){if(!this.el)return;const e=this.measureHeight();e>0&&(this.el.style.height=`${e}px`,this.heightLocked=!0)}unlockHeight(){this.el&&(this.el.style.removeProperty("height"),this.heightLocked=!1)}appendWords(e,t){t.forEach((s,i)=>{if(this.opts.splitMode==="both"){const h=this.createWordSpan(i,s);for(const f of p(s)){const m=this.createCharSpan(f);h.append(m)}e.append(h)}else{const h=this.createWordSpan(i);h.append(document.createTextNode(s)),e.append(h)}i<t.length-1&&e.append(this.createSpaceSpan())})}appendChars(e,t){for(const s of p(t)){const i=this.createCharSpan(s);e.append(i)}}createWordSpan(e,t=""){const s=document.createElement("span");return s.classList.add(o.word),this.opts.dataAttributes&&t&&s.setAttribute("data-word",t),this.opts.cssVariables&&s.style.setProperty("--word-index",String(e)),s}createCharSpan(e){const t=document.createElement("span");return t.textContent=e,this.opts.dataAttributes&&t.setAttribute("data-char",e),e===a?(t.classList.add(o.whitespace),this.opts.keepWhitespaceNodes||(t.textContent=a)):(t.classList.add(o.char),this.opts.cssVariables&&t.style.setProperty("--char-index",String(this.charIndex)),this.charIndex+=1),t}createSpaceSpan(){const e=document.createElement("span");return e.classList.add(o.whitespace),e.textContent=a,e}measureHeight(){if(!this.el)return 0;this.el.classList.add(d),this.el.offsetHeight;let e=this.el.offsetHeight||this.el.clientHeight||0;return e||(e=Math.round(this.el.getBoundingClientRect().height)),this.el.classList.remove(d),Math.max(0,Math.ceil(e))}initHeightObserver(){this.el&&(this.resizeObserver=new ResizeObserver(()=>{const e=this.measureHeight();e>0&&this.el.style.setProperty(l,`${e}px`)}),this.resizeObserver.observe(this.el))}},Object.defineProperty(r,Symbol.toStringTag,{value:"Module"})});