text-slicer 1.5.0-dev.5 → 2.0.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 +88 -129
- package/dist/index.cjs +212 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +52 -0
- package/dist/index.d.ts +47 -44
- package/dist/index.js +209 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -31
- package/dist/index.cjs.js +0 -1
- package/dist/index.es.js +0 -111
- package/dist/index.umd.js +0 -1
package/README.md
CHANGED
|
@@ -1,174 +1,133 @@
|
|
|
1
|
-
|
|
1
|
+
# text-slicer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Split text inside an HTML element into words and/or characters, wrapping each fragment in its own span.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/text-slicer)
|
|
6
|
-
[](https://www.npmjs.org/package/text-slicer)
|
|
6
|
+
[](https://www.npmjs.com/package/text-slicer)
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
[Demo](https://codepen.io/ux-ui/full/vYMoGoG)
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
<p align="center"><a href="https://codepen.io/ux-ui/full/vYMoGoG">Demo</a></p>
|
|
13
|
-
<br>
|
|
10
|
+
---
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
- Split by words, characters, or both (`splitMode`).
|
|
13
|
+
- Optional CSS variables (`--word-index`, `--char-index`, `--word-total`, `--char-total`, `--container-height`).
|
|
14
|
+
- Optional `data-word` / `data-char` attributes for styling or scripting.
|
|
15
|
+
- Lifecycle helpers: `init`, `reinit`, `clear`, `split`, `destroy`, `updateOptions`, `lockHeight`, `unlockHeight`.
|
|
16
|
+
- `onAfterRender` callback with word/char totals and timestamp.
|
|
17
|
+
- ~1.5kB gzipped.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
16
22
|
|
|
17
23
|
```bash
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
npm install text-slicer
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
HTML:
|
|
30
|
+
|
|
31
|
+
```html
|
|
32
|
+
<p class="text-slicer">Hello world</p>
|
|
21
33
|
```
|
|
22
|
-
<br>
|
|
23
34
|
|
|
24
|
-
|
|
35
|
+
JavaScript:
|
|
25
36
|
|
|
26
37
|
```ts
|
|
27
38
|
import { TextSlicer } from 'text-slicer';
|
|
28
39
|
|
|
29
|
-
const
|
|
40
|
+
const textSlicer = new TextSlicer({ container: '.text-slicer' });
|
|
30
41
|
|
|
31
|
-
|
|
42
|
+
textSlicer.init();
|
|
32
43
|
```
|
|
33
44
|
|
|
34
|
-
|
|
45
|
+
With options and a callback:
|
|
35
46
|
|
|
36
47
|
```ts
|
|
37
|
-
document.
|
|
38
|
-
const
|
|
48
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
49
|
+
const textSlicer = new TextSlicer(
|
|
50
|
+
{
|
|
51
|
+
container: '.text-slicer',
|
|
52
|
+
splitMode: 'both',
|
|
53
|
+
cssVariables: true,
|
|
54
|
+
dataAttributes: true,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
onAfterRender: (metrics) => console.log(metrics),
|
|
58
|
+
},
|
|
59
|
+
);
|
|
39
60
|
|
|
40
|
-
|
|
61
|
+
textSlicer.init();
|
|
41
62
|
});
|
|
42
63
|
```
|
|
43
|
-
<br>
|
|
44
|
-
|
|
45
|
-
## API
|
|
46
64
|
|
|
47
|
-
|
|
65
|
+
Apply to every matching element on the page:
|
|
48
66
|
|
|
49
67
|
```ts
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
68
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
69
|
+
document.querySelectorAll('.text-slicer').forEach((element) => {
|
|
70
|
+
const textSlicer = new TextSlicer({ container: element });
|
|
75
71
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
72
|
+
textSlicer.init();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
89
75
|
```
|
|
90
76
|
|
|
91
|
-
|
|
77
|
+
## API
|
|
78
|
+
|
|
79
|
+
Named exports:
|
|
92
80
|
|
|
93
81
|
```ts
|
|
94
|
-
|
|
82
|
+
import { TextSlicer, CLASSNAMES } from 'text-slicer';
|
|
95
83
|
```
|
|
96
84
|
|
|
97
|
-
|
|
85
|
+
- `TextSlicer` — main class.
|
|
86
|
+
- `CLASSNAMES` — BEM-style class map (`word`, `char`, `whitespace`).
|
|
87
|
+
- Types: `SplitMode`, `TextSlicerOptions`, `TextSlicerMetrics`, `TextSlicerCallbacks`.
|
|
98
88
|
|
|
99
|
-
|
|
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.
|
|
89
|
+
Constructor: `new TextSlicer(options?, callbacks?)`.
|
|
106
90
|
|
|
107
|
-
|
|
91
|
+
## Options
|
|
108
92
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
93
|
+
| Option | Type | Default | Description |
|
|
94
|
+
|:----------------------|:------------------------|:-----------:|:----------------------------------------------------------------------------|
|
|
95
|
+
| `container` | `HTMLElement \| string` | `undefined` | Target element or selector for text splitting. |
|
|
96
|
+
| `splitMode` | `'words' \| 'chars' \| 'both'` | `both` | Split by words, characters, or both. |
|
|
97
|
+
| `cssVariables` | `boolean` | `false` | Sets `--word-index`, `--char-index`, `--word-total`, `--char-total` on spans. |
|
|
98
|
+
| `dataAttributes` | `boolean` | `false` | Adds `data-word` and `data-char` attributes. |
|
|
99
|
+
| `keepWhitespaceNodes` | `boolean` | `true` | When `false`, whitespace nodes are ignored in character mode. |
|
|
100
|
+
| `containerHeightVar` | `boolean` | `false` | Sets dynamic `--container-height` via `ResizeObserver`. |
|
|
115
101
|
|
|
116
|
-
|
|
102
|
+
## Callbacks
|
|
117
103
|
|
|
118
|
-
|
|
119
|
-
|
|
104
|
+
| Callback | Arguments | Description |
|
|
105
|
+
|-----------------|---------------------|--------------------------------------------------------------------------|
|
|
106
|
+
| `onAfterRender` | `TextSlicerMetrics` | Called after render; provides `wordTotal`, `charTotal`, `renderedAt`. |
|
|
120
107
|
|
|
121
|
-
|
|
108
|
+
## Methods
|
|
122
109
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
```
|
|
110
|
+
| Method | Description |
|
|
111
|
+
|--------------------------------|----------------------------------------------------------|
|
|
112
|
+
| `init()` | Initializes and renders text splitting. |
|
|
113
|
+
| `reinit(newText?, options?)` | Re-initializes with optional new text and updated options. |
|
|
114
|
+
| `clear()` | Clears all content inside the container. |
|
|
115
|
+
| `split()` | Manually triggers splitting and rendering. |
|
|
116
|
+
| `destroy()` | Cleans up instance, observers, and styles. |
|
|
117
|
+
| `updateOptions(options)` | Updates options at runtime; re-renders if mounted. |
|
|
118
|
+
| `lockHeight()` | Locks container height to its measured value. |
|
|
119
|
+
| `unlockHeight()` | Unlocks container height. |
|
|
120
|
+
| `metrics` (getter) | Returns `wordTotal`, `charTotal`, and `renderedAt`. |
|
|
135
121
|
|
|
136
|
-
|
|
122
|
+
## Styling
|
|
137
123
|
|
|
138
|
-
|
|
139
|
-
const slicer = new TextSlicer({
|
|
140
|
-
container: '.headline',
|
|
141
|
-
splitMode: 'both',
|
|
142
|
-
freezeWordWidths: true,
|
|
143
|
-
});
|
|
144
|
-
slicer.init();
|
|
145
|
-
```
|
|
124
|
+
Exported `CLASSNAMES`:
|
|
146
125
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
```
|
|
171
|
-
<br>
|
|
126
|
+
- `ts-word` — word wrapper (when splitting includes words).
|
|
127
|
+
- `ts-char` — character span.
|
|
128
|
+
- `ts-whitespace` — whitespace span.
|
|
129
|
+
|
|
130
|
+
When `cssVariables: true`, spans receive `--word-index`, `--char-index`; the container may receive `--word-total`, `--char-total`, and (with `containerHeightVar`) `--container-height`.
|
|
172
131
|
|
|
173
132
|
## License
|
|
174
133
|
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
|
|
3
|
+
//#region src/index.ts
|
|
4
|
+
const DEFAULT_OPTIONS = {
|
|
5
|
+
splitMode: "both",
|
|
6
|
+
cssVariables: false,
|
|
7
|
+
dataAttributes: false,
|
|
8
|
+
keepWhitespaceNodes: true,
|
|
9
|
+
containerHeightVar: false
|
|
10
|
+
};
|
|
11
|
+
const CLASSNAMES = Object.freeze({
|
|
12
|
+
word: "ts-word",
|
|
13
|
+
char: "ts-char",
|
|
14
|
+
whitespace: "ts-whitespace"
|
|
15
|
+
});
|
|
16
|
+
const SPACE = " ";
|
|
17
|
+
const CSS_VAR_WORD_TOTAL = "--word-total";
|
|
18
|
+
const CSS_VAR_CHAR_TOTAL = "--char-total";
|
|
19
|
+
const CSS_VAR_WORD_INDEX = "--word-index";
|
|
20
|
+
const CSS_VAR_CHAR_INDEX = "--char-index";
|
|
21
|
+
const CSS_VAR_CONTAINER_HEIGHT = "--container-height";
|
|
22
|
+
const MEASURING_CLASS = "ts-measuring";
|
|
23
|
+
const canUseDOM = () => typeof window !== "undefined" && typeof document !== "undefined";
|
|
24
|
+
const resolveContainer = (container) => {
|
|
25
|
+
if (!canUseDOM()) return null;
|
|
26
|
+
if (!container) return null;
|
|
27
|
+
return typeof container === "string" ? document.querySelector(container) : container;
|
|
28
|
+
};
|
|
29
|
+
const splitIntoGraphemes = (text) => {
|
|
30
|
+
const Seg = Intl.Segmenter;
|
|
31
|
+
if (typeof Seg === "function") {
|
|
32
|
+
const segmenter = new Seg("en", { granularity: "grapheme" });
|
|
33
|
+
return Array.from(segmenter.segment(text), (s) => s.segment);
|
|
34
|
+
}
|
|
35
|
+
return Array.from(text);
|
|
36
|
+
};
|
|
37
|
+
const splitIntoWords = (text) => text.split(SPACE);
|
|
38
|
+
const emptyElement = (el) => {
|
|
39
|
+
el.replaceChildren();
|
|
40
|
+
};
|
|
41
|
+
const isHTMLElement = (el) => !!el && typeof HTMLElement !== "undefined" && el instanceof HTMLElement;
|
|
42
|
+
const omitUndefined = (obj) => {
|
|
43
|
+
const out = {};
|
|
44
|
+
Object.keys(obj).forEach((key) => {
|
|
45
|
+
const val = obj[key];
|
|
46
|
+
if (val !== void 0) out[key] = val;
|
|
47
|
+
});
|
|
48
|
+
return out;
|
|
49
|
+
};
|
|
50
|
+
var TextSlicer = class {
|
|
51
|
+
el;
|
|
52
|
+
original;
|
|
53
|
+
opts;
|
|
54
|
+
callbacks;
|
|
55
|
+
charIndex;
|
|
56
|
+
mounted;
|
|
57
|
+
resizeObserver;
|
|
58
|
+
constructor(options = {}, callbacks) {
|
|
59
|
+
const el = resolveContainer(options.container);
|
|
60
|
+
this.el = el;
|
|
61
|
+
this.original = isHTMLElement(el) ? el.textContent?.toString() ?? "" : "";
|
|
62
|
+
this.opts = {
|
|
63
|
+
...DEFAULT_OPTIONS,
|
|
64
|
+
...omitUndefined(options)
|
|
65
|
+
};
|
|
66
|
+
this.callbacks = callbacks;
|
|
67
|
+
this.charIndex = 0;
|
|
68
|
+
this.mounted = false;
|
|
69
|
+
}
|
|
70
|
+
get metrics() {
|
|
71
|
+
const text = this.original;
|
|
72
|
+
return {
|
|
73
|
+
wordTotal: text.length ? splitIntoWords(text).length : 0,
|
|
74
|
+
charTotal: text.length,
|
|
75
|
+
renderedAt: Date.now()
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
init() {
|
|
79
|
+
if (!this.el) return;
|
|
80
|
+
this.mounted = true;
|
|
81
|
+
this.split();
|
|
82
|
+
if (this.opts.containerHeightVar) this.initHeightObserver();
|
|
83
|
+
}
|
|
84
|
+
reinit(newText, next) {
|
|
85
|
+
if (!this.el) return;
|
|
86
|
+
if (typeof newText === "string") this.original = newText;
|
|
87
|
+
if (next) this.opts = {
|
|
88
|
+
...this.opts,
|
|
89
|
+
...omitUndefined(next)
|
|
90
|
+
};
|
|
91
|
+
this.split();
|
|
92
|
+
}
|
|
93
|
+
clear() {
|
|
94
|
+
if (!this.el) return;
|
|
95
|
+
emptyElement(this.el);
|
|
96
|
+
}
|
|
97
|
+
split() {
|
|
98
|
+
if (!this.el) return;
|
|
99
|
+
this.clear();
|
|
100
|
+
this.charIndex = 0;
|
|
101
|
+
const text = this.original;
|
|
102
|
+
const fragment = document.createDocumentFragment();
|
|
103
|
+
const words = splitIntoWords(text);
|
|
104
|
+
if (this.opts.splitMode === "chars") this.appendChars(fragment, text);
|
|
105
|
+
else this.appendWords(fragment, words);
|
|
106
|
+
this.el.appendChild(fragment);
|
|
107
|
+
if (this.opts.cssVariables) {
|
|
108
|
+
this.el.style.setProperty(CSS_VAR_WORD_TOTAL, String(words.length));
|
|
109
|
+
this.el.style.setProperty(CSS_VAR_CHAR_TOTAL, String(text.length));
|
|
110
|
+
}
|
|
111
|
+
this.callbacks?.onAfterRender?.(this.metrics);
|
|
112
|
+
}
|
|
113
|
+
destroy() {
|
|
114
|
+
if (!this.el) return;
|
|
115
|
+
this.clear();
|
|
116
|
+
this.unlockHeight();
|
|
117
|
+
if (this.resizeObserver) {
|
|
118
|
+
this.resizeObserver.disconnect();
|
|
119
|
+
this.resizeObserver = void 0;
|
|
120
|
+
}
|
|
121
|
+
if (this.opts.containerHeightVar) this.el.style.removeProperty(CSS_VAR_CONTAINER_HEIGHT);
|
|
122
|
+
this.mounted = false;
|
|
123
|
+
}
|
|
124
|
+
updateOptions(next) {
|
|
125
|
+
this.opts = {
|
|
126
|
+
...this.opts,
|
|
127
|
+
...omitUndefined(next)
|
|
128
|
+
};
|
|
129
|
+
if (this.mounted) this.split();
|
|
130
|
+
}
|
|
131
|
+
lockHeight() {
|
|
132
|
+
if (!this.el) return;
|
|
133
|
+
const h = this.measureHeight();
|
|
134
|
+
if (h > 0) this.el.style.height = `${h}px`;
|
|
135
|
+
}
|
|
136
|
+
unlockHeight() {
|
|
137
|
+
if (!this.el) return;
|
|
138
|
+
this.el.style.removeProperty("height");
|
|
139
|
+
}
|
|
140
|
+
appendWords(fragment, words) {
|
|
141
|
+
words.forEach((word, wordIndex) => {
|
|
142
|
+
if (this.opts.splitMode === "both") {
|
|
143
|
+
const wordSpan = this.createWordSpan(wordIndex, word);
|
|
144
|
+
for (const ch of splitIntoGraphemes(word)) {
|
|
145
|
+
const charSpan = this.createCharSpan(ch);
|
|
146
|
+
wordSpan.append(charSpan);
|
|
147
|
+
}
|
|
148
|
+
fragment.append(wordSpan);
|
|
149
|
+
} else {
|
|
150
|
+
const wordSpan = this.createWordSpan(wordIndex);
|
|
151
|
+
wordSpan.append(document.createTextNode(word));
|
|
152
|
+
fragment.append(wordSpan);
|
|
153
|
+
}
|
|
154
|
+
if (wordIndex < words.length - 1) fragment.append(this.createSpaceSpan());
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
appendChars(fragment, text) {
|
|
158
|
+
for (const ch of splitIntoGraphemes(text)) {
|
|
159
|
+
const span = this.createCharSpan(ch);
|
|
160
|
+
fragment.append(span);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
createWordSpan(index, word = "") {
|
|
164
|
+
const span = document.createElement("span");
|
|
165
|
+
span.classList.add(CLASSNAMES.word);
|
|
166
|
+
if (this.opts.dataAttributes && word) span.setAttribute("data-word", word);
|
|
167
|
+
if (this.opts.cssVariables) span.style.setProperty(CSS_VAR_WORD_INDEX, String(index));
|
|
168
|
+
return span;
|
|
169
|
+
}
|
|
170
|
+
createCharSpan(ch) {
|
|
171
|
+
const span = document.createElement("span");
|
|
172
|
+
span.textContent = ch;
|
|
173
|
+
if (this.opts.dataAttributes) span.setAttribute("data-char", ch);
|
|
174
|
+
if (ch === SPACE) {
|
|
175
|
+
span.classList.add(CLASSNAMES.whitespace);
|
|
176
|
+
if (!this.opts.keepWhitespaceNodes) span.textContent = SPACE;
|
|
177
|
+
} else {
|
|
178
|
+
span.classList.add(CLASSNAMES.char);
|
|
179
|
+
if (this.opts.cssVariables) span.style.setProperty(CSS_VAR_CHAR_INDEX, String(this.charIndex));
|
|
180
|
+
this.charIndex += 1;
|
|
181
|
+
}
|
|
182
|
+
return span;
|
|
183
|
+
}
|
|
184
|
+
createSpaceSpan() {
|
|
185
|
+
const span = document.createElement("span");
|
|
186
|
+
span.classList.add(CLASSNAMES.whitespace);
|
|
187
|
+
span.textContent = SPACE;
|
|
188
|
+
return span;
|
|
189
|
+
}
|
|
190
|
+
measureHeight() {
|
|
191
|
+
if (!this.el) return 0;
|
|
192
|
+
this.el.classList.add(MEASURING_CLASS);
|
|
193
|
+
this.el.offsetHeight;
|
|
194
|
+
let h = this.el.offsetHeight || this.el.clientHeight || 0;
|
|
195
|
+
if (!h) h = Math.round(this.el.getBoundingClientRect().height);
|
|
196
|
+
this.el.classList.remove(MEASURING_CLASS);
|
|
197
|
+
return Math.max(0, Math.ceil(h));
|
|
198
|
+
}
|
|
199
|
+
initHeightObserver() {
|
|
200
|
+
if (!this.el) return;
|
|
201
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
202
|
+
const h = this.measureHeight();
|
|
203
|
+
if (h > 0) this.el?.style.setProperty(CSS_VAR_CONTAINER_HEIGHT, `${h}px`);
|
|
204
|
+
});
|
|
205
|
+
this.resizeObserver.observe(this.el);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
//#endregion
|
|
210
|
+
exports.CLASSNAMES = CLASSNAMES;
|
|
211
|
+
exports.TextSlicer = TextSlicer;
|
|
212
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["export type SplitMode = 'words' | 'chars' | 'both';\n\nexport interface TextSlicerOptions {\n container?: HTMLElement | string;\n splitMode?: SplitMode;\n cssVariables?: boolean;\n dataAttributes?: boolean;\n keepWhitespaceNodes?: boolean;\n containerHeightVar?: boolean;\n}\n\nexport interface TextSlicerMetrics {\n wordTotal: number;\n charTotal: number;\n renderedAt: number;\n}\n\nexport interface TextSlicerCallbacks {\n onAfterRender?: (metrics: TextSlicerMetrics) => void;\n}\n\ntype RuntimeOptions = Required<Omit<TextSlicerOptions, 'container'>>;\n\nconst DEFAULT_OPTIONS: RuntimeOptions = {\n splitMode: 'both',\n cssVariables: false,\n dataAttributes: false,\n keepWhitespaceNodes: true,\n containerHeightVar: false,\n};\n\nexport const CLASSNAMES = Object.freeze({\n word: 'ts-word',\n char: 'ts-char',\n whitespace: 'ts-whitespace',\n});\n\nconst SPACE = ' ';\nconst CSS_VAR_WORD_TOTAL = '--word-total';\nconst CSS_VAR_CHAR_TOTAL = '--char-total';\nconst CSS_VAR_WORD_INDEX = '--word-index';\nconst CSS_VAR_CHAR_INDEX = '--char-index';\nconst CSS_VAR_CONTAINER_HEIGHT = '--container-height';\nconst MEASURING_CLASS = 'ts-measuring';\n\nconst canUseDOM = (): boolean => typeof window !== 'undefined' && typeof document !== 'undefined';\n\nconst resolveContainer = (container?: HTMLElement | string): HTMLElement | null => {\n if (!canUseDOM()) return null;\n if (!container) return null;\n\n return typeof container === 'string' ? document.querySelector(container) : container;\n};\n\ntype IntlWithSegmenter = typeof Intl & {\n Segmenter?: new (locales?: string | string[], options?: Intl.SegmenterOptions) => Intl.Segmenter;\n};\n\nconst splitIntoGraphemes = (text: string): string[] => {\n const Seg = (Intl as IntlWithSegmenter).Segmenter;\n\n if (typeof Seg === 'function') {\n const segmenter = new Seg('en', { granularity: 'grapheme' });\n return Array.from(segmenter.segment(text), (s: Intl.SegmentData) => s.segment);\n }\n\n return Array.from(text);\n};\n\nconst splitIntoWords = (text: string): string[] => text.split(SPACE);\n\nconst emptyElement = (el: HTMLElement): void => {\n el.replaceChildren();\n};\n\nconst isHTMLElement = (el: unknown): el is HTMLElement =>\n !!el && typeof HTMLElement !== 'undefined' && el instanceof HTMLElement;\n\ntype NonUndefined<T> = T extends undefined ? never : T;\ntype OmitUndefined<T extends Record<string, unknown>> = {\n [K in keyof T as T[K] extends undefined ? never : K]: NonUndefined<T[K]>;\n};\n\nconst omitUndefined = <T extends Record<string, unknown>>(obj: T): OmitUndefined<T> => {\n const out = {} as OmitUndefined<T>;\n\n (Object.keys(obj) as Array<keyof T>).forEach((key) => {\n const val = obj[key];\n\n if (val !== undefined) {\n (out as Record<string, unknown>)[key as string] = val as unknown;\n }\n });\n\n return out;\n};\n\nexport class TextSlicer {\n private readonly el: HTMLElement | null;\n private original: string;\n private opts: RuntimeOptions;\n private callbacks: TextSlicerCallbacks | undefined;\n private charIndex: number;\n private mounted: boolean;\n private resizeObserver?: ResizeObserver;\n\n constructor(options: TextSlicerOptions = {}, callbacks?: TextSlicerCallbacks) {\n const el = resolveContainer(options.container);\n this.el = el;\n this.original = isHTMLElement(el) ? (el.textContent?.toString() ?? '') : '';\n this.opts = {\n ...DEFAULT_OPTIONS,\n ...omitUndefined<Omit<TextSlicerOptions, 'container'>>(options),\n } as RuntimeOptions;\n\n this.callbacks = callbacks;\n this.charIndex = 0;\n this.mounted = false;\n }\n\n get metrics(): TextSlicerMetrics {\n const text = this.original;\n\n return {\n wordTotal: text.length ? splitIntoWords(text).length : 0,\n charTotal: text.length,\n renderedAt: Date.now(),\n };\n }\n\n init(): void {\n if (!this.el) return;\n\n this.mounted = true;\n this.split();\n\n if (this.opts.containerHeightVar) {\n this.initHeightObserver();\n }\n }\n\n reinit(newText?: string, next?: Partial<TextSlicerOptions>): void {\n if (!this.el) return;\n if (typeof newText === 'string') this.original = newText;\n if (next) this.opts = { ...this.opts, ...omitUndefined(next) } as RuntimeOptions;\n\n this.split();\n }\n\n clear(): void {\n if (!this.el) return;\n\n emptyElement(this.el);\n }\n\n split(): void {\n if (!this.el) return;\n\n this.clear();\n this.charIndex = 0;\n\n const text = this.original;\n const fragment = document.createDocumentFragment();\n const words = splitIntoWords(text);\n\n if (this.opts.splitMode === 'chars') {\n this.appendChars(fragment, text);\n } else {\n this.appendWords(fragment, words);\n }\n\n this.el.appendChild(fragment);\n\n if (this.opts.cssVariables) {\n this.el.style.setProperty(CSS_VAR_WORD_TOTAL, String(words.length));\n this.el.style.setProperty(CSS_VAR_CHAR_TOTAL, String(text.length));\n }\n\n this.callbacks?.onAfterRender?.(this.metrics);\n }\n\n destroy(): void {\n if (!this.el) return;\n\n this.clear();\n this.unlockHeight();\n\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = undefined;\n }\n\n if (this.opts.containerHeightVar) {\n this.el.style.removeProperty(CSS_VAR_CONTAINER_HEIGHT);\n }\n\n this.mounted = false;\n }\n\n updateOptions(next: Partial<TextSlicerOptions>): void {\n this.opts = { ...this.opts, ...omitUndefined(next) } as RuntimeOptions;\n\n if (this.mounted) this.split();\n }\n\n lockHeight(): void {\n if (!this.el) return;\n\n const h = this.measureHeight();\n\n if (h > 0) {\n this.el.style.height = `${h}px`;\n }\n }\n\n unlockHeight(): void {\n if (!this.el) return;\n\n this.el.style.removeProperty('height');\n }\n\n private appendWords(fragment: DocumentFragment, words: string[]): void {\n words.forEach((word, wordIndex) => {\n if (this.opts.splitMode === 'both') {\n const wordSpan = this.createWordSpan(wordIndex, word);\n\n for (const ch of splitIntoGraphemes(word)) {\n const charSpan = this.createCharSpan(ch);\n\n wordSpan.append(charSpan);\n }\n\n fragment.append(wordSpan);\n } else {\n const wordSpan = this.createWordSpan(wordIndex);\n\n wordSpan.append(document.createTextNode(word));\n fragment.append(wordSpan);\n }\n\n if (wordIndex < words.length - 1) {\n fragment.append(this.createSpaceSpan());\n }\n });\n }\n\n private appendChars(fragment: DocumentFragment, text: string): void {\n for (const ch of splitIntoGraphemes(text)) {\n const span = this.createCharSpan(ch);\n\n fragment.append(span);\n }\n }\n\n private createWordSpan(index: number, word: string = ''): HTMLSpanElement {\n const span = document.createElement('span');\n\n span.classList.add(CLASSNAMES.word);\n\n if (this.opts.dataAttributes && word) {\n span.setAttribute('data-word', word);\n }\n\n if (this.opts.cssVariables) {\n span.style.setProperty(CSS_VAR_WORD_INDEX, String(index));\n }\n\n return span;\n }\n\n private createCharSpan(ch: string): HTMLSpanElement {\n const span = document.createElement('span');\n\n span.textContent = ch;\n\n if (this.opts.dataAttributes) span.setAttribute('data-char', ch);\n\n if (ch === SPACE) {\n span.classList.add(CLASSNAMES.whitespace);\n\n if (!this.opts.keepWhitespaceNodes) span.textContent = SPACE;\n } else {\n span.classList.add(CLASSNAMES.char);\n\n if (this.opts.cssVariables) {\n span.style.setProperty(CSS_VAR_CHAR_INDEX, String(this.charIndex));\n }\n\n this.charIndex += 1;\n }\n\n return span;\n }\n\n private createSpaceSpan(): HTMLSpanElement {\n const span = document.createElement('span');\n\n span.classList.add(CLASSNAMES.whitespace);\n span.textContent = SPACE;\n\n return span;\n }\n\n private measureHeight(): number {\n if (!this.el) return 0;\n\n this.el.classList.add(MEASURING_CLASS);\n void this.el.offsetHeight;\n\n let h = this.el.offsetHeight || this.el.clientHeight || 0;\n\n if (!h) {\n h = Math.round(this.el.getBoundingClientRect().height);\n }\n\n this.el.classList.remove(MEASURING_CLASS);\n\n return Math.max(0, Math.ceil(h));\n }\n\n private initHeightObserver(): void {\n if (!this.el) return;\n\n this.resizeObserver = new ResizeObserver(() => {\n const h = this.measureHeight();\n\n if (h > 0) {\n this.el?.style.setProperty(CSS_VAR_CONTAINER_HEIGHT, `${h}px`);\n }\n });\n\n this.resizeObserver.observe(this.el);\n }\n}\n"],"mappings":"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"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
type SplitMode = 'words' | 'chars' | 'both';
|
|
3
|
+
interface TextSlicerOptions {
|
|
4
|
+
container?: HTMLElement | string;
|
|
5
|
+
splitMode?: SplitMode;
|
|
6
|
+
cssVariables?: boolean;
|
|
7
|
+
dataAttributes?: boolean;
|
|
8
|
+
keepWhitespaceNodes?: boolean;
|
|
9
|
+
containerHeightVar?: boolean;
|
|
10
|
+
}
|
|
11
|
+
interface TextSlicerMetrics {
|
|
12
|
+
wordTotal: number;
|
|
13
|
+
charTotal: number;
|
|
14
|
+
renderedAt: number;
|
|
15
|
+
}
|
|
16
|
+
interface TextSlicerCallbacks {
|
|
17
|
+
onAfterRender?: (metrics: TextSlicerMetrics) => void;
|
|
18
|
+
}
|
|
19
|
+
declare const CLASSNAMES: Readonly<{
|
|
20
|
+
word: "ts-word";
|
|
21
|
+
char: "ts-char";
|
|
22
|
+
whitespace: "ts-whitespace";
|
|
23
|
+
}>;
|
|
24
|
+
declare class TextSlicer {
|
|
25
|
+
private readonly el;
|
|
26
|
+
private original;
|
|
27
|
+
private opts;
|
|
28
|
+
private callbacks;
|
|
29
|
+
private charIndex;
|
|
30
|
+
private mounted;
|
|
31
|
+
private resizeObserver?;
|
|
32
|
+
constructor(options?: TextSlicerOptions, callbacks?: TextSlicerCallbacks);
|
|
33
|
+
get metrics(): TextSlicerMetrics;
|
|
34
|
+
init(): void;
|
|
35
|
+
reinit(newText?: string, next?: Partial<TextSlicerOptions>): void;
|
|
36
|
+
clear(): void;
|
|
37
|
+
split(): void;
|
|
38
|
+
destroy(): void;
|
|
39
|
+
updateOptions(next: Partial<TextSlicerOptions>): void;
|
|
40
|
+
lockHeight(): void;
|
|
41
|
+
unlockHeight(): void;
|
|
42
|
+
private appendWords;
|
|
43
|
+
private appendChars;
|
|
44
|
+
private createWordSpan;
|
|
45
|
+
private createCharSpan;
|
|
46
|
+
private createSpaceSpan;
|
|
47
|
+
private measureHeight;
|
|
48
|
+
private initHeightObserver;
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { CLASSNAMES, SplitMode, TextSlicer, TextSlicerCallbacks, TextSlicerMetrics, TextSlicerOptions };
|
|
52
|
+
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,49 +1,52 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
type SplitMode = 'words' | 'chars' | 'both';
|
|
3
|
+
interface TextSlicerOptions {
|
|
4
|
+
container?: HTMLElement | string;
|
|
5
|
+
splitMode?: SplitMode;
|
|
6
|
+
cssVariables?: boolean;
|
|
7
|
+
dataAttributes?: boolean;
|
|
8
|
+
keepWhitespaceNodes?: boolean;
|
|
9
|
+
containerHeightVar?: boolean;
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
interface TextSlicerMetrics {
|
|
12
|
+
wordTotal: number;
|
|
13
|
+
charTotal: number;
|
|
14
|
+
renderedAt: number;
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
interface TextSlicerCallbacks {
|
|
17
|
+
onAfterRender?: (metrics: TextSlicerMetrics) => void;
|
|
17
18
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
declare const CLASSNAMES: Readonly<{
|
|
20
|
+
word: "ts-word";
|
|
21
|
+
char: "ts-char";
|
|
22
|
+
whitespace: "ts-whitespace";
|
|
22
23
|
}>;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
private initHeightObserver;
|
|
24
|
+
declare class TextSlicer {
|
|
25
|
+
private readonly el;
|
|
26
|
+
private original;
|
|
27
|
+
private opts;
|
|
28
|
+
private callbacks;
|
|
29
|
+
private charIndex;
|
|
30
|
+
private mounted;
|
|
31
|
+
private resizeObserver?;
|
|
32
|
+
constructor(options?: TextSlicerOptions, callbacks?: TextSlicerCallbacks);
|
|
33
|
+
get metrics(): TextSlicerMetrics;
|
|
34
|
+
init(): void;
|
|
35
|
+
reinit(newText?: string, next?: Partial<TextSlicerOptions>): void;
|
|
36
|
+
clear(): void;
|
|
37
|
+
split(): void;
|
|
38
|
+
destroy(): void;
|
|
39
|
+
updateOptions(next: Partial<TextSlicerOptions>): void;
|
|
40
|
+
lockHeight(): void;
|
|
41
|
+
unlockHeight(): void;
|
|
42
|
+
private appendWords;
|
|
43
|
+
private appendChars;
|
|
44
|
+
private createWordSpan;
|
|
45
|
+
private createCharSpan;
|
|
46
|
+
private createSpaceSpan;
|
|
47
|
+
private measureHeight;
|
|
48
|
+
private initHeightObserver;
|
|
49
49
|
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { CLASSNAMES, SplitMode, TextSlicer, TextSlicerCallbacks, TextSlicerMetrics, TextSlicerOptions };
|
|
52
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
//#region src/index.ts
|
|
2
|
+
const DEFAULT_OPTIONS = {
|
|
3
|
+
splitMode: "both",
|
|
4
|
+
cssVariables: false,
|
|
5
|
+
dataAttributes: false,
|
|
6
|
+
keepWhitespaceNodes: true,
|
|
7
|
+
containerHeightVar: false
|
|
8
|
+
};
|
|
9
|
+
const CLASSNAMES = Object.freeze({
|
|
10
|
+
word: "ts-word",
|
|
11
|
+
char: "ts-char",
|
|
12
|
+
whitespace: "ts-whitespace"
|
|
13
|
+
});
|
|
14
|
+
const SPACE = " ";
|
|
15
|
+
const CSS_VAR_WORD_TOTAL = "--word-total";
|
|
16
|
+
const CSS_VAR_CHAR_TOTAL = "--char-total";
|
|
17
|
+
const CSS_VAR_WORD_INDEX = "--word-index";
|
|
18
|
+
const CSS_VAR_CHAR_INDEX = "--char-index";
|
|
19
|
+
const CSS_VAR_CONTAINER_HEIGHT = "--container-height";
|
|
20
|
+
const MEASURING_CLASS = "ts-measuring";
|
|
21
|
+
const canUseDOM = () => typeof window !== "undefined" && typeof document !== "undefined";
|
|
22
|
+
const resolveContainer = (container) => {
|
|
23
|
+
if (!canUseDOM()) return null;
|
|
24
|
+
if (!container) return null;
|
|
25
|
+
return typeof container === "string" ? document.querySelector(container) : container;
|
|
26
|
+
};
|
|
27
|
+
const splitIntoGraphemes = (text) => {
|
|
28
|
+
const Seg = Intl.Segmenter;
|
|
29
|
+
if (typeof Seg === "function") {
|
|
30
|
+
const segmenter = new Seg("en", { granularity: "grapheme" });
|
|
31
|
+
return Array.from(segmenter.segment(text), (s) => s.segment);
|
|
32
|
+
}
|
|
33
|
+
return Array.from(text);
|
|
34
|
+
};
|
|
35
|
+
const splitIntoWords = (text) => text.split(SPACE);
|
|
36
|
+
const emptyElement = (el) => {
|
|
37
|
+
el.replaceChildren();
|
|
38
|
+
};
|
|
39
|
+
const isHTMLElement = (el) => !!el && typeof HTMLElement !== "undefined" && el instanceof HTMLElement;
|
|
40
|
+
const omitUndefined = (obj) => {
|
|
41
|
+
const out = {};
|
|
42
|
+
Object.keys(obj).forEach((key) => {
|
|
43
|
+
const val = obj[key];
|
|
44
|
+
if (val !== void 0) out[key] = val;
|
|
45
|
+
});
|
|
46
|
+
return out;
|
|
47
|
+
};
|
|
48
|
+
var TextSlicer = class {
|
|
49
|
+
el;
|
|
50
|
+
original;
|
|
51
|
+
opts;
|
|
52
|
+
callbacks;
|
|
53
|
+
charIndex;
|
|
54
|
+
mounted;
|
|
55
|
+
resizeObserver;
|
|
56
|
+
constructor(options = {}, callbacks) {
|
|
57
|
+
const el = resolveContainer(options.container);
|
|
58
|
+
this.el = el;
|
|
59
|
+
this.original = isHTMLElement(el) ? el.textContent?.toString() ?? "" : "";
|
|
60
|
+
this.opts = {
|
|
61
|
+
...DEFAULT_OPTIONS,
|
|
62
|
+
...omitUndefined(options)
|
|
63
|
+
};
|
|
64
|
+
this.callbacks = callbacks;
|
|
65
|
+
this.charIndex = 0;
|
|
66
|
+
this.mounted = false;
|
|
67
|
+
}
|
|
68
|
+
get metrics() {
|
|
69
|
+
const text = this.original;
|
|
70
|
+
return {
|
|
71
|
+
wordTotal: text.length ? splitIntoWords(text).length : 0,
|
|
72
|
+
charTotal: text.length,
|
|
73
|
+
renderedAt: Date.now()
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
init() {
|
|
77
|
+
if (!this.el) return;
|
|
78
|
+
this.mounted = true;
|
|
79
|
+
this.split();
|
|
80
|
+
if (this.opts.containerHeightVar) this.initHeightObserver();
|
|
81
|
+
}
|
|
82
|
+
reinit(newText, next) {
|
|
83
|
+
if (!this.el) return;
|
|
84
|
+
if (typeof newText === "string") this.original = newText;
|
|
85
|
+
if (next) this.opts = {
|
|
86
|
+
...this.opts,
|
|
87
|
+
...omitUndefined(next)
|
|
88
|
+
};
|
|
89
|
+
this.split();
|
|
90
|
+
}
|
|
91
|
+
clear() {
|
|
92
|
+
if (!this.el) return;
|
|
93
|
+
emptyElement(this.el);
|
|
94
|
+
}
|
|
95
|
+
split() {
|
|
96
|
+
if (!this.el) return;
|
|
97
|
+
this.clear();
|
|
98
|
+
this.charIndex = 0;
|
|
99
|
+
const text = this.original;
|
|
100
|
+
const fragment = document.createDocumentFragment();
|
|
101
|
+
const words = splitIntoWords(text);
|
|
102
|
+
if (this.opts.splitMode === "chars") this.appendChars(fragment, text);
|
|
103
|
+
else this.appendWords(fragment, words);
|
|
104
|
+
this.el.appendChild(fragment);
|
|
105
|
+
if (this.opts.cssVariables) {
|
|
106
|
+
this.el.style.setProperty(CSS_VAR_WORD_TOTAL, String(words.length));
|
|
107
|
+
this.el.style.setProperty(CSS_VAR_CHAR_TOTAL, String(text.length));
|
|
108
|
+
}
|
|
109
|
+
this.callbacks?.onAfterRender?.(this.metrics);
|
|
110
|
+
}
|
|
111
|
+
destroy() {
|
|
112
|
+
if (!this.el) return;
|
|
113
|
+
this.clear();
|
|
114
|
+
this.unlockHeight();
|
|
115
|
+
if (this.resizeObserver) {
|
|
116
|
+
this.resizeObserver.disconnect();
|
|
117
|
+
this.resizeObserver = void 0;
|
|
118
|
+
}
|
|
119
|
+
if (this.opts.containerHeightVar) this.el.style.removeProperty(CSS_VAR_CONTAINER_HEIGHT);
|
|
120
|
+
this.mounted = false;
|
|
121
|
+
}
|
|
122
|
+
updateOptions(next) {
|
|
123
|
+
this.opts = {
|
|
124
|
+
...this.opts,
|
|
125
|
+
...omitUndefined(next)
|
|
126
|
+
};
|
|
127
|
+
if (this.mounted) this.split();
|
|
128
|
+
}
|
|
129
|
+
lockHeight() {
|
|
130
|
+
if (!this.el) return;
|
|
131
|
+
const h = this.measureHeight();
|
|
132
|
+
if (h > 0) this.el.style.height = `${h}px`;
|
|
133
|
+
}
|
|
134
|
+
unlockHeight() {
|
|
135
|
+
if (!this.el) return;
|
|
136
|
+
this.el.style.removeProperty("height");
|
|
137
|
+
}
|
|
138
|
+
appendWords(fragment, words) {
|
|
139
|
+
words.forEach((word, wordIndex) => {
|
|
140
|
+
if (this.opts.splitMode === "both") {
|
|
141
|
+
const wordSpan = this.createWordSpan(wordIndex, word);
|
|
142
|
+
for (const ch of splitIntoGraphemes(word)) {
|
|
143
|
+
const charSpan = this.createCharSpan(ch);
|
|
144
|
+
wordSpan.append(charSpan);
|
|
145
|
+
}
|
|
146
|
+
fragment.append(wordSpan);
|
|
147
|
+
} else {
|
|
148
|
+
const wordSpan = this.createWordSpan(wordIndex);
|
|
149
|
+
wordSpan.append(document.createTextNode(word));
|
|
150
|
+
fragment.append(wordSpan);
|
|
151
|
+
}
|
|
152
|
+
if (wordIndex < words.length - 1) fragment.append(this.createSpaceSpan());
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
appendChars(fragment, text) {
|
|
156
|
+
for (const ch of splitIntoGraphemes(text)) {
|
|
157
|
+
const span = this.createCharSpan(ch);
|
|
158
|
+
fragment.append(span);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
createWordSpan(index, word = "") {
|
|
162
|
+
const span = document.createElement("span");
|
|
163
|
+
span.classList.add(CLASSNAMES.word);
|
|
164
|
+
if (this.opts.dataAttributes && word) span.setAttribute("data-word", word);
|
|
165
|
+
if (this.opts.cssVariables) span.style.setProperty(CSS_VAR_WORD_INDEX, String(index));
|
|
166
|
+
return span;
|
|
167
|
+
}
|
|
168
|
+
createCharSpan(ch) {
|
|
169
|
+
const span = document.createElement("span");
|
|
170
|
+
span.textContent = ch;
|
|
171
|
+
if (this.opts.dataAttributes) span.setAttribute("data-char", ch);
|
|
172
|
+
if (ch === SPACE) {
|
|
173
|
+
span.classList.add(CLASSNAMES.whitespace);
|
|
174
|
+
if (!this.opts.keepWhitespaceNodes) span.textContent = SPACE;
|
|
175
|
+
} else {
|
|
176
|
+
span.classList.add(CLASSNAMES.char);
|
|
177
|
+
if (this.opts.cssVariables) span.style.setProperty(CSS_VAR_CHAR_INDEX, String(this.charIndex));
|
|
178
|
+
this.charIndex += 1;
|
|
179
|
+
}
|
|
180
|
+
return span;
|
|
181
|
+
}
|
|
182
|
+
createSpaceSpan() {
|
|
183
|
+
const span = document.createElement("span");
|
|
184
|
+
span.classList.add(CLASSNAMES.whitespace);
|
|
185
|
+
span.textContent = SPACE;
|
|
186
|
+
return span;
|
|
187
|
+
}
|
|
188
|
+
measureHeight() {
|
|
189
|
+
if (!this.el) return 0;
|
|
190
|
+
this.el.classList.add(MEASURING_CLASS);
|
|
191
|
+
this.el.offsetHeight;
|
|
192
|
+
let h = this.el.offsetHeight || this.el.clientHeight || 0;
|
|
193
|
+
if (!h) h = Math.round(this.el.getBoundingClientRect().height);
|
|
194
|
+
this.el.classList.remove(MEASURING_CLASS);
|
|
195
|
+
return Math.max(0, Math.ceil(h));
|
|
196
|
+
}
|
|
197
|
+
initHeightObserver() {
|
|
198
|
+
if (!this.el) return;
|
|
199
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
200
|
+
const h = this.measureHeight();
|
|
201
|
+
if (h > 0) this.el?.style.setProperty(CSS_VAR_CONTAINER_HEIGHT, `${h}px`);
|
|
202
|
+
});
|
|
203
|
+
this.resizeObserver.observe(this.el);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
//#endregion
|
|
208
|
+
export { CLASSNAMES, TextSlicer };
|
|
209
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["export type SplitMode = 'words' | 'chars' | 'both';\n\nexport interface TextSlicerOptions {\n container?: HTMLElement | string;\n splitMode?: SplitMode;\n cssVariables?: boolean;\n dataAttributes?: boolean;\n keepWhitespaceNodes?: boolean;\n containerHeightVar?: boolean;\n}\n\nexport interface TextSlicerMetrics {\n wordTotal: number;\n charTotal: number;\n renderedAt: number;\n}\n\nexport interface TextSlicerCallbacks {\n onAfterRender?: (metrics: TextSlicerMetrics) => void;\n}\n\ntype RuntimeOptions = Required<Omit<TextSlicerOptions, 'container'>>;\n\nconst DEFAULT_OPTIONS: RuntimeOptions = {\n splitMode: 'both',\n cssVariables: false,\n dataAttributes: false,\n keepWhitespaceNodes: true,\n containerHeightVar: false,\n};\n\nexport const CLASSNAMES = Object.freeze({\n word: 'ts-word',\n char: 'ts-char',\n whitespace: 'ts-whitespace',\n});\n\nconst SPACE = ' ';\nconst CSS_VAR_WORD_TOTAL = '--word-total';\nconst CSS_VAR_CHAR_TOTAL = '--char-total';\nconst CSS_VAR_WORD_INDEX = '--word-index';\nconst CSS_VAR_CHAR_INDEX = '--char-index';\nconst CSS_VAR_CONTAINER_HEIGHT = '--container-height';\nconst MEASURING_CLASS = 'ts-measuring';\n\nconst canUseDOM = (): boolean => typeof window !== 'undefined' && typeof document !== 'undefined';\n\nconst resolveContainer = (container?: HTMLElement | string): HTMLElement | null => {\n if (!canUseDOM()) return null;\n if (!container) return null;\n\n return typeof container === 'string' ? document.querySelector(container) : container;\n};\n\ntype IntlWithSegmenter = typeof Intl & {\n Segmenter?: new (locales?: string | string[], options?: Intl.SegmenterOptions) => Intl.Segmenter;\n};\n\nconst splitIntoGraphemes = (text: string): string[] => {\n const Seg = (Intl as IntlWithSegmenter).Segmenter;\n\n if (typeof Seg === 'function') {\n const segmenter = new Seg('en', { granularity: 'grapheme' });\n return Array.from(segmenter.segment(text), (s: Intl.SegmentData) => s.segment);\n }\n\n return Array.from(text);\n};\n\nconst splitIntoWords = (text: string): string[] => text.split(SPACE);\n\nconst emptyElement = (el: HTMLElement): void => {\n el.replaceChildren();\n};\n\nconst isHTMLElement = (el: unknown): el is HTMLElement =>\n !!el && typeof HTMLElement !== 'undefined' && el instanceof HTMLElement;\n\ntype NonUndefined<T> = T extends undefined ? never : T;\ntype OmitUndefined<T extends Record<string, unknown>> = {\n [K in keyof T as T[K] extends undefined ? never : K]: NonUndefined<T[K]>;\n};\n\nconst omitUndefined = <T extends Record<string, unknown>>(obj: T): OmitUndefined<T> => {\n const out = {} as OmitUndefined<T>;\n\n (Object.keys(obj) as Array<keyof T>).forEach((key) => {\n const val = obj[key];\n\n if (val !== undefined) {\n (out as Record<string, unknown>)[key as string] = val as unknown;\n }\n });\n\n return out;\n};\n\nexport class TextSlicer {\n private readonly el: HTMLElement | null;\n private original: string;\n private opts: RuntimeOptions;\n private callbacks: TextSlicerCallbacks | undefined;\n private charIndex: number;\n private mounted: boolean;\n private resizeObserver?: ResizeObserver;\n\n constructor(options: TextSlicerOptions = {}, callbacks?: TextSlicerCallbacks) {\n const el = resolveContainer(options.container);\n this.el = el;\n this.original = isHTMLElement(el) ? (el.textContent?.toString() ?? '') : '';\n this.opts = {\n ...DEFAULT_OPTIONS,\n ...omitUndefined<Omit<TextSlicerOptions, 'container'>>(options),\n } as RuntimeOptions;\n\n this.callbacks = callbacks;\n this.charIndex = 0;\n this.mounted = false;\n }\n\n get metrics(): TextSlicerMetrics {\n const text = this.original;\n\n return {\n wordTotal: text.length ? splitIntoWords(text).length : 0,\n charTotal: text.length,\n renderedAt: Date.now(),\n };\n }\n\n init(): void {\n if (!this.el) return;\n\n this.mounted = true;\n this.split();\n\n if (this.opts.containerHeightVar) {\n this.initHeightObserver();\n }\n }\n\n reinit(newText?: string, next?: Partial<TextSlicerOptions>): void {\n if (!this.el) return;\n if (typeof newText === 'string') this.original = newText;\n if (next) this.opts = { ...this.opts, ...omitUndefined(next) } as RuntimeOptions;\n\n this.split();\n }\n\n clear(): void {\n if (!this.el) return;\n\n emptyElement(this.el);\n }\n\n split(): void {\n if (!this.el) return;\n\n this.clear();\n this.charIndex = 0;\n\n const text = this.original;\n const fragment = document.createDocumentFragment();\n const words = splitIntoWords(text);\n\n if (this.opts.splitMode === 'chars') {\n this.appendChars(fragment, text);\n } else {\n this.appendWords(fragment, words);\n }\n\n this.el.appendChild(fragment);\n\n if (this.opts.cssVariables) {\n this.el.style.setProperty(CSS_VAR_WORD_TOTAL, String(words.length));\n this.el.style.setProperty(CSS_VAR_CHAR_TOTAL, String(text.length));\n }\n\n this.callbacks?.onAfterRender?.(this.metrics);\n }\n\n destroy(): void {\n if (!this.el) return;\n\n this.clear();\n this.unlockHeight();\n\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = undefined;\n }\n\n if (this.opts.containerHeightVar) {\n this.el.style.removeProperty(CSS_VAR_CONTAINER_HEIGHT);\n }\n\n this.mounted = false;\n }\n\n updateOptions(next: Partial<TextSlicerOptions>): void {\n this.opts = { ...this.opts, ...omitUndefined(next) } as RuntimeOptions;\n\n if (this.mounted) this.split();\n }\n\n lockHeight(): void {\n if (!this.el) return;\n\n const h = this.measureHeight();\n\n if (h > 0) {\n this.el.style.height = `${h}px`;\n }\n }\n\n unlockHeight(): void {\n if (!this.el) return;\n\n this.el.style.removeProperty('height');\n }\n\n private appendWords(fragment: DocumentFragment, words: string[]): void {\n words.forEach((word, wordIndex) => {\n if (this.opts.splitMode === 'both') {\n const wordSpan = this.createWordSpan(wordIndex, word);\n\n for (const ch of splitIntoGraphemes(word)) {\n const charSpan = this.createCharSpan(ch);\n\n wordSpan.append(charSpan);\n }\n\n fragment.append(wordSpan);\n } else {\n const wordSpan = this.createWordSpan(wordIndex);\n\n wordSpan.append(document.createTextNode(word));\n fragment.append(wordSpan);\n }\n\n if (wordIndex < words.length - 1) {\n fragment.append(this.createSpaceSpan());\n }\n });\n }\n\n private appendChars(fragment: DocumentFragment, text: string): void {\n for (const ch of splitIntoGraphemes(text)) {\n const span = this.createCharSpan(ch);\n\n fragment.append(span);\n }\n }\n\n private createWordSpan(index: number, word: string = ''): HTMLSpanElement {\n const span = document.createElement('span');\n\n span.classList.add(CLASSNAMES.word);\n\n if (this.opts.dataAttributes && word) {\n span.setAttribute('data-word', word);\n }\n\n if (this.opts.cssVariables) {\n span.style.setProperty(CSS_VAR_WORD_INDEX, String(index));\n }\n\n return span;\n }\n\n private createCharSpan(ch: string): HTMLSpanElement {\n const span = document.createElement('span');\n\n span.textContent = ch;\n\n if (this.opts.dataAttributes) span.setAttribute('data-char', ch);\n\n if (ch === SPACE) {\n span.classList.add(CLASSNAMES.whitespace);\n\n if (!this.opts.keepWhitespaceNodes) span.textContent = SPACE;\n } else {\n span.classList.add(CLASSNAMES.char);\n\n if (this.opts.cssVariables) {\n span.style.setProperty(CSS_VAR_CHAR_INDEX, String(this.charIndex));\n }\n\n this.charIndex += 1;\n }\n\n return span;\n }\n\n private createSpaceSpan(): HTMLSpanElement {\n const span = document.createElement('span');\n\n span.classList.add(CLASSNAMES.whitespace);\n span.textContent = SPACE;\n\n return span;\n }\n\n private measureHeight(): number {\n if (!this.el) return 0;\n\n this.el.classList.add(MEASURING_CLASS);\n void this.el.offsetHeight;\n\n let h = this.el.offsetHeight || this.el.clientHeight || 0;\n\n if (!h) {\n h = Math.round(this.el.getBoundingClientRect().height);\n }\n\n this.el.classList.remove(MEASURING_CLASS);\n\n return Math.max(0, Math.ceil(h));\n }\n\n private initHeightObserver(): void {\n if (!this.el) return;\n\n this.resizeObserver = new ResizeObserver(() => {\n const h = this.measureHeight();\n\n if (h > 0) {\n this.el?.style.setProperty(CSS_VAR_CONTAINER_HEIGHT, `${h}px`);\n }\n });\n\n this.resizeObserver.observe(this.el);\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "text-slicer",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.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",
|
|
@@ -13,46 +13,58 @@
|
|
|
13
13
|
},
|
|
14
14
|
"homepage": "https://github.com/ux-ui-pro/text-slicer",
|
|
15
15
|
"sideEffects": false,
|
|
16
|
+
"type": "module",
|
|
17
|
+
"packageManager": "npm@10.9.3",
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20.19.0"
|
|
20
|
+
},
|
|
16
21
|
"scripts": {
|
|
17
22
|
"clean": "rimraf dist",
|
|
18
|
-
"build": "
|
|
19
|
-
"
|
|
20
|
-
"lint
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
+
"build": "tsdown",
|
|
24
|
+
"verify": "npm run lint && npm run typecheck && npm run build && npm run test:smoke && npm run pack:check",
|
|
25
|
+
"lint": "biome check src tests tsdown.config.ts",
|
|
26
|
+
"lint:fix": "biome check --write src tests tsdown.config.ts",
|
|
27
|
+
"format": "biome format --write src tests tsdown.config.ts",
|
|
28
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
29
|
+
"test:smoke": "node --test \"tests/**/*.js\"",
|
|
30
|
+
"pack:check": "npm pack --dry-run && publint && attw --pack . --profile node16",
|
|
31
|
+
"prepublishOnly": "npm run clean && npm run verify"
|
|
23
32
|
},
|
|
24
33
|
"source": "src/index.ts",
|
|
25
|
-
"main": "dist/index.cjs
|
|
26
|
-
"module": "dist/index.
|
|
27
|
-
"
|
|
28
|
-
"types": "dist/index.d.ts",
|
|
34
|
+
"main": "./dist/index.cjs",
|
|
35
|
+
"module": "./dist/index.js",
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
29
37
|
"exports": {
|
|
30
38
|
".": {
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
"import": {
|
|
40
|
+
"types": "./dist/index.d.ts",
|
|
41
|
+
"default": "./dist/index.js"
|
|
42
|
+
},
|
|
43
|
+
"require": {
|
|
44
|
+
"types": "./dist/index.d.cts",
|
|
45
|
+
"default": "./dist/index.cjs"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
36
48
|
},
|
|
37
49
|
"files": [
|
|
38
|
-
"dist
|
|
50
|
+
"dist",
|
|
51
|
+
"README.md",
|
|
52
|
+
"LICENSE"
|
|
39
53
|
],
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
},
|
|
40
57
|
"devDependencies": {
|
|
41
|
-
"@
|
|
42
|
-
"@
|
|
43
|
-
"@types/node": "
|
|
44
|
-
"@
|
|
45
|
-
"@
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"prettier": "3.6.2",
|
|
52
|
-
"rimraf": "6.0.1",
|
|
53
|
-
"typescript": "5.9.2",
|
|
54
|
-
"vite": "7.1.3",
|
|
55
|
-
"vite-plugin-dts": "4.5.4"
|
|
58
|
+
"@arethetypeswrong/cli": "^0.18.3",
|
|
59
|
+
"@biomejs/biome": "2.4.16",
|
|
60
|
+
"@types/node": "25.9.2",
|
|
61
|
+
"@ux-ui/biome-config": "^0.1.0",
|
|
62
|
+
"@ux-ui/tsconfig-base": "^0.1.0",
|
|
63
|
+
"@ux-ui/tsdown-config": "^0.1.0",
|
|
64
|
+
"publint": "^0.3.21",
|
|
65
|
+
"rimraf": "6.1.3",
|
|
66
|
+
"tsdown": "0.22.3",
|
|
67
|
+
"typescript": "6.0.3"
|
|
56
68
|
},
|
|
57
69
|
"keywords": [
|
|
58
70
|
"text",
|
package/dist/index.cjs.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
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.es.js
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
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
|
-
const t = Intl.Segmenter;
|
|
3
|
-
if (typeof t == "function") {
|
|
4
|
-
const e = new t("en", { granularity: "grapheme" });
|
|
5
|
-
return Array.from(e.segment(i), (s) => s.segment);
|
|
6
|
-
}
|
|
7
|
-
return Array.from(i);
|
|
8
|
-
}, d = (i) => i.split(a), o = (i) => {
|
|
9
|
-
const t = {};
|
|
10
|
-
return Object.keys(i).forEach((e) => {
|
|
11
|
-
const s = i[e];
|
|
12
|
-
s !== void 0 && (t[e] = s);
|
|
13
|
-
}), t;
|
|
14
|
-
};
|
|
15
|
-
class y {
|
|
16
|
-
el;
|
|
17
|
-
original;
|
|
18
|
-
opts;
|
|
19
|
-
callbacks;
|
|
20
|
-
charIndex;
|
|
21
|
-
mounted;
|
|
22
|
-
heightLocked;
|
|
23
|
-
resizeObserver;
|
|
24
|
-
constructor(t = {}, e) {
|
|
25
|
-
const s = f(t.container);
|
|
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;
|
|
27
|
-
}
|
|
28
|
-
get metrics() {
|
|
29
|
-
const t = this.original;
|
|
30
|
-
return { wordTotal: t.length ? d(t).length : 0, charTotal: t.length, renderedAt: Date.now() };
|
|
31
|
-
}
|
|
32
|
-
init() {
|
|
33
|
-
this.el && (this.mounted = !0, this.split(), this.opts.containerHeightVar && this.initHeightObserver());
|
|
34
|
-
}
|
|
35
|
-
reinit(t, e) {
|
|
36
|
-
this.el && (typeof t == "string" && (this.original = t), e && (this.opts = { ...this.opts, ...o(e) }), this.split());
|
|
37
|
-
}
|
|
38
|
-
clear() {
|
|
39
|
-
this.el && this.el.replaceChildren();
|
|
40
|
-
}
|
|
41
|
-
split() {
|
|
42
|
-
if (!this.el) return;
|
|
43
|
-
this.clear(), this.charIndex = 0;
|
|
44
|
-
const t = this.original, e = document.createDocumentFragment(), s = d(t);
|
|
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);
|
|
46
|
-
}
|
|
47
|
-
destroy() {
|
|
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);
|
|
49
|
-
}
|
|
50
|
-
updateOptions(t) {
|
|
51
|
-
this.opts = { ...this.opts, ...o(t) }, this.mounted && this.split();
|
|
52
|
-
}
|
|
53
|
-
lockHeight() {
|
|
54
|
-
if (!this.el) return;
|
|
55
|
-
const t = this.measureHeight();
|
|
56
|
-
t > 0 && (this.el.style.height = `${t}px`, this.heightLocked = !0);
|
|
57
|
-
}
|
|
58
|
-
unlockHeight() {
|
|
59
|
-
this.el && (this.el.style.removeProperty("height"), this.heightLocked = !1);
|
|
60
|
-
}
|
|
61
|
-
appendWords(t, e) {
|
|
62
|
-
e.forEach((s, r) => {
|
|
63
|
-
if (this.opts.splitMode === "both") {
|
|
64
|
-
const n = this.createWordSpan(r, s);
|
|
65
|
-
for (const g of p(s)) {
|
|
66
|
-
const u = this.createCharSpan(g);
|
|
67
|
-
n.append(u);
|
|
68
|
-
}
|
|
69
|
-
t.append(n);
|
|
70
|
-
} else {
|
|
71
|
-
const n = this.createWordSpan(r);
|
|
72
|
-
n.append(document.createTextNode(s)), t.append(n);
|
|
73
|
-
}
|
|
74
|
-
r < e.length - 1 && t.append(this.createSpaceSpan());
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
appendChars(t, e) {
|
|
78
|
-
for (const s of p(e)) {
|
|
79
|
-
const r = this.createCharSpan(s);
|
|
80
|
-
t.append(r);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
createWordSpan(t, e = "") {
|
|
84
|
-
const s = document.createElement("span");
|
|
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;
|
|
86
|
-
}
|
|
87
|
-
createCharSpan(t) {
|
|
88
|
-
const e = document.createElement("span");
|
|
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;
|
|
90
|
-
}
|
|
91
|
-
createSpaceSpan() {
|
|
92
|
-
const t = document.createElement("span");
|
|
93
|
-
return t.classList.add(h.whitespace), t.textContent = a, t;
|
|
94
|
-
}
|
|
95
|
-
measureHeight() {
|
|
96
|
-
if (!this.el) return 0;
|
|
97
|
-
this.el.classList.add(l), this.el.offsetHeight;
|
|
98
|
-
let t = this.el.offsetHeight || this.el.clientHeight || 0;
|
|
99
|
-
return t || (t = Math.round(this.el.getBoundingClientRect().height)), this.el.classList.remove(l), Math.max(0, Math.ceil(t));
|
|
100
|
-
}
|
|
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));
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
export {
|
|
109
|
-
h as CLASSNAMES,
|
|
110
|
-
y as TextSlicer
|
|
111
|
-
};
|
package/dist/index.umd.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
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"})});
|