text-slicer 1.4.0-dev.9 → 1.4.1

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 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)}};
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"}),n=" ",u=e=>typeof window>"u"||typeof document>"u"?null:e?typeof e=="string"?document.querySelector(e):e:null,a=e=>{const t=Intl.Segmenter;if(typeof t=="function"){const s=new t("en",{granularity:"grapheme"});return Array.from(s.segment(e),r=>r.segment)}return Array.from(e)},d=e=>e.split(n),h=e=>{const t={};return Object.keys(e).forEach(s=>{const r=e[s];r!==void 0&&(t[s]=r)}),t};exports.CLASSNAMES=i,exports.TextSlicer=class{el;original;opts;callbacks;charIndex;mounted;ro=null;frozen=!1;freezeScheduled=!1;freezeWordWidthsBound=()=>{this.scheduleFreezeNow()};constructor(e={},t){const s=u(e.container);this.el=s,this.original=(r=>!!r&&typeof HTMLElement<"u"&&r instanceof HTMLElement)(s)?s.textContent?.toString()??"":"",this.opts={...p,...h(e)},this.callbacks=t,this.charIndex=0,this.mounted=!1}get metrics(){const e=this.original;return{wordTotal:e.length?d(e).length:0,charTotal:e.length,renderedAt:Date.now()}}init(){this.el&&(this.mounted=!0,this.split())}reinit(e,t){this.el&&(typeof e=="string"&&(this.original=e),t&&(this.opts={...this.opts,...h(t)}),this.split())}clear(){this.el&&(this.unfreezeWordWidths(),this.el.replaceChildren())}split(){if(!this.el)return;this.clear(),this.charIndex=0;const e=this.original,t=document.createDocumentFragment(),s=d(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.scheduleFreezeWordWidths().then(()=>{this.callbacks?.onAfterRender?.(this.metrics)}).catch(()=>{})}destroy(){this.el&&(this.detachResizeObserver(),this.clear(),this.mounted=!1)}updateOptions(e){this.opts={...this.opts,...h(e)},this.mounted&&this.split()}appendWords(e,t){t.forEach((s,r)=>{if(this.opts.splitMode==="both"){const o=this.createWordSpan(r,s);for(const c of a(s)){const l=this.createCharSpan(c);o.append(l)}e.append(o)}else{const o=this.createWordSpan(r);o.append(document.createTextNode(s)),e.append(o)}r<t.length-1&&e.append(this.createSpaceSpan())})}appendChars(e,t){for(const s of a(t)){const r=this.createCharSpan(s);e.append(r)}}createWordSpan(e,t=""){const s=document.createElement("span");return s.classList.add(i.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===n?(t.classList.add(i.whitespace),this.opts.keepWhitespaceNodes||(t.textContent=n)):(t.classList.add(i.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(i.whitespace),e.textContent=n,e}scheduleFreezeNow(){this.el&&this.opts.freezeWordWidths&&(this.freezeScheduled||(this.freezeScheduled=!0,requestAnimationFrame(()=>{this.freezeScheduled=!1,this.freezeWordWidths()})))}async scheduleFreezeWordWidths(){if(this.el&&this.opts.freezeWordWidths){try{await document.fonts?.ready}catch{}await new Promise(e=>requestAnimationFrame(()=>e())),this.scheduleFreezeNow(),this.attachResizeObserver()}}measureWordWidths(){if(!this.el)return[];const e=this.el.querySelectorAll(`.${i.word}`);return Array.from(e,t=>{t.style.width="",t.style.flex="";const{width:s}=t.getBoundingClientRect();return Math.ceil(s)})}applyWordWidths(e){this.el&&this.el.querySelectorAll(`.${i.word}`).forEach((t,s)=>{const r=e[s]??0;t.style.width=`${r}px`,t.style.flex="0 0 auto"})}freezeWordWidths(){if(!this.el)return;const e=this.measureWordWidths();this.applyWordWidths(e),this.frozen=!0}unfreezeWordWidths(){!this.el||!this.frozen||(this.el.querySelectorAll(`.${i.word}`).forEach(e=>{e.style.width="",e.style.flex=""}),this.frozen=!1)}attachResizeObserver(){if(this.el&&this.opts.freezeWordWidths){if(!this.ro){this.ro=new ResizeObserver(()=>{this.scheduleFreezeNow()});try{this.ro.observe(this.el,{box:"border-box"})}catch{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
@@ -29,6 +29,7 @@ export declare class TextSlicer {
29
29
  private mounted;
30
30
  private ro;
31
31
  private frozen;
32
+ private freezeScheduled;
32
33
  private freezeWordWidthsBound;
33
34
  constructor(options?: TextSlicerOptions, callbacks?: TextSlicerCallbacks);
34
35
  get metrics(): TextSlicerMetrics;
@@ -43,7 +44,10 @@ export declare class TextSlicer {
43
44
  private createWordSpan;
44
45
  private createCharSpan;
45
46
  private createSpaceSpan;
47
+ private scheduleFreezeNow;
46
48
  private scheduleFreezeWordWidths;
49
+ private measureWordWidths;
50
+ private applyWordWidths;
47
51
  private freezeWordWidths;
48
52
  private unfreezeWordWidths;
49
53
  private attachResizeObserver;
package/dist/index.es.js CHANGED
@@ -1,16 +1,16 @@
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
- const t = Intl.Segmenter;
3
- if (typeof t == "function") {
4
- const e = new t("en", { granularity: "grapheme" });
5
- return Array.from(e.segment(r), (s) => s.segment);
1
+ const u = { splitMode: "both", cssVariables: !1, dataAttributes: !1, keepWhitespaceNodes: !0, freezeWordWidths: !1 }, o = 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
+ const e = Intl.Segmenter;
3
+ if (typeof e == "function") {
4
+ const t = new e("en", { granularity: "grapheme" });
5
+ return Array.from(t.segment(r), (s) => s.segment);
6
6
  }
7
7
  return Array.from(r);
8
8
  }, c = (r) => r.split(h), a = (r) => {
9
- const t = {};
10
- return Object.keys(r).forEach((e) => {
11
- const s = r[e];
12
- s !== void 0 && (t[e] = s);
13
- }), t;
9
+ const e = {};
10
+ return Object.keys(r).forEach((t) => {
11
+ const s = r[t];
12
+ s !== void 0 && (e[t] = s);
13
+ }), e;
14
14
  };
15
15
  class W {
16
16
  el;
@@ -21,22 +21,23 @@ class W {
21
21
  mounted;
22
22
  ro = null;
23
23
  frozen = !1;
24
+ freezeScheduled = !1;
24
25
  freezeWordWidthsBound = () => {
25
- this.freezeWordWidths();
26
+ this.scheduleFreezeNow();
26
27
  };
27
- constructor(t = {}, e) {
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;
28
+ constructor(e = {}, t) {
29
+ const s = f(e.container);
30
+ this.el = s, this.original = ((i) => !!i && typeof HTMLElement < "u" && i instanceof HTMLElement)(s) ? s.textContent?.toString() ?? "" : "", this.opts = { ...u, ...a(e) }, this.callbacks = t, this.charIndex = 0, this.mounted = !1;
30
31
  }
31
32
  get metrics() {
32
- const t = this.original;
33
- return { wordTotal: t.length ? c(t).length : 0, charTotal: t.length, renderedAt: Date.now() };
33
+ const e = this.original;
34
+ return { wordTotal: e.length ? c(e).length : 0, charTotal: e.length, renderedAt: Date.now() };
34
35
  }
35
36
  init() {
36
37
  this.el && (this.mounted = !0, this.split());
37
38
  }
38
- reinit(t, e) {
39
- this.el && (typeof t == "string" && (this.original = t), e && (this.opts = { ...this.opts, ...a(e) }), this.split());
39
+ reinit(e, t) {
40
+ this.el && (typeof e == "string" && (this.original = e), t && (this.opts = { ...this.opts, ...a(t) }), this.split());
40
41
  }
41
42
  clear() {
42
43
  this.el && (this.unfreezeWordWidths(), this.el.replaceChildren());
@@ -44,8 +45,8 @@ class W {
44
45
  split() {
45
46
  if (!this.el) return;
46
47
  this.clear(), this.charIndex = 0;
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(() => {
48
+ const e = this.original, t = document.createDocumentFragment(), s = c(e);
49
+ 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.scheduleFreezeWordWidths().then(() => {
49
50
  this.callbacks?.onAfterRender?.(this.metrics);
50
51
  }).catch(() => {
51
52
  });
@@ -53,42 +54,47 @@ class W {
53
54
  destroy() {
54
55
  this.el && (this.detachResizeObserver(), this.clear(), this.mounted = !1);
55
56
  }
56
- updateOptions(t) {
57
- this.opts = { ...this.opts, ...a(t) }, this.mounted && this.split();
57
+ updateOptions(e) {
58
+ this.opts = { ...this.opts, ...a(e) }, this.mounted && this.split();
58
59
  }
59
- appendWords(t, e) {
60
- e.forEach((s, i) => {
60
+ appendWords(e, t) {
61
+ t.forEach((s, i) => {
61
62
  if (this.opts.splitMode === "both") {
62
- const o = this.createWordSpan(i, s);
63
+ const n = this.createWordSpan(i, s);
63
64
  for (const l of d(s)) {
64
65
  const p = this.createCharSpan(l);
65
- o.append(p);
66
+ n.append(p);
66
67
  }
67
- t.append(o);
68
+ e.append(n);
68
69
  } else {
69
- const o = this.createWordSpan(i);
70
- o.append(document.createTextNode(s)), t.append(o);
70
+ const n = this.createWordSpan(i);
71
+ n.append(document.createTextNode(s)), e.append(n);
71
72
  }
72
- i < e.length - 1 && t.append(this.createSpaceSpan());
73
+ i < t.length - 1 && e.append(this.createSpaceSpan());
73
74
  });
74
75
  }
75
- appendChars(t, e) {
76
- for (const s of d(e)) {
76
+ appendChars(e, t) {
77
+ for (const s of d(t)) {
77
78
  const i = this.createCharSpan(s);
78
- t.append(i);
79
+ e.append(i);
79
80
  }
80
81
  }
81
- createWordSpan(t, e = "") {
82
+ createWordSpan(e, t = "") {
82
83
  const s = document.createElement("span");
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;
84
+ 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;
84
85
  }
85
- createCharSpan(t) {
86
- const e = document.createElement("span");
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;
86
+ createCharSpan(e) {
87
+ const t = document.createElement("span");
88
+ return t.textContent = e, this.opts.dataAttributes && t.setAttribute("data-char", e), e === h ? (t.classList.add(o.whitespace), this.opts.keepWhitespaceNodes || (t.textContent = h)) : (t.classList.add(o.char), this.opts.cssVariables && t.style.setProperty("--char-index", String(this.charIndex)), this.charIndex += 1), t;
88
89
  }
89
90
  createSpaceSpan() {
90
- const t = document.createElement("span");
91
- return t.classList.add(n.whitespace), t.textContent = h, t;
91
+ const e = document.createElement("span");
92
+ return e.classList.add(o.whitespace), e.textContent = h, e;
93
+ }
94
+ scheduleFreezeNow() {
95
+ this.el && this.opts.freezeWordWidths && (this.freezeScheduled || (this.freezeScheduled = !0, requestAnimationFrame(() => {
96
+ this.freezeScheduled = !1, this.freezeWordWidths();
97
+ })));
92
98
  }
93
99
  async scheduleFreezeWordWidths() {
94
100
  if (this.el && this.opts.freezeWordWidths) {
@@ -96,31 +102,54 @@ class W {
96
102
  await document.fonts?.ready;
97
103
  } catch {
98
104
  }
99
- await new Promise((t) => requestAnimationFrame(() => t())), this.freezeWordWidths(), this.attachResizeObserver();
105
+ await new Promise((e) => requestAnimationFrame(() => e())), this.scheduleFreezeNow(), this.attachResizeObserver();
100
106
  }
101
107
  }
102
- freezeWordWidths() {
103
- this.el && (this.el.querySelectorAll(`.${n.word}`).forEach((t) => {
108
+ measureWordWidths() {
109
+ if (!this.el) return [];
110
+ const e = this.el.querySelectorAll(`.${o.word}`);
111
+ return Array.from(e, (t) => {
104
112
  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);
113
+ const { width: s } = t.getBoundingClientRect();
114
+ return Math.ceil(s);
115
+ });
116
+ }
117
+ applyWordWidths(e) {
118
+ this.el && this.el.querySelectorAll(`.${o.word}`).forEach((t, s) => {
119
+ const i = e[s] ?? 0;
120
+ t.style.width = `${i}px`, t.style.flex = "0 0 auto";
121
+ });
122
+ }
123
+ freezeWordWidths() {
124
+ if (!this.el) return;
125
+ const e = this.measureWordWidths();
126
+ this.applyWordWidths(e), this.frozen = !0;
108
127
  }
109
128
  unfreezeWordWidths() {
110
- !this.el || !this.frozen || (this.el.querySelectorAll(`.${n.word}`).forEach((t) => {
111
- t.style.width = "", t.style.flex = "";
129
+ !this.el || !this.frozen || (this.el.querySelectorAll(`.${o.word}`).forEach((e) => {
130
+ e.style.width = "", e.style.flex = "";
112
131
  }), this.frozen = !1);
113
132
  }
114
133
  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 }));
134
+ if (this.el && this.opts.freezeWordWidths) {
135
+ if (!this.ro) {
136
+ this.ro = new ResizeObserver(() => {
137
+ this.scheduleFreezeNow();
138
+ });
139
+ try {
140
+ this.ro.observe(this.el, { box: "border-box" });
141
+ } catch {
142
+ this.ro.observe(this.el);
143
+ }
144
+ }
145
+ window.addEventListener("resize", this.freezeWordWidthsBound, { passive: !0 });
146
+ }
118
147
  }
119
148
  detachResizeObserver() {
120
149
  this.ro && (this.ro.disconnect(), this.ro = null), window.removeEventListener("resize", this.freezeWordWidthsBound);
121
150
  }
122
151
  }
123
152
  export {
124
- n as CLASSNAMES,
153
+ o as CLASSNAMES,
125
154
  W as TextSlicer
126
155
  };
package/dist/index.umd.js CHANGED
@@ -1 +1 @@
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"})});
1
+ (function(o,n){typeof exports=="object"&&typeof module<"u"?n(exports):typeof define=="function"&&define.amd?define(["exports"],n):n((o=typeof globalThis<"u"?globalThis:o||self).TextSlicer={})})(this,function(o){"use strict";const n={splitMode:"both",cssVariables:!1,dataAttributes:!1,keepWhitespaceNodes:!0,freezeWordWidths:!1},i=Object.freeze({word:"ts-word",char:"ts-char",whitespace:"ts-whitespace"}),d=" ",p=e=>typeof window>"u"||typeof document>"u"?null:e?typeof e=="string"?document.querySelector(e):e:null,c=e=>{const t=Intl.Segmenter;if(typeof t=="function"){const s=new t("en",{granularity:"grapheme"});return Array.from(s.segment(e),r=>r.segment)}return Array.from(e)},l=e=>e.split(d),a=e=>{const t={};return Object.keys(e).forEach(s=>{const r=e[s];r!==void 0&&(t[s]=r)}),t};o.CLASSNAMES=i,o.TextSlicer=class{el;original;opts;callbacks;charIndex;mounted;ro=null;frozen=!1;freezeScheduled=!1;freezeWordWidthsBound=()=>{this.scheduleFreezeNow()};constructor(e={},t){const s=p(e.container);this.el=s,this.original=(r=>!!r&&typeof HTMLElement<"u"&&r instanceof HTMLElement)(s)?s.textContent?.toString()??"":"",this.opts={...n,...a(e)},this.callbacks=t,this.charIndex=0,this.mounted=!1}get metrics(){const e=this.original;return{wordTotal:e.length?l(e).length:0,charTotal:e.length,renderedAt:Date.now()}}init(){this.el&&(this.mounted=!0,this.split())}reinit(e,t){this.el&&(typeof e=="string"&&(this.original=e),t&&(this.opts={...this.opts,...a(t)}),this.split())}clear(){this.el&&(this.unfreezeWordWidths(),this.el.replaceChildren())}split(){if(!this.el)return;this.clear(),this.charIndex=0;const e=this.original,t=document.createDocumentFragment(),s=l(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.scheduleFreezeWordWidths().then(()=>{this.callbacks?.onAfterRender?.(this.metrics)}).catch(()=>{})}destroy(){this.el&&(this.detachResizeObserver(),this.clear(),this.mounted=!1)}updateOptions(e){this.opts={...this.opts,...a(e)},this.mounted&&this.split()}appendWords(e,t){t.forEach((s,r)=>{if(this.opts.splitMode==="both"){const h=this.createWordSpan(r,s);for(const u of c(s)){const f=this.createCharSpan(u);h.append(f)}e.append(h)}else{const h=this.createWordSpan(r);h.append(document.createTextNode(s)),e.append(h)}r<t.length-1&&e.append(this.createSpaceSpan())})}appendChars(e,t){for(const s of c(t)){const r=this.createCharSpan(s);e.append(r)}}createWordSpan(e,t=""){const s=document.createElement("span");return s.classList.add(i.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===d?(t.classList.add(i.whitespace),this.opts.keepWhitespaceNodes||(t.textContent=d)):(t.classList.add(i.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(i.whitespace),e.textContent=d,e}scheduleFreezeNow(){this.el&&this.opts.freezeWordWidths&&(this.freezeScheduled||(this.freezeScheduled=!0,requestAnimationFrame(()=>{this.freezeScheduled=!1,this.freezeWordWidths()})))}async scheduleFreezeWordWidths(){if(this.el&&this.opts.freezeWordWidths){try{await document.fonts?.ready}catch{}await new Promise(e=>requestAnimationFrame(()=>e())),this.scheduleFreezeNow(),this.attachResizeObserver()}}measureWordWidths(){if(!this.el)return[];const e=this.el.querySelectorAll(`.${i.word}`);return Array.from(e,t=>{t.style.width="",t.style.flex="";const{width:s}=t.getBoundingClientRect();return Math.ceil(s)})}applyWordWidths(e){this.el&&this.el.querySelectorAll(`.${i.word}`).forEach((t,s)=>{const r=e[s]??0;t.style.width=`${r}px`,t.style.flex="0 0 auto"})}freezeWordWidths(){if(!this.el)return;const e=this.measureWordWidths();this.applyWordWidths(e),this.frozen=!0}unfreezeWordWidths(){!this.el||!this.frozen||(this.el.querySelectorAll(`.${i.word}`).forEach(e=>{e.style.width="",e.style.flex=""}),this.frozen=!1)}attachResizeObserver(){if(this.el&&this.opts.freezeWordWidths){if(!this.ro){this.ro=new ResizeObserver(()=>{this.scheduleFreezeNow()});try{this.ro.observe(this.el,{box:"border-box"})}catch{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(o,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.9",
3
+ "version": "1.4.1",
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",