selective-ui 1.2.5 → 1.2.6

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.
Files changed (55) hide show
  1. package/README.md +7 -0
  2. package/dist/selective-ui.css +64 -58
  3. package/dist/selective-ui.css.map +1 -1
  4. package/dist/selective-ui.esm.js +240 -125
  5. package/dist/selective-ui.esm.js.map +1 -1
  6. package/dist/selective-ui.esm.min.js +2 -2
  7. package/dist/selective-ui.esm.min.js.br +0 -0
  8. package/dist/selective-ui.min.css +1 -1
  9. package/dist/selective-ui.min.css.br +0 -0
  10. package/dist/selective-ui.min.js +2 -2
  11. package/dist/selective-ui.min.js.br +0 -0
  12. package/dist/selective-ui.umd.js +245 -126
  13. package/dist/selective-ui.umd.js.map +1 -1
  14. package/package.json +3 -3
  15. package/src/css/components/accessorybox.css +1 -1
  16. package/src/css/components/directive.css +2 -2
  17. package/src/css/components/option-handle.css +4 -4
  18. package/src/css/components/placeholder.css +1 -1
  19. package/src/css/components/popup/empty-state.css +3 -3
  20. package/src/css/components/popup/loading-state.css +3 -3
  21. package/src/css/components/popup/popup.css +5 -5
  22. package/src/css/components/searchbox.css +2 -2
  23. package/src/css/components/selectbox.css +7 -7
  24. package/src/css/views/group-view.css +8 -8
  25. package/src/css/views/option-view.css +22 -22
  26. package/src/ts/adapter/mixed-adapter.ts +1 -1
  27. package/src/ts/components/accessorybox.ts +9 -9
  28. package/src/ts/components/directive.ts +2 -2
  29. package/src/ts/components/option-handle.ts +9 -9
  30. package/src/ts/components/placeholder.ts +5 -5
  31. package/src/ts/components/popup/empty-state.ts +4 -4
  32. package/src/ts/components/popup/loading-state.ts +4 -4
  33. package/src/ts/components/popup/popup.ts +19 -38
  34. package/src/ts/components/searchbox.ts +6 -6
  35. package/src/ts/components/selectbox.ts +93 -11
  36. package/src/ts/core/base/adapter.ts +2 -2
  37. package/src/ts/core/base/virtual-recyclerview.ts +6 -6
  38. package/src/ts/core/model-manager.ts +10 -11
  39. package/src/ts/core/search-controller.ts +2 -2
  40. package/src/ts/global.ts +26 -5
  41. package/src/ts/index.ts +22 -3
  42. package/src/ts/models/option-model.ts +13 -5
  43. package/src/ts/services/refresher.ts +2 -1
  44. package/src/ts/services/resize-observer.ts +4 -4
  45. package/src/ts/types/core/base/view.type.ts +3 -3
  46. package/src/ts/types/core/base/virtual-recyclerview.type.ts +1 -1
  47. package/src/ts/types/plugins/plugin.type.ts +46 -0
  48. package/src/ts/types/utils/istorage.type.ts +8 -4
  49. package/src/ts/types/utils/libs.type.ts +2 -2
  50. package/src/ts/types/utils/selective.type.ts +14 -1
  51. package/src/ts/utils/callback-scheduler.ts +4 -4
  52. package/src/ts/utils/libs.ts +41 -65
  53. package/src/ts/utils/selective.ts +85 -21
  54. package/src/ts/views/group-view.ts +6 -6
  55. package/src/ts/views/option-view.ts +11 -11
@@ -1,4 +1,4 @@
1
- /*! Selective UI v1.2.5 | MIT License */
1
+ /*! Selective UI v1.2.6 | MIT License */
2
2
  (function (global, factory) {
3
3
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
4
4
  typeof define === 'function' && define.amd ? define(['exports'], factory) :
@@ -213,7 +213,7 @@
213
213
  * @public
214
214
  * @param {TimerKey} key - Key whose callbacks will be scheduled.
215
215
  * @param {...any[]} params - Parameters passed as a shared payload to all callbacks.
216
- * @returns {Promise<void> | void} Promise resolving when all callbacks finish execution.
216
+ * @returns {Promise<void>} Promise resolving when all callbacks finish execution.
217
217
  */
218
218
  run(key, ...params) {
219
219
  const executes = this.executeStored.get(key);
@@ -332,11 +332,11 @@
332
332
  return result;
333
333
  }
334
334
  /**
335
- * Resolves a selector, NodeList, or single Element into an array of elements.
335
+ * Resolves a selector, NodeList, or single HTMLElement into an array of elements.
336
336
  * Returns an empty array if nothing is found.
337
337
  *
338
- * @param {string|NodeListOf<Element>|Element|HTMLElement|ArrayLike<Element>|null} queryCommon - CSS selector, NodeList, or Element.
339
- * @returns {Element[]} - Array of matched elements (empty if none).
338
+ * @param {string|NodeListOf<HTMLElement>|HTMLElement|HTMLElement|ArrayLike<HTMLElement>|null} queryCommon - CSS selector, NodeList, or HTMLElement.
339
+ * @returns {HTMLElement[]} - Array of matched elements (empty if none).
340
340
  */
341
341
  static getElements(queryCommon) {
342
342
  if (!queryCommon)
@@ -345,7 +345,7 @@
345
345
  const nodeList = document.querySelectorAll(queryCommon);
346
346
  return Array.from(nodeList);
347
347
  }
348
- if (queryCommon instanceof Element) {
348
+ if (queryCommon instanceof HTMLElement) {
349
349
  return [queryCommon];
350
350
  }
351
351
  // NodeList or array-like
@@ -355,27 +355,27 @@
355
355
  return [];
356
356
  }
357
357
  /**
358
- * Creates a new Element based on a NodeSpec and applies attributes, classes, styles, dataset, and events.
358
+ * Creates a new HTMLElement based on a NodeSpec and applies attributes, classes, styles, dataset, and events.
359
359
  *
360
360
  * @param {NodeSpec} data - Specification describing the element to create.
361
- * @returns {Element} - The created element.
361
+ * @returns {HTMLElement} - The created element.
362
362
  */
363
363
  static nodeCreator(data = {}) {
364
364
  const nodeName = (data.node ?? "div");
365
365
  return this.nodeCloner(document.createElement(nodeName), data, true);
366
366
  }
367
367
  /**
368
- * Clones an element (or converts a Node to Element) and applies NodeSpec options.
368
+ * Clones an element (or converts a Node to HTMLElement) and applies NodeSpec options.
369
369
  * When systemNodeCreate=true, uses the provided node as-is.
370
370
  *
371
- * @param {Element} node - The element to clone or use.
371
+ * @param {HTMLElement} node - The element to clone or use.
372
372
  * @param {NodeSpec|null} _nodeOption - Options (classList, style, dataset, event, other props).
373
373
  * @param {boolean} systemNodeCreate - If true, do not clone; use original node.
374
- * @returns {Element} - The processed element.
374
+ * @returns {HTMLElement} - The processed element.
375
375
  */
376
376
  static nodeCloner(node = document.documentElement, _nodeOption = null, systemNodeCreate = false) {
377
377
  const nodeOption = { ...(_nodeOption ?? {}) };
378
- const element_creation = systemNodeCreate ? node : this.nodeToElement(node.cloneNode(true));
378
+ const element_creation = systemNodeCreate ? node : node.cloneNode(true);
379
379
  const classList = nodeOption.classList;
380
380
  if (typeof classList === "string") {
381
381
  element_creation.classList.add(classList);
@@ -435,40 +435,17 @@
435
435
  });
436
436
  return element_creation;
437
437
  }
438
- /**
439
- * Ensures the given Node is an Element; throws if not.
440
- *
441
- * @param {Node} node - The node to validate.
442
- * @returns {Element} - The element cast.
443
- * @throws {TypeError} - If node is not an Element.
444
- */
445
- static nodeToElement(node) {
446
- if (node instanceof Element)
447
- return node;
448
- throw new TypeError("Node is not an Element");
449
- }
450
- /**
451
- * Mounts a view from a plain object specification and returns a typed result
452
- * containing the root element and a tag map.
453
- *
454
- * @template TTags
455
- * @param {object} rawObj - The specification describing elements and tags.
456
- * @returns {MountViewResult<TTags>} - The mounted view and its tag references.
457
- */
458
- static mountView(rawObj) {
459
- return this.mountNode(rawObj);
460
- }
461
438
  /**
462
439
  * Recursively builds DOM nodes from a specification object, appends/prepends them
463
440
  * to an optional parent, and returns either a tag map or a full MountViewResult.
464
441
  *
465
442
  * @template TTags
466
443
  * @param {Object<string, any>} rawObj - Node spec (keys -> { tag, child }).
467
- * @param {Element|null} [parentE=null] - Parent to attach into; if null, returns root.
444
+ * @param {HTMLElement|null} [parentE=null] - Parent to attach into; if null, returns root.
468
445
  * @param {boolean} [isPrepend=false] - If true, prepend; otherwise append.
469
446
  * @param {boolean} [isRecusive=false] - Internal flag for recursion control.
470
447
  * @param {TTags|Object} [recursiveTemp={}] - Accumulator for tag references.
471
- * @returns {MountViewResult<TTags>|TTags} - Tag map or the final mount result.
448
+ * @returns {TTags} - Tag map or the final mount result.
472
449
  */
473
450
  static mountNode(rawObj, parentE = null, isPrepend = false, isRecusive = false, recursiveTemp = {}) {
474
451
  let view = null;
@@ -587,7 +564,7 @@
587
564
  /**
588
565
  * Removes a binder map entry for the given element from the global storage.
589
566
  *
590
- * @param {HTMLElement} element - Element key to remove from the binder map.
567
+ * @param {HTMLElement} element - HTMLElement key to remove from the binder map.
591
568
  * @returns {boolean} - True if an entry existed and was removed.
592
569
  */
593
570
  static removeBinderMap(element) {
@@ -596,8 +573,8 @@
596
573
  /**
597
574
  * Retrieves the binder map entry associated with the given element.
598
575
  *
599
- * @param {HTMLElement} item - Element key whose binder map is requested.
600
- * @returns {BinderMap | null} - The stored binder map value or undefined if absent.
576
+ * @param {HTMLElement} item - HTMLElement key whose binder map is requested.
577
+ * @returns {BinderMap | any} - The stored binder map value or undefined if absent.
601
578
  */
602
579
  static getBinderMap(item) {
603
580
  return this.iStorage.bindedMap.get(item);
@@ -605,7 +582,7 @@
605
582
  /**
606
583
  * Sets or updates the binder map entry for a given element.
607
584
  *
608
- * @param {HTMLElement} item - Element key to associate with the binder map.
585
+ * @param {HTMLElement} item - HTMLElement key to associate with the binder map.
609
586
  * @param {BinderMap} bindMap - Value to store in the binder map.
610
587
  */
611
588
  static setBinderMap(item, bindMap) {
@@ -614,7 +591,7 @@
614
591
  /**
615
592
  * Removes an unbinder map entry for the given element from the global storage.
616
593
  *
617
- * @param {HTMLElement} element - Element key to remove from the unbinder map.
594
+ * @param {HTMLElement} element - HTMLElement key to remove from the unbinder map.
618
595
  * @returns {boolean} - True if an entry existed and was removed.
619
596
  */
620
597
  static removeUnbinderMap(element) {
@@ -623,7 +600,7 @@
623
600
  /**
624
601
  * Retrieves the unbinder map entry associated with the given element.
625
602
  *
626
- * @param {HTMLElement} item - Element key whose unbinder map is requested.
603
+ * @param {HTMLElement} item - HTMLElement key whose unbinder map is requested.
627
604
  * @returns {unknown} - The stored unbinder map value or undefined if absent.
628
605
  */
629
606
  static getUnbinderMap(item) {
@@ -632,7 +609,7 @@
632
609
  /**
633
610
  * Sets or updates the unbinder map entry for a given element.
634
611
  *
635
- * @param {HTMLElement} item - Element key to associate with the unbinder map.
612
+ * @param {HTMLElement} item - HTMLElement key to associate with the unbinder map.
636
613
  * @param {BinderMap} bindMap - Value to store in the unbinder map.
637
614
  */
638
615
  static setUnbinderMap(item, bindMap) {
@@ -1280,7 +1257,7 @@
1280
1257
  * getter/setter APIs for the placeholder content.
1281
1258
  *
1282
1259
  * ### Responsibility
1283
- * - Create and own the placeholder DOM element (`.selective-ui-placeholder`).
1260
+ * - Create and own the placeholder DOM element (`.seui-placeholder`).
1284
1261
  * - Render placeholder content from {@link SelectiveOptions.placeholder}.
1285
1262
  * - Support runtime updates via {@link set}, optionally persisting into options.
1286
1263
  * - Participate in the shared {@link Lifecycle} FSM.
@@ -1338,7 +1315,7 @@
1338
1315
  * Builds the placeholder DOM node and starts the lifecycle.
1339
1316
  *
1340
1317
  * Side effects:
1341
- * - Creates a `div.selective-ui-placeholder` node via {@link Libs.nodeCreator}.
1318
+ * - Creates a `div.seui-placeholder` node via {@link Libs.nodeCreator}.
1342
1319
  * - Writes initial placeholder content into `innerHTML`.
1343
1320
  * - Transitions the lifecycle by calling `init()`.
1344
1321
  *
@@ -1348,7 +1325,7 @@
1348
1325
  initialize(options) {
1349
1326
  this.node = Libs.nodeCreator({
1350
1327
  node: "div",
1351
- classList: "selective-ui-placeholder",
1328
+ classList: "seui-placeholder",
1352
1329
  innerHTML: options.placeholder,
1353
1330
  });
1354
1331
  this.options = options;
@@ -1469,7 +1446,7 @@
1469
1446
  // is guaranteed to be an HTMLElement in this context.
1470
1447
  this.node = Libs.nodeCreator({
1471
1448
  node: "div",
1472
- classList: "selective-ui-directive",
1449
+ classList: "seui-directive",
1473
1450
  role: "button",
1474
1451
  ariaLabel: "Toggle dropdown",
1475
1452
  });
@@ -1608,9 +1585,9 @@
1608
1585
  * Initializes DOM and binds event handlers.
1609
1586
  *
1610
1587
  * DOM structure (conceptually):
1611
- * - Root: `div.selective-ui-option-handle.hide`
1612
- * - Child: `a.selective-ui-option-handle-item` ("Select all")
1613
- * - Child: `a.selective-ui-option-handle-item` ("Deselect all")
1588
+ * - Root: `div.seui-option-handle.hide`
1589
+ * - Child: `a.seui-option-handle-item` ("Select all")
1590
+ * - Child: `a.seui-option-handle-item` ("Deselect all")
1614
1591
  *
1615
1592
  * Click handlers:
1616
1593
  * - "Select all" → dispatches {@link actionOnSelectAll} via {@link iEvents.callFunctions}
@@ -1626,12 +1603,12 @@
1626
1603
  initialize(options) {
1627
1604
  this.nodeMounted = Libs.mountNode({
1628
1605
  OptionHandle: {
1629
- tag: { node: "div", classList: ["selective-ui-option-handle", "hide"] },
1606
+ tag: { node: "div", classList: ["seui-option-handle", "hide"] },
1630
1607
  child: {
1631
1608
  SelectAll: {
1632
1609
  tag: {
1633
1610
  node: "a",
1634
- classList: "selective-ui-option-handle-item",
1611
+ classList: "seui-option-handle-item",
1635
1612
  textContent: options.textSelectAll,
1636
1613
  onclick: () => {
1637
1614
  iEvents.callFunctions(this.actionOnSelectAll);
@@ -1641,7 +1618,7 @@
1641
1618
  DeSelectAll: {
1642
1619
  tag: {
1643
1620
  node: "a",
1644
- classList: "selective-ui-option-handle-item",
1621
+ classList: "seui-option-handle-item",
1645
1622
  textContent: options.textDeselectAll,
1646
1623
  onclick: () => {
1647
1624
  iEvents.callFunctions(this.actionOnDeSelectAll);
@@ -1839,7 +1816,7 @@
1839
1816
  *
1840
1817
  * Side effects:
1841
1818
  * - Creates the root `div` node with `role="status"` and `aria-live="polite"`.
1842
- * - Applies base CSS classes: `"selective-ui-empty-state"` and `"hide"`.
1819
+ * - Applies base CSS classes: `"seui-empty-state"` and `"hide"`.
1843
1820
  * - Stores the options reference and calls {@link Lifecycle.init}.
1844
1821
  *
1845
1822
  * @param {SelectiveOptions} options - Configuration object containing empty state messages.
@@ -1849,7 +1826,7 @@
1849
1826
  this.options = options;
1850
1827
  this.node = Libs.nodeCreator({
1851
1828
  node: "div",
1852
- classList: ["selective-ui-empty-state", "hide"],
1829
+ classList: ["seui-empty-state", "hide"],
1853
1830
  role: "status",
1854
1831
  ariaLive: "polite",
1855
1832
  });
@@ -1982,7 +1959,7 @@
1982
1959
  * Initializes internal resources for this component.
1983
1960
  *
1984
1961
  * Side effects:
1985
- * - Creates the root `div` node with base CSS classes: `"selective-ui-loading-state"` and `"hide"`.
1962
+ * - Creates the root `div` node with base CSS classes: `"seui-loading-state"` and `"hide"`.
1986
1963
  * - Sets initial text to `options.textLoading`.
1987
1964
  * - Applies `role="status"` and `aria-live="polite"`.
1988
1965
  * - Stores the options reference and calls {@link Lifecycle.init}.
@@ -1994,7 +1971,7 @@
1994
1971
  this.options = options;
1995
1972
  this.node = Libs.nodeCreator({
1996
1973
  node: "div",
1997
- classList: ["selective-ui-loading-state", "hide"],
1974
+ classList: ["seui-loading-state", "hide"],
1998
1975
  textContent: options.textLoading,
1999
1976
  role: "status",
2000
1977
  ariaLive: "polite",
@@ -2255,7 +2232,7 @@
2255
2232
  * Not idempotent. Call {@link disconnect} before calling `connect()` again to avoid duplicates.
2256
2233
  */
2257
2234
  connect(element) {
2258
- if (!(element instanceof Element)) {
2235
+ if (!(element instanceof HTMLElement)) {
2259
2236
  throw new Error("Invalid element");
2260
2237
  }
2261
2238
  this.element = element;
@@ -2395,7 +2372,7 @@
2395
2372
  PopupContainer: {
2396
2373
  tag: {
2397
2374
  node: "div",
2398
- classList: "selective-ui-popup",
2375
+ classList: "seui-popup",
2399
2376
  style: { maxHeight: options.panelHeight },
2400
2377
  },
2401
2378
  child: {
@@ -2404,7 +2381,7 @@
2404
2381
  tag: {
2405
2382
  id: options.SEID_LIST,
2406
2383
  node: "div",
2407
- classList: "selective-ui-options-container",
2384
+ classList: "seui-options-container",
2408
2385
  role: "listbox",
2409
2386
  },
2410
2387
  },
@@ -2688,10 +2665,8 @@
2688
2665
  if (this.is(LifecycleState.DESTROYED)) {
2689
2666
  return;
2690
2667
  }
2691
- if (this.hideLoadHandle) {
2692
- clearTimeout(this.hideLoadHandle);
2693
- this.hideLoadHandle = null;
2694
- }
2668
+ clearTimeout(this.hideLoadHandle);
2669
+ this.hideLoadHandle = null;
2695
2670
  if (this.node && this.scrollListener) {
2696
2671
  this.node.removeEventListener("scroll", this.scrollListener);
2697
2672
  this.scrollListener = null;
@@ -2699,19 +2674,14 @@
2699
2674
  this.emptyState.destroy();
2700
2675
  this.loadingState.destroy();
2701
2676
  this.optionHandle.destroy();
2702
- try {
2703
- this.resizeObser?.disconnect();
2704
- }
2705
- catch (_) { }
2706
- this.resizeObser = null;
2707
- try {
2708
- this.effSvc?.setElement?.(null);
2709
- }
2710
- catch (_) { }
2711
- this.effSvc = null;
2677
+ this.resizeObser?.disconnect?.();
2678
+ this.effSvc?.setElement?.(null);
2679
+ this.modelManager?.skipEvent?.(false);
2680
+ this.recyclerView?.clear?.();
2681
+ this.node?.remove?.();
2712
2682
  if (this.node) {
2713
2683
  try {
2714
- const clone = this.node.cloneNode(true);
2684
+ const clone = Libs.nodeCloner(this.node);
2715
2685
  this.node.replaceWith(clone);
2716
2686
  clone.remove();
2717
2687
  }
@@ -2721,15 +2691,6 @@
2721
2691
  }
2722
2692
  this.node = null;
2723
2693
  this.optionsContainer = null;
2724
- try {
2725
- this.modelManager?.skipEvent?.(false);
2726
- this.recyclerView?.clear?.();
2727
- this.recyclerView = null;
2728
- this.optionAdapter = null;
2729
- // Original behavior kept intentionally.
2730
- this.node.remove();
2731
- }
2732
- catch (_) { }
2733
2694
  this.modelManager = null;
2734
2695
  this.optionHandle = null;
2735
2696
  this.emptyState = null;
@@ -2737,6 +2698,10 @@
2737
2698
  this.parent = null;
2738
2699
  this.options = null;
2739
2700
  this.isCreated = false;
2701
+ this.effSvc = null;
2702
+ this.resizeObser = null;
2703
+ this.recyclerView = null;
2704
+ this.optionAdapter = null;
2740
2705
  super.destroy();
2741
2706
  }
2742
2707
  /**
@@ -2970,8 +2935,8 @@
2970
2935
  * Initializes DOM, ARIA attributes, and interaction listeners.
2971
2936
  *
2972
2937
  * DOM structure (conceptually):
2973
- * - Root: `div.selective-ui-searchbox.hide`
2974
- * - Child: `input[type="search"].selective-ui-searchbox-input`
2938
+ * - Root: `div.seui-searchbox.hide`
2939
+ * - Child: `input[type="search"].seui-searchbox-input`
2975
2940
  *
2976
2941
  * Accessibility attributes set on the input:
2977
2942
  * - `role="searchbox"`: announces search field semantics
@@ -3001,14 +2966,14 @@
3001
2966
  initialize(options) {
3002
2967
  this.nodeMounted = Libs.mountNode({
3003
2968
  SearchBox: {
3004
- tag: { node: "div", classList: ["selective-ui-searchbox", "hide"] },
2969
+ tag: { node: "div", classList: ["seui-searchbox", "hide"] },
3005
2970
  child: {
3006
2971
  SearchInput: {
3007
2972
  tag: {
3008
2973
  id: Libs.randomString(),
3009
2974
  node: "input",
3010
2975
  type: "search",
3011
- classList: ["selective-ui-searchbox-input"],
2976
+ classList: ["seui-searchbox-input"],
3012
2977
  placeholder: options.placeholder,
3013
2978
  role: "searchbox",
3014
2979
  ariaControls: options.SEID_LIST,
@@ -4196,15 +4161,17 @@
4196
4161
  set selectedNonTrigger(value) {
4197
4162
  const input = this.view?.view?.tags?.OptionInput;
4198
4163
  const viewEl = this.view?.getView?.();
4199
- if (input)
4164
+ if (input) {
4200
4165
  input.checked = value;
4166
+ }
4201
4167
  if (viewEl && this.targetElement) {
4202
4168
  viewEl.classList.toggle("checked", !!value);
4203
4169
  viewEl.setAttribute("aria-selected", value ? "true" : "false");
4204
4170
  this.targetElement.toggleAttribute("selected", !!value);
4205
4171
  }
4206
- if (this.targetElement)
4172
+ if (this.targetElement) {
4207
4173
  this.targetElement.selected = value;
4174
+ }
4208
4175
  iEvents.callEvent([this, value], ...this.privOnInternalSelected);
4209
4176
  }
4210
4177
  /**
@@ -4328,8 +4295,12 @@
4328
4295
  }
4329
4296
  const imageTag = this.view.view.tags.OptionImage;
4330
4297
  if (imageTag && this.hasImage) {
4331
- imageTag.src = this.imageSrc;
4332
- imageTag.alt = this.text;
4298
+ if (imageTag.src != this.imageSrc) {
4299
+ imageTag.src = this.imageSrc;
4300
+ }
4301
+ if (imageTag.alt != this.text) {
4302
+ imageTag.alt = this.text;
4303
+ }
4333
4304
  }
4334
4305
  if (this.targetElement)
4335
4306
  this.selectedNonTrigger = this.targetElement.selected;
@@ -4461,9 +4432,8 @@
4461
4432
  this.privModelList.push(currentGroup);
4462
4433
  }
4463
4434
  else if (data.tagName === "OPTION") {
4464
- const optionEl = data;
4465
- const optionModel = new OptionModel(this.options, optionEl);
4466
- const parentGroup = optionEl["__parentGroup"];
4435
+ const optionModel = new OptionModel(this.options, data);
4436
+ const parentGroup = data["__parentGroup"];
4467
4437
  if (parentGroup && currentGroup && parentGroup === currentGroup.targetElement) {
4468
4438
  currentGroup.addItem(optionModel);
4469
4439
  optionModel.group = currentGroup;
@@ -4874,7 +4844,7 @@
4874
4844
  * popup/layout logic to recompute geometry.
4875
4845
  *
4876
4846
  * ### DOM & a11y side effects
4877
- * - Creates a root `<div>` with classes `selective-ui-accessorybox hide`.
4847
+ * - Creates a root `<div>` with classes `seui-accessorybox hide`.
4878
4848
  * - Stops `mouseup` propagation on the root to avoid "outside click" behaviors.
4879
4849
  * - Each chip has:
4880
4850
  * - a `<span role="button">` with `aria-label`/`title` for screen readers and tooltips,
@@ -4954,7 +4924,7 @@
4954
4924
  * Guarded: runs only when state is `NEW`.
4955
4925
  *
4956
4926
  * Side effects:
4957
- * - Creates the root node with base classes (`selective-ui-accessorybox`, `hide`).
4927
+ * - Creates the root node with base classes (`seui-accessorybox`, `hide`).
4958
4928
  * - Stops `mouseup` propagation to avoid outside-click handlers reacting to chip interactions.
4959
4929
  *
4960
4930
  * @returns {void}
@@ -4967,7 +4937,7 @@
4967
4937
  AccessoryBox: {
4968
4938
  tag: {
4969
4939
  node: "div",
4970
- classList: ["selective-ui-accessorybox", "hide"],
4940
+ classList: ["seui-accessorybox", "hide"],
4971
4941
  onmouseup: (evt) => {
4972
4942
  // Prevent outside listeners from reacting to chip clicks
4973
4943
  evt.stopPropagation();
@@ -5034,7 +5004,7 @@
5034
5004
  /**
5035
5005
  * Assigns the {@link ModelManager} used to run selection pipelines and mutate selection state.
5036
5006
  *
5037
- * @param {ModelManager<MixedItem, MixedAdapter> | null} modelManager - Model manager controlling option state.
5007
+ * @param {ModelManager<MixedItem, MixedAdapter>} modelManager - Model manager controlling option state.
5038
5008
  * @returns {void}
5039
5009
  */
5040
5010
  setModelManager(modelManager) {
@@ -6521,7 +6491,7 @@
6521
6491
  *
6522
6492
  * Creation flow:
6523
6493
  * 1. Generates unique group ID (7-character random string).
6524
- * 2. Creates DOM structure via {@link Libs.mountView}:
6494
+ * 2. Creates DOM structure via {@link Libs.mountNode}:
6525
6495
  * - Root: `<div role="group" aria-labelledby="seui-{id}-header">`
6526
6496
  * - Header: `<div role="presentation" id="seui-{id}-header">`
6527
6497
  * - Items: `<div role="group">` (nested group for child items)
@@ -6543,11 +6513,11 @@
6543
6513
  */
6544
6514
  mount() {
6545
6515
  const group_id = Libs.randomString(7);
6546
- this.view = Libs.mountView({
6516
+ this.view = Libs.mountNode({
6547
6517
  GroupView: {
6548
6518
  tag: {
6549
6519
  node: "div",
6550
- classList: ["selective-ui-group"],
6520
+ classList: ["seui-group"],
6551
6521
  role: "group",
6552
6522
  ariaLabelledby: `seui-${group_id}-header`,
6553
6523
  id: `seui-${group_id}-group`,
@@ -6556,7 +6526,7 @@
6556
6526
  GroupHeader: {
6557
6527
  tag: {
6558
6528
  node: "div",
6559
- classList: ["selective-ui-group-header"],
6529
+ classList: ["seui-group-header"],
6560
6530
  role: "presentation",
6561
6531
  id: `seui-${group_id}-header`,
6562
6532
  },
@@ -6564,7 +6534,7 @@
6564
6534
  GroupItems: {
6565
6535
  tag: {
6566
6536
  node: "div",
6567
- classList: ["selective-ui-group-items"],
6537
+ classList: ["seui-group-items"],
6568
6538
  role: "group",
6569
6539
  },
6570
6540
  },
@@ -7009,7 +6979,7 @@
7009
6979
  * - **OptionImage** (conditional): `<img>` with inline styles (width/height/borderRadius).
7010
6980
  * - **OptionLabel**: `<label htmlFor="{inputID}">` with alignment classes.
7011
6981
  * - **LabelContent**: `<div>` (content placeholder).
7012
- * 4. Creates DOM via {@link Libs.mountView}.
6982
+ * 4. Creates DOM via {@link Libs.mountNode}.
7013
6983
  * 5. Appends root to {@link parent}.
7014
6984
  * 6. Sets {@link isRendered} to `true` (enables reactive updates).
7015
6985
  * 7. Transitions `INITIALIZED → MOUNTED` via `super.mount()`.
@@ -7027,7 +6997,7 @@
7027
6997
  * @override
7028
6998
  */
7029
6999
  mount() {
7030
- const viewClass = ["selective-ui-option-view"];
7000
+ const viewClass = ["seui-option-view"];
7031
7001
  const opt_id = Libs.randomString(7);
7032
7002
  const inputID = `option_${opt_id}`;
7033
7003
  if (this.config.isMultiple)
@@ -7071,7 +7041,7 @@
7071
7041
  },
7072
7042
  },
7073
7043
  };
7074
- this.view = Libs.mountView({
7044
+ this.view = Libs.mountNode({
7075
7045
  OptionView: {
7076
7046
  tag: {
7077
7047
  node: "div",
@@ -8119,15 +8089,15 @@
8119
8089
  return;
8120
8090
  this.viewElement.replaceChildren();
8121
8091
  const nodeMounted = Libs.mountNode({
8122
- PadTop: { tag: { node: "div", classList: "selective-ui-virtual-pad-top" } },
8123
- ItemsHost: { tag: { node: "div", classList: "selective-ui-virtual-items" } },
8124
- PadBottom: { tag: { node: "div", classList: "selective-ui-virtual-pad-bottom" } },
8092
+ PadTop: { tag: { node: "div", classList: "seui-virtual-pad-top" } },
8093
+ ItemsHost: { tag: { node: "div", classList: "seui-virtual-items" } },
8094
+ PadBottom: { tag: { node: "div", classList: "seui-virtual-pad-bottom" } },
8125
8095
  }, this.viewElement);
8126
8096
  this.PadTop = nodeMounted.PadTop;
8127
8097
  this.ItemsHost = nodeMounted.ItemsHost;
8128
8098
  this.PadBottom = nodeMounted.PadBottom;
8129
8099
  this.scrollEl = this.opts.scrollEl
8130
- ?? this.viewElement.closest(".selective-ui-popup")
8100
+ ?? this.viewElement.closest(".seui-popup")
8131
8101
  ?? this.viewElement.parentElement;
8132
8102
  if (!this.scrollEl)
8133
8103
  throw new Error("VirtualRecyclerView: scrollEl not found");
@@ -8456,7 +8426,7 @@
8456
8426
  const now = performance.now();
8457
8427
  if (now - this.stickyCacheTick < 16)
8458
8428
  return this.stickyCacheVal;
8459
- const sticky = this.scrollEl.querySelector(".selective-ui-option-handle:not(.hide)");
8429
+ const sticky = this.scrollEl.querySelector(".seui-option-handle:not(.hide)");
8460
8430
  this.stickyCacheVal = sticky?.offsetHeight ?? 0;
8461
8431
  this.stickyCacheTick = now;
8462
8432
  return this.stickyCacheVal;
@@ -9019,12 +8989,29 @@
9019
8989
  * @internal
9020
8990
  */
9021
8991
  this.isBeforeSearch = false;
8992
+ /**
8993
+ * Tracks whether {@link deInit} has already run.
8994
+ *
8995
+ * This guards teardown work (including plugin lifecycle hooks) from running more than once
8996
+ * when {@link deInit} is called separately before {@link destroy}.
8997
+ *
8998
+ * @internal
8999
+ */
9000
+ this.hasDeInitialized = false;
9022
9001
  /**
9023
9002
  * Selective context (global helper / registry).
9024
9003
  *
9025
9004
  * Used to locate the instance wrapper via `Selective.find(...)` and to close other open instances.
9026
9005
  */
9027
9006
  this.Selective = null;
9007
+ /**
9008
+ * Registered plugins for this SelectBox instance.
9009
+ */
9010
+ this.plugins = [];
9011
+ /**
9012
+ * Cached plugin context for this SelectBox instance.
9013
+ */
9014
+ this.pluginContext = null;
9028
9015
  if (select && Selective)
9029
9016
  this.initialize(select, Selective);
9030
9017
  }
@@ -9141,12 +9128,12 @@
9141
9128
  placeholder.node.id = String(options.SEID_HOLDER ?? "");
9142
9129
  const container = Libs.mountNode({
9143
9130
  Container: {
9144
- tag: { node: "div", classList: "selective-ui-MAIN" },
9131
+ tag: { node: "div", classList: "seui-MAIN" },
9145
9132
  child: {
9146
9133
  ViewPanel: {
9147
9134
  tag: {
9148
9135
  node: "div",
9149
- classList: "selective-ui-view",
9136
+ classList: "seui-view",
9150
9137
  tabIndex: 0,
9151
9138
  onkeydown: (e) => {
9152
9139
  if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
@@ -9206,6 +9193,20 @@
9206
9193
  accessoryBox.setModelManager(optionModelManager);
9207
9194
  this.setupEventHandlers(select, container, options, searchController, searchbox);
9208
9195
  this.setupObservers(selectObserver, datasetObserver, select, optionModelManager);
9196
+ this.plugins = this.Selective?.getPlugins?.() ?? [];
9197
+ if (this.plugins.length) {
9198
+ const resources = optionModelManager.getResources();
9199
+ const pluginContext = {
9200
+ selectBox: this,
9201
+ options,
9202
+ adapter: resources.adapter,
9203
+ recycler: resources.recyclerView,
9204
+ viewTags: container.tags,
9205
+ actions: this.getAction(),
9206
+ };
9207
+ this.pluginContext = pluginContext;
9208
+ this.runPluginHook("init", (plugin) => plugin.init?.(pluginContext));
9209
+ }
9209
9210
  // Initial states
9210
9211
  this.isDisabled = Libs.string2Boolean(options.disabled);
9211
9212
  this.isReadOnly = Libs.string2Boolean(options.readonly);
@@ -9403,12 +9404,21 @@
9403
9404
  * preventing memory leaks and unintended background updates.
9404
9405
  */
9405
9406
  deInit() {
9407
+ if (this.hasDeInitialized) {
9408
+ return;
9409
+ }
9406
9410
  const c = this.container ?? {};
9407
9411
  const { selectObserver, datasetObserver } = c;
9412
+ if (this.plugins.length) {
9413
+ this.runPluginHook("destroy", (plugin) => plugin.destroy?.());
9414
+ }
9415
+ this.plugins = [];
9416
+ this.pluginContext = null;
9408
9417
  if (selectObserver?.disconnect)
9409
9418
  selectObserver.disconnect();
9410
9419
  if (datasetObserver?.disconnect)
9411
9420
  datasetObserver.disconnect();
9421
+ this.hasDeInitialized = true;
9412
9422
  }
9413
9423
  /**
9414
9424
  * Lifecycle: `destroy` (teardown stage).
@@ -9722,6 +9732,9 @@
9722
9732
  if (bindedOptions.multiple)
9723
9733
  ViewPanel.setAttribute("aria-multiselectable", "true");
9724
9734
  iEvents.callEvent([getInstance()], ...bindedOptions.on.show);
9735
+ if (superThis.pluginContext) {
9736
+ superThis.runPluginHook("onOpen", (plugin) => plugin.onOpen?.(superThis.pluginContext));
9737
+ }
9725
9738
  return;
9726
9739
  },
9727
9740
  close() {
@@ -9738,6 +9751,9 @@
9738
9751
  container.searchbox.hide();
9739
9752
  container.tags.ViewPanel.setAttribute("aria-expanded", "false");
9740
9753
  iEvents.callEvent([getInstance()], ...bindedOptions.on.close);
9754
+ if (superThis.pluginContext) {
9755
+ superThis.runPluginHook("onClose", (plugin) => plugin.onClose?.(superThis.pluginContext));
9756
+ }
9741
9757
  return;
9742
9758
  },
9743
9759
  toggle() {
@@ -9776,6 +9792,10 @@
9776
9792
  if (superThis.is(LifecycleState.MOUNTED)) {
9777
9793
  superThis.update();
9778
9794
  }
9795
+ if (superThis.pluginContext && superThis.optionModelManager) {
9796
+ const resources = superThis.optionModelManager.getResources();
9797
+ superThis.runPluginHook("onChange", (plugin) => plugin.onChange?.(this.value, resources.modelList, resources.adapter, superThis.pluginContext));
9798
+ }
9779
9799
  },
9780
9800
  refreshMask() {
9781
9801
  let mask = bindedOptions.placeholder;
@@ -9908,6 +9928,27 @@
9908
9928
  }
9909
9929
  return flatOptions;
9910
9930
  }
9931
+ /**
9932
+ * Safely runs a hook across all registered plugins.
9933
+ *
9934
+ * Any plugin failure is isolated to prevent breaking the current flow.
9935
+ *
9936
+ * @param hook - Hook name for logging context.
9937
+ * @param runner - Hook invocation handler.
9938
+ * @internal
9939
+ */
9940
+ runPluginHook(hook, runner) {
9941
+ if (!this.plugins.length)
9942
+ return;
9943
+ this.plugins.forEach((plugin) => {
9944
+ try {
9945
+ runner(plugin);
9946
+ }
9947
+ catch (error) {
9948
+ console.error(`Plugin "${plugin.id}" ${hook} error:`, error);
9949
+ }
9950
+ });
9951
+ }
9911
9952
  }
9912
9953
 
9913
9954
  /**
@@ -10134,6 +10175,15 @@
10134
10175
  * @private
10135
10176
  */
10136
10177
  this.bindedQueries = new Map();
10178
+ /**
10179
+ * Registry of Selective plugins keyed by plugin ID.
10180
+ *
10181
+ * - Managed via {@link registerPlugin}, {@link unregisterPlugin}, and {@link getPlugin}.
10182
+ * - Cleared during {@link destroyAll} after invoking plugin teardown hooks.
10183
+ *
10184
+ * @private
10185
+ */
10186
+ this.plugins = new Map();
10137
10187
  this.init();
10138
10188
  }
10139
10189
  /**
@@ -10142,6 +10192,7 @@
10142
10192
  * Behavior:
10143
10193
  * - No-op if not in {@link LifecycleState.NEW} (idempotent guard).
10144
10194
  * - Initializes {@link bindedQueries} as empty `Map`.
10195
+ * - Initializes {@link plugins} as empty `Map`.
10145
10196
  * - Transitions `NEW → INITIALIZED` via `super.init()`.
10146
10197
  *
10147
10198
  * Notes:
@@ -10157,6 +10208,7 @@
10157
10208
  return;
10158
10209
  // Initialize core properties
10159
10210
  this.bindedQueries = new Map();
10211
+ this.plugins = new Map();
10160
10212
  super.init();
10161
10213
  }
10162
10214
  /**
@@ -10324,6 +10376,14 @@
10324
10376
  }
10325
10377
  return response;
10326
10378
  }
10379
+ /**
10380
+ * Returns all registered Selective plugins.
10381
+ *
10382
+ * @returns The list of plugins in registration order.
10383
+ */
10384
+ getPlugins() {
10385
+ return Array.from(this.plugins.values());
10386
+ }
10327
10387
  /**
10328
10388
  * Activates auto-binding for newly added `<select>` elements.
10329
10389
  *
@@ -10395,14 +10455,51 @@
10395
10455
  this.update();
10396
10456
  }
10397
10457
  }
10458
+ /**
10459
+ * Registers a plugin for Selective lifecycle integration.
10460
+ *
10461
+ * @public
10462
+ * @param {SelectivePlugin} plugin - Plugin instance to register.
10463
+ * @returns {void}
10464
+ */
10465
+ registerPlugin(plugin) {
10466
+ if (!plugin?.id)
10467
+ return;
10468
+ this.plugins.set(plugin.id, plugin);
10469
+ }
10470
+ /**
10471
+ * Unregisters a plugin by ID.
10472
+ *
10473
+ * @public
10474
+ * @param {string} id - Plugin ID to remove.
10475
+ * @returns {void}
10476
+ */
10477
+ unregisterPlugin(id) {
10478
+ if (!id)
10479
+ return;
10480
+ this.plugins.delete(id);
10481
+ }
10482
+ /**
10483
+ * Retrieves a plugin by ID.
10484
+ *
10485
+ * @public
10486
+ * @param {string} id - Plugin ID to retrieve.
10487
+ * @returns {SelectivePlugin | undefined} Plugin instance if found.
10488
+ */
10489
+ getPlugin(id) {
10490
+ if (!id)
10491
+ return undefined;
10492
+ return this.plugins.get(id);
10493
+ }
10398
10494
  /**
10399
10495
  * Destroys all bound Selective instances and releases global resources.
10400
10496
  *
10401
10497
  * Teardown flow:
10402
10498
  * 1. Iterates all registered queries and calls {@link destroyByQuery}.
10403
10499
  * 2. Clears {@link bindedQueries} and {@link Libs.getBindedCommand}.
10404
- * 3. Disconnects {@link EAObserver} (stops auto-binding).
10405
- * 4. Transitions to {@link LifecycleState.DESTROYED} via `super.destroy()`.
10500
+ * 3. Invokes plugin teardown hooks and clears {@link plugins}.
10501
+ * 4. Disconnects {@link EAObserver} (stops auto-binding).
10502
+ * 5. Transitions to {@link LifecycleState.DESTROYED} via `super.destroy()`.
10406
10503
  *
10407
10504
  * Idempotency:
10408
10505
  * - No-op if already {@link LifecycleState.DESTROYED}.
@@ -10417,7 +10514,13 @@
10417
10514
  bindedCommands.forEach((query) => this.destroyByQuery(query));
10418
10515
  this.bindedQueries.clear();
10419
10516
  Libs.getBindedCommand().length = 0;
10517
+ this.plugins.forEach((plugin) => {
10518
+ plugin.destroy?.();
10519
+ plugin.onDestroy?.();
10520
+ });
10521
+ this.plugins.clear();
10420
10522
  this.EAObserver?.disconnect();
10523
+ this.plugins.clear();
10421
10524
  // Call parent lifecycle destroy
10422
10525
  super.destroy();
10423
10526
  }
@@ -10484,12 +10587,8 @@
10484
10587
  const wasObserving = !!this.EAObserver;
10485
10588
  if (wasObserving)
10486
10589
  this.EAObserver?.disconnect();
10487
- try {
10488
- bindMap.self?.deInit?.();
10489
- }
10490
- catch (_) { }
10491
- const wrapper = bindMap.container?.element ??
10492
- selectElement.parentElement;
10590
+ bindMap.self?.deInit?.();
10591
+ const wrapper = (bindMap.container?.element) ?? selectElement.parentElement;
10493
10592
  selectElement.style.display = "";
10494
10593
  selectElement.style.visibility = "";
10495
10594
  selectElement.disabled = false;
@@ -10706,13 +10805,15 @@
10706
10805
  if (typeof globalThis.GLOBAL_SEUI == "undefined") {
10707
10806
  const SECLASS = new Selective();
10708
10807
  globalThis.GLOBAL_SEUI = {
10709
- version: "1.2.5",
10808
+ version: "1.2.6",
10710
10809
  name: "SelectiveUI",
10711
10810
  bind: SECLASS.bind.bind(SECLASS),
10712
10811
  find: SECLASS.find.bind(SECLASS),
10713
10812
  destroy: SECLASS.destroy.bind(SECLASS),
10714
10813
  effector: Effector.bind(Effector),
10715
- rebind: SECLASS.rebind.bind(SECLASS)
10814
+ rebind: SECLASS.rebind.bind(SECLASS),
10815
+ registerPlugin: SECLASS.registerPlugin.bind(SECLASS),
10816
+ unregisterPlugin: SECLASS.unregisterPlugin.bind(SECLASS)
10716
10817
  };
10717
10818
  let domInitialized = false;
10718
10819
  function init() {
@@ -10737,7 +10838,7 @@
10737
10838
  init();
10738
10839
  }
10739
10840
  }
10740
- console.log(`[${"SelectiveUI"}] v${"1.2.5"} loaded successfully`);
10841
+ console.log(`[${"SelectiveUI"}] v${"1.2.6"} loaded successfully`);
10741
10842
  }
10742
10843
  else {
10743
10844
  console.warn(`[${globalThis.GLOBAL_SEUI.name}] Already loaded (v${globalThis.GLOBAL_SEUI.version}). ` +
@@ -10827,6 +10928,22 @@
10827
10928
  function effector(element) {
10828
10929
  return globalThis.GLOBAL_SEUI.effector(element);
10829
10930
  }
10931
+ /**
10932
+ * Register a Selective plugin implementation.
10933
+ *
10934
+ * @param plugin - Plugin instance to register.
10935
+ */
10936
+ function registerPlugin(plugin) {
10937
+ globalThis.GLOBAL_SEUI.registerPlugin(plugin);
10938
+ }
10939
+ /**
10940
+ * Unregister a Selective plugin implementation by id.
10941
+ *
10942
+ * @param id - Plugin id to remove.
10943
+ */
10944
+ function unregisterPlugin(id) {
10945
+ globalThis.GLOBAL_SEUI.unregisterPlugin(id);
10946
+ }
10830
10947
 
10831
10948
  exports.bind = bind;
10832
10949
  exports.destroy = destroy;
@@ -10834,6 +10951,8 @@
10834
10951
  exports.find = find;
10835
10952
  exports.name = name;
10836
10953
  exports.rebind = rebind;
10954
+ exports.registerPlugin = registerPlugin;
10955
+ exports.unregisterPlugin = unregisterPlugin;
10837
10956
  exports.version = version;
10838
10957
 
10839
10958
  }));