@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.
@@ -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 };
@@ -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 };