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 +139 -43
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.es.js +81 -52
- package/dist/index.umd.js +1 -1
- package/package.json +1 -1
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">
|
|
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><span></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
|
-
|
|
15
|
+
## Install
|
|
18
16
|
|
|
19
|
-
```
|
|
17
|
+
```bash
|
|
20
18
|
yarn add text-slicer
|
|
19
|
+
# or
|
|
20
|
+
npm i text-slicer
|
|
21
21
|
```
|
|
22
22
|
<br>
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { TextSlicer } from 'text-slicer';
|
|
28
|
+
|
|
29
|
+
const slicer = new TextSlicer({ container: '.text-slicer' });
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
79
|
+
// Classes applied to generated spans
|
|
80
|
+
CLASSNAMES.word // 'ts-word'
|
|
81
|
+
CLASSNAMES.char // 'ts-char'
|
|
82
|
+
CLASSNAMES.whitespace // 'ts-whitespace'
|
|
35
83
|
|
|
36
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
173
|
+
## License
|
|
78
174
|
|
|
79
|
-
|
|
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"}),
|
|
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 },
|
|
2
|
-
const
|
|
3
|
-
if (typeof
|
|
4
|
-
const
|
|
5
|
-
return Array.from(
|
|
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
|
|
10
|
-
return Object.keys(r).forEach((
|
|
11
|
-
const s = r[
|
|
12
|
-
s !== void 0 && (t
|
|
13
|
-
}),
|
|
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.
|
|
26
|
+
this.scheduleFreezeNow();
|
|
26
27
|
};
|
|
27
|
-
constructor(
|
|
28
|
-
const s = f(
|
|
29
|
-
this.el = s, this.original = ((i) => !!i && typeof HTMLElement < "u" && i instanceof HTMLElement)(s) ? s.textContent?.toString() ?? "" : "", this.opts = { ...u, ...a(
|
|
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
|
|
33
|
-
return { wordTotal:
|
|
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(
|
|
39
|
-
this.el && (typeof
|
|
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
|
|
48
|
-
this.opts.splitMode === "chars" ? this.appendChars(
|
|
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(
|
|
57
|
-
this.opts = { ...this.opts, ...a(
|
|
57
|
+
updateOptions(e) {
|
|
58
|
+
this.opts = { ...this.opts, ...a(e) }, this.mounted && this.split();
|
|
58
59
|
}
|
|
59
|
-
appendWords(
|
|
60
|
-
|
|
60
|
+
appendWords(e, t) {
|
|
61
|
+
t.forEach((s, i) => {
|
|
61
62
|
if (this.opts.splitMode === "both") {
|
|
62
|
-
const
|
|
63
|
+
const n = this.createWordSpan(i, s);
|
|
63
64
|
for (const l of d(s)) {
|
|
64
65
|
const p = this.createCharSpan(l);
|
|
65
|
-
|
|
66
|
+
n.append(p);
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
+
e.append(n);
|
|
68
69
|
} else {
|
|
69
|
-
const
|
|
70
|
-
|
|
70
|
+
const n = this.createWordSpan(i);
|
|
71
|
+
n.append(document.createTextNode(s)), e.append(n);
|
|
71
72
|
}
|
|
72
|
-
i <
|
|
73
|
+
i < t.length - 1 && e.append(this.createSpaceSpan());
|
|
73
74
|
});
|
|
74
75
|
}
|
|
75
|
-
appendChars(
|
|
76
|
-
for (const s of d(
|
|
76
|
+
appendChars(e, t) {
|
|
77
|
+
for (const s of d(t)) {
|
|
77
78
|
const i = this.createCharSpan(s);
|
|
78
|
-
|
|
79
|
+
e.append(i);
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
|
-
createWordSpan(
|
|
82
|
+
createWordSpan(e, t = "") {
|
|
82
83
|
const s = document.createElement("span");
|
|
83
|
-
return s.classList.add(
|
|
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(
|
|
86
|
-
const
|
|
87
|
-
return
|
|
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
|
|
91
|
-
return
|
|
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((
|
|
105
|
+
await new Promise((e) => requestAnimationFrame(() => e())), this.scheduleFreezeNow(), this.attachResizeObserver();
|
|
100
106
|
}
|
|
101
107
|
}
|
|
102
|
-
|
|
103
|
-
|
|
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:
|
|
106
|
-
|
|
107
|
-
})
|
|
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(`.${
|
|
111
|
-
|
|
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
|
|
116
|
-
this.
|
|
117
|
-
|
|
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
|
-
|
|
153
|
+
o as CLASSNAMES,
|
|
125
154
|
W as TextSlicer
|
|
126
155
|
};
|
package/dist/index.umd.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(n
|
|
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.
|
|
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",
|