text-slicer 1.4.0-dev.8 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -43
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.es.js +70 -33
- 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
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const p={splitMode:"both",cssVariables:!1,dataAttributes:!1,keepWhitespaceNodes:!0,freezeWordWidths:!1},i=Object.freeze({word:"ts-word",char:"ts-char",whitespace:"ts-whitespace"}),o=" ",u=t=>typeof window>"u"||typeof document>"u"?null:t?typeof t=="string"?document.querySelector(t):t:null,a=t=>{const e=Intl.Segmenter;if(typeof e=="function"){const s=new e("en",{granularity:"grapheme"});return Array.from(s.segment(t),r=>r.segment)}return Array.from(t)},d=t=>t.split(o),h=t=>{const e={};return Object.keys(t).forEach(s=>{const r=t[s];r!==void 0&&(e[s]=r)}),e};exports.CLASSNAMES=i,exports.TextSlicer=class{el;original;opts;callbacks;charIndex;mounted;ro=null;frozen=!1;freezeWordWidthsBound=()=>{this.freezeWordWidths()};constructor(t={},e){const s=u(t.container);this.el=s,this.original=(r=>!!r&&typeof HTMLElement<"u"&&r instanceof HTMLElement)(s)?s.textContent?.toString()??"":"",this.opts={...p,...h(t)},this.callbacks=e,this.charIndex=0,this.mounted=!1}get metrics(){const t=this.original;return{wordTotal:t.length?d(t).length:0,charTotal:t.length,renderedAt:Date.now()}}init(){this.el&&(this.mounted=!0,this.split())}reinit(t,e){this.el&&(typeof t=="string"&&(this.original=t),e&&(this.opts={...this.opts,...h(e)}),this.split())}clear(){this.el&&(this.unfreezeWordWidths(),this.el.replaceChildren())}split(){if(!this.el)return;this.clear(),this.charIndex=0;const t=this.original,e=document.createDocumentFragment(),s=d(t);this.opts.splitMode==="chars"?this.appendChars(e,t):this.appendWords(e,s),this.el.appendChild(e),this.opts.cssVariables&&(this.el.style.setProperty("--word-total",String(s.length)),this.el.style.setProperty("--char-total",String(t.length))),this.scheduleFreezeWordWidths().then(()=>{this.callbacks?.onAfterRender?.(this.metrics)}).catch(()=>{})}destroy(){this.el&&(this.detachResizeObserver(),this.clear(),this.mounted=!1)}updateOptions(t){this.opts={...this.opts,...h(t)},this.mounted&&this.split()}appendWords(t,e){e.forEach((s,r)=>{if(this.opts.splitMode==="both"){const n=this.createWordSpan(r,s);for(const c of a(s)){const l=this.createCharSpan(c);n.append(l)}t.append(n)}else{const n=this.createWordSpan(r);n.append(document.createTextNode(s)),t.append(n)}r<e.length-1&&t.append(this.createSpaceSpan())})}appendChars(t,e){for(const s of a(e)){const r=this.createCharSpan(s);t.append(r)}}createWordSpan(t,e=""){const s=document.createElement("span");return s.classList.add(i.word),this.opts.dataAttributes&&e&&s.setAttribute("data-word",e),this.opts.cssVariables&&s.style.setProperty("--word-index",String(t)),s}createCharSpan(t){const e=document.createElement("span");return e.textContent=t,this.opts.dataAttributes&&e.setAttribute("data-char",t),t===o?(e.classList.add(i.whitespace),this.opts.keepWhitespaceNodes||(e.textContent=o)):(e.classList.add(i.char),this.opts.cssVariables&&e.style.setProperty("--char-index",String(this.charIndex)),this.charIndex+=1),e}createSpaceSpan(){const t=document.createElement("span");return t.classList.add(i.whitespace),t.textContent=o,t}async scheduleFreezeWordWidths(){if(this.el&&this.opts.freezeWordWidths){try{await document.fonts?.ready}catch{}await new Promise(t=>requestAnimationFrame(()=>t())),this.freezeWordWidths(),this.attachResizeObserver()}}freezeWordWidths(){this.el&&(this.el.querySelectorAll(`.${i.word}`).forEach(t=>{t.style.width="",t.style.flex="";const{width:e}=t.getBoundingClientRect();t.style.width=`${Math.ceil(e)}px`,t.style.flex="0 0 auto"}),this.frozen=!0)}unfreezeWordWidths(){!this.el||!this.frozen||(this.el.querySelectorAll(`.${i.word}`).forEach(t=>{t.style.width="",t.style.flex=""}),this.frozen=!1)}attachResizeObserver(){this.el&&this.opts.freezeWordWidths&&(this.ro||(this.ro=new ResizeObserver(()=>{this.freezeWordWidths()}),this.ro.observe(this.el)),window.addEventListener("resize",this.freezeWordWidthsBound,{passive:!0}))}detachResizeObserver(){this.ro&&(this.ro.disconnect(),this.ro=null),window.removeEventListener("resize",this.freezeWordWidthsBound)}};
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface TextSlicerOptions {
|
|
|
5
5
|
cssVariables?: boolean;
|
|
6
6
|
dataAttributes?: boolean;
|
|
7
7
|
keepWhitespaceNodes?: boolean;
|
|
8
|
+
freezeWordWidths?: boolean;
|
|
8
9
|
}
|
|
9
10
|
export interface TextSlicerMetrics {
|
|
10
11
|
wordTotal: number;
|
|
@@ -26,6 +27,9 @@ export declare class TextSlicer {
|
|
|
26
27
|
private callbacks;
|
|
27
28
|
private charIndex;
|
|
28
29
|
private mounted;
|
|
30
|
+
private ro;
|
|
31
|
+
private frozen;
|
|
32
|
+
private freezeWordWidthsBound;
|
|
29
33
|
constructor(options?: TextSlicerOptions, callbacks?: TextSlicerCallbacks);
|
|
30
34
|
get metrics(): TextSlicerMetrics;
|
|
31
35
|
init(): void;
|
|
@@ -39,4 +43,9 @@ export declare class TextSlicer {
|
|
|
39
43
|
private createWordSpan;
|
|
40
44
|
private createCharSpan;
|
|
41
45
|
private createSpaceSpan;
|
|
46
|
+
private scheduleFreezeWordWidths;
|
|
47
|
+
private freezeWordWidths;
|
|
48
|
+
private unfreezeWordWidths;
|
|
49
|
+
private attachResizeObserver;
|
|
50
|
+
private detachResizeObserver;
|
|
42
51
|
}
|
package/dist/index.es.js
CHANGED
|
@@ -1,89 +1,126 @@
|
|
|
1
|
-
const u = { splitMode: "both", cssVariables: !1, dataAttributes: !1, keepWhitespaceNodes: !0 },
|
|
1
|
+
const u = { splitMode: "both", cssVariables: !1, dataAttributes: !1, keepWhitespaceNodes: !0, freezeWordWidths: !1 }, n = Object.freeze({ word: "ts-word", char: "ts-char", whitespace: "ts-whitespace" }), h = " ", f = (r) => typeof window > "u" || typeof document > "u" ? null : r ? typeof r == "string" ? document.querySelector(r) : r : null, d = (r) => {
|
|
2
2
|
const t = Intl.Segmenter;
|
|
3
3
|
if (typeof t == "function") {
|
|
4
4
|
const e = new t("en", { granularity: "grapheme" });
|
|
5
|
-
return Array.from(e.segment(
|
|
5
|
+
return Array.from(e.segment(r), (s) => s.segment);
|
|
6
6
|
}
|
|
7
|
-
return Array.from(
|
|
8
|
-
},
|
|
7
|
+
return Array.from(r);
|
|
8
|
+
}, c = (r) => r.split(h), a = (r) => {
|
|
9
9
|
const t = {};
|
|
10
|
-
return Object.keys(
|
|
11
|
-
const s =
|
|
10
|
+
return Object.keys(r).forEach((e) => {
|
|
11
|
+
const s = r[e];
|
|
12
12
|
s !== void 0 && (t[e] = s);
|
|
13
13
|
}), t;
|
|
14
14
|
};
|
|
15
|
-
class
|
|
15
|
+
class W {
|
|
16
16
|
el;
|
|
17
17
|
original;
|
|
18
18
|
opts;
|
|
19
19
|
callbacks;
|
|
20
20
|
charIndex;
|
|
21
21
|
mounted;
|
|
22
|
+
ro = null;
|
|
23
|
+
frozen = !1;
|
|
24
|
+
freezeWordWidthsBound = () => {
|
|
25
|
+
this.freezeWordWidths();
|
|
26
|
+
};
|
|
22
27
|
constructor(t = {}, e) {
|
|
23
|
-
const s =
|
|
24
|
-
this.el = s, this.original = ((
|
|
28
|
+
const s = f(t.container);
|
|
29
|
+
this.el = s, this.original = ((i) => !!i && typeof HTMLElement < "u" && i instanceof HTMLElement)(s) ? s.textContent?.toString() ?? "" : "", this.opts = { ...u, ...a(t) }, this.callbacks = e, this.charIndex = 0, this.mounted = !1;
|
|
25
30
|
}
|
|
26
31
|
get metrics() {
|
|
27
32
|
const t = this.original;
|
|
28
|
-
return { wordTotal: t.length ?
|
|
33
|
+
return { wordTotal: t.length ? c(t).length : 0, charTotal: t.length, renderedAt: Date.now() };
|
|
29
34
|
}
|
|
30
35
|
init() {
|
|
31
36
|
this.el && (this.mounted = !0, this.split());
|
|
32
37
|
}
|
|
33
38
|
reinit(t, e) {
|
|
34
|
-
this.el && (typeof t == "string" && (this.original = t), e && (this.opts = { ...this.opts, ...
|
|
39
|
+
this.el && (typeof t == "string" && (this.original = t), e && (this.opts = { ...this.opts, ...a(e) }), this.split());
|
|
35
40
|
}
|
|
36
41
|
clear() {
|
|
37
|
-
this.el && this.el.replaceChildren();
|
|
42
|
+
this.el && (this.unfreezeWordWidths(), this.el.replaceChildren());
|
|
38
43
|
}
|
|
39
44
|
split() {
|
|
40
45
|
if (!this.el) return;
|
|
41
46
|
this.clear(), this.charIndex = 0;
|
|
42
|
-
const t = this.original, e = document.createDocumentFragment(), s =
|
|
43
|
-
this.opts.splitMode === "chars" ? this.appendChars(e, t) : this.appendWords(e, s), this.el.appendChild(e), this.opts.cssVariables && (this.el.style.setProperty("--word-total", String(s.length)), this.el.style.setProperty("--char-total", String(t.length))), this.
|
|
47
|
+
const t = this.original, e = document.createDocumentFragment(), s = c(t);
|
|
48
|
+
this.opts.splitMode === "chars" ? this.appendChars(e, t) : this.appendWords(e, s), this.el.appendChild(e), this.opts.cssVariables && (this.el.style.setProperty("--word-total", String(s.length)), this.el.style.setProperty("--char-total", String(t.length))), this.scheduleFreezeWordWidths().then(() => {
|
|
49
|
+
this.callbacks?.onAfterRender?.(this.metrics);
|
|
50
|
+
}).catch(() => {
|
|
51
|
+
});
|
|
44
52
|
}
|
|
45
53
|
destroy() {
|
|
46
|
-
this.el && (this.clear(), this.mounted = !1);
|
|
54
|
+
this.el && (this.detachResizeObserver(), this.clear(), this.mounted = !1);
|
|
47
55
|
}
|
|
48
56
|
updateOptions(t) {
|
|
49
|
-
this.opts = { ...this.opts, ...
|
|
57
|
+
this.opts = { ...this.opts, ...a(t) }, this.mounted && this.split();
|
|
50
58
|
}
|
|
51
59
|
appendWords(t, e) {
|
|
52
|
-
e.forEach((s,
|
|
60
|
+
e.forEach((s, i) => {
|
|
53
61
|
if (this.opts.splitMode === "both") {
|
|
54
|
-
const
|
|
55
|
-
for (const
|
|
56
|
-
const
|
|
57
|
-
|
|
62
|
+
const o = this.createWordSpan(i, s);
|
|
63
|
+
for (const l of d(s)) {
|
|
64
|
+
const p = this.createCharSpan(l);
|
|
65
|
+
o.append(p);
|
|
58
66
|
}
|
|
59
|
-
t.append(
|
|
67
|
+
t.append(o);
|
|
60
68
|
} else {
|
|
61
|
-
const
|
|
62
|
-
|
|
69
|
+
const o = this.createWordSpan(i);
|
|
70
|
+
o.append(document.createTextNode(s)), t.append(o);
|
|
63
71
|
}
|
|
64
|
-
|
|
72
|
+
i < e.length - 1 && t.append(this.createSpaceSpan());
|
|
65
73
|
});
|
|
66
74
|
}
|
|
67
75
|
appendChars(t, e) {
|
|
68
|
-
for (const s of
|
|
69
|
-
const
|
|
70
|
-
t.append(
|
|
76
|
+
for (const s of d(e)) {
|
|
77
|
+
const i = this.createCharSpan(s);
|
|
78
|
+
t.append(i);
|
|
71
79
|
}
|
|
72
80
|
}
|
|
73
81
|
createWordSpan(t, e = "") {
|
|
74
82
|
const s = document.createElement("span");
|
|
75
|
-
return s.classList.add(
|
|
83
|
+
return s.classList.add(n.word), this.opts.dataAttributes && e && s.setAttribute("data-word", e), this.opts.cssVariables && s.style.setProperty("--word-index", String(t)), s;
|
|
76
84
|
}
|
|
77
85
|
createCharSpan(t) {
|
|
78
86
|
const e = document.createElement("span");
|
|
79
|
-
return e.textContent = t, this.opts.dataAttributes && e.setAttribute("data-char", t), t ===
|
|
87
|
+
return e.textContent = t, this.opts.dataAttributes && e.setAttribute("data-char", t), t === h ? (e.classList.add(n.whitespace), this.opts.keepWhitespaceNodes || (e.textContent = h)) : (e.classList.add(n.char), this.opts.cssVariables && e.style.setProperty("--char-index", String(this.charIndex)), this.charIndex += 1), e;
|
|
80
88
|
}
|
|
81
89
|
createSpaceSpan() {
|
|
82
90
|
const t = document.createElement("span");
|
|
83
|
-
return t.classList.add(
|
|
91
|
+
return t.classList.add(n.whitespace), t.textContent = h, t;
|
|
92
|
+
}
|
|
93
|
+
async scheduleFreezeWordWidths() {
|
|
94
|
+
if (this.el && this.opts.freezeWordWidths) {
|
|
95
|
+
try {
|
|
96
|
+
await document.fonts?.ready;
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
await new Promise((t) => requestAnimationFrame(() => t())), this.freezeWordWidths(), this.attachResizeObserver();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
freezeWordWidths() {
|
|
103
|
+
this.el && (this.el.querySelectorAll(`.${n.word}`).forEach((t) => {
|
|
104
|
+
t.style.width = "", t.style.flex = "";
|
|
105
|
+
const { width: e } = t.getBoundingClientRect();
|
|
106
|
+
t.style.width = `${Math.ceil(e)}px`, t.style.flex = "0 0 auto";
|
|
107
|
+
}), this.frozen = !0);
|
|
108
|
+
}
|
|
109
|
+
unfreezeWordWidths() {
|
|
110
|
+
!this.el || !this.frozen || (this.el.querySelectorAll(`.${n.word}`).forEach((t) => {
|
|
111
|
+
t.style.width = "", t.style.flex = "";
|
|
112
|
+
}), this.frozen = !1);
|
|
113
|
+
}
|
|
114
|
+
attachResizeObserver() {
|
|
115
|
+
this.el && this.opts.freezeWordWidths && (this.ro || (this.ro = new ResizeObserver(() => {
|
|
116
|
+
this.freezeWordWidths();
|
|
117
|
+
}), this.ro.observe(this.el)), window.addEventListener("resize", this.freezeWordWidthsBound, { passive: !0 }));
|
|
118
|
+
}
|
|
119
|
+
detachResizeObserver() {
|
|
120
|
+
this.ro && (this.ro.disconnect(), this.ro = null), window.removeEventListener("resize", this.freezeWordWidthsBound);
|
|
84
121
|
}
|
|
85
122
|
}
|
|
86
123
|
export {
|
|
87
|
-
|
|
88
|
-
|
|
124
|
+
n as CLASSNAMES,
|
|
125
|
+
W as TextSlicer
|
|
89
126
|
};
|
package/dist/index.umd.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(
|
|
1
|
+
(function(n,o){typeof exports=="object"&&typeof module<"u"?o(exports):typeof define=="function"&&define.amd?define(["exports"],o):o((n=typeof globalThis<"u"?globalThis:n||self).TextSlicer={})})(this,function(n){"use strict";const o={splitMode:"both",cssVariables:!1,dataAttributes:!1,keepWhitespaceNodes:!0,freezeWordWidths:!1},i=Object.freeze({word:"ts-word",char:"ts-char",whitespace:"ts-whitespace"}),a=" ",p=t=>typeof window>"u"||typeof document>"u"?null:t?typeof t=="string"?document.querySelector(t):t:null,c=t=>{const e=Intl.Segmenter;if(typeof e=="function"){const s=new e("en",{granularity:"grapheme"});return Array.from(s.segment(t),r=>r.segment)}return Array.from(t)},l=t=>t.split(a),d=t=>{const e={};return Object.keys(t).forEach(s=>{const r=t[s];r!==void 0&&(e[s]=r)}),e};n.CLASSNAMES=i,n.TextSlicer=class{el;original;opts;callbacks;charIndex;mounted;ro=null;frozen=!1;freezeWordWidthsBound=()=>{this.freezeWordWidths()};constructor(t={},e){const s=p(t.container);this.el=s,this.original=(r=>!!r&&typeof HTMLElement<"u"&&r instanceof HTMLElement)(s)?s.textContent?.toString()??"":"",this.opts={...o,...d(t)},this.callbacks=e,this.charIndex=0,this.mounted=!1}get metrics(){const t=this.original;return{wordTotal:t.length?l(t).length:0,charTotal:t.length,renderedAt:Date.now()}}init(){this.el&&(this.mounted=!0,this.split())}reinit(t,e){this.el&&(typeof t=="string"&&(this.original=t),e&&(this.opts={...this.opts,...d(e)}),this.split())}clear(){this.el&&(this.unfreezeWordWidths(),this.el.replaceChildren())}split(){if(!this.el)return;this.clear(),this.charIndex=0;const t=this.original,e=document.createDocumentFragment(),s=l(t);this.opts.splitMode==="chars"?this.appendChars(e,t):this.appendWords(e,s),this.el.appendChild(e),this.opts.cssVariables&&(this.el.style.setProperty("--word-total",String(s.length)),this.el.style.setProperty("--char-total",String(t.length))),this.scheduleFreezeWordWidths().then(()=>{this.callbacks?.onAfterRender?.(this.metrics)}).catch(()=>{})}destroy(){this.el&&(this.detachResizeObserver(),this.clear(),this.mounted=!1)}updateOptions(t){this.opts={...this.opts,...d(t)},this.mounted&&this.split()}appendWords(t,e){e.forEach((s,r)=>{if(this.opts.splitMode==="both"){const h=this.createWordSpan(r,s);for(const f of c(s)){const u=this.createCharSpan(f);h.append(u)}t.append(h)}else{const h=this.createWordSpan(r);h.append(document.createTextNode(s)),t.append(h)}r<e.length-1&&t.append(this.createSpaceSpan())})}appendChars(t,e){for(const s of c(e)){const r=this.createCharSpan(s);t.append(r)}}createWordSpan(t,e=""){const s=document.createElement("span");return s.classList.add(i.word),this.opts.dataAttributes&&e&&s.setAttribute("data-word",e),this.opts.cssVariables&&s.style.setProperty("--word-index",String(t)),s}createCharSpan(t){const e=document.createElement("span");return e.textContent=t,this.opts.dataAttributes&&e.setAttribute("data-char",t),t===a?(e.classList.add(i.whitespace),this.opts.keepWhitespaceNodes||(e.textContent=a)):(e.classList.add(i.char),this.opts.cssVariables&&e.style.setProperty("--char-index",String(this.charIndex)),this.charIndex+=1),e}createSpaceSpan(){const t=document.createElement("span");return t.classList.add(i.whitespace),t.textContent=a,t}async scheduleFreezeWordWidths(){if(this.el&&this.opts.freezeWordWidths){try{await document.fonts?.ready}catch{}await new Promise(t=>requestAnimationFrame(()=>t())),this.freezeWordWidths(),this.attachResizeObserver()}}freezeWordWidths(){this.el&&(this.el.querySelectorAll(`.${i.word}`).forEach(t=>{t.style.width="",t.style.flex="";const{width:e}=t.getBoundingClientRect();t.style.width=`${Math.ceil(e)}px`,t.style.flex="0 0 auto"}),this.frozen=!0)}unfreezeWordWidths(){!this.el||!this.frozen||(this.el.querySelectorAll(`.${i.word}`).forEach(t=>{t.style.width="",t.style.flex=""}),this.frozen=!1)}attachResizeObserver(){this.el&&this.opts.freezeWordWidths&&(this.ro||(this.ro=new ResizeObserver(()=>{this.freezeWordWidths()}),this.ro.observe(this.el)),window.addEventListener("resize",this.freezeWordWidthsBound,{passive:!0}))}detachResizeObserver(){this.ro&&(this.ro.disconnect(),this.ro=null),window.removeEventListener("resize",this.freezeWordWidthsBound)}},Object.defineProperty(n,Symbol.toStringTag,{value:"Module"})});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "text-slicer",
|
|
3
|
-
"version": "1.4.0
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "TextSlicer is designed to split text within an HTML element into separate words and/or characters, wrapping each word and/or character in separate span elements.",
|
|
5
5
|
"author": "ux-ui.pro",
|
|
6
6
|
"license": "MIT",
|