@mustib/web-components 0.0.0-alpha.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.
@@ -0,0 +1,1117 @@
1
+ import { css, LitElement } from 'lit';
2
+ import { property } from 'lit/decorators.js';
3
+ import { staticProperty } from './decorators.js';
4
+
5
+ /******************************************************************************
6
+ Copyright (c) Microsoft Corporation.
7
+
8
+ Permission to use, copy, modify, and/or distribute this software for any
9
+ purpose with or without fee is hereby granted.
10
+
11
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
12
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
13
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
14
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
15
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
16
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
17
+ PERFORMANCE OF THIS SOFTWARE.
18
+ ***************************************************************************** */
19
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
20
+
21
+
22
+ function __decorate(decorators, target, key, desc) {
23
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
24
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
25
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
26
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
27
+ }
28
+
29
+ function __classPrivateFieldGet(receiver, state, kind, f) {
30
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
31
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
32
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
33
+ }
34
+
35
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
36
+ var e = new Error(message);
37
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
38
+ };
39
+
40
+ const capitalizeFirst = (str) => str[0].toUpperCase().concat(str.slice(1).toLowerCase());
41
+ /**
42
+ * Capitalizes the first letter of a string or each word in a string.
43
+ *
44
+ * @param {string} str - The string to capitalize.
45
+ * @param {object} [options] - Optional parameters.
46
+ * @param {boolean} [options.onlyFirstWord=false] - Whether to capitalize only the first word (default: false).
47
+ * @param {string} [options.splitter=' '] - The delimiter to split the string into words (default: ' ').
48
+ * @param {string} [options.joiner=options.splitter] - The delimiter to join the capitalized words (default: options.splitter).
49
+ * @returns {string} The capitalized string.
50
+ */
51
+ function capitalize(str, options) {
52
+ const { onlyFirstWord: onlyFirst = false, splitter = ' ', joiner = splitter, } = options || {};
53
+ if (typeof str !== 'string' || str === '')
54
+ return str;
55
+ return (onlyFirst ? [str] : str.split(splitter))
56
+ .map(capitalizeFirst)
57
+ .join(joiner);
58
+ }
59
+
60
+ class AppError extends Error {
61
+ options;
62
+ static throw(type, error, options) {
63
+ return new AppError(options)
64
+ .push(type, error, options?.pushOptions)
65
+ .throw();
66
+ }
67
+ static async aggregate(aggregateFunc, options) {
68
+ const appError = new AppError(options);
69
+ try {
70
+ await aggregateFunc(appError);
71
+ appError.end();
72
+ }
73
+ catch (error) {
74
+ if (error instanceof Error) {
75
+ Error.captureStackTrace(error, options?.stackTraceConstructor ?? AppError.aggregate);
76
+ }
77
+ throw error;
78
+ }
79
+ }
80
+ length = 0;
81
+ errors = {};
82
+ get message() {
83
+ return this.toString();
84
+ }
85
+ constructor(options) {
86
+ super();
87
+ this.options = options;
88
+ }
89
+ async catch(catchFunc) {
90
+ try {
91
+ await catchFunc();
92
+ }
93
+ catch (error) {
94
+ if (error instanceof AppError) {
95
+ for (const [type, errors] of Object.entries(error.errors)) {
96
+ if (errors)
97
+ errors.forEach((err) => this.push(type, err.message, { scope: err.scope }));
98
+ }
99
+ }
100
+ else {
101
+ if (error instanceof Error) {
102
+ Error.captureStackTrace(error, this.catch);
103
+ }
104
+ throw error;
105
+ }
106
+ }
107
+ }
108
+ toString(options) {
109
+ const formattedErrors = [];
110
+ Object.keys(this.errors).forEach((errorType) => {
111
+ const rawErrors = this.errors[errorType];
112
+ if (!rawErrors)
113
+ return;
114
+ const { indentation = 4 } = this.options || {};
115
+ const formattedErrorType = rawErrors.reduce((result, err) => {
116
+ const hasMatchedScope = this.matchesScope({
117
+ errScope: err.scope,
118
+ includesScope: options?.includesScope,
119
+ excludesScope: options?.excludesScope,
120
+ });
121
+ if (hasMatchedScope) {
122
+ result.push(`${result.length + 1}- ${err.message}.`);
123
+ }
124
+ return result;
125
+ }, []);
126
+ const hasManyErrors = formattedErrorType.length > 1;
127
+ const indentationPrefix = `${' '.repeat(indentation)}`;
128
+ if (formattedErrorType.length > 0)
129
+ formattedErrors.push(`${errorType} Error${hasManyErrors ? 's' : ''}:\n${indentationPrefix}${formattedErrorType.join(`\n${indentationPrefix}`)}`);
130
+ });
131
+ return formattedErrors.join('\n');
132
+ }
133
+ matchesScope({ errScope, includesScope, excludesScope, }) {
134
+ if (includesScope === undefined && excludesScope === undefined)
135
+ return true;
136
+ if (errScope === undefined)
137
+ return false;
138
+ if (excludesScope) {
139
+ return !excludesScope.some((scope) => errScope.includes(scope));
140
+ }
141
+ if (includesScope) {
142
+ return includesScope.some((scope) => errScope.includes(scope));
143
+ }
144
+ return false;
145
+ }
146
+ push(type, error, options) {
147
+ const errorType = capitalize(type, { onlyFirstWord: true });
148
+ const errors = this.errors[errorType];
149
+ const newError = Array.isArray(error)
150
+ ? error.map((err) => ({ message: err, scope: options?.scope }))
151
+ : [{ message: error, scope: options?.scope }];
152
+ if (Array.isArray(errors)) {
153
+ errors.push(...newError);
154
+ }
155
+ else {
156
+ this.errors[errorType] = newError;
157
+ this.length++;
158
+ }
159
+ return this;
160
+ }
161
+ throw() {
162
+ Error.captureStackTrace(this, this.options?.stackTraceConstructor || this.throw);
163
+ throw this;
164
+ }
165
+ end() {
166
+ if (this.length > 0)
167
+ this.throw();
168
+ }
169
+ }
170
+
171
+ const LIBRARY_ERROR_SCOPE = Symbol('@mustib/utils');
172
+
173
+ /**
174
+ * Creates a debounced version of the provided function that delays its execution until after
175
+ * a specified number of milliseconds have elapsed since the last time it was invoked.
176
+ *
177
+ * @param func - The function to debounce.
178
+ * @param ms - The number of milliseconds to delay; defaults to 100ms.
179
+ * @returns A debounced version of the input function.
180
+ */
181
+ function debounce(func, ms = 100) {
182
+ let timeoutId;
183
+ return function debounced(...args) {
184
+ clearTimeout(timeoutId);
185
+ timeoutId = setTimeout(() => func(...args), ms);
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Returns a promise that resolves after a specified number of milliseconds.
191
+ *
192
+ * @param milliseconds - The number of milliseconds to wait before resolving the promise. Defaults to 0.
193
+ * @returns A promise that resolves after the specified delay.
194
+ */
195
+ function wait(milliseconds = 0) {
196
+ return new Promise(r => { setTimeout(r, milliseconds); });
197
+ }
198
+
199
+ /**
200
+ * Creates a throttled version of the given function that, when invoked repeatedly,
201
+ * will only call the original function at most once every `ms` milliseconds.
202
+ *
203
+ * @param func - The function to throttle.
204
+ * @param ms - The number of milliseconds to throttle invocations to. Defaults to 100ms.
205
+ * @returns A throttled version of the input function.
206
+ */
207
+ function throttle(func, ms = 100) {
208
+ let isThrottled = false;
209
+ return function throttled(...args) {
210
+ if (isThrottled)
211
+ return;
212
+ isThrottled = true;
213
+ func(...args);
214
+ setTimeout(() => { isThrottled = false; }, ms);
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Parses a string as JSON and returns the parsed value. If the string cannot be parsed as JSON,
220
+ * it returns undefined.
221
+ *
222
+ * @param {string} value - The string to parse as JSON.
223
+ * @return {T | undefined} - The parsed JSON value or undefined (since undefined is not a valid JSON).
224
+ */
225
+ function parseJson(value) {
226
+ try {
227
+ return JSON.parse(value);
228
+ }
229
+ catch (error) {
230
+ return undefined;
231
+ }
232
+ }
233
+
234
+ function getElementBoundaries(element) {
235
+ const { top: elementTop, bottom, left: elementLeft, right, width, height, } = element.getBoundingClientRect();
236
+ const pageWidth = document.documentElement.clientWidth;
237
+ const pageHeight = document.documentElement.clientHeight;
238
+ const elementRight = pageWidth - right;
239
+ const elementBottom = pageHeight - bottom;
240
+ const isTopInPage = elementTop >= 0 && elementTop <= pageHeight;
241
+ const isBottomInPage = elementBottom >= 0 && elementBottom <= pageHeight;
242
+ const isLeftInPage = elementLeft >= 0 && elementLeft <= pageWidth;
243
+ const isRightInPage = elementRight >= 0 && elementRight <= pageWidth;
244
+ const isTopVisible = isTopInPage && (isLeftInPage || isRightInPage);
245
+ const isTopFullyVisible = isTopInPage && isLeftInPage && isRightInPage;
246
+ const isBottomVisible = isBottomInPage && (isLeftInPage || isRightInPage);
247
+ const isBottomFullyVisible = isBottomInPage && isLeftInPage && isRightInPage;
248
+ const isLeftVisible = isLeftInPage && (isTopInPage || isBottomInPage);
249
+ const isLeftFullyVisible = isLeftInPage && isTopInPage && isBottomInPage;
250
+ const isRightVisible = isRightInPage && (isTopInPage || isBottomInPage);
251
+ const isRightFullyVisible = isRightInPage && isTopInPage && isBottomInPage;
252
+ const isFullyVisible = isTopFullyVisible &&
253
+ isBottomFullyVisible &&
254
+ isLeftFullyVisible &&
255
+ isRightFullyVisible;
256
+ return {
257
+ elementTop,
258
+ elementBottom,
259
+ elementLeft,
260
+ elementRight,
261
+ width,
262
+ height,
263
+ isTopVisible,
264
+ isTopFullyVisible,
265
+ isBottomVisible,
266
+ isBottomFullyVisible,
267
+ isLeftVisible,
268
+ isLeftFullyVisible,
269
+ isRightVisible,
270
+ isRightFullyVisible,
271
+ isFullyVisible,
272
+ };
273
+ }
274
+
275
+ const getScrollbarWidthErrorScope = [Symbol('@mustib/utils/getScrollbarWidth'), LIBRARY_ERROR_SCOPE];
276
+ /**
277
+ * Returns the width of the scrollbar for a given HTML element or the document.
278
+ * Handles both horizontal ('x') and vertical ('y') scrollbars, and accounts for element borders.
279
+ *
280
+ * - For the document, calculates the scrollbar width using window and document dimensions.
281
+ * - For an HTMLElement, computes the difference between offset and client dimensions,
282
+ * subtracting border widths to isolate the scrollbar size.
283
+ *
284
+ * @param options - Configuration object.
285
+ * @param options.element - The target element or document. Defaults to `document`.
286
+ * @param options.direction - Scrollbar direction: `'x'` for horizontal, `'y'` for vertical. Defaults to `'y'`.
287
+ * @returns The scrollbar width in pixels. Returns `0` if no scrollbar is present or calculation is not possible.
288
+ *
289
+ * @throws AppError<'Invalid'> If `element` is provided and is not an HTMLElement or Document.
290
+ */
291
+ function getScrollbarWidth(options) {
292
+ const { element = document, direction = 'y' } = options ?? {};
293
+ if (!element || element === document)
294
+ return direction === 'x' ? window.innerHeight - document.documentElement.clientHeight : window.innerWidth - document.documentElement.clientWidth;
295
+ if (!(element instanceof HTMLElement)) {
296
+ return AppError.throw('Invalid', "Invalid argument: 'element' must be an HTMLElement.", {
297
+ pushOptions: {
298
+ scope: getScrollbarWidthErrorScope
299
+ }
300
+ });
301
+ }
302
+ // Get the computed styles of the element to determine border widths.
303
+ const computedStyle = window.getComputedStyle(element);
304
+ // Parse float values for border widths, defaulting to 0 if not a number.
305
+ const borderStartWidth = parseFloat(direction === 'x' ? computedStyle.borderTopWidth : computedStyle.borderLeftWidth) ?? 0;
306
+ const borderEndWidth = parseFloat(direction === 'x' ? computedStyle.borderBottomWidth : computedStyle.borderRightWidth) ?? 0;
307
+ // Calculate the total horizontal border width.
308
+ const totalBorderWidth = borderStartWidth + borderEndWidth;
309
+ // Calculate the raw difference between offsetWidth (includes borders and scrollbar)
310
+ // and clientWidth (includes content and padding, but not borders or scrollbar).
311
+ const rawDiff = direction === 'x' ? element.offsetHeight - element.clientHeight : element.offsetWidth - element.clientWidth;
312
+ // The scrollbar width is this raw difference minus the total border width.
313
+ // We use Math.max to ensure the result is not negative, as overlay scrollbars
314
+ // might result in a 0 or negative difference with their layout behavior.
315
+ const scrollbarWidth = Math.max(0, rawDiff - totalBorderWidth);
316
+ return scrollbarWidth;
317
+ }
318
+
319
+ /**
320
+ * Finds the closest ancestor element (including the starting element) that matches the given selector,
321
+ * traversing through shadow DOM boundaries if necessary.
322
+ *
323
+ * This function behaves like `Element.closest`, but will continue searching across shadow roots
324
+ * by moving up to the host element of each shadow root encountered.
325
+ *
326
+ * @param selector - A string containing a selector to match against.
327
+ * @param startEl - The element from which to start searching.
328
+ * @returns The closest matching ancestor element, or `null` if none is found.
329
+ */
330
+ function closestPierce(selector, startEl) {
331
+ let el = startEl;
332
+ while (el && el instanceof HTMLElement) {
333
+ const found = el.closest(selector);
334
+ if (found)
335
+ return found;
336
+ const root = el.getRootNode();
337
+ // only continue if in shadow dom, otherwise we're done
338
+ el = root instanceof ShadowRoot ? root.host : null;
339
+ }
340
+ return null;
341
+ }
342
+
343
+ const disableAttribute = 'data-mustib-scroll-disabled';
344
+ const marginRightVar = `--${disableAttribute}-margin-right-end`;
345
+ const scrollbarWidthVar = `--${disableAttribute}-scrollbar-width`;
346
+ const style = document.createElement('style');
347
+ style.dataset.id = (`${disableAttribute}-style`);
348
+ style.innerHTML = `
349
+ [${disableAttribute}] {
350
+ overflow: hidden !important;
351
+ overscroll-behavior: contain !important;
352
+ margin-right: calc(var(${marginRightVar}) + var(${scrollbarWidthVar})) !important;
353
+ }
354
+ `;
355
+ function disableElementScroll(options) {
356
+ const { element = document.body, scrollbarElement = document } = {};
357
+ if (!style.isConnected)
358
+ document.head.appendChild(style);
359
+ if (element.hasAttribute(disableAttribute))
360
+ return;
361
+ const baseMarginRight = getComputedStyle(element).marginRight;
362
+ const baseMarginRightWithoutPixels = +baseMarginRight.slice(0, -2);
363
+ const scrollbarWidth = getScrollbarWidth({ element: scrollbarElement });
364
+ element.setAttribute(disableAttribute, '');
365
+ element.style.setProperty(marginRightVar, `${baseMarginRightWithoutPixels}px`);
366
+ element.style.setProperty(scrollbarWidthVar, `${scrollbarWidth}px`);
367
+ }
368
+ function enableElementScroll(options) {
369
+ const element = document.body;
370
+ if (!element.hasAttribute(disableAttribute))
371
+ return;
372
+ element.removeAttribute(disableAttribute);
373
+ element.style.removeProperty(marginRightVar);
374
+ element.style.removeProperty(scrollbarWidthVar);
375
+ }
376
+
377
+ /* eslint-disable no-multi-assign */
378
+ /* eslint-disable no-console */
379
+ const defaultGenerateDataHandler = (data) => data;
380
+ class EventAction {
381
+ /**
382
+ * A map of memoized actions where keys are the matchedTargets and the values are an object where the keys are the registered EventAction event names for that element and the values are an the parsed actions array for that event
383
+ */
384
+ static memoizedElementActions = new Map();
385
+ static defaultActions = {
386
+ '#prevent': {
387
+ handler(data) {
388
+ data.event.preventDefault();
389
+ },
390
+ generateDataHandler: defaultGenerateDataHandler,
391
+ overridable: false
392
+ },
393
+ '#stop': {
394
+ handler(data) {
395
+ data.event.stopPropagation();
396
+ },
397
+ generateDataHandler: defaultGenerateDataHandler,
398
+ overridable: false
399
+ },
400
+ "#nothing": {
401
+ handler() { },
402
+ generateDataHandler: defaultGenerateDataHandler,
403
+ overridable: false,
404
+ },
405
+ '#debug': {
406
+ handler: console.log,
407
+ generateDataHandler: defaultGenerateDataHandler,
408
+ overridable: false
409
+ },
410
+ '#log': {
411
+ handler(data) {
412
+ const param = data.actionParam;
413
+ if (param !== '') {
414
+ console.log(param);
415
+ }
416
+ else {
417
+ console.log(`(${data.eventName}) event dispatched by (${data.matchedTarget.tagName.toLowerCase()}) element ${data._parsedAction.hasOr ? '(with or action type)' : ''}`, `with switches(${data._parsedAction.switches?.map(s => `${s.name}${s.param !== '' ? `:${s.param}` : ''}`).join(', ')})`);
418
+ }
419
+ },
420
+ generateDataHandler: defaultGenerateDataHandler,
421
+ overridable: false
422
+ }
423
+ };
424
+ static defaultSwitches = {
425
+ '#key': {
426
+ handler(data) {
427
+ if (!(data.event instanceof KeyboardEvent))
428
+ return false;
429
+ let keysArray;
430
+ if (Array.isArray(data.switchParam)) {
431
+ keysArray = data.switchParam;
432
+ }
433
+ else if (typeof data.switchParam === 'string') {
434
+ keysArray = data.switchParam.replace(/Space/g, ' ').split(',').map(k => k === '' ? ',' : k);
435
+ }
436
+ if (!keysArray || keysArray.length === 0)
437
+ return false;
438
+ return keysArray.includes(data.event.key);
439
+ },
440
+ overridable: false,
441
+ dynamic: false,
442
+ },
443
+ '#special-key': {
444
+ handler(data) {
445
+ if (!(data.event instanceof KeyboardEvent))
446
+ return false;
447
+ switch (data.switchParam) {
448
+ case 'ctrl':
449
+ return data.event.ctrlKey;
450
+ case 'alt':
451
+ return data.event.altKey;
452
+ case 'shift':
453
+ return data.event.shiftKey;
454
+ case 'meta':
455
+ return data.event.metaKey;
456
+ default:
457
+ return false;
458
+ }
459
+ },
460
+ overridable: false,
461
+ dynamic: false
462
+ }
463
+ };
464
+ /**
465
+ * Finds the first matched element from the `event.composedPath()` that is contained in the `event.currentTarget` and matches the given attribute.
466
+ *
467
+ * It receives an object with the following parameters:
468
+ *
469
+ * - attributeName: The name of the attribute to match (without brackets).
470
+ * - event: The Event whose composedPath and currentTarget are used for the search.
471
+ */
472
+ static getMatchedTarget({ attributeName, event }) {
473
+ const { currentTarget } = event;
474
+ if (!(currentTarget instanceof HTMLElement))
475
+ return undefined;
476
+ for (const el of event.composedPath()) {
477
+ if (!(el instanceof HTMLElement))
478
+ continue;
479
+ if (!currentTarget.contains(el))
480
+ break;
481
+ if (el.matches(`[${attributeName}]`)) {
482
+ return el;
483
+ }
484
+ }
485
+ return undefined;
486
+ }
487
+ /**
488
+ * **NOTE:** It must be used when working on shadow dom elements, because {@link EventAction.getMatchedTarget getMatchedTarget} does not work for shadow dom elements.
489
+ *
490
+ * This function is similar to {@link EventAction.getMatchedTarget getMatchedTarget}, but instead uses {@link closestPierce} to determine if the target matches the attributeName is contained in the closest element that matches currTargetSelector.
491
+ *
492
+ * it receives an object with the following parameters:
493
+ *
494
+ * - attributeName: The name of the attribute to match (without brackets).
495
+ * - event: The Event whose composedPath and currentTarget are used for the search.
496
+ * - currTargetSelector: The selector that is used to determine if the target is contained in the currentTarget.
497
+ */
498
+ static getMatchedTargetPierce({ attributeName, event, currTargetSelector }) {
499
+ for (const el of event.composedPath()) {
500
+ if (!(el instanceof HTMLElement))
501
+ continue;
502
+ if (!closestPierce(currTargetSelector, el))
503
+ break;
504
+ if (el.hasAttribute(attributeName)) {
505
+ return el;
506
+ }
507
+ }
508
+ return undefined;
509
+ }
510
+ /**
511
+ * Returns the HTML data attribute name for the given event name.
512
+ *
513
+ * @param eventName - The name of the event.
514
+ * @returns The HTML data attribute name for the event.
515
+ */
516
+ static getEventAttributeName(eventName) {
517
+ return `data-${eventName}`;
518
+ }
519
+ /**
520
+ * Parse an action name and returns an object with the following properties:
521
+ * - name: The name of the action.
522
+ * - hasOr: A boolean indicating whether the action name starts with "||".
523
+ *
524
+ * @param name - The name of the action to parse.
525
+ */
526
+ static parseActionName(name) {
527
+ const trimmedName = name.trim();
528
+ const hasOr = trimmedName.startsWith('||');
529
+ return { name: hasOr ? trimmedName.slice(2) : trimmedName, hasOr };
530
+ }
531
+ /**
532
+ * Parse an action string and returns a {@link ParsedAction parsed action}.
533
+ *
534
+ * @param actionString - The action string to parse.
535
+ *
536
+ * @example
537
+ *
538
+ * parseActionString('switch1:param1? switch2:param2? ||action:param');
539
+ * {
540
+ * name: 'action',
541
+ * param: 'param',
542
+ * hasOr: true,
543
+ * switches: [
544
+ * {name: 'switch1', param: 'param1'},
545
+ * {name: 'switch2', param: 'param2'}
546
+ * ]
547
+ * }
548
+ */
549
+ static parseActionString(actionString) {
550
+ const trimmed = actionString.replace(/\s+/g, ' ').trim();
551
+ if (trimmed === '')
552
+ return { name: '', param: '', switches: [], hasOr: false };
553
+ const switchIndex = trimmed.lastIndexOf('?');
554
+ const hasSwitches = switchIndex !== -1;
555
+ const paramIndex = trimmed.lastIndexOf(':');
556
+ const hasParam = paramIndex !== -1 && paramIndex > switchIndex;
557
+ const name = hasSwitches ? trimmed.slice(switchIndex + 1, hasParam ? paramIndex : undefined) : trimmed.slice(0, hasParam ? paramIndex : undefined);
558
+ const param = hasParam ? trimmed.slice(paramIndex + 1) : '';
559
+ let switches = [];
560
+ if (hasSwitches) {
561
+ const _switchString = trimmed.slice(0, switchIndex);
562
+ const switchesArray = _switchString.split('?');
563
+ switches = switchesArray.map(switchString => {
564
+ const switchParamIndex = switchString.indexOf(':');
565
+ const hasSwitchParam = switchParamIndex !== -1;
566
+ const switchName = hasSwitchParam ? switchString.slice(0, switchParamIndex) : switchString;
567
+ const switchParam = hasSwitchParam ? switchString.slice(switchParamIndex + 1) : '';
568
+ return { name: switchName.trim(), param: switchParam.trim() };
569
+ });
570
+ }
571
+ return { param: param.trim(), switches, ...EventAction.parseActionName(name) };
572
+ }
573
+ actions = { ...EventAction.defaultActions };
574
+ switches = {};
575
+ currTargetSelector;
576
+ /**
577
+ * A custom function to get the event attribute name.
578
+ * @see {@link EventAction.getEventAttributeName getEventAttributeName}
579
+ */
580
+ getEventAttributeName;
581
+ /**
582
+ * A custom function to get the matched target element.
583
+ * @see {@link EventAction.getMatchedTarget getMatchedTarget}
584
+ */
585
+ getMatchedTarget;
586
+ constructor(options) {
587
+ this.registerSwitches(EventAction.defaultSwitches);
588
+ const { actions, generateDataHandler, switches, getEventAttributeName, getMatchedTarget, currTargetSelector } = options ?? {};
589
+ if (getMatchedTarget && currTargetSelector !== undefined) {
590
+ console.warn(`currentTargetSelector (${currTargetSelector}) is useless when getTargetElement is defined`, this);
591
+ }
592
+ this.currTargetSelector = currTargetSelector;
593
+ if (getMatchedTarget)
594
+ (this.getMatchedTarget = getMatchedTarget);
595
+ if (getEventAttributeName)
596
+ (this.getEventAttributeName = getEventAttributeName);
597
+ if (switches)
598
+ this.registerSwitches(switches);
599
+ if (actions)
600
+ this.registerActions(actions, { generateDataHandler });
601
+ }
602
+ /**
603
+ * A quick way to add multiple event action listeners to an element.
604
+ *
605
+ * @param element - The element to register the event listeners on.
606
+ * @param eventsNames - An array of event names to register listeners for element.
607
+ *
608
+ * @returns This instance, for method chaining.
609
+ */
610
+ addListeners(element, eventsNames) {
611
+ eventsNames.forEach(eventName => {
612
+ element.addEventListener(eventName, this.listener);
613
+ });
614
+ return this;
615
+ }
616
+ /**
617
+ * A quick way to remove multiple event action listeners from an element.
618
+ *
619
+ * @param element - The element to remove event listeners from.
620
+ * @param eventsNames - An array of event names to remove their listeners from element.
621
+ *
622
+ * @returns This instance, for method chaining.
623
+ */
624
+ removeListeners(element, eventsNames) {
625
+ eventsNames.forEach(eventName => {
626
+ element.removeEventListener(eventName, this.listener);
627
+ });
628
+ EventAction.memoizedElementActions.delete(element);
629
+ return this;
630
+ }
631
+ /**
632
+ * Adds switches to the event action instance.
633
+ *
634
+ * @param switches - An object where the keys are switch names and the values are either a switch handler or a custom switch object.
635
+ *
636
+ * @see {@link SwitchHandlerOrCustomSwitch}.
637
+ *
638
+ * @returns This instance, for method chaining.
639
+ */
640
+ registerSwitches(switches) {
641
+ for (const [name, _switch] of Object.entries(switches)) {
642
+ const existingSwitch = this.switches[name];
643
+ if (existingSwitch && !existingSwitch.overridable) {
644
+ console.warn(`Switch named (${name}) is already registered and cannot be overridden`, this);
645
+ continue;
646
+ }
647
+ const $switch = typeof _switch === 'function' ? {
648
+ handler: _switch,
649
+ override: false,
650
+ overridable: true,
651
+ dynamic: false,
652
+ } : _switch;
653
+ const switchData = {
654
+ handler: $switch.handler,
655
+ overridable: $switch.overridable ?? true,
656
+ dynamic: $switch.dynamic ?? false
657
+ };
658
+ if (existingSwitch && !$switch.override) {
659
+ console.warn(`Switch named (${name}) is already registered and need (override) option to be true to be overridden`);
660
+ continue;
661
+ }
662
+ this.switches[name] = switchData;
663
+ }
664
+ return this;
665
+ }
666
+ /**
667
+ * Adds actions to the event action.
668
+ *
669
+ * @param actions - An object where the keys are action names and the values are either an action handler or a custom action object.
670
+ * @param options - An optional object with these properties:
671
+ * - {@link RegisteredActionData.generateDataHandler `generateDataHandler`} A default `generateDataHandler` for all provided actions instead of adding it to each action.
672
+ *
673
+ * @see {@link ActionHandlerOrCustomAction}.
674
+ *
675
+ * @returns This instance, for method chaining.
676
+ */
677
+ registerActions(actions, options) {
678
+ for (const [name, _action] of Object.entries(actions)) {
679
+ const existingAction = this.actions[name];
680
+ if (existingAction && !existingAction.overridable) {
681
+ console.warn(`Action named (${name}) is already registered and cannot be overridden`, this);
682
+ continue;
683
+ }
684
+ const action = typeof _action === 'function' ? {
685
+ handler: _action,
686
+ override: false,
687
+ overridable: true,
688
+ generateDataHandler: options?.generateDataHandler
689
+ } : _action;
690
+ const actionData = {
691
+ handler: action.handler,
692
+ overridable: action.overridable ?? true,
693
+ generateDataHandler: action.generateDataHandler ?? options?.generateDataHandler
694
+ };
695
+ if (existingAction && !action.override) {
696
+ console.warn(`Action named (${name}) is already registered and need (override) option to be true to be overridden`);
697
+ continue;
698
+ }
699
+ this.actions[name] = actionData;
700
+ }
701
+ return this;
702
+ }
703
+ /**
704
+ * Parses an attribute string and returns a parsed actions array.
705
+ */
706
+ parseActionsString(attributeString) {
707
+ const json = parseJson(attributeString);
708
+ if (Array.isArray(json)) {
709
+ return json.reduce((result, actionData) => {
710
+ if (typeof actionData === 'string') {
711
+ result.push(EventAction.parseActionString(actionData));
712
+ }
713
+ else if (Array.isArray(actionData) &&
714
+ actionData.length > 0 &&
715
+ typeof actionData[0] === 'string') {
716
+ switch (actionData.length) {
717
+ case 1:
718
+ result.push(EventAction.parseActionString(actionData[0]));
719
+ break;
720
+ case 2:
721
+ result.push({
722
+ param: actionData[1],
723
+ switches: [],
724
+ ...EventAction.parseActionName(actionData[0])
725
+ });
726
+ break;
727
+ default:
728
+ result.push({
729
+ param: actionData[1],
730
+ switches: actionData.slice(2).map(switchData => {
731
+ const [name = '', param = ''] = Array.isArray(switchData) ? switchData : switchData.split(':');
732
+ return {
733
+ name,
734
+ param
735
+ };
736
+ }),
737
+ ...EventAction.parseActionName(actionData[0])
738
+ });
739
+ break;
740
+ }
741
+ }
742
+ else if (typeof actionData === 'object' && actionData !== null) {
743
+ result.push(actionData);
744
+ }
745
+ return result;
746
+ }, []);
747
+ }
748
+ return attributeString
749
+ .split('&&')
750
+ .map(EventAction.parseActionString);
751
+ }
752
+ /**
753
+ * for internal use
754
+ *
755
+ * Used to get the matched target for an event
756
+ */
757
+ _getMatchedTarget({ attributeName, event }) {
758
+ if (this.getMatchedTarget) {
759
+ return this.getMatchedTarget({
760
+ attributeName,
761
+ event
762
+ });
763
+ }
764
+ if (this.currTargetSelector !== undefined && this.currTargetSelector !== '') {
765
+ return EventAction.getMatchedTargetPierce({
766
+ attributeName,
767
+ event,
768
+ currTargetSelector: this.currTargetSelector
769
+ });
770
+ }
771
+ return EventAction.getMatchedTarget({
772
+ attributeName,
773
+ event
774
+ });
775
+ }
776
+ /**
777
+ * Executes the parsed actions array and their switches.
778
+ *
779
+ * It expects an object with the following properties:
780
+ * - `parsedActions`: the parsed actions array.
781
+ * - `matchedTarget`: the matched target for the event.
782
+ * - `event`: the event.
783
+ * - `eventName`: the name of the event.
784
+ *
785
+ * @returns an object with the following properties:
786
+ * - `executedActions`: the executed actions array.
787
+ */
788
+ executeParsedActions({ event, eventName, matchedTarget, parsedActions }) {
789
+ const staticSwitches = {};
790
+ const executedActions = [];
791
+ for (const parsedAction of parsedActions) {
792
+ if (parsedAction.name === '')
793
+ continue;
794
+ const action = this.actions[parsedAction.name];
795
+ if (!action) {
796
+ console.warn(`There is no registered action with the name (${parsedAction.name}) for the event (${eventName})`, matchedTarget);
797
+ continue;
798
+ }
799
+ const checkSwitches = parsedAction.switches.length === 0 ? true : parsedAction.switches.every(switchData => {
800
+ const staticValue = staticSwitches[switchData.name]?.get(switchData.param);
801
+ if (staticValue !== undefined)
802
+ return staticValue;
803
+ const switchAction = this.switches[switchData.name];
804
+ if (!switchAction) {
805
+ console.warn(`There is no registered switch with the name (${switchData.name}) for the event (${eventName})`, matchedTarget);
806
+ return false;
807
+ }
808
+ const value = switchAction.handler({
809
+ actionParam: parsedAction.param,
810
+ event: event,
811
+ matchedTarget,
812
+ eventName,
813
+ switchParam: switchData.param,
814
+ actionName: parsedAction.name,
815
+ _parsedAction: parsedAction
816
+ });
817
+ if (!switchAction.dynamic) {
818
+ const map = staticSwitches[switchData.name] ||= new Map();
819
+ map.set(switchData.param, value);
820
+ }
821
+ return value;
822
+ });
823
+ if (!checkSwitches)
824
+ continue;
825
+ const handlerData = {
826
+ actionParam: parsedAction.param,
827
+ event: event,
828
+ matchedTarget,
829
+ eventName,
830
+ _parsedAction: parsedAction,
831
+ };
832
+ action.handler(action.generateDataHandler ? action.generateDataHandler(handlerData) : handlerData);
833
+ executedActions.push(parsedAction);
834
+ if (parsedAction.hasOr) {
835
+ break;
836
+ }
837
+ }
838
+ return { executedActions };
839
+ }
840
+ /**
841
+ * Memoizes and Parses an action string into a parsed actions array if it is not memoized,
842
+ *
843
+ * @param data - An object containing the matched target, event name, and action string.
844
+ * @returns The memoized parsed actions array.
845
+ */
846
+ getOrMemoizeParsedActions(data) {
847
+ const { matchedTarget, eventName, actionStr } = data;
848
+ let memoizedTargetEvents = EventAction.memoizedElementActions.get(matchedTarget);
849
+ if (!memoizedTargetEvents) {
850
+ const events = {};
851
+ EventAction.memoizedElementActions.set(matchedTarget, events);
852
+ memoizedTargetEvents = events;
853
+ }
854
+ const parsedActions = memoizedTargetEvents[eventName] ||= this.parseActionsString(actionStr);
855
+ return parsedActions;
856
+ }
857
+ /**
858
+ * The event listener function.
859
+ *
860
+ * it returns undefined or an object with the following properties:
861
+ * - `matchedTarget`: the matched target for the event.
862
+ * - `attributeName`: the name of the attribute that contains the event actions.
863
+ * - `parsedActions`: the parsed actions array.
864
+ * - `executedActions`: the executed actions array.
865
+ *
866
+ * The returned object can be useful for debugging purposes, and can also be used when the listener is not the actual event listener handler if that is needed.
867
+ */
868
+ listener = (e) => {
869
+ const eventName = e.type;
870
+ const attributeName = (this.getEventAttributeName ?? EventAction.getEventAttributeName)(eventName);
871
+ const matchedTarget = this._getMatchedTarget({ attributeName, event: e });
872
+ if (!matchedTarget)
873
+ return undefined;
874
+ const actionStr = matchedTarget.getAttribute(attributeName);
875
+ if (actionStr === null || actionStr === '')
876
+ return undefined;
877
+ const parsedActions = this.getOrMemoizeParsedActions({
878
+ matchedTarget,
879
+ eventName,
880
+ actionStr
881
+ });
882
+ const { executedActions } = this.executeParsedActions({
883
+ event: e,
884
+ eventName,
885
+ matchedTarget,
886
+ parsedActions
887
+ });
888
+ return {
889
+ matchedTarget,
890
+ attributeName,
891
+ parsedActions,
892
+ executedActions
893
+ };
894
+ };
895
+ }
896
+
897
+ var _a, _MUElement_muElements;
898
+ let count = 0;
899
+ class MUElement extends LitElement {
900
+ static register(tagName) {
901
+ if (customElements.get(tagName))
902
+ return;
903
+ customElements.define(tagName, this);
904
+ }
905
+ get interactable() {
906
+ return !(this.disabled || this.readonly);
907
+ }
908
+ constructor() {
909
+ super();
910
+ this.disabled = false;
911
+ this.readonly = false;
912
+ /**
913
+ * If true, do not add {@link https://mustib.github.io/mustib-utils/v2/utilities/browser/eventaction/ event action} attributes
914
+ *
915
+ * By default mu-element will call `_addEventActionAttributes` in connectedCallback if it is not undefined and `noEventActionAttributes` is false
916
+ */
917
+ this.noEventActionAttributes = false;
918
+ Reflect.defineProperty(this, 'muId', {
919
+ value: `mu-element-${++count}`,
920
+ configurable: false,
921
+ writable: false,
922
+ enumerable: true
923
+ });
924
+ }
925
+ generateIsReadyPromise({ timeout = 1000, onTimeout = () => {
926
+ console.warn('timeout reached for isReady promise', this);
927
+ } } = {}) {
928
+ const obj = {
929
+ status: 'pending',
930
+ resolved: false,
931
+ };
932
+ obj.promise = new Promise((r) => {
933
+ const timeoutId = setTimeout(() => {
934
+ if (obj.resolved)
935
+ return;
936
+ obj.resolved = true;
937
+ obj.status = 'fail';
938
+ onTimeout?.();
939
+ r(true);
940
+ }, timeout);
941
+ obj.resolve = () => {
942
+ if (obj.resolved)
943
+ return;
944
+ clearTimeout(timeoutId);
945
+ obj.resolved = true;
946
+ obj.status = 'success';
947
+ r(true);
948
+ };
949
+ });
950
+ return obj;
951
+ }
952
+ getMuElementById(id) {
953
+ return __classPrivateFieldGet(_a, _a, "f", _MUElement_muElements).get(id);
954
+ }
955
+ closestPierce(selector) {
956
+ return _a.closestPierce(selector, this);
957
+ }
958
+ /**
959
+ * This method gets the next or previous navigable item from the given index.
960
+ *
961
+ * If an item not found and switchBack is true, it will go the other direction to find an item
962
+ *
963
+ * It takes An optional object with the following properties:
964
+ * - `fromIndex` - The index to start searching from.
965
+ *
966
+ * - `direction` - The direction to search in, can be (`next` or `prev`).
967
+ *
968
+ * - `switchBack` - A boolean Whether to search in the other direction if an item is not found.
969
+ *
970
+ * - `items` - The array of items to search in.
971
+ *
972
+ * - `isNavigable` - A function that takes an item and returns a boolean indicating whether the item is navigable.
973
+ *
974
+ * - `shouldBreak` - An optional function that takes an item and returns a boolean indicating whether to break the loop.
975
+ */
976
+ getNavigationItem(data) {
977
+ const { direction, switchBack = false, items, fromIndex = direction === 'next' ? -1 : items.length, isNavigable, shouldBreak } = data;
978
+ let navigationItem;
979
+ const getItem = ({ endIndex, startIndex }) => {
980
+ const numOfRetries = endIndex + 1 - startIndex;
981
+ for (let i = 0; i < numOfRetries && !navigationItem; i++) {
982
+ const itemIndex = direction === 'next' ? startIndex + i : endIndex - i;
983
+ const item = items[itemIndex];
984
+ if (item) {
985
+ if (shouldBreak?.(item))
986
+ break;
987
+ if (isNavigable(item)) {
988
+ navigationItem = item;
989
+ }
990
+ }
991
+ }
992
+ };
993
+ if (direction === 'next') {
994
+ getItem({
995
+ startIndex: fromIndex + 1,
996
+ endIndex: items.length - 1
997
+ });
998
+ }
999
+ else {
1000
+ getItem({
1001
+ startIndex: 0,
1002
+ endIndex: fromIndex - 1
1003
+ });
1004
+ }
1005
+ if (!navigationItem && switchBack) {
1006
+ if (direction === 'next') {
1007
+ getItem({
1008
+ startIndex: 0,
1009
+ endIndex: fromIndex - 1
1010
+ });
1011
+ }
1012
+ else {
1013
+ getItem({
1014
+ startIndex: fromIndex + 1,
1015
+ endIndex: items.length - 1
1016
+ });
1017
+ }
1018
+ }
1019
+ return navigationItem;
1020
+ }
1021
+ connectedCallback() {
1022
+ super.connectedCallback();
1023
+ this.dataset.muId = this.muId;
1024
+ if (!this.id)
1025
+ this.id = this.muId;
1026
+ __classPrivateFieldGet(_a, _a, "f", _MUElement_muElements).set(this.muId, { element: this });
1027
+ this.updateComplete.then(() => {
1028
+ this.eventActionData?.eventAction.addListeners(this, this.eventActionEvents || this.eventActionData.events);
1029
+ if (!this.noEventActionAttributes)
1030
+ this._addEventActionAttributes?.();
1031
+ });
1032
+ }
1033
+ disconnectedCallback() {
1034
+ super.disconnectedCallback();
1035
+ __classPrivateFieldGet(_a, _a, "f", _MUElement_muElements).delete(this.muId);
1036
+ this.eventActionData?.eventAction.removeListeners(this, this.eventActionEvents || this.eventActionData.events);
1037
+ }
1038
+ }
1039
+ _a = MUElement;
1040
+ _MUElement_muElements = { value: new Map() };
1041
+ MUElement.closestPierce = closestPierce;
1042
+ MUElement.css = {
1043
+ focus: css `
1044
+ --focus-color: oklch(from currentColor l c h / 0.5);
1045
+ box-shadow: 0 0 0 var(--focus-width, 2px) var(--mu-focus-color, var(--focus-color)) inset;
1046
+ `
1047
+ };
1048
+ MUElement.cssColors = css `
1049
+ --mu-color-100: hsl(var(--mu-hue), 20%, 95%);
1050
+ --mu-color-200: hsl(var(--mu-hue), 30%, 85%);
1051
+ --mu-color-300: hsl(var(--mu-hue), 40%, 75%);
1052
+ --mu-color-400: hsl(var(--mu-hue), 50%, 65%);
1053
+ --mu-color-500: hsl(var(--mu-hue), 60%, 55%);
1054
+ --mu-color-600: hsl(var(--mu-hue), 70%, 45%);
1055
+ --mu-color-700: hsl(var(--mu-hue), 80%, 35%);
1056
+ --mu-color-800: hsl(var(--mu-hue), 90%, 25%);
1057
+ --mu-color-900: hsl(var(--mu-hue), 100%, 15%);
1058
+ `;
1059
+ MUElement.cssBase = css `
1060
+ *, *::before, *::after, :where(:host) {
1061
+ margin: 0;
1062
+ padding: 0;
1063
+ box-sizing: border-box;
1064
+ font: inherit;
1065
+ border: none;
1066
+ outline: none;
1067
+ }
1068
+
1069
+ .truncate {
1070
+ overflow: hidden;
1071
+ text-overflow: ellipsis;
1072
+ white-space: nowrap;
1073
+ }
1074
+
1075
+ :where(:host) {
1076
+ ${_a.cssColors}
1077
+ --mu-base-rem: 10px;
1078
+ --mu-hue: 240;
1079
+ display: block;
1080
+ font-size: calc(var(--mu-base-rem) * 1.6);
1081
+ color: var(--mu-color-100);
1082
+ font-family: sans-serif;
1083
+ margin: 0;
1084
+ padding: 0;
1085
+ box-sizing: border-box;
1086
+ max-width: 100%;
1087
+ }
1088
+
1089
+ @media (prefers-color-scheme: light) {
1090
+ :where(:host) {
1091
+ color: var(--mu-color-900);
1092
+ }
1093
+ }
1094
+
1095
+ :where(:host([readonly]), :host([disabled])) {
1096
+ user-select: none;
1097
+ cursor: not-allowed;
1098
+ }
1099
+ `;
1100
+ __decorate([
1101
+ property({ type: Boolean, reflect: true })
1102
+ ], MUElement.prototype, "disabled", void 0);
1103
+ __decorate([
1104
+ property({ type: Boolean, reflect: true })
1105
+ ], MUElement.prototype, "readonly", void 0);
1106
+ __decorate([
1107
+ staticProperty({
1108
+ converter: Boolean
1109
+ })
1110
+ ], MUElement.prototype, "noEventActionAttributes", void 0);
1111
+ __decorate([
1112
+ staticProperty({
1113
+ converter: parseJson
1114
+ })
1115
+ ], MUElement.prototype, "eventActionEvents", void 0);
1116
+
1117
+ export { EventAction as E, MUElement as M, __decorate as _, disableElementScroll as a, debounce as d, enableElementScroll as e, getElementBoundaries as g, parseJson as p, throttle as t, wait as w };