text-slicer 1.5.0-dev.4 → 1.5.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
@@ -8,168 +8,98 @@
8
8
 
9
9
  </div>
10
10
 
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>
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>
12
13
  <p align="center"><a href="https://codepen.io/ux-ui/full/vYMoGoG">Demo</a></p>
13
14
  <br>
14
15
 
15
- ## Install
16
+ **Install**
16
17
 
17
- ```bash
18
+ ```console
18
19
  yarn add text-slicer
19
- # or
20
- npm i text-slicer
21
20
  ```
22
21
  <br>
23
22
 
24
- ## Quick start
23
+ **Import**
25
24
 
26
- ```ts
25
+ ```javascript
27
26
  import { TextSlicer } from 'text-slicer';
28
-
29
- const slicer = new TextSlicer({ container: '.text-slicer' });
30
-
31
- slicer.init();
32
27
  ```
28
+ <br>
33
29
 
34
- Initialize per element:
30
+ **Usage**
35
31
 
36
- ```ts
37
- document.querySelectorAll('.text-slicer').forEach((el) => {
38
- const slicer = new TextSlicer({ container: el });
32
+ ```javascript
33
+ const textSlicer = new TextSlicer();
39
34
 
40
- slicer.init();
41
- });
35
+ textSlicer.init();
42
36
  ```
43
- <br>
44
37
 
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
- }
38
+ <sub>Initialization with specified parameters</sub>
39
+ ```javascript
40
+ 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
+
50
+ textSlicer.init();
51
+ });
72
52
  ```
73
53
 
74
- ### Classnames & CSS vars
75
-
76
- ```ts
77
- import { CLASSNAMES } from 'text-slicer';
54
+ <sub>How to apply the TextSlicer class to all elements on a page</sub>
55
+ ```javascript
56
+ document.addEventListener('DOMContentLoaded', () => {
57
+ document.querySelectorAll('.text-slicer').forEach((element) => {
58
+ const textSlicer = new TextSlicer({ container: element });
78
59
 
79
- // Classes applied to generated spans
80
- CLASSNAMES.word // 'ts-word'
81
- CLASSNAMES.char // 'ts-char'
82
- CLASSNAMES.whitespace // 'ts-whitespace'
83
-
84
- // CSS variables placed on container and items (when cssVariables: true)
85
- --word-total
86
- --char-total
87
- --word-index
88
- --char-index
60
+ textSlicer.init();
61
+ });
62
+ });
89
63
  ```
64
+ <br>
90
65
 
91
- ### Constructor
66
+ **Options**
92
67
 
93
- ```ts
94
- new TextSlicer(options?: TextSlicerOptions, callbacks?: TextSlicerCallbacks)
95
- ```
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. |
96
76
 
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
- ```
77
+ <br>
135
78
 
136
- ### Responsive width freezing
79
+ **Callbacks**
137
80
 
138
- ```ts
139
- const slicer = new TextSlicer({
140
- container: '.headline',
141
- splitMode: 'both',
142
- freezeWordWidths: true,
143
- });
144
- slicer.init();
145
- ```
81
+ | Callback | Arguments | Description |
82
+ |-----------------|-----------------------|---------------------------------------------------------------------------------------------------------|
83
+ | `onAfterRender` | `TextSlicerMetrics` | Invoked after rendering. Provides `wordTotal`, `charTotal`, and `renderedAt` timestamp. |
146
84
 
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
85
  <br>
150
86
 
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
- ```
87
+ **API Methods**
88
+
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`. |
100
+
171
101
  <br>
172
102
 
173
- ## License
103
+ **License**
174
104
 
175
- MIT
105
+ text-slicer is released under MIT license
package/dist/index.cjs.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const g={splitMode:"both",cssVariables:!1,dataAttributes:!1,keepWhitespaceNodes:!0,containerHeightVar:!1},r=Object.freeze({word:"ts-word",char:"ts-char",whitespace:"ts-whitespace"}),a=" ",h="--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)},p=t=>t.split(a),o=t=>{const e={};return Object.keys(t).forEach(s=>{const i=t[s];i!==void 0&&(e[s]=i)}),e};exports.CLASSNAMES=r,exports.TextSlicer=class{el;original;opts;callbacks;charIndex;mounted;heightLocked;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={...g,...o(t)},this.callbacks=e,this.charIndex=0,this.mounted=!1,this.heightLocked=!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,...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=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.opts.containerHeightVar&&this.applyContainerHeightVar(),this.callbacks?.onAfterRender?.(this.metrics)}destroy(){this.el&&(this.clear(),this.unlockHeight(),this.opts.containerHeightVar&&this.el.style.removeProperty(h),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 n=this.createWordSpan(i,s);for(const d of l(s)){const u=this.createCharSpan(d);n.append(u)}t.append(n)}else{const n=this.createWordSpan(i);n.append(document.createTextNode(s)),t.append(n)}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(r.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(r.whitespace),this.opts.keepWhitespaceNodes||(e.textContent=a)):(e.classList.add(r.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(r.whitespace),t.textContent=a,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))}applyContainerHeightVar(){if(!this.el)return;const t=()=>{const e=this.measureHeight();this.el.style.setProperty(h,`${e}px`)};typeof requestAnimationFrame=="function"?requestAnimationFrame(()=>requestAnimationFrame(t)):t()}};
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.d.ts CHANGED
@@ -28,6 +28,7 @@ export declare class TextSlicer {
28
28
  private charIndex;
29
29
  private mounted;
30
30
  private heightLocked;
31
+ private resizeObserver?;
31
32
  constructor(options?: TextSlicerOptions, callbacks?: TextSlicerCallbacks);
32
33
  get metrics(): TextSlicerMetrics;
33
34
  init(): void;
@@ -44,5 +45,5 @@ export declare class TextSlicer {
44
45
  private createCharSpan;
45
46
  private createSpaceSpan;
46
47
  private measureHeight;
47
- private applyContainerHeightVar;
48
+ private initHeightObserver;
48
49
  }
package/dist/index.es.js CHANGED
@@ -1,11 +1,11 @@
1
- const m = { splitMode: "both", cssVariables: !1, dataAttributes: !1, keepWhitespaceNodes: !0, containerHeightVar: !1 }, a = Object.freeze({ word: "ts-word", char: "ts-char", whitespace: "ts-whitespace" }), h = " ", 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) => {
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
2
  const t = Intl.Segmenter;
3
3
  if (typeof t == "function") {
4
4
  const e = new t("en", { granularity: "grapheme" });
5
5
  return Array.from(e.segment(i), (s) => s.segment);
6
6
  }
7
7
  return Array.from(i);
8
- }, d = (i) => i.split(h), o = (i) => {
8
+ }, d = (i) => i.split(a), o = (i) => {
9
9
  const t = {};
10
10
  return Object.keys(i).forEach((e) => {
11
11
  const s = i[e];
@@ -20,16 +20,17 @@ class y {
20
20
  charIndex;
21
21
  mounted;
22
22
  heightLocked;
23
+ resizeObserver;
23
24
  constructor(t = {}, e) {
24
25
  const s = f(t.container);
25
- this.el = s, this.original = ((n) => !!n && typeof HTMLElement < "u" && n instanceof HTMLElement)(s) ? s.textContent?.toString() ?? "" : "", this.opts = { ...m, ...o(t) }, this.callbacks = e, this.charIndex = 0, this.mounted = !1, this.heightLocked = !1;
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;
26
27
  }
27
28
  get metrics() {
28
29
  const t = this.original;
29
30
  return { wordTotal: t.length ? d(t).length : 0, charTotal: t.length, renderedAt: Date.now() };
30
31
  }
31
32
  init() {
32
- this.el && (this.mounted = !0, this.split());
33
+ this.el && (this.mounted = !0, this.split(), this.opts.containerHeightVar && this.initHeightObserver());
33
34
  }
34
35
  reinit(t, e) {
35
36
  this.el && (typeof t == "string" && (this.original = t), e && (this.opts = { ...this.opts, ...o(e) }), this.split());
@@ -41,10 +42,10 @@ class y {
41
42
  if (!this.el) return;
42
43
  this.clear(), this.charIndex = 0;
43
44
  const t = this.original, e = document.createDocumentFragment(), s = d(t);
44
- 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.opts.containerHeightVar && this.applyContainerHeightVar(), this.callbacks?.onAfterRender?.(this.metrics);
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);
45
46
  }
46
47
  destroy() {
47
- this.el && (this.clear(), this.unlockHeight(), this.opts.containerHeightVar && this.el.style.removeProperty(c), this.mounted = !1);
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);
48
49
  }
49
50
  updateOptions(t) {
50
51
  this.opts = { ...this.opts, ...o(t) }, this.mounted && this.split();
@@ -58,38 +59,38 @@ class y {
58
59
  this.el && (this.el.style.removeProperty("height"), this.heightLocked = !1);
59
60
  }
60
61
  appendWords(t, e) {
61
- e.forEach((s, n) => {
62
+ e.forEach((s, r) => {
62
63
  if (this.opts.splitMode === "both") {
63
- const r = this.createWordSpan(n, s);
64
- for (const u of p(s)) {
65
- const g = this.createCharSpan(u);
66
- r.append(g);
64
+ const n = this.createWordSpan(r, s);
65
+ for (const g of p(s)) {
66
+ const u = this.createCharSpan(g);
67
+ n.append(u);
67
68
  }
68
- t.append(r);
69
+ t.append(n);
69
70
  } else {
70
- const r = this.createWordSpan(n);
71
- r.append(document.createTextNode(s)), t.append(r);
71
+ const n = this.createWordSpan(r);
72
+ n.append(document.createTextNode(s)), t.append(n);
72
73
  }
73
- n < e.length - 1 && t.append(this.createSpaceSpan());
74
+ r < e.length - 1 && t.append(this.createSpaceSpan());
74
75
  });
75
76
  }
76
77
  appendChars(t, e) {
77
78
  for (const s of p(e)) {
78
- const n = this.createCharSpan(s);
79
- t.append(n);
79
+ const r = this.createCharSpan(s);
80
+ t.append(r);
80
81
  }
81
82
  }
82
83
  createWordSpan(t, e = "") {
83
84
  const s = document.createElement("span");
84
- 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;
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;
85
86
  }
86
87
  createCharSpan(t) {
87
88
  const e = document.createElement("span");
88
- return e.textContent = t, this.opts.dataAttributes && e.setAttribute("data-char", t), t === h ? (e.classList.add(a.whitespace), this.opts.keepWhitespaceNodes || (e.textContent = h)) : (e.classList.add(a.char), this.opts.cssVariables && e.style.setProperty("--char-index", String(this.charIndex)), this.charIndex += 1), e;
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;
89
90
  }
90
91
  createSpaceSpan() {
91
92
  const t = document.createElement("span");
92
- return t.classList.add(a.whitespace), t.textContent = h, t;
93
+ return t.classList.add(h.whitespace), t.textContent = a, t;
93
94
  }
94
95
  measureHeight() {
95
96
  if (!this.el) return 0;
@@ -97,16 +98,14 @@ class y {
97
98
  let t = this.el.offsetHeight || this.el.clientHeight || 0;
98
99
  return t || (t = Math.round(this.el.getBoundingClientRect().height)), this.el.classList.remove(l), Math.max(0, Math.ceil(t));
99
100
  }
100
- applyContainerHeightVar() {
101
- if (!this.el) return;
102
- const t = () => {
103
- const e = this.measureHeight();
104
- this.el.style.setProperty(c, `${e}px`);
105
- };
106
- typeof requestAnimationFrame == "function" ? requestAnimationFrame(() => requestAnimationFrame(t)) : t();
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));
107
106
  }
108
107
  }
109
108
  export {
110
- a as CLASSNAMES,
109
+ h as CLASSNAMES,
111
110
  y as TextSlicer
112
111
  };
package/dist/index.umd.js CHANGED
@@ -1 +1 @@
1
- (function(n,r){typeof exports=="object"&&typeof module<"u"?r(exports):typeof define=="function"&&define.amd?define(["exports"],r):r((n=typeof globalThis<"u"?globalThis:n||self).TextSlicer={})})(this,function(n){"use strict";const r={splitMode:"both",cssVariables:!1,dataAttributes:!1,keepWhitespaceNodes:!0,containerHeightVar:!1},o=Object.freeze({word:"ts-word",char:"ts-char",whitespace:"ts-whitespace"}),h=" ",l="--container-height",p="ts-measuring",g=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),i=>i.segment)}return Array.from(t)},u=t=>t.split(h),c=t=>{const e={};return Object.keys(t).forEach(s=>{const i=t[s];i!==void 0&&(e[s]=i)}),e};n.CLASSNAMES=o,n.TextSlicer=class{el;original;opts;callbacks;charIndex;mounted;heightLocked;constructor(t={},e){const s=g(t.container);this.el=s,this.original=(i=>!!i&&typeof HTMLElement<"u"&&i instanceof HTMLElement)(s)?s.textContent?.toString()??"":"",this.opts={...r,...c(t)},this.callbacks=e,this.charIndex=0,this.mounted=!1,this.heightLocked=!1}get metrics(){const t=this.original;return{wordTotal:t.length?u(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,...c(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=u(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.opts.containerHeightVar&&this.applyContainerHeightVar(),this.callbacks?.onAfterRender?.(this.metrics)}destroy(){this.el&&(this.clear(),this.unlockHeight(),this.opts.containerHeightVar&&this.el.style.removeProperty(l),this.mounted=!1)}updateOptions(t){this.opts={...this.opts,...c(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 a=this.createWordSpan(i,s);for(const f of d(s)){const m=this.createCharSpan(f);a.append(m)}t.append(a)}else{const a=this.createWordSpan(i);a.append(document.createTextNode(s)),t.append(a)}i<e.length-1&&t.append(this.createSpaceSpan())})}appendChars(t,e){for(const s of d(e)){const i=this.createCharSpan(s);t.append(i)}}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===h?(e.classList.add(o.whitespace),this.opts.keepWhitespaceNodes||(e.textContent=h)):(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=h,t}measureHeight(){if(!this.el)return 0;this.el.classList.add(p),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(p),Math.max(0,Math.ceil(t))}applyContainerHeightVar(){if(!this.el)return;const t=()=>{const e=this.measureHeight();this.el.style.setProperty(l,`${e}px`)};typeof requestAnimationFrame=="function"?requestAnimationFrame(()=>requestAnimationFrame(t)):t()}},Object.defineProperty(n,Symbol.toStringTag,{value:"Module"})});
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"})});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "text-slicer",
3
- "version": "1.5.0-dev.4",
3
+ "version": "1.5.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",