@optionfactory/ful 6.0.8 → 7.0.1

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/ful.mjs CHANGED
@@ -977,59 +977,108 @@ class AuthorizationCodeFlowInterceptor {
977
977
  }
978
978
  }
979
979
 
980
+ /**
981
+ * @typedef {Object} AsyncExtension
982
+ * @property {Promise<any>[]} promises
983
+ * @typedef {Event & { async?: AsyncExtension }} AsyncEvent
984
+ */
980
985
  class AsyncEvents {
981
- static async fireAsync(el, evt) {
986
+ /**
987
+ * Dispatches an event and handles asynchronous resolution based on the execution mode.
988
+ * @param {HTMLElement} el - The target element dispatching the event.
989
+ * @param {AsyncEvent} evt - The event instance.
990
+ * @param {{mode?: 'broadcast' | 'pipeline' | 'delegate'}} [options] - Configuration options (defaults to 'broadcast').
991
+ * @returns {Promise<any>} Resolves with an array of values for broadcasts, a single value for pipelines/delegates, or undefined.
992
+ */
993
+ static async fireAsync(el, evt, options) {
982
994
  el.dispatchEvent(evt);
983
- return await evt.async?.promise;
995
+ const promises = evt.async?.promises ?? [];
996
+ const mode = options?.mode ?? 'broadcast';
997
+ if (mode === 'pipeline' && promises.length > 1) {
998
+ throw new Error(`[AsyncEvents] Event "${evt.type}" is configured in 'pipeline' mode and expects at most one async listener, but ${promises.length} listeners were triggered on this element.`);
999
+ }
1000
+ if (mode === 'delegate' && promises.length !== 1) {
1001
+ throw new Error(`[AsyncEvents] Event "${evt.type}" is configured in 'delegate' mode and requires exactly one async listener, but ${promises.length} were registered.`);
1002
+ }
1003
+ return mode === 'broadcast' ? Promise.all(promises) : Promise.resolve(promises[0]);
984
1004
  }
1005
+
985
1006
  /**
986
- *
987
- * @param {*} el
988
- * @param {*} type
989
- * @param {*} fn returning the result
990
- * @param {*} options
991
- * @returns
1007
+ * Registers an asynchronous event listener wrapper.
1008
+ * @param {HTMLElement} el - The target element.
1009
+ * @param {string} type - The event name/type.
1010
+ * @param {Function} fn - The async listener middleware function returning the execution result.
1011
+ * @param {AddEventListenerOptions} [options] - Native addEventListener options.
1012
+ * @returns {EventListener} The underlying proxy listener function needed for cleanup via asyncOff.
992
1013
  */
993
1014
  static asyncOn(el, type, fn, options) {
1015
+ /** @type {(evt: Event) => Promise<void>} */
994
1016
  const listener = async (event) => {
995
- let resolve, reject;
996
- const promise = new Promise((res, rej) => {
997
- resolve = res;
998
- reject = rej;
999
- });
1000
- event.async = { promise };
1017
+ const ae = /** @type {AsyncEvent} */ (event);
1018
+ if (!ae.async) {
1019
+ ae.async = { promises: [] };
1020
+ }
1021
+ const { promise, resolve, reject } = Promise.withResolvers();
1022
+ ae.async.promises.push(promise);
1001
1023
  try {
1002
- //@ts-ignore
1003
- resolve(await fn(event));
1024
+ resolve(await fn(ae));
1004
1025
  } catch (e) {
1005
- //@ts-ignore
1006
1026
  reject(e);
1007
1027
  }
1008
1028
  };
1029
+
1009
1030
  el.addEventListener(type, listener, options);
1010
1031
  return listener;
1011
1032
  }
1033
+
1012
1034
  /**
1013
- *
1014
- * @param {*} el
1015
- * @param {*} type
1016
- * @param {*} listener the listener returned by asyncOn
1017
- * @param {*} options
1018
- */
1035
+ * Unregisters an asynchronous event listener proxy.
1036
+ * @param {HTMLElement} el - The target element.
1037
+ * @param {string} type - The event name/type.
1038
+ * @param {EventListener} listener - The proxy listener instance previously returned by asyncOn.
1039
+ * @param {EventListenerOptions} [options] - Native removeEventListener options.
1040
+ */
1019
1041
  static asyncOff(el, type, listener, options) {
1020
1042
  el.removeEventListener(type, listener, options);
1021
1043
  }
1044
+
1045
+ /**
1046
+ * Mixes the asynchronous execution engine extensions into target class prototypes.
1047
+ * @param {...Function} classes - The target class constructors to decorate.
1048
+ */
1022
1049
  static mixInto(...classes) {
1023
1050
  for (const k of classes) {
1024
1051
  Object.assign(k.prototype, {
1025
- async fireAsync(evt) {
1026
- return await AsyncEvents.fireAsync(this, evt);
1052
+ /**
1053
+ * @this {HTMLElement}
1054
+ * @param {AsyncEvent} evt
1055
+ * @param {{mode?: 'broadcast' | 'pipeline' | 'delegate'}} [options]
1056
+ * @returns {Promise<any>}
1057
+ */
1058
+ async fireAsync(evt, options) {
1059
+ return await AsyncEvents.fireAsync(this, evt, options);
1027
1060
  },
1061
+
1062
+ /**
1063
+ * @this {HTMLElement}
1064
+ * @param {string} type
1065
+ * @param {Function} fn
1066
+ * @param {AddEventListenerOptions} [options]
1067
+ * @returns {EventListener}
1068
+ */
1028
1069
  asyncOn(type, fn, options) {
1029
1070
  return AsyncEvents.asyncOn(this, type, fn, options);
1030
1071
  },
1072
+
1073
+ /**
1074
+ * @this {HTMLElement}
1075
+ * @param {string} type
1076
+ * @param {EventListener} listener
1077
+ * @param {EventListenerOptions} [options]
1078
+ * @returns {void}
1079
+ */
1031
1080
  asyncOff(type, listener, options) {
1032
- return AsyncEvents.asyncOff(this, type, listener, options);
1081
+ AsyncEvents.asyncOff(this, type, listener, options);
1033
1082
  }
1034
1083
  });
1035
1084
  }
@@ -1383,7 +1432,7 @@ class Form extends ParsedElement {
1383
1432
  }
1384
1433
  this.errors = [];
1385
1434
  const sre = new CustomEvent('submit:requested', { bubbles: true, cancelable: false, detail: { submitter, values: se.detail.values, request: se.detail.request} });
1386
- let response = await AsyncEvents.fireAsync(this, sre);
1435
+ let response = await AsyncEvents.fireAsync(this, sre, {mode: "pipeline"});
1387
1436
  request = sre.detail.request;
1388
1437
 
1389
1438
  response = await loader.submit(request, this, response);
@@ -1532,7 +1581,6 @@ class Input extends ParsedElement {
1532
1581
  Attributes.toggle(this, 'readonly', v);
1533
1582
  });
1534
1583
  }
1535
- //@ts-ignore
1536
1584
  get disabled() {
1537
1585
  return this._input.hasAttribute('disabled');
1538
1586
  }
@@ -2157,11 +2205,12 @@ class Dropdown extends ParsedElement {
2157
2205
  this.#menu = fragment.querySelector("menu");
2158
2206
  this.#menu.addEventListener('click', evt => {
2159
2207
  evt.stopPropagation();
2160
- if (!evt.target.matches('li')) {
2208
+ const li = evt.target.closest('li');
2209
+ if (!li) {
2161
2210
  this.hide();
2162
2211
  return;
2163
2212
  }
2164
- this.#change(evt.target);
2213
+ this.#change(li);
2165
2214
  });
2166
2215
  this.replaceChildren(fragment);
2167
2216
  }
@@ -2460,7 +2509,6 @@ class Select extends ParsedElement {
2460
2509
  }
2461
2510
  return [...this.#values.entries()][0] ?? null;
2462
2511
  }
2463
- //@ts-ignore
2464
2512
  get disabled() {
2465
2513
  return this.#input.hasAttribute('disabled');
2466
2514
  }
@@ -2608,7 +2656,6 @@ class RadioGroup extends ParsedElement {
2608
2656
  Attributes.toggle(this, 'readonly', v);
2609
2657
  });
2610
2658
  }
2611
- //@ts-ignore
2612
2659
  get disabled(){
2613
2660
  return this.#fieldset.hasAttribute('disabled');
2614
2661
  }
@@ -2718,7 +2765,6 @@ class Checkbox extends ParsedElement {
2718
2765
  Attributes.toggle(this, 'readonly', v);
2719
2766
  });
2720
2767
  }
2721
- //@ts-ignore
2722
2768
  get disabled() {
2723
2769
  return this.#input.hasAttribute('disabled');
2724
2770
  }