@vettvangur/vanilla 0.0.48 → 0.0.50
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/dist/data.esm.js +167 -0
- package/dist/dom.esm.js +91 -0
- package/dist/hyphenation.esm.js +350 -0
- package/dist/index.esm.js +794 -2
- package/dist/server.esm.js +217 -2
- package/dist/string.esm.js +128 -0
- package/dist/types/data.d.ts +1 -0
- package/dist/types/dom.d.ts +1 -0
- package/dist/types/hyphenation.d.ts +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/string.d.ts +1 -0
- package/package.json +19 -2
- package/dist/index.esm.js.map +0 -1
- package/dist/server.esm.js.map +0 -1
package/dist/data.esm.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vanilla :: index.mjs.
|
|
3
|
+
*
|
|
4
|
+
* Vanilla JS helper utilities.
|
|
5
|
+
*
|
|
6
|
+
* These docs are generated from inline JSDoc in the repo and are intended for contributors.
|
|
7
|
+
*
|
|
8
|
+
* @generated
|
|
9
|
+
* @module vanilla
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Fetcher.
|
|
15
|
+
*
|
|
16
|
+
* @param {object} options - Options object.
|
|
17
|
+
* @param {string} options.url - Value.
|
|
18
|
+
* @param {any} [options.options=null] - Options that control behavior.
|
|
19
|
+
* @param {boolean} [options.throwOnError=false] - Value.
|
|
20
|
+
* @returns Result of the operation.
|
|
21
|
+
* @throws When the operation fails.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // vanilla (src/tools/javascript/vanilla/index.mjs)
|
|
25
|
+
* // import { fetcher } from '@vettvangur/vanilla'
|
|
26
|
+
*
|
|
27
|
+
* await fetcher({ url: url, options: {}, throwOnError: throwOnError })
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
async function fetcher({
|
|
31
|
+
url,
|
|
32
|
+
options = null,
|
|
33
|
+
throwOnError = false
|
|
34
|
+
}) {
|
|
35
|
+
const errorMessage = `[@vettvangur/vanilla :: fetcher] Error: ${url}`;
|
|
36
|
+
try {
|
|
37
|
+
const request = await fetch(url, options);
|
|
38
|
+
if (!request.ok) {
|
|
39
|
+
const errorDetails = await request.text();
|
|
40
|
+
const fullErrorMessage = `${errorMessage} - Status: ${request.status}, ${request.statusText}, Details: ${errorDetails}`;
|
|
41
|
+
console.error(fullErrorMessage);
|
|
42
|
+
if (throwOnError) {
|
|
43
|
+
throw new Error(fullErrorMessage);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Return null instead of undefined on error for predictable handling
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return await request.json();
|
|
50
|
+
} catch (error) {
|
|
51
|
+
const fullErrorMessage = `${errorMessage} - ${error}`;
|
|
52
|
+
console.error(fullErrorMessage);
|
|
53
|
+
if (throwOnError) {
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Return null instead of undefined on error for predictable handling
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Checks if a given string is a valid regular expression pattern.
|
|
64
|
+
*
|
|
65
|
+
* @ignore
|
|
66
|
+
* @param {string} pattern - The regex pattern to validate.
|
|
67
|
+
* @returns {boolean} Returns `true` if the pattern is a valid regex, otherwise `false`.
|
|
68
|
+
*/
|
|
69
|
+
function isValidRegex(pattern) {
|
|
70
|
+
try {
|
|
71
|
+
new RegExp(pattern);
|
|
72
|
+
return true;
|
|
73
|
+
} catch (_unused) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Regexr.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* console.log(regexr)
|
|
83
|
+
*/
|
|
84
|
+
const regexr = {
|
|
85
|
+
/**
|
|
86
|
+
* Tests whether a given value matches a regex pattern.
|
|
87
|
+
*
|
|
88
|
+
* @param {Object} params - Parameters for testing regex.
|
|
89
|
+
* @param {string} params.value - The string to test against the regex.
|
|
90
|
+
* @param {string} params.pattern - The regex pattern to match.
|
|
91
|
+
* @param {string} [params.flags='g'] - Regex flags (default: 'g').
|
|
92
|
+
* @returns {boolean} Returns `true` if the value matches the pattern, otherwise `false`.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* const isMatch = regexr.match(pattern)
|
|
96
|
+
* console.log(isMatch) // return true or false
|
|
97
|
+
*/
|
|
98
|
+
test({
|
|
99
|
+
value,
|
|
100
|
+
pattern,
|
|
101
|
+
flags = 'g'
|
|
102
|
+
}) {
|
|
103
|
+
if (!value || !pattern || !isValidRegex(pattern)) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
const regexp = new RegExp(pattern, flags);
|
|
107
|
+
return regexp.test(value);
|
|
108
|
+
},
|
|
109
|
+
/**
|
|
110
|
+
* Matches a given value against a regex pattern and returns the matches.
|
|
111
|
+
*
|
|
112
|
+
* @param {Object} params - Parameters for matching regex.
|
|
113
|
+
* @param {string} params.value - The string to match against the regex.
|
|
114
|
+
* @param {string} params.pattern - The regex pattern to match.
|
|
115
|
+
* @param {string} [params.flags='g'] - Regex flags (default: 'g').
|
|
116
|
+
* @returns {Array<string>|null} Returns an array of matches, or `null` if no match is found.
|
|
117
|
+
*/
|
|
118
|
+
match({
|
|
119
|
+
value,
|
|
120
|
+
pattern,
|
|
121
|
+
flags = 'g'
|
|
122
|
+
}) {
|
|
123
|
+
if (!value || !pattern || !isValidRegex(pattern)) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const regexp = new RegExp(pattern, flags);
|
|
127
|
+
return value.match(regexp);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Flatten.
|
|
133
|
+
*
|
|
134
|
+
* @param obj - Value.
|
|
135
|
+
* @param prefix - Value.
|
|
136
|
+
* @returns Result of the operation.
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* // vanilla (src/tools/javascript/vanilla/index.mjs)
|
|
140
|
+
* // import { flatten } from '@vettvangur/vanilla'
|
|
141
|
+
*
|
|
142
|
+
* flatten(obj, prefix)
|
|
143
|
+
*/
|
|
144
|
+
const flatten = (obj, prefix = '') => {
|
|
145
|
+
try {
|
|
146
|
+
if (!obj || typeof obj !== 'object') {
|
|
147
|
+
console.error('❌ FLATTEN ERROR: Received invalid data:', obj);
|
|
148
|
+
return {};
|
|
149
|
+
}
|
|
150
|
+
return Object.keys(obj).reduce((acc, key) => {
|
|
151
|
+
const fullKey = key === 'DEFAULT' ? prefix : prefix ? `${prefix}-${key}` : key;
|
|
152
|
+
if (typeof obj[key] === 'object' && key !== 'DEFAULT') {
|
|
153
|
+
Object.assign(acc, flatten(obj[key], fullKey));
|
|
154
|
+
} else if (typeof obj[key] === 'string') {
|
|
155
|
+
acc[fullKey] = obj[key];
|
|
156
|
+
} else {
|
|
157
|
+
console.warn('! Skipping Invalid Key:', key, 'Value:', obj[key]);
|
|
158
|
+
}
|
|
159
|
+
return acc;
|
|
160
|
+
}, {});
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('🚨 Flatten Function Error:', error);
|
|
163
|
+
return {}; // Prevents Tailwind from crashing
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export { fetcher, flatten, regexr };
|
package/dist/dom.esm.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vanilla :: index.mjs.
|
|
3
|
+
*
|
|
4
|
+
* Vanilla JS helper utilities.
|
|
5
|
+
*
|
|
6
|
+
* These docs are generated from inline JSDoc in the repo and are intended for contributors.
|
|
7
|
+
*
|
|
8
|
+
* @generated
|
|
9
|
+
* @module vanilla
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Preload timeout.
|
|
15
|
+
*
|
|
16
|
+
* @returns Nothing.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // vanilla (src/tools/javascript/vanilla/index.mjs)
|
|
20
|
+
* // import { preloadTimeout } from '@vettvangur/vanilla'
|
|
21
|
+
*
|
|
22
|
+
* preloadTimeout()
|
|
23
|
+
*/
|
|
24
|
+
function preloadTimeout() {
|
|
25
|
+
const body = document.querySelector('.body');
|
|
26
|
+
if (!body) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
setTimeout(() => body.classList.add('loaded'), 100);
|
|
30
|
+
setTimeout(() => {
|
|
31
|
+
body.classList.add('preload--hidden');
|
|
32
|
+
body.classList.remove('preload--transitions');
|
|
33
|
+
}, 400);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Click outside.
|
|
38
|
+
*
|
|
39
|
+
* @param callback - Value.
|
|
40
|
+
* @returns Nothing.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* // vanilla (src/tools/javascript/vanilla/index.mjs)
|
|
44
|
+
* // import { clickOutside } from '@vettvangur/vanilla'
|
|
45
|
+
*
|
|
46
|
+
* clickOutside(callback)
|
|
47
|
+
*/
|
|
48
|
+
function clickOutside(callback) {
|
|
49
|
+
// Clicks
|
|
50
|
+
document.addEventListener('click', e => {
|
|
51
|
+
const target = e.target;
|
|
52
|
+
const classTargets = document.querySelectorAll('.co-el');
|
|
53
|
+
|
|
54
|
+
// Check if the click target is inside an element with class `.co-trigger`
|
|
55
|
+
// @ts-ignore
|
|
56
|
+
if (target.closest('.co-trigger')) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Execute the callback function
|
|
61
|
+
callback(e);
|
|
62
|
+
|
|
63
|
+
// Close open elements
|
|
64
|
+
Array.prototype.forEach.call(classTargets, item => {
|
|
65
|
+
if (item.classList.contains('open')) {
|
|
66
|
+
item.classList.remove('open');
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}, false);
|
|
70
|
+
|
|
71
|
+
// ESC key
|
|
72
|
+
document.addEventListener('keyup', e => {
|
|
73
|
+
const openElements = document.querySelectorAll('.open');
|
|
74
|
+
const keys = e.keyCode || e.which;
|
|
75
|
+
|
|
76
|
+
// If ESC key (key code 27) is pressed
|
|
77
|
+
if (keys === 27) {
|
|
78
|
+
// Execute the callback function
|
|
79
|
+
callback(e);
|
|
80
|
+
|
|
81
|
+
// Close open elements
|
|
82
|
+
Array.prototype.forEach.call(openElements, item => {
|
|
83
|
+
if (item.classList.contains('open')) {
|
|
84
|
+
item.classList.remove('open');
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { clickOutside, preloadTimeout };
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy-loaded hyphenation functions. Loaded on-demand so consumers that
|
|
3
|
+
* do not use hyphenation do not pay the bundle cost.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @type {Promise<((word: string) => Promise<string>) | null> | null} */
|
|
7
|
+
let hyphenateEnPromise = null;
|
|
8
|
+
/** @type {Promise<((word: string) => Promise<string>) | null> | null} */
|
|
9
|
+
let hyphenateIsPromise = null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Stores the original (non-hyphenated) text content for elements.
|
|
13
|
+
* Used to ensure idempotent re-runs on resize/font load.
|
|
14
|
+
* @type {WeakMap<HTMLElement, string>}
|
|
15
|
+
*/
|
|
16
|
+
const originalText = new WeakMap();
|
|
17
|
+
let resizeListenerBound = false;
|
|
18
|
+
let resizeTimer;
|
|
19
|
+
/** @type {HyphenationOptions | undefined} */
|
|
20
|
+
let resizeOptions;
|
|
21
|
+
let fontsListenerBound = false;
|
|
22
|
+
const HYHEN_CLASS_MANUAL = 'js-hyphen-manual';
|
|
23
|
+
const HYPHEN_CLASS_EMERGENCY = 'js-hyphen-emergency';
|
|
24
|
+
|
|
25
|
+
/** @type {CanvasRenderingContext2D | null} */
|
|
26
|
+
let measureCtx = null;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Gets (and lazily creates) the canvas context used for text measurement.
|
|
30
|
+
*
|
|
31
|
+
* @returns {CanvasRenderingContext2D | null}
|
|
32
|
+
*/
|
|
33
|
+
function getMeasureCtx() {
|
|
34
|
+
if (measureCtx) {
|
|
35
|
+
return measureCtx;
|
|
36
|
+
}
|
|
37
|
+
if (typeof document === 'undefined') {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const canvas = document.createElement('canvas');
|
|
41
|
+
measureCtx = canvas.getContext('2d');
|
|
42
|
+
return measureCtx;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Loads the `hyphen` package module and extracts a `hyphenate()` function,
|
|
47
|
+
* handling common CJS/ESM interop shapes.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} moduleId
|
|
50
|
+
* @returns {Promise<((word: string) => Promise<string>) | null>}
|
|
51
|
+
*/
|
|
52
|
+
async function loadHyphenate(moduleId) {
|
|
53
|
+
try {
|
|
54
|
+
var _ref, _mod$hyphenate, _mod$default, _mod$default2;
|
|
55
|
+
const mod = await import(moduleId);
|
|
56
|
+
const hyphenate = (_ref = (_mod$hyphenate = mod === null || mod === void 0 ? void 0 : mod.hyphenate) !== null && _mod$hyphenate !== void 0 ? _mod$hyphenate : mod === null || mod === void 0 || (_mod$default = mod.default) === null || _mod$default === void 0 ? void 0 : _mod$default.hyphenate) !== null && _ref !== void 0 ? _ref : mod === null || mod === void 0 || (_mod$default2 = mod.default) === null || _mod$default2 === void 0 || (_mod$default2 = _mod$default2.default) === null || _mod$default2 === void 0 ? void 0 : _mod$default2.hyphenate;
|
|
57
|
+
if (typeof hyphenate === 'function') {
|
|
58
|
+
return hyphenate;
|
|
59
|
+
}
|
|
60
|
+
console.error('[vanilla] initHyphenation: hyphenate() export not found (CJS/ESM interop issue).', {
|
|
61
|
+
moduleId,
|
|
62
|
+
availableKeys: mod ? Object.keys(mod) : null,
|
|
63
|
+
defaultKeys: mod !== null && mod !== void 0 && mod.default && typeof mod.default === 'object' ? Object.keys(mod.default) : null
|
|
64
|
+
});
|
|
65
|
+
return null;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error('[vanilla] initHyphenation: failed to load hyphenation module.', {
|
|
68
|
+
moduleId,
|
|
69
|
+
error
|
|
70
|
+
});
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {string} lang
|
|
77
|
+
* @returns {Promise<((word: string) => Promise<string>) | null>}
|
|
78
|
+
*/
|
|
79
|
+
function getHyphenateForLang(lang) {
|
|
80
|
+
const isIcelandic = lang.startsWith('is');
|
|
81
|
+
if (isIcelandic) {
|
|
82
|
+
if (!hyphenateIsPromise) {
|
|
83
|
+
hyphenateIsPromise = loadHyphenate('hyphen/is/index.js');
|
|
84
|
+
}
|
|
85
|
+
return hyphenateIsPromise;
|
|
86
|
+
}
|
|
87
|
+
if (!hyphenateEnPromise) {
|
|
88
|
+
hyphenateEnPromise = loadHyphenate('hyphen/en/index.js');
|
|
89
|
+
}
|
|
90
|
+
return hyphenateEnPromise;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Applies a CSS-like text-transform to a string.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} text
|
|
97
|
+
* @param {string} transform
|
|
98
|
+
* @returns {string}
|
|
99
|
+
*/
|
|
100
|
+
function applyTextTransform(text, transform) {
|
|
101
|
+
switch (transform) {
|
|
102
|
+
case 'uppercase':
|
|
103
|
+
return text.toUpperCase();
|
|
104
|
+
case 'lowercase':
|
|
105
|
+
return text.toLowerCase();
|
|
106
|
+
default:
|
|
107
|
+
return text;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Measures rendered text width using canvas, approximating letter-spacing.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} text
|
|
115
|
+
* @param {CSSStyleDeclaration} style
|
|
116
|
+
* @returns {number}
|
|
117
|
+
*/
|
|
118
|
+
function measureTextWidth(text, style) {
|
|
119
|
+
const ctx = getMeasureCtx();
|
|
120
|
+
if (!ctx) {
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// `style.font` is a computed shorthand like "700 16px/20px Foo".
|
|
125
|
+
ctx.font = style.font;
|
|
126
|
+
const transformed = applyTextTransform(text, style.textTransform);
|
|
127
|
+
const metrics = ctx.measureText(transformed);
|
|
128
|
+
let width = metrics.width;
|
|
129
|
+
|
|
130
|
+
// Canvas does not support letter-spacing; approximate when it's a pixel value.
|
|
131
|
+
if (style.letterSpacing && style.letterSpacing !== 'normal') {
|
|
132
|
+
const ls = Number.parseFloat(style.letterSpacing);
|
|
133
|
+
if (Number.isFinite(ls) && transformed.length > 1) {
|
|
134
|
+
width += (transformed.length - 1) * ls;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return width;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Options for initHyphenation.
|
|
142
|
+
*
|
|
143
|
+
* @typedef {Object} HyphenationOptions
|
|
144
|
+
* @property {string[]} [classes]
|
|
145
|
+
* Class names or prefixes to target. If omitted, defaults to `[class*="headline"]`.
|
|
146
|
+
*/
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Escapes a string for use inside a CSS attribute selector.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} value
|
|
152
|
+
* @returns {string}
|
|
153
|
+
*/
|
|
154
|
+
function escapeForCssAttrValue(value) {
|
|
155
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Normalizes a class token by trimming and removing a leading dot.
|
|
160
|
+
*
|
|
161
|
+
* @param {string} className
|
|
162
|
+
* @returns {string}
|
|
163
|
+
*/
|
|
164
|
+
function normalizeClassToken(className) {
|
|
165
|
+
return className.trim().replace(/^\./, '');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Finds the element that defines the usable line width.
|
|
170
|
+
* Inline elements are walked upward until a non-inline ancestor is found.
|
|
171
|
+
*
|
|
172
|
+
* @param {HTMLElement} el
|
|
173
|
+
* @returns {HTMLElement}
|
|
174
|
+
*/
|
|
175
|
+
function findLineWidthElement(el) {
|
|
176
|
+
let current = el;
|
|
177
|
+
while (current) {
|
|
178
|
+
const display = getComputedStyle(current).display;
|
|
179
|
+
if (display !== 'inline') {
|
|
180
|
+
return current;
|
|
181
|
+
}
|
|
182
|
+
current = current.parentElement;
|
|
183
|
+
}
|
|
184
|
+
return el;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Computes the available line width for an element, excluding horizontal padding.
|
|
189
|
+
*
|
|
190
|
+
* @param {HTMLElement} el
|
|
191
|
+
* @returns {number}
|
|
192
|
+
*/
|
|
193
|
+
function getAvailableLineWidth(el) {
|
|
194
|
+
const widthEl = findLineWidthElement(el);
|
|
195
|
+
const rectWidth = widthEl.getBoundingClientRect().width;
|
|
196
|
+
const base = widthEl.clientWidth || rectWidth;
|
|
197
|
+
if (!base) {
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
const style = getComputedStyle(widthEl);
|
|
201
|
+
const paddingLeft = Number.parseFloat(style.paddingLeft) || 0;
|
|
202
|
+
const paddingRight = Number.parseFloat(style.paddingRight) || 0;
|
|
203
|
+
return Math.max(0, Math.floor(base - paddingLeft - paddingRight));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Removes legacy inline hyphenation styles so class-based styling can take over.
|
|
208
|
+
*
|
|
209
|
+
* @param {HTMLElement} el
|
|
210
|
+
*/
|
|
211
|
+
function removeLegacyInlineHyphenStyles(el) {
|
|
212
|
+
el.style.removeProperty('hyphens');
|
|
213
|
+
el.style.removeProperty('overflow-wrap');
|
|
214
|
+
el.style.removeProperty('word-break');
|
|
215
|
+
const styleAttr = el.getAttribute('style');
|
|
216
|
+
if (styleAttr != null && styleAttr.trim() === '') {
|
|
217
|
+
el.removeAttribute('style');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Initializes responsive, language-aware hyphenation for text elements.
|
|
223
|
+
*
|
|
224
|
+
* - Inserts soft hyphens for words that cannot fit on a line by themselves.
|
|
225
|
+
* - Re-runs on window resize and font loading when called on `document`.
|
|
226
|
+
* - Adds `js-hyphen-manual` when soft hyphens are inserted.
|
|
227
|
+
* - Adds `js-hyphen-emergency` as a last-resort overflow fallback.
|
|
228
|
+
*
|
|
229
|
+
* @param {Document | ParentNode} [root=document]
|
|
230
|
+
* Root node to scope the query to.
|
|
231
|
+
*
|
|
232
|
+
* - Use `document` to scan the whole page and enable auto re-runs on resize/font load.
|
|
233
|
+
* - Use an element (or other `ParentNode`) to scope hyphenation to a subtree (no global listeners).
|
|
234
|
+
* @param {HyphenationOptions} [options={}]
|
|
235
|
+
* Configuration options.
|
|
236
|
+
*
|
|
237
|
+
* - `options.classes`: array of class tokens or prefixes to match.
|
|
238
|
+
* Each token matches either an exact class or a prefix (e.g. `headline` matches `headline` and `headline-xl`).
|
|
239
|
+
* If omitted, defaults to targeting elements matching `[class*="headline"]`.
|
|
240
|
+
* @returns {Promise<void>}
|
|
241
|
+
*/
|
|
242
|
+
async function initHyphenation(root = document, options = {}) {
|
|
243
|
+
var _options$classes;
|
|
244
|
+
if (typeof document === 'undefined') {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (root === document) {
|
|
248
|
+
resizeOptions = options;
|
|
249
|
+
if (!resizeListenerBound) {
|
|
250
|
+
resizeListenerBound = true;
|
|
251
|
+
window.addEventListener('resize', () => {
|
|
252
|
+
if (resizeTimer) {
|
|
253
|
+
window.clearTimeout(resizeTimer);
|
|
254
|
+
}
|
|
255
|
+
resizeTimer = window.setTimeout(() => {
|
|
256
|
+
initHyphenation(document, resizeOptions || {}).catch(e => console.error('hyphenation', e));
|
|
257
|
+
}, 150);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
if (!fontsListenerBound && 'fonts' in document) {
|
|
261
|
+
var _fontSet$ready;
|
|
262
|
+
fontsListenerBound = true;
|
|
263
|
+
const fontSet = document.fonts;
|
|
264
|
+
fontSet === null || fontSet === void 0 || (_fontSet$ready = fontSet.ready) === null || _fontSet$ready === void 0 || _fontSet$ready.then(() => initHyphenation(document, resizeOptions || {})).catch(e => console.error('hyphenation fonts', e));
|
|
265
|
+
try {
|
|
266
|
+
var _fontSet$addEventList;
|
|
267
|
+
fontSet === null || fontSet === void 0 || (_fontSet$addEventList = fontSet.addEventListener) === null || _fontSet$addEventList === void 0 || _fontSet$addEventList.call(fontSet, 'loadingdone', () => {
|
|
268
|
+
initHyphenation(document, resizeOptions || {}).catch(e => console.error('hyphenation fonts', e));
|
|
269
|
+
});
|
|
270
|
+
} catch (_unused) {
|
|
271
|
+
// ignore
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const candidates = (_options$classes = options.classes) !== null && _options$classes !== void 0 && _options$classes.length ? Array.from(options.classes.reduce((acc, className) => {
|
|
276
|
+
const token = normalizeClassToken(className);
|
|
277
|
+
if (!token) {
|
|
278
|
+
return acc;
|
|
279
|
+
}
|
|
280
|
+
const escaped = escapeForCssAttrValue(token);
|
|
281
|
+
root.querySelectorAll(`[class*="${escaped}"]`).forEach(el => {
|
|
282
|
+
const matches = Array.from(el.classList).some(cls => cls === token || cls.startsWith(token));
|
|
283
|
+
if (matches) {
|
|
284
|
+
acc.add(el);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
return acc;
|
|
288
|
+
}, new Set())) : Array.from(root.querySelectorAll('[class*="headline"]'));
|
|
289
|
+
if (candidates.length === 0) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
await Promise.all(candidates.map(async el => {
|
|
293
|
+
var _el$closest;
|
|
294
|
+
removeLegacyInlineHyphenStyles(el);
|
|
295
|
+
el.classList.remove(HYHEN_CLASS_MANUAL, HYPHEN_CLASS_EMERGENCY);
|
|
296
|
+
const current = (el.textContent || '').replace(/\u00ad/g, '');
|
|
297
|
+
const stored = originalText.get(el);
|
|
298
|
+
const text = stored && stored === current ? stored : current;
|
|
299
|
+
originalText.set(el, text);
|
|
300
|
+
if (!text.trim()) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (el.textContent !== text) {
|
|
304
|
+
el.textContent = text;
|
|
305
|
+
}
|
|
306
|
+
const availableWidth = getAvailableLineWidth(el);
|
|
307
|
+
if (!availableWidth) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const lang = (((_el$closest = el.closest('[lang]')) === null || _el$closest === void 0 ? void 0 : _el$closest.lang) || document.documentElement.lang || 'en-us').toLowerCase();
|
|
311
|
+
const style = getComputedStyle(el);
|
|
312
|
+
const parts = text.split(/(\s+)/);
|
|
313
|
+
let didChange = false;
|
|
314
|
+
let needsEmergencyBreak = false;
|
|
315
|
+
|
|
316
|
+
/** @type {((word: string) => Promise<string>) | null} */
|
|
317
|
+
let hyphenFn = null;
|
|
318
|
+
for (let i = 0; i < parts.length; i++) {
|
|
319
|
+
const part = parts[i];
|
|
320
|
+
if (!part || /^\s+$/.test(part)) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (measureTextWidth(part, style) <= availableWidth) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (!hyphenFn) {
|
|
327
|
+
hyphenFn = await getHyphenateForLang(lang);
|
|
328
|
+
if (!hyphenFn) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const hyphenatedWord = await hyphenFn(part);
|
|
333
|
+
if (hyphenatedWord !== part) {
|
|
334
|
+
parts[i] = hyphenatedWord;
|
|
335
|
+
didChange = true;
|
|
336
|
+
} else {
|
|
337
|
+
needsEmergencyBreak = true;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (didChange) {
|
|
341
|
+
el.classList.add(HYHEN_CLASS_MANUAL);
|
|
342
|
+
el.textContent = parts.join('');
|
|
343
|
+
}
|
|
344
|
+
if (needsEmergencyBreak) {
|
|
345
|
+
el.classList.add(HYPHEN_CLASS_EMERGENCY);
|
|
346
|
+
}
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export { initHyphenation };
|