text-slicer 1.4.0-dev.8 → 1.4.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,4 +1,3 @@
1
- <br>
2
1
  <p align="center"><strong>text-slicer</strong></p>
3
2
 
4
3
  <div align="center">
@@ -9,71 +8,168 @@
9
8
 
10
9
  </div>
11
10
 
12
- <p align="center">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.</p>
13
- <p align="center"><sup>1.5kB gzipped</sup></p>
11
+ <p align="center">Split text inside an HTML element into words and/or characters, wrapping each in a dedicated <code>&lt;span&gt;</code>. Built for robust animation pipelines and i18n-safe rendering.</p>
14
12
  <p align="center"><a href="https://codepen.io/ux-ui/full/vYMoGoG">Demo</a></p>
15
13
  <br>
16
14
 
17
- &#10148; **Install**
15
+ ## Install
18
16
 
19
- ```console
17
+ ```bash
20
18
  yarn add text-slicer
19
+ # or
20
+ npm i text-slicer
21
21
  ```
22
22
  <br>
23
23
 
24
- &#10148; **Import**
24
+ ## Quick start
25
+
26
+ ```ts
27
+ import { TextSlicer } from 'text-slicer';
28
+
29
+ const slicer = new TextSlicer({ container: '.text-slicer' });
25
30
 
26
- ```javascript
27
- import TextSlicer from 'text-slicer';
31
+ slicer.init();
32
+ ```
33
+
34
+ Initialize per element:
35
+
36
+ ```ts
37
+ document.querySelectorAll('.text-slicer').forEach((el) => {
38
+ const slicer = new TextSlicer({ container: el });
39
+
40
+ slicer.init();
41
+ });
28
42
  ```
29
43
  <br>
30
44
 
31
- &#10148; **Usage**
45
+ ## API
46
+
47
+ ### Types
48
+
49
+ ```ts
50
+ export type SplitMode = 'words' | 'chars' | 'both';
51
+
52
+ export interface TextSlicerOptions {
53
+ container?: HTMLElement | string;
54
+ splitMode?: SplitMode;
55
+ cssVariables?: boolean;
56
+ dataAttributes?: boolean;
57
+ /** Keep dedicated whitespace nodes between words (for precise animations). Default: true */
58
+ keepWhitespaceNodes?: boolean;
59
+ /** Freeze measured word widths to avoid reflow jitter on responsive layouts. Default: false */
60
+ freezeWordWidths?: boolean;
61
+ }
62
+
63
+ export interface TextSlicerMetrics {
64
+ wordTotal: number;
65
+ charTotal: number;
66
+ renderedAt: number;
67
+ }
68
+
69
+ export interface TextSlicerCallbacks {
70
+ onAfterRender?: (metrics: TextSlicerMetrics) => void;
71
+ }
72
+ ```
73
+
74
+ ### Classnames & CSS vars
75
+
76
+ ```ts
77
+ import { CLASSNAMES } from 'text-slicer';
32
78
 
33
- ```javascript
34
- const textSlicer = new TextSlicer();
79
+ // Classes applied to generated spans
80
+ CLASSNAMES.word // 'ts-word'
81
+ CLASSNAMES.char // 'ts-char'
82
+ CLASSNAMES.whitespace // 'ts-whitespace'
35
83
 
36
- textSlicer.init();
84
+ // CSS variables placed on container and items (when cssVariables: true)
85
+ --word-total
86
+ --char-total
87
+ --word-index
88
+ --char-index
37
89
  ```
38
90
 
39
- <sub>Initialization with specified parameters</sub>
40
- ```javascript
41
- document.addEventListener('DOMContentLoaded', () => {
42
- const textSlicer = new TextSlicer({
43
- container: '.text-slicer',
44
- splitMode: 'both',
45
- cssVariables: true,
46
- dataAttributes: true,
47
- });
48
-
49
- textSlicer.init();
50
- });
91
+ ### Constructor
92
+
93
+ ```ts
94
+ new TextSlicer(options?: TextSlicerOptions, callbacks?: TextSlicerCallbacks)
51
95
  ```
52
96
 
53
- <sub>How to apply the TextSlicer class to all elements on a page</sub>
54
- ```javascript
55
- document.addEventListener('DOMContentLoaded', () => {
56
- document.querySelectorAll('.text-slicer').forEach((element) => {
57
- const textSlicer = new TextSlicer({
58
- container: element,
59
- });
97
+ ### Methods
98
+
99
+ - `init(): void` – Perform initial split.
100
+ - `reinit(newText?: string, nextOpts?: Partial<TextSlicerOptions>): void` – Update text and/or options and re-split.
101
+ - `updateOptions(next: Partial<TextSlicerOptions>): void` – Merge options and re-split (if mounted).
102
+ - `clear(): void` – Remove generated nodes and unfreeze widths.
103
+ - `split(): void` – (Re)build DOM (called internally by `init`/`reinit`/`updateOptions`).
104
+ - `destroy(): void` – Detach observers, clear DOM, and mark unmounted.
105
+ - `get metrics(): TextSlicerMetrics` – Read-only metrics collected on the last render.
106
+
107
+ ### Options in detail
108
+
109
+ - `splitMode` – `'words' | 'chars' | 'both'`. When `'both'`, each word is wrapped and further split into graphemes.
110
+ - `cssVariables` – When `true`, indexes and totals are exposed as CSS custom properties for stagger animations.
111
+ - `dataAttributes` – When `true`, adds `data-word` / `data-char` attributes.
112
+ - `keepWhitespaceNodes` – When `true`, explicit whitespace nodes are inserted between words (class `ts-whitespace`).
113
+ - `freezeWordWidths` – When `true`, measured widths of `.ts-word` nodes are frozen (after fonts load + next frame) and
114
+ kept in sync on container/window resize to prevent layout jitter during animations.
115
+
116
+ ### i18n-friendly grapheme splitting
117
+
118
+ Characters are split using `Intl.Segmenter` (when available) with `{ granularity: 'grapheme' }`, so compound emoji and
119
+ grapheme clusters render as expected. Environments without `Intl.Segmenter` gracefully fall back to `Array.from(text)`.
120
+
121
+ ### Callbacks
122
+
123
+ ```ts
124
+ const slicer = new TextSlicer(
125
+ { container: '.title', cssVariables: true },
126
+ {
127
+ onAfterRender(metrics) {
128
+ // e.g. attach animation based on metrics.charTotal
129
+ console.log(metrics);
130
+ },
131
+ }
132
+ );
133
+ slicer.init();
134
+ ```
60
135
 
61
- textSlicer.init();
62
- });
136
+ ### Responsive width freezing
137
+
138
+ ```ts
139
+ const slicer = new TextSlicer({
140
+ container: '.headline',
141
+ splitMode: 'both',
142
+ freezeWordWidths: true,
63
143
  });
144
+ slicer.init();
64
145
  ```
65
- <br>
66
146
 
67
- &#10148; **Parameters**
147
+ When enabled, widths are measured after fonts are ready and then frozen (`flex: 0 0 auto; width: <px>`). A `ResizeObserver`
148
+ watches the container and a `resize` handler remeasures on viewport changes.
149
+ <br>
68
150
 
69
- | Option | Type | Default | Description |
70
- |:-----------------:|:-----------------------:|:--------------:|:------------------------------------------------------------------------------------------------------------------------------------------------------|
71
- | `container` | `HTMLElement \| string` | `.text-slicer` | The container element or a selector for the element containing the text to be split. You can pass either a DOM element or a CSS selector string. |
72
- | `splitMode` | `string` | `both` | Determines how the text will be split: 'words' to split into words, 'chars' to split into characters, 'both' to split into both words and characters. |
73
- | `cssVariables` | `boolean` | `false` | If `true`, CSS variables for word and character indexes will be added to the spans. |
74
- | `dataAttributes` | `boolean` | `false` | If `true`, `data-word` and `data-char` attributes will be added to the generated word and character spans for additional data handling or styling. |
151
+ ## CSS usage example
152
+
153
+ ```css
154
+ .ts-char {
155
+ display: inline-block;
156
+ transform: translateY(0.75em);
157
+ opacity: 0;
158
+ transition: transform 400ms ease, opacity 400ms ease;
159
+ }
160
+
161
+ .ts-char.appear {
162
+ transform: translateY(0);
163
+ opacity: 1;
164
+ }
165
+
166
+ /* stagger via CSS variables */
167
+ .ts-char {
168
+ transition-delay: calc(var(--char-index, 0) * 10ms);
169
+ }
170
+ ```
75
171
  <br>
76
172
 
77
- &#10148; **License**
173
+ ## License
78
174
 
79
- text-slicer is released under MIT license
175
+ MIT
package/dist/index.cjs.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const l={splitMode:"both",cssVariables:!1,dataAttributes:!1,keepWhitespaceNodes:!0},a=Object.freeze({word:"ts-word",char:"ts-char",whitespace:"ts-whitespace"}),i=" ",u=t=>typeof window>"u"||typeof document>"u"?null:t?typeof t=="string"?document.querySelector(t):t:null,c=t=>{const e=Intl.Segmenter;if(typeof e=="function"){const s=new e("en",{granularity:"grapheme"});return Array.from(s.segment(t),n=>n.segment)}return Array.from(t)},h=t=>t.split(i),o=t=>{const e={};return Object.keys(t).forEach(s=>{const n=t[s];n!==void 0&&(e[s]=n)}),e};exports.CLASSNAMES=a,exports.TextSlicer=class{el;original;opts;callbacks;charIndex;mounted;constructor(t={},e){const s=u(t.container);this.el=s,this.original=(n=>!!n&&typeof HTMLElement<"u"&&n instanceof HTMLElement)(s)?s.textContent?.toString()??"":"",this.opts={...l,...o(t)},this.callbacks=e,this.charIndex=0,this.mounted=!1}get metrics(){const t=this.original;return{wordTotal:t.length?h(t).length:0,charTotal:t.length,renderedAt:Date.now()}}init(){this.el&&(this.mounted=!0,this.split())}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=h(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.mounted=!1)}updateOptions(t){this.opts={...this.opts,...o(t)},this.mounted&&this.split()}appendWords(t,e){e.forEach((s,n)=>{if(this.opts.splitMode==="both"){const r=this.createWordSpan(n,s);for(const p of c(s)){const d=this.createCharSpan(p);r.append(d)}t.append(r)}else{const r=this.createWordSpan(n);r.append(document.createTextNode(s)),t.append(r)}n<e.length-1&&t.append(this.createSpaceSpan())})}appendChars(t,e){for(const s of c(e)){const n=this.createCharSpan(s);t.append(n)}}createWordSpan(t,e=""){const s=document.createElement("span");return s.classList.add(a.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===i?(e.classList.add(a.whitespace),this.opts.keepWhitespaceNodes||(e.textContent=i)):(e.classList.add(a.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(a.whitespace),t.textContent=i,t}};
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const p={splitMode:"both",cssVariables:!1,dataAttributes:!1,keepWhitespaceNodes:!0,freezeWordWidths:!1},i=Object.freeze({word:"ts-word",char:"ts-char",whitespace:"ts-whitespace"}),o=" ",u=t=>typeof window>"u"||typeof document>"u"?null:t?typeof t=="string"?document.querySelector(t):t:null,a=t=>{const e=Intl.Segmenter;if(typeof e=="function"){const s=new e("en",{granularity:"grapheme"});return Array.from(s.segment(t),r=>r.segment)}return Array.from(t)},d=t=>t.split(o),h=t=>{const e={};return Object.keys(t).forEach(s=>{const r=t[s];r!==void 0&&(e[s]=r)}),e};exports.CLASSNAMES=i,exports.TextSlicer=class{el;original;opts;callbacks;charIndex;mounted;ro=null;frozen=!1;freezeWordWidthsBound=()=>{this.freezeWordWidths()};constructor(t={},e){const s=u(t.container);this.el=s,this.original=(r=>!!r&&typeof HTMLElement<"u"&&r instanceof HTMLElement)(s)?s.textContent?.toString()??"":"",this.opts={...p,...h(t)},this.callbacks=e,this.charIndex=0,this.mounted=!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())}reinit(t,e){this.el&&(typeof t=="string"&&(this.original=t),e&&(this.opts={...this.opts,...h(e)}),this.split())}clear(){this.el&&(this.unfreezeWordWidths(),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.scheduleFreezeWordWidths().then(()=>{this.callbacks?.onAfterRender?.(this.metrics)}).catch(()=>{})}destroy(){this.el&&(this.detachResizeObserver(),this.clear(),this.mounted=!1)}updateOptions(t){this.opts={...this.opts,...h(t)},this.mounted&&this.split()}appendWords(t,e){e.forEach((s,r)=>{if(this.opts.splitMode==="both"){const n=this.createWordSpan(r,s);for(const c of a(s)){const l=this.createCharSpan(c);n.append(l)}t.append(n)}else{const n=this.createWordSpan(r);n.append(document.createTextNode(s)),t.append(n)}r<e.length-1&&t.append(this.createSpaceSpan())})}appendChars(t,e){for(const s of a(e)){const r=this.createCharSpan(s);t.append(r)}}createWordSpan(t,e=""){const s=document.createElement("span");return s.classList.add(i.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===o?(e.classList.add(i.whitespace),this.opts.keepWhitespaceNodes||(e.textContent=o)):(e.classList.add(i.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(i.whitespace),t.textContent=o,t}async scheduleFreezeWordWidths(){if(this.el&&this.opts.freezeWordWidths){try{await document.fonts?.ready}catch{}await new Promise(t=>requestAnimationFrame(()=>t())),this.freezeWordWidths(),this.attachResizeObserver()}}freezeWordWidths(){this.el&&(this.el.querySelectorAll(`.${i.word}`).forEach(t=>{t.style.width="",t.style.flex="";const{width:e}=t.getBoundingClientRect();t.style.width=`${Math.ceil(e)}px`,t.style.flex="0 0 auto"}),this.frozen=!0)}unfreezeWordWidths(){!this.el||!this.frozen||(this.el.querySelectorAll(`.${i.word}`).forEach(t=>{t.style.width="",t.style.flex=""}),this.frozen=!1)}attachResizeObserver(){this.el&&this.opts.freezeWordWidths&&(this.ro||(this.ro=new ResizeObserver(()=>{this.freezeWordWidths()}),this.ro.observe(this.el)),window.addEventListener("resize",this.freezeWordWidthsBound,{passive:!0}))}detachResizeObserver(){this.ro&&(this.ro.disconnect(),this.ro=null),window.removeEventListener("resize",this.freezeWordWidthsBound)}};
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export interface TextSlicerOptions {
5
5
  cssVariables?: boolean;
6
6
  dataAttributes?: boolean;
7
7
  keepWhitespaceNodes?: boolean;
8
+ freezeWordWidths?: boolean;
8
9
  }
9
10
  export interface TextSlicerMetrics {
10
11
  wordTotal: number;
@@ -26,6 +27,9 @@ export declare class TextSlicer {
26
27
  private callbacks;
27
28
  private charIndex;
28
29
  private mounted;
30
+ private ro;
31
+ private frozen;
32
+ private freezeWordWidthsBound;
29
33
  constructor(options?: TextSlicerOptions, callbacks?: TextSlicerCallbacks);
30
34
  get metrics(): TextSlicerMetrics;
31
35
  init(): void;
@@ -39,4 +43,9 @@ export declare class TextSlicer {
39
43
  private createWordSpan;
40
44
  private createCharSpan;
41
45
  private createSpaceSpan;
46
+ private scheduleFreezeWordWidths;
47
+ private freezeWordWidths;
48
+ private unfreezeWordWidths;
49
+ private attachResizeObserver;
50
+ private detachResizeObserver;
42
51
  }
package/dist/index.es.js CHANGED
@@ -1,89 +1,126 @@
1
- const u = { splitMode: "both", cssVariables: !1, dataAttributes: !1, keepWhitespaceNodes: !0 }, i = Object.freeze({ word: "ts-word", char: "ts-char", whitespace: "ts-whitespace" }), o = " ", m = (n) => typeof window > "u" || typeof document > "u" ? null : n ? typeof n == "string" ? document.querySelector(n) : n : null, h = (n) => {
1
+ const u = { splitMode: "both", cssVariables: !1, dataAttributes: !1, keepWhitespaceNodes: !0, freezeWordWidths: !1 }, n = Object.freeze({ word: "ts-word", char: "ts-char", whitespace: "ts-whitespace" }), h = " ", f = (r) => typeof window > "u" || typeof document > "u" ? null : r ? typeof r == "string" ? document.querySelector(r) : r : null, d = (r) => {
2
2
  const t = Intl.Segmenter;
3
3
  if (typeof t == "function") {
4
4
  const e = new t("en", { granularity: "grapheme" });
5
- return Array.from(e.segment(n), (s) => s.segment);
5
+ return Array.from(e.segment(r), (s) => s.segment);
6
6
  }
7
- return Array.from(n);
8
- }, p = (n) => n.split(o), c = (n) => {
7
+ return Array.from(r);
8
+ }, c = (r) => r.split(h), a = (r) => {
9
9
  const t = {};
10
- return Object.keys(n).forEach((e) => {
11
- const s = n[e];
10
+ return Object.keys(r).forEach((e) => {
11
+ const s = r[e];
12
12
  s !== void 0 && (t[e] = s);
13
13
  }), t;
14
14
  };
15
- class g {
15
+ class W {
16
16
  el;
17
17
  original;
18
18
  opts;
19
19
  callbacks;
20
20
  charIndex;
21
21
  mounted;
22
+ ro = null;
23
+ frozen = !1;
24
+ freezeWordWidthsBound = () => {
25
+ this.freezeWordWidths();
26
+ };
22
27
  constructor(t = {}, e) {
23
- const s = m(t.container);
24
- this.el = s, this.original = ((r) => !!r && typeof HTMLElement < "u" && r instanceof HTMLElement)(s) ? s.textContent?.toString() ?? "" : "", this.opts = { ...u, ...c(t) }, this.callbacks = e, this.charIndex = 0, this.mounted = !1;
28
+ const s = f(t.container);
29
+ this.el = s, this.original = ((i) => !!i && typeof HTMLElement < "u" && i instanceof HTMLElement)(s) ? s.textContent?.toString() ?? "" : "", this.opts = { ...u, ...a(t) }, this.callbacks = e, this.charIndex = 0, this.mounted = !1;
25
30
  }
26
31
  get metrics() {
27
32
  const t = this.original;
28
- return { wordTotal: t.length ? p(t).length : 0, charTotal: t.length, renderedAt: Date.now() };
33
+ return { wordTotal: t.length ? c(t).length : 0, charTotal: t.length, renderedAt: Date.now() };
29
34
  }
30
35
  init() {
31
36
  this.el && (this.mounted = !0, this.split());
32
37
  }
33
38
  reinit(t, e) {
34
- this.el && (typeof t == "string" && (this.original = t), e && (this.opts = { ...this.opts, ...c(e) }), this.split());
39
+ this.el && (typeof t == "string" && (this.original = t), e && (this.opts = { ...this.opts, ...a(e) }), this.split());
35
40
  }
36
41
  clear() {
37
- this.el && this.el.replaceChildren();
42
+ this.el && (this.unfreezeWordWidths(), this.el.replaceChildren());
38
43
  }
39
44
  split() {
40
45
  if (!this.el) return;
41
46
  this.clear(), this.charIndex = 0;
42
- const t = this.original, e = document.createDocumentFragment(), s = p(t);
43
- 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);
47
+ const t = this.original, e = document.createDocumentFragment(), s = c(t);
48
+ 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.scheduleFreezeWordWidths().then(() => {
49
+ this.callbacks?.onAfterRender?.(this.metrics);
50
+ }).catch(() => {
51
+ });
44
52
  }
45
53
  destroy() {
46
- this.el && (this.clear(), this.mounted = !1);
54
+ this.el && (this.detachResizeObserver(), this.clear(), this.mounted = !1);
47
55
  }
48
56
  updateOptions(t) {
49
- this.opts = { ...this.opts, ...c(t) }, this.mounted && this.split();
57
+ this.opts = { ...this.opts, ...a(t) }, this.mounted && this.split();
50
58
  }
51
59
  appendWords(t, e) {
52
- e.forEach((s, r) => {
60
+ e.forEach((s, i) => {
53
61
  if (this.opts.splitMode === "both") {
54
- const a = this.createWordSpan(r, s);
55
- for (const d of h(s)) {
56
- const l = this.createCharSpan(d);
57
- a.append(l);
62
+ const o = this.createWordSpan(i, s);
63
+ for (const l of d(s)) {
64
+ const p = this.createCharSpan(l);
65
+ o.append(p);
58
66
  }
59
- t.append(a);
67
+ t.append(o);
60
68
  } else {
61
- const a = this.createWordSpan(r);
62
- a.append(document.createTextNode(s)), t.append(a);
69
+ const o = this.createWordSpan(i);
70
+ o.append(document.createTextNode(s)), t.append(o);
63
71
  }
64
- r < e.length - 1 && t.append(this.createSpaceSpan());
72
+ i < e.length - 1 && t.append(this.createSpaceSpan());
65
73
  });
66
74
  }
67
75
  appendChars(t, e) {
68
- for (const s of h(e)) {
69
- const r = this.createCharSpan(s);
70
- t.append(r);
76
+ for (const s of d(e)) {
77
+ const i = this.createCharSpan(s);
78
+ t.append(i);
71
79
  }
72
80
  }
73
81
  createWordSpan(t, e = "") {
74
82
  const s = document.createElement("span");
75
- return s.classList.add(i.word), this.opts.dataAttributes && e && s.setAttribute("data-word", e), this.opts.cssVariables && s.style.setProperty("--word-index", String(t)), s;
83
+ 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;
76
84
  }
77
85
  createCharSpan(t) {
78
86
  const e = document.createElement("span");
79
- return e.textContent = t, this.opts.dataAttributes && e.setAttribute("data-char", t), t === o ? (e.classList.add(i.whitespace), this.opts.keepWhitespaceNodes || (e.textContent = o)) : (e.classList.add(i.char), this.opts.cssVariables && e.style.setProperty("--char-index", String(this.charIndex)), this.charIndex += 1), e;
87
+ 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;
80
88
  }
81
89
  createSpaceSpan() {
82
90
  const t = document.createElement("span");
83
- return t.classList.add(i.whitespace), t.textContent = o, t;
91
+ return t.classList.add(n.whitespace), t.textContent = h, t;
92
+ }
93
+ async scheduleFreezeWordWidths() {
94
+ if (this.el && this.opts.freezeWordWidths) {
95
+ try {
96
+ await document.fonts?.ready;
97
+ } catch {
98
+ }
99
+ await new Promise((t) => requestAnimationFrame(() => t())), this.freezeWordWidths(), this.attachResizeObserver();
100
+ }
101
+ }
102
+ freezeWordWidths() {
103
+ this.el && (this.el.querySelectorAll(`.${n.word}`).forEach((t) => {
104
+ t.style.width = "", t.style.flex = "";
105
+ const { width: e } = t.getBoundingClientRect();
106
+ t.style.width = `${Math.ceil(e)}px`, t.style.flex = "0 0 auto";
107
+ }), this.frozen = !0);
108
+ }
109
+ unfreezeWordWidths() {
110
+ !this.el || !this.frozen || (this.el.querySelectorAll(`.${n.word}`).forEach((t) => {
111
+ t.style.width = "", t.style.flex = "";
112
+ }), this.frozen = !1);
113
+ }
114
+ attachResizeObserver() {
115
+ this.el && this.opts.freezeWordWidths && (this.ro || (this.ro = new ResizeObserver(() => {
116
+ this.freezeWordWidths();
117
+ }), this.ro.observe(this.el)), window.addEventListener("resize", this.freezeWordWidthsBound, { passive: !0 }));
118
+ }
119
+ detachResizeObserver() {
120
+ this.ro && (this.ro.disconnect(), this.ro = null), window.removeEventListener("resize", this.freezeWordWidthsBound);
84
121
  }
85
122
  }
86
123
  export {
87
- i as CLASSNAMES,
88
- g as TextSlicer
124
+ n as CLASSNAMES,
125
+ W as TextSlicer
89
126
  };
package/dist/index.umd.js CHANGED
@@ -1 +1 @@
1
- (function(i,r){typeof exports=="object"&&typeof module<"u"?r(exports):typeof define=="function"&&define.amd?define(["exports"],r):r((i=typeof globalThis<"u"?globalThis:i||self).TextSlicer={})})(this,function(i){"use strict";const r={splitMode:"both",cssVariables:!1,dataAttributes:!1,keepWhitespaceNodes:!0},o=Object.freeze({word:"ts-word",char:"ts-char",whitespace:"ts-whitespace"}),c=" ",l=t=>typeof window>"u"||typeof document>"u"?null:t?typeof t=="string"?document.querySelector(t):t:null,d=t=>{const e=Intl.Segmenter;if(typeof e=="function"){const s=new e("en",{granularity:"grapheme"});return Array.from(s.segment(t),n=>n.segment)}return Array.from(t)},p=t=>t.split(c),h=t=>{const e={};return Object.keys(t).forEach(s=>{const n=t[s];n!==void 0&&(e[s]=n)}),e};i.CLASSNAMES=o,i.TextSlicer=class{el;original;opts;callbacks;charIndex;mounted;constructor(t={},e){const s=l(t.container);this.el=s,this.original=(n=>!!n&&typeof HTMLElement<"u"&&n instanceof HTMLElement)(s)?s.textContent?.toString()??"":"",this.opts={...r,...h(t)},this.callbacks=e,this.charIndex=0,this.mounted=!1}get metrics(){const t=this.original;return{wordTotal:t.length?p(t).length:0,charTotal:t.length,renderedAt:Date.now()}}init(){this.el&&(this.mounted=!0,this.split())}reinit(t,e){this.el&&(typeof t=="string"&&(this.original=t),e&&(this.opts={...this.opts,...h(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=p(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.mounted=!1)}updateOptions(t){this.opts={...this.opts,...h(t)},this.mounted&&this.split()}appendWords(t,e){e.forEach((s,n)=>{if(this.opts.splitMode==="both"){const a=this.createWordSpan(n,s);for(const u of d(s)){const f=this.createCharSpan(u);a.append(f)}t.append(a)}else{const a=this.createWordSpan(n);a.append(document.createTextNode(s)),t.append(a)}n<e.length-1&&t.append(this.createSpaceSpan())})}appendChars(t,e){for(const s of d(e)){const n=this.createCharSpan(s);t.append(n)}}createWordSpan(t,e=""){const s=document.createElement("span");return s.classList.add(o.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===c?(e.classList.add(o.whitespace),this.opts.keepWhitespaceNodes||(e.textContent=c)):(e.classList.add(o.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(o.whitespace),t.textContent=c,t}},Object.defineProperty(i,Symbol.toStringTag,{value:"Module"})});
1
+ (function(n,o){typeof exports=="object"&&typeof module<"u"?o(exports):typeof define=="function"&&define.amd?define(["exports"],o):o((n=typeof globalThis<"u"?globalThis:n||self).TextSlicer={})})(this,function(n){"use strict";const o={splitMode:"both",cssVariables:!1,dataAttributes:!1,keepWhitespaceNodes:!0,freezeWordWidths:!1},i=Object.freeze({word:"ts-word",char:"ts-char",whitespace:"ts-whitespace"}),a=" ",p=t=>typeof window>"u"||typeof document>"u"?null:t?typeof t=="string"?document.querySelector(t):t:null,c=t=>{const e=Intl.Segmenter;if(typeof e=="function"){const s=new e("en",{granularity:"grapheme"});return Array.from(s.segment(t),r=>r.segment)}return Array.from(t)},l=t=>t.split(a),d=t=>{const e={};return Object.keys(t).forEach(s=>{const r=t[s];r!==void 0&&(e[s]=r)}),e};n.CLASSNAMES=i,n.TextSlicer=class{el;original;opts;callbacks;charIndex;mounted;ro=null;frozen=!1;freezeWordWidthsBound=()=>{this.freezeWordWidths()};constructor(t={},e){const s=p(t.container);this.el=s,this.original=(r=>!!r&&typeof HTMLElement<"u"&&r instanceof HTMLElement)(s)?s.textContent?.toString()??"":"",this.opts={...o,...d(t)},this.callbacks=e,this.charIndex=0,this.mounted=!1}get metrics(){const t=this.original;return{wordTotal:t.length?l(t).length:0,charTotal:t.length,renderedAt:Date.now()}}init(){this.el&&(this.mounted=!0,this.split())}reinit(t,e){this.el&&(typeof t=="string"&&(this.original=t),e&&(this.opts={...this.opts,...d(e)}),this.split())}clear(){this.el&&(this.unfreezeWordWidths(),this.el.replaceChildren())}split(){if(!this.el)return;this.clear(),this.charIndex=0;const t=this.original,e=document.createDocumentFragment(),s=l(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.scheduleFreezeWordWidths().then(()=>{this.callbacks?.onAfterRender?.(this.metrics)}).catch(()=>{})}destroy(){this.el&&(this.detachResizeObserver(),this.clear(),this.mounted=!1)}updateOptions(t){this.opts={...this.opts,...d(t)},this.mounted&&this.split()}appendWords(t,e){e.forEach((s,r)=>{if(this.opts.splitMode==="both"){const h=this.createWordSpan(r,s);for(const f of c(s)){const u=this.createCharSpan(f);h.append(u)}t.append(h)}else{const h=this.createWordSpan(r);h.append(document.createTextNode(s)),t.append(h)}r<e.length-1&&t.append(this.createSpaceSpan())})}appendChars(t,e){for(const s of c(e)){const r=this.createCharSpan(s);t.append(r)}}createWordSpan(t,e=""){const s=document.createElement("span");return s.classList.add(i.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===a?(e.classList.add(i.whitespace),this.opts.keepWhitespaceNodes||(e.textContent=a)):(e.classList.add(i.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(i.whitespace),t.textContent=a,t}async scheduleFreezeWordWidths(){if(this.el&&this.opts.freezeWordWidths){try{await document.fonts?.ready}catch{}await new Promise(t=>requestAnimationFrame(()=>t())),this.freezeWordWidths(),this.attachResizeObserver()}}freezeWordWidths(){this.el&&(this.el.querySelectorAll(`.${i.word}`).forEach(t=>{t.style.width="",t.style.flex="";const{width:e}=t.getBoundingClientRect();t.style.width=`${Math.ceil(e)}px`,t.style.flex="0 0 auto"}),this.frozen=!0)}unfreezeWordWidths(){!this.el||!this.frozen||(this.el.querySelectorAll(`.${i.word}`).forEach(t=>{t.style.width="",t.style.flex=""}),this.frozen=!1)}attachResizeObserver(){this.el&&this.opts.freezeWordWidths&&(this.ro||(this.ro=new ResizeObserver(()=>{this.freezeWordWidths()}),this.ro.observe(this.el)),window.addEventListener("resize",this.freezeWordWidthsBound,{passive:!0}))}detachResizeObserver(){this.ro&&(this.ro.disconnect(),this.ro=null),window.removeEventListener("resize",this.freezeWordWidthsBound)}},Object.defineProperty(n,Symbol.toStringTag,{value:"Module"})});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "text-slicer",
3
- "version": "1.4.0-dev.8",
3
+ "version": "1.4.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",