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
  /**
3
3
  * @class
4
4
  */
@@ -207,7 +207,7 @@ class CallbackScheduler {
207
207
  * @public
208
208
  * @param {TimerKey} key - Key whose callbacks will be scheduled.
209
209
  * @param {...any[]} params - Parameters passed as a shared payload to all callbacks.
210
- * @returns {Promise<void> | void} Promise resolving when all callbacks finish execution.
210
+ * @returns {Promise<void>} Promise resolving when all callbacks finish execution.
211
211
  */
212
212
  run(key, ...params) {
213
213
  const executes = this.executeStored.get(key);
@@ -326,11 +326,11 @@ class Libs {
326
326
  return result;
327
327
  }
328
328
  /**
329
- * Resolves a selector, NodeList, or single Element into an array of elements.
329
+ * Resolves a selector, NodeList, or single HTMLElement into an array of elements.
330
330
  * Returns an empty array if nothing is found.
331
331
  *
332
- * @param {string|NodeListOf<Element>|Element|HTMLElement|ArrayLike<Element>|null} queryCommon - CSS selector, NodeList, or Element.
333
- * @returns {Element[]} - Array of matched elements (empty if none).
332
+ * @param {string|NodeListOf<HTMLElement>|HTMLElement|HTMLElement|ArrayLike<HTMLElement>|null} queryCommon - CSS selector, NodeList, or HTMLElement.
333
+ * @returns {HTMLElement[]} - Array of matched elements (empty if none).
334
334
  */
335
335
  static getElements(queryCommon) {
336
336
  if (!queryCommon)
@@ -339,7 +339,7 @@ class Libs {
339
339
  const nodeList = document.querySelectorAll(queryCommon);
340
340
  return Array.from(nodeList);
341
341
  }
342
- if (queryCommon instanceof Element) {
342
+ if (queryCommon instanceof HTMLElement) {
343
343
  return [queryCommon];
344
344
  }
345
345
  // NodeList or array-like
@@ -349,27 +349,27 @@ class Libs {
349
349
  return [];
350
350
  }
351
351
  /**
352
- * Creates a new Element based on a NodeSpec and applies attributes, classes, styles, dataset, and events.
352
+ * Creates a new HTMLElement based on a NodeSpec and applies attributes, classes, styles, dataset, and events.
353
353
  *
354
354
  * @param {NodeSpec} data - Specification describing the element to create.
355
- * @returns {Element} - The created element.
355
+ * @returns {HTMLElement} - The created element.
356
356
  */
357
357
  static nodeCreator(data = {}) {
358
358
  const nodeName = (data.node ?? "div");
359
359
  return this.nodeCloner(document.createElement(nodeName), data, true);
360
360
  }
361
361
  /**
362
- * Clones an element (or converts a Node to Element) and applies NodeSpec options.
362
+ * Clones an element (or converts a Node to HTMLElement) and applies NodeSpec options.
363
363
  * When systemNodeCreate=true, uses the provided node as-is.
364
364
  *
365
- * @param {Element} node - The element to clone or use.
365
+ * @param {HTMLElement} node - The element to clone or use.
366
366
  * @param {NodeSpec|null} _nodeOption - Options (classList, style, dataset, event, other props).
367
367
  * @param {boolean} systemNodeCreate - If true, do not clone; use original node.
368
- * @returns {Element} - The processed element.
368
+ * @returns {HTMLElement} - The processed element.
369
369
  */
370
370
  static nodeCloner(node = document.documentElement, _nodeOption = null, systemNodeCreate = false) {
371
371
  const nodeOption = { ...(_nodeOption ?? {}) };
372
- const element_creation = systemNodeCreate ? node : this.nodeToElement(node.cloneNode(true));
372
+ const element_creation = systemNodeCreate ? node : node.cloneNode(true);
373
373
  const classList = nodeOption.classList;
374
374
  if (typeof classList === "string") {
375
375
  element_creation.classList.add(classList);
@@ -429,40 +429,17 @@ class Libs {
429
429
  });
430
430
  return element_creation;
431
431
  }
432
- /**
433
- * Ensures the given Node is an Element; throws if not.
434
- *
435
- * @param {Node} node - The node to validate.
436
- * @returns {Element} - The element cast.
437
- * @throws {TypeError} - If node is not an Element.
438
- */
439
- static nodeToElement(node) {
440
- if (node instanceof Element)
441
- return node;
442
- throw new TypeError("Node is not an Element");
443
- }
444
- /**
445
- * Mounts a view from a plain object specification and returns a typed result
446
- * containing the root element and a tag map.
447
- *
448
- * @template TTags
449
- * @param {object} rawObj - The specification describing elements and tags.
450
- * @returns {MountViewResult<TTags>} - The mounted view and its tag references.
451
- */
452
- static mountView(rawObj) {
453
- return this.mountNode(rawObj);
454
- }
455
432
  /**
456
433
  * Recursively builds DOM nodes from a specification object, appends/prepends them
457
434
  * to an optional parent, and returns either a tag map or a full MountViewResult.
458
435
  *
459
436
  * @template TTags
460
437
  * @param {Object<string, any>} rawObj - Node spec (keys -> { tag, child }).
461
- * @param {Element|null} [parentE=null] - Parent to attach into; if null, returns root.
438
+ * @param {HTMLElement|null} [parentE=null] - Parent to attach into; if null, returns root.
462
439
  * @param {boolean} [isPrepend=false] - If true, prepend; otherwise append.
463
440
  * @param {boolean} [isRecusive=false] - Internal flag for recursion control.
464
441
  * @param {TTags|Object} [recursiveTemp={}] - Accumulator for tag references.
465
- * @returns {MountViewResult<TTags>|TTags} - Tag map or the final mount result.
442
+ * @returns {TTags} - Tag map or the final mount result.
466
443
  */
467
444
  static mountNode(rawObj, parentE = null, isPrepend = false, isRecusive = false, recursiveTemp = {}) {
468
445
  let view = null;
@@ -581,7 +558,7 @@ class Libs {
581
558
  /**
582
559
  * Removes a binder map entry for the given element from the global storage.
583
560
  *
584
- * @param {HTMLElement} element - Element key to remove from the binder map.
561
+ * @param {HTMLElement} element - HTMLElement key to remove from the binder map.
585
562
  * @returns {boolean} - True if an entry existed and was removed.
586
563
  */
587
564
  static removeBinderMap(element) {
@@ -590,8 +567,8 @@ class Libs {
590
567
  /**
591
568
  * Retrieves the binder map entry associated with the given element.
592
569
  *
593
- * @param {HTMLElement} item - Element key whose binder map is requested.
594
- * @returns {BinderMap | null} - The stored binder map value or undefined if absent.
570
+ * @param {HTMLElement} item - HTMLElement key whose binder map is requested.
571
+ * @returns {BinderMap | any} - The stored binder map value or undefined if absent.
595
572
  */
596
573
  static getBinderMap(item) {
597
574
  return this.iStorage.bindedMap.get(item);
@@ -599,7 +576,7 @@ class Libs {
599
576
  /**
600
577
  * Sets or updates the binder map entry for a given element.
601
578
  *
602
- * @param {HTMLElement} item - Element key to associate with the binder map.
579
+ * @param {HTMLElement} item - HTMLElement key to associate with the binder map.
603
580
  * @param {BinderMap} bindMap - Value to store in the binder map.
604
581
  */
605
582
  static setBinderMap(item, bindMap) {
@@ -608,7 +585,7 @@ class Libs {
608
585
  /**
609
586
  * Removes an unbinder map entry for the given element from the global storage.
610
587
  *
611
- * @param {HTMLElement} element - Element key to remove from the unbinder map.
588
+ * @param {HTMLElement} element - HTMLElement key to remove from the unbinder map.
612
589
  * @returns {boolean} - True if an entry existed and was removed.
613
590
  */
614
591
  static removeUnbinderMap(element) {
@@ -617,7 +594,7 @@ class Libs {
617
594
  /**
618
595
  * Retrieves the unbinder map entry associated with the given element.
619
596
  *
620
- * @param {HTMLElement} item - Element key whose unbinder map is requested.
597
+ * @param {HTMLElement} item - HTMLElement key whose unbinder map is requested.
621
598
  * @returns {unknown} - The stored unbinder map value or undefined if absent.
622
599
  */
623
600
  static getUnbinderMap(item) {
@@ -626,7 +603,7 @@ class Libs {
626
603
  /**
627
604
  * Sets or updates the unbinder map entry for a given element.
628
605
  *
629
- * @param {HTMLElement} item - Element key to associate with the unbinder map.
606
+ * @param {HTMLElement} item - HTMLElement key to associate with the unbinder map.
630
607
  * @param {BinderMap} bindMap - Value to store in the unbinder map.
631
608
  */
632
609
  static setUnbinderMap(item, bindMap) {
@@ -1274,7 +1251,7 @@ class Lifecycle {
1274
1251
  * getter/setter APIs for the placeholder content.
1275
1252
  *
1276
1253
  * ### Responsibility
1277
- * - Create and own the placeholder DOM element (`.selective-ui-placeholder`).
1254
+ * - Create and own the placeholder DOM element (`.seui-placeholder`).
1278
1255
  * - Render placeholder content from {@link SelectiveOptions.placeholder}.
1279
1256
  * - Support runtime updates via {@link set}, optionally persisting into options.
1280
1257
  * - Participate in the shared {@link Lifecycle} FSM.
@@ -1332,7 +1309,7 @@ class PlaceHolder extends Lifecycle {
1332
1309
  * Builds the placeholder DOM node and starts the lifecycle.
1333
1310
  *
1334
1311
  * Side effects:
1335
- * - Creates a `div.selective-ui-placeholder` node via {@link Libs.nodeCreator}.
1312
+ * - Creates a `div.seui-placeholder` node via {@link Libs.nodeCreator}.
1336
1313
  * - Writes initial placeholder content into `innerHTML`.
1337
1314
  * - Transitions the lifecycle by calling `init()`.
1338
1315
  *
@@ -1342,7 +1319,7 @@ class PlaceHolder extends Lifecycle {
1342
1319
  initialize(options) {
1343
1320
  this.node = Libs.nodeCreator({
1344
1321
  node: "div",
1345
- classList: "selective-ui-placeholder",
1322
+ classList: "seui-placeholder",
1346
1323
  innerHTML: options.placeholder,
1347
1324
  });
1348
1325
  this.options = options;
@@ -1463,7 +1440,7 @@ class Directive extends Lifecycle {
1463
1440
  // is guaranteed to be an HTMLElement in this context.
1464
1441
  this.node = Libs.nodeCreator({
1465
1442
  node: "div",
1466
- classList: "selective-ui-directive",
1443
+ classList: "seui-directive",
1467
1444
  role: "button",
1468
1445
  ariaLabel: "Toggle dropdown",
1469
1446
  });
@@ -1602,9 +1579,9 @@ class OptionHandle extends Lifecycle {
1602
1579
  * Initializes DOM and binds event handlers.
1603
1580
  *
1604
1581
  * DOM structure (conceptually):
1605
- * - Root: `div.selective-ui-option-handle.hide`
1606
- * - Child: `a.selective-ui-option-handle-item` ("Select all")
1607
- * - Child: `a.selective-ui-option-handle-item` ("Deselect all")
1582
+ * - Root: `div.seui-option-handle.hide`
1583
+ * - Child: `a.seui-option-handle-item` ("Select all")
1584
+ * - Child: `a.seui-option-handle-item` ("Deselect all")
1608
1585
  *
1609
1586
  * Click handlers:
1610
1587
  * - "Select all" → dispatches {@link actionOnSelectAll} via {@link iEvents.callFunctions}
@@ -1620,12 +1597,12 @@ class OptionHandle extends Lifecycle {
1620
1597
  initialize(options) {
1621
1598
  this.nodeMounted = Libs.mountNode({
1622
1599
  OptionHandle: {
1623
- tag: { node: "div", classList: ["selective-ui-option-handle", "hide"] },
1600
+ tag: { node: "div", classList: ["seui-option-handle", "hide"] },
1624
1601
  child: {
1625
1602
  SelectAll: {
1626
1603
  tag: {
1627
1604
  node: "a",
1628
- classList: "selective-ui-option-handle-item",
1605
+ classList: "seui-option-handle-item",
1629
1606
  textContent: options.textSelectAll,
1630
1607
  onclick: () => {
1631
1608
  iEvents.callFunctions(this.actionOnSelectAll);
@@ -1635,7 +1612,7 @@ class OptionHandle extends Lifecycle {
1635
1612
  DeSelectAll: {
1636
1613
  tag: {
1637
1614
  node: "a",
1638
- classList: "selective-ui-option-handle-item",
1615
+ classList: "seui-option-handle-item",
1639
1616
  textContent: options.textDeselectAll,
1640
1617
  onclick: () => {
1641
1618
  iEvents.callFunctions(this.actionOnDeSelectAll);
@@ -1833,7 +1810,7 @@ class EmptyState extends Lifecycle {
1833
1810
  *
1834
1811
  * Side effects:
1835
1812
  * - Creates the root `div` node with `role="status"` and `aria-live="polite"`.
1836
- * - Applies base CSS classes: `"selective-ui-empty-state"` and `"hide"`.
1813
+ * - Applies base CSS classes: `"seui-empty-state"` and `"hide"`.
1837
1814
  * - Stores the options reference and calls {@link Lifecycle.init}.
1838
1815
  *
1839
1816
  * @param {SelectiveOptions} options - Configuration object containing empty state messages.
@@ -1843,7 +1820,7 @@ class EmptyState extends Lifecycle {
1843
1820
  this.options = options;
1844
1821
  this.node = Libs.nodeCreator({
1845
1822
  node: "div",
1846
- classList: ["selective-ui-empty-state", "hide"],
1823
+ classList: ["seui-empty-state", "hide"],
1847
1824
  role: "status",
1848
1825
  ariaLive: "polite",
1849
1826
  });
@@ -1976,7 +1953,7 @@ class LoadingState extends Lifecycle {
1976
1953
  * Initializes internal resources for this component.
1977
1954
  *
1978
1955
  * Side effects:
1979
- * - Creates the root `div` node with base CSS classes: `"selective-ui-loading-state"` and `"hide"`.
1956
+ * - Creates the root `div` node with base CSS classes: `"seui-loading-state"` and `"hide"`.
1980
1957
  * - Sets initial text to `options.textLoading`.
1981
1958
  * - Applies `role="status"` and `aria-live="polite"`.
1982
1959
  * - Stores the options reference and calls {@link Lifecycle.init}.
@@ -1988,7 +1965,7 @@ class LoadingState extends Lifecycle {
1988
1965
  this.options = options;
1989
1966
  this.node = Libs.nodeCreator({
1990
1967
  node: "div",
1991
- classList: ["selective-ui-loading-state", "hide"],
1968
+ classList: ["seui-loading-state", "hide"],
1992
1969
  textContent: options.textLoading,
1993
1970
  role: "status",
1994
1971
  ariaLive: "polite",
@@ -2249,7 +2226,7 @@ class ResizeObserverService {
2249
2226
  * Not idempotent. Call {@link disconnect} before calling `connect()` again to avoid duplicates.
2250
2227
  */
2251
2228
  connect(element) {
2252
- if (!(element instanceof Element)) {
2229
+ if (!(element instanceof HTMLElement)) {
2253
2230
  throw new Error("Invalid element");
2254
2231
  }
2255
2232
  this.element = element;
@@ -2389,7 +2366,7 @@ class Popup extends Lifecycle {
2389
2366
  PopupContainer: {
2390
2367
  tag: {
2391
2368
  node: "div",
2392
- classList: "selective-ui-popup",
2369
+ classList: "seui-popup",
2393
2370
  style: { maxHeight: options.panelHeight },
2394
2371
  },
2395
2372
  child: {
@@ -2398,7 +2375,7 @@ class Popup extends Lifecycle {
2398
2375
  tag: {
2399
2376
  id: options.SEID_LIST,
2400
2377
  node: "div",
2401
- classList: "selective-ui-options-container",
2378
+ classList: "seui-options-container",
2402
2379
  role: "listbox",
2403
2380
  },
2404
2381
  },
@@ -2682,10 +2659,8 @@ class Popup extends Lifecycle {
2682
2659
  if (this.is(LifecycleState.DESTROYED)) {
2683
2660
  return;
2684
2661
  }
2685
- if (this.hideLoadHandle) {
2686
- clearTimeout(this.hideLoadHandle);
2687
- this.hideLoadHandle = null;
2688
- }
2662
+ clearTimeout(this.hideLoadHandle);
2663
+ this.hideLoadHandle = null;
2689
2664
  if (this.node && this.scrollListener) {
2690
2665
  this.node.removeEventListener("scroll", this.scrollListener);
2691
2666
  this.scrollListener = null;
@@ -2693,19 +2668,14 @@ class Popup extends Lifecycle {
2693
2668
  this.emptyState.destroy();
2694
2669
  this.loadingState.destroy();
2695
2670
  this.optionHandle.destroy();
2696
- try {
2697
- this.resizeObser?.disconnect();
2698
- }
2699
- catch (_) { }
2700
- this.resizeObser = null;
2701
- try {
2702
- this.effSvc?.setElement?.(null);
2703
- }
2704
- catch (_) { }
2705
- this.effSvc = null;
2671
+ this.resizeObser?.disconnect?.();
2672
+ this.effSvc?.setElement?.(null);
2673
+ this.modelManager?.skipEvent?.(false);
2674
+ this.recyclerView?.clear?.();
2675
+ this.node?.remove?.();
2706
2676
  if (this.node) {
2707
2677
  try {
2708
- const clone = this.node.cloneNode(true);
2678
+ const clone = Libs.nodeCloner(this.node);
2709
2679
  this.node.replaceWith(clone);
2710
2680
  clone.remove();
2711
2681
  }
@@ -2715,15 +2685,6 @@ class Popup extends Lifecycle {
2715
2685
  }
2716
2686
  this.node = null;
2717
2687
  this.optionsContainer = null;
2718
- try {
2719
- this.modelManager?.skipEvent?.(false);
2720
- this.recyclerView?.clear?.();
2721
- this.recyclerView = null;
2722
- this.optionAdapter = null;
2723
- // Original behavior kept intentionally.
2724
- this.node.remove();
2725
- }
2726
- catch (_) { }
2727
2688
  this.modelManager = null;
2728
2689
  this.optionHandle = null;
2729
2690
  this.emptyState = null;
@@ -2731,6 +2692,10 @@ class Popup extends Lifecycle {
2731
2692
  this.parent = null;
2732
2693
  this.options = null;
2733
2694
  this.isCreated = false;
2695
+ this.effSvc = null;
2696
+ this.resizeObser = null;
2697
+ this.recyclerView = null;
2698
+ this.optionAdapter = null;
2734
2699
  super.destroy();
2735
2700
  }
2736
2701
  /**
@@ -2964,8 +2929,8 @@ class SearchBox extends Lifecycle {
2964
2929
  * Initializes DOM, ARIA attributes, and interaction listeners.
2965
2930
  *
2966
2931
  * DOM structure (conceptually):
2967
- * - Root: `div.selective-ui-searchbox.hide`
2968
- * - Child: `input[type="search"].selective-ui-searchbox-input`
2932
+ * - Root: `div.seui-searchbox.hide`
2933
+ * - Child: `input[type="search"].seui-searchbox-input`
2969
2934
  *
2970
2935
  * Accessibility attributes set on the input:
2971
2936
  * - `role="searchbox"`: announces search field semantics
@@ -2995,14 +2960,14 @@ class SearchBox extends Lifecycle {
2995
2960
  initialize(options) {
2996
2961
  this.nodeMounted = Libs.mountNode({
2997
2962
  SearchBox: {
2998
- tag: { node: "div", classList: ["selective-ui-searchbox", "hide"] },
2963
+ tag: { node: "div", classList: ["seui-searchbox", "hide"] },
2999
2964
  child: {
3000
2965
  SearchInput: {
3001
2966
  tag: {
3002
2967
  id: Libs.randomString(),
3003
2968
  node: "input",
3004
2969
  type: "search",
3005
- classList: ["selective-ui-searchbox-input"],
2970
+ classList: ["seui-searchbox-input"],
3006
2971
  placeholder: options.placeholder,
3007
2972
  role: "searchbox",
3008
2973
  ariaControls: options.SEID_LIST,
@@ -4190,15 +4155,17 @@ class OptionModel extends Model {
4190
4155
  set selectedNonTrigger(value) {
4191
4156
  const input = this.view?.view?.tags?.OptionInput;
4192
4157
  const viewEl = this.view?.getView?.();
4193
- if (input)
4158
+ if (input) {
4194
4159
  input.checked = value;
4160
+ }
4195
4161
  if (viewEl && this.targetElement) {
4196
4162
  viewEl.classList.toggle("checked", !!value);
4197
4163
  viewEl.setAttribute("aria-selected", value ? "true" : "false");
4198
4164
  this.targetElement.toggleAttribute("selected", !!value);
4199
4165
  }
4200
- if (this.targetElement)
4166
+ if (this.targetElement) {
4201
4167
  this.targetElement.selected = value;
4168
+ }
4202
4169
  iEvents.callEvent([this, value], ...this.privOnInternalSelected);
4203
4170
  }
4204
4171
  /**
@@ -4322,8 +4289,12 @@ class OptionModel extends Model {
4322
4289
  }
4323
4290
  const imageTag = this.view.view.tags.OptionImage;
4324
4291
  if (imageTag && this.hasImage) {
4325
- imageTag.src = this.imageSrc;
4326
- imageTag.alt = this.text;
4292
+ if (imageTag.src != this.imageSrc) {
4293
+ imageTag.src = this.imageSrc;
4294
+ }
4295
+ if (imageTag.alt != this.text) {
4296
+ imageTag.alt = this.text;
4297
+ }
4327
4298
  }
4328
4299
  if (this.targetElement)
4329
4300
  this.selectedNonTrigger = this.targetElement.selected;
@@ -4455,9 +4426,8 @@ class ModelManager extends Lifecycle {
4455
4426
  this.privModelList.push(currentGroup);
4456
4427
  }
4457
4428
  else if (data.tagName === "OPTION") {
4458
- const optionEl = data;
4459
- const optionModel = new OptionModel(this.options, optionEl);
4460
- const parentGroup = optionEl["__parentGroup"];
4429
+ const optionModel = new OptionModel(this.options, data);
4430
+ const parentGroup = data["__parentGroup"];
4461
4431
  if (parentGroup && currentGroup && parentGroup === currentGroup.targetElement) {
4462
4432
  currentGroup.addItem(optionModel);
4463
4433
  optionModel.group = currentGroup;
@@ -4868,7 +4838,7 @@ class RecyclerView extends Lifecycle {
4868
4838
  * popup/layout logic to recompute geometry.
4869
4839
  *
4870
4840
  * ### DOM & a11y side effects
4871
- * - Creates a root `<div>` with classes `selective-ui-accessorybox hide`.
4841
+ * - Creates a root `<div>` with classes `seui-accessorybox hide`.
4872
4842
  * - Stops `mouseup` propagation on the root to avoid "outside click" behaviors.
4873
4843
  * - Each chip has:
4874
4844
  * - a `<span role="button">` with `aria-label`/`title` for screen readers and tooltips,
@@ -4948,7 +4918,7 @@ class AccessoryBox extends Lifecycle {
4948
4918
  * Guarded: runs only when state is `NEW`.
4949
4919
  *
4950
4920
  * Side effects:
4951
- * - Creates the root node with base classes (`selective-ui-accessorybox`, `hide`).
4921
+ * - Creates the root node with base classes (`seui-accessorybox`, `hide`).
4952
4922
  * - Stops `mouseup` propagation to avoid outside-click handlers reacting to chip interactions.
4953
4923
  *
4954
4924
  * @returns {void}
@@ -4961,7 +4931,7 @@ class AccessoryBox extends Lifecycle {
4961
4931
  AccessoryBox: {
4962
4932
  tag: {
4963
4933
  node: "div",
4964
- classList: ["selective-ui-accessorybox", "hide"],
4934
+ classList: ["seui-accessorybox", "hide"],
4965
4935
  onmouseup: (evt) => {
4966
4936
  // Prevent outside listeners from reacting to chip clicks
4967
4937
  evt.stopPropagation();
@@ -5028,7 +4998,7 @@ class AccessoryBox extends Lifecycle {
5028
4998
  /**
5029
4999
  * Assigns the {@link ModelManager} used to run selection pipelines and mutate selection state.
5030
5000
  *
5031
- * @param {ModelManager<MixedItem, MixedAdapter> | null} modelManager - Model manager controlling option state.
5001
+ * @param {ModelManager<MixedItem, MixedAdapter>} modelManager - Model manager controlling option state.
5032
5002
  * @returns {void}
5033
5003
  */
5034
5004
  setModelManager(modelManager) {
@@ -6515,7 +6485,7 @@ class GroupView extends View {
6515
6485
  *
6516
6486
  * Creation flow:
6517
6487
  * 1. Generates unique group ID (7-character random string).
6518
- * 2. Creates DOM structure via {@link Libs.mountView}:
6488
+ * 2. Creates DOM structure via {@link Libs.mountNode}:
6519
6489
  * - Root: `<div role="group" aria-labelledby="seui-{id}-header">`
6520
6490
  * - Header: `<div role="presentation" id="seui-{id}-header">`
6521
6491
  * - Items: `<div role="group">` (nested group for child items)
@@ -6537,11 +6507,11 @@ class GroupView extends View {
6537
6507
  */
6538
6508
  mount() {
6539
6509
  const group_id = Libs.randomString(7);
6540
- this.view = Libs.mountView({
6510
+ this.view = Libs.mountNode({
6541
6511
  GroupView: {
6542
6512
  tag: {
6543
6513
  node: "div",
6544
- classList: ["selective-ui-group"],
6514
+ classList: ["seui-group"],
6545
6515
  role: "group",
6546
6516
  ariaLabelledby: `seui-${group_id}-header`,
6547
6517
  id: `seui-${group_id}-group`,
@@ -6550,7 +6520,7 @@ class GroupView extends View {
6550
6520
  GroupHeader: {
6551
6521
  tag: {
6552
6522
  node: "div",
6553
- classList: ["selective-ui-group-header"],
6523
+ classList: ["seui-group-header"],
6554
6524
  role: "presentation",
6555
6525
  id: `seui-${group_id}-header`,
6556
6526
  },
@@ -6558,7 +6528,7 @@ class GroupView extends View {
6558
6528
  GroupItems: {
6559
6529
  tag: {
6560
6530
  node: "div",
6561
- classList: ["selective-ui-group-items"],
6531
+ classList: ["seui-group-items"],
6562
6532
  role: "group",
6563
6533
  },
6564
6534
  },
@@ -7003,7 +6973,7 @@ class OptionView extends View {
7003
6973
  * - **OptionImage** (conditional): `<img>` with inline styles (width/height/borderRadius).
7004
6974
  * - **OptionLabel**: `<label htmlFor="{inputID}">` with alignment classes.
7005
6975
  * - **LabelContent**: `<div>` (content placeholder).
7006
- * 4. Creates DOM via {@link Libs.mountView}.
6976
+ * 4. Creates DOM via {@link Libs.mountNode}.
7007
6977
  * 5. Appends root to {@link parent}.
7008
6978
  * 6. Sets {@link isRendered} to `true` (enables reactive updates).
7009
6979
  * 7. Transitions `INITIALIZED → MOUNTED` via `super.mount()`.
@@ -7021,7 +6991,7 @@ class OptionView extends View {
7021
6991
  * @override
7022
6992
  */
7023
6993
  mount() {
7024
- const viewClass = ["selective-ui-option-view"];
6994
+ const viewClass = ["seui-option-view"];
7025
6995
  const opt_id = Libs.randomString(7);
7026
6996
  const inputID = `option_${opt_id}`;
7027
6997
  if (this.config.isMultiple)
@@ -7065,7 +7035,7 @@ class OptionView extends View {
7065
7035
  },
7066
7036
  },
7067
7037
  };
7068
- this.view = Libs.mountView({
7038
+ this.view = Libs.mountNode({
7069
7039
  OptionView: {
7070
7040
  tag: {
7071
7041
  node: "div",
@@ -8113,15 +8083,15 @@ class VirtualRecyclerView extends RecyclerView {
8113
8083
  return;
8114
8084
  this.viewElement.replaceChildren();
8115
8085
  const nodeMounted = Libs.mountNode({
8116
- PadTop: { tag: { node: "div", classList: "selective-ui-virtual-pad-top" } },
8117
- ItemsHost: { tag: { node: "div", classList: "selective-ui-virtual-items" } },
8118
- PadBottom: { tag: { node: "div", classList: "selective-ui-virtual-pad-bottom" } },
8086
+ PadTop: { tag: { node: "div", classList: "seui-virtual-pad-top" } },
8087
+ ItemsHost: { tag: { node: "div", classList: "seui-virtual-items" } },
8088
+ PadBottom: { tag: { node: "div", classList: "seui-virtual-pad-bottom" } },
8119
8089
  }, this.viewElement);
8120
8090
  this.PadTop = nodeMounted.PadTop;
8121
8091
  this.ItemsHost = nodeMounted.ItemsHost;
8122
8092
  this.PadBottom = nodeMounted.PadBottom;
8123
8093
  this.scrollEl = this.opts.scrollEl
8124
- ?? this.viewElement.closest(".selective-ui-popup")
8094
+ ?? this.viewElement.closest(".seui-popup")
8125
8095
  ?? this.viewElement.parentElement;
8126
8096
  if (!this.scrollEl)
8127
8097
  throw new Error("VirtualRecyclerView: scrollEl not found");
@@ -8450,7 +8420,7 @@ class VirtualRecyclerView extends RecyclerView {
8450
8420
  const now = performance.now();
8451
8421
  if (now - this.stickyCacheTick < 16)
8452
8422
  return this.stickyCacheVal;
8453
- const sticky = this.scrollEl.querySelector(".selective-ui-option-handle:not(.hide)");
8423
+ const sticky = this.scrollEl.querySelector(".seui-option-handle:not(.hide)");
8454
8424
  this.stickyCacheVal = sticky?.offsetHeight ?? 0;
8455
8425
  this.stickyCacheTick = now;
8456
8426
  return this.stickyCacheVal;
@@ -9013,12 +8983,29 @@ class SelectBox extends Lifecycle {
9013
8983
  * @internal
9014
8984
  */
9015
8985
  this.isBeforeSearch = false;
8986
+ /**
8987
+ * Tracks whether {@link deInit} has already run.
8988
+ *
8989
+ * This guards teardown work (including plugin lifecycle hooks) from running more than once
8990
+ * when {@link deInit} is called separately before {@link destroy}.
8991
+ *
8992
+ * @internal
8993
+ */
8994
+ this.hasDeInitialized = false;
9016
8995
  /**
9017
8996
  * Selective context (global helper / registry).
9018
8997
  *
9019
8998
  * Used to locate the instance wrapper via `Selective.find(...)` and to close other open instances.
9020
8999
  */
9021
9000
  this.Selective = null;
9001
+ /**
9002
+ * Registered plugins for this SelectBox instance.
9003
+ */
9004
+ this.plugins = [];
9005
+ /**
9006
+ * Cached plugin context for this SelectBox instance.
9007
+ */
9008
+ this.pluginContext = null;
9022
9009
  if (select && Selective)
9023
9010
  this.initialize(select, Selective);
9024
9011
  }
@@ -9135,12 +9122,12 @@ class SelectBox extends Lifecycle {
9135
9122
  placeholder.node.id = String(options.SEID_HOLDER ?? "");
9136
9123
  const container = Libs.mountNode({
9137
9124
  Container: {
9138
- tag: { node: "div", classList: "selective-ui-MAIN" },
9125
+ tag: { node: "div", classList: "seui-MAIN" },
9139
9126
  child: {
9140
9127
  ViewPanel: {
9141
9128
  tag: {
9142
9129
  node: "div",
9143
- classList: "selective-ui-view",
9130
+ classList: "seui-view",
9144
9131
  tabIndex: 0,
9145
9132
  onkeydown: (e) => {
9146
9133
  if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
@@ -9200,6 +9187,20 @@ class SelectBox extends Lifecycle {
9200
9187
  accessoryBox.setModelManager(optionModelManager);
9201
9188
  this.setupEventHandlers(select, container, options, searchController, searchbox);
9202
9189
  this.setupObservers(selectObserver, datasetObserver, select, optionModelManager);
9190
+ this.plugins = this.Selective?.getPlugins?.() ?? [];
9191
+ if (this.plugins.length) {
9192
+ const resources = optionModelManager.getResources();
9193
+ const pluginContext = {
9194
+ selectBox: this,
9195
+ options,
9196
+ adapter: resources.adapter,
9197
+ recycler: resources.recyclerView,
9198
+ viewTags: container.tags,
9199
+ actions: this.getAction(),
9200
+ };
9201
+ this.pluginContext = pluginContext;
9202
+ this.runPluginHook("init", (plugin) => plugin.init?.(pluginContext));
9203
+ }
9203
9204
  // Initial states
9204
9205
  this.isDisabled = Libs.string2Boolean(options.disabled);
9205
9206
  this.isReadOnly = Libs.string2Boolean(options.readonly);
@@ -9397,12 +9398,21 @@ class SelectBox extends Lifecycle {
9397
9398
  * preventing memory leaks and unintended background updates.
9398
9399
  */
9399
9400
  deInit() {
9401
+ if (this.hasDeInitialized) {
9402
+ return;
9403
+ }
9400
9404
  const c = this.container ?? {};
9401
9405
  const { selectObserver, datasetObserver } = c;
9406
+ if (this.plugins.length) {
9407
+ this.runPluginHook("destroy", (plugin) => plugin.destroy?.());
9408
+ }
9409
+ this.plugins = [];
9410
+ this.pluginContext = null;
9402
9411
  if (selectObserver?.disconnect)
9403
9412
  selectObserver.disconnect();
9404
9413
  if (datasetObserver?.disconnect)
9405
9414
  datasetObserver.disconnect();
9415
+ this.hasDeInitialized = true;
9406
9416
  }
9407
9417
  /**
9408
9418
  * Lifecycle: `destroy` (teardown stage).
@@ -9716,6 +9726,9 @@ class SelectBox extends Lifecycle {
9716
9726
  if (bindedOptions.multiple)
9717
9727
  ViewPanel.setAttribute("aria-multiselectable", "true");
9718
9728
  iEvents.callEvent([getInstance()], ...bindedOptions.on.show);
9729
+ if (superThis.pluginContext) {
9730
+ superThis.runPluginHook("onOpen", (plugin) => plugin.onOpen?.(superThis.pluginContext));
9731
+ }
9719
9732
  return;
9720
9733
  },
9721
9734
  close() {
@@ -9732,6 +9745,9 @@ class SelectBox extends Lifecycle {
9732
9745
  container.searchbox.hide();
9733
9746
  container.tags.ViewPanel.setAttribute("aria-expanded", "false");
9734
9747
  iEvents.callEvent([getInstance()], ...bindedOptions.on.close);
9748
+ if (superThis.pluginContext) {
9749
+ superThis.runPluginHook("onClose", (plugin) => plugin.onClose?.(superThis.pluginContext));
9750
+ }
9735
9751
  return;
9736
9752
  },
9737
9753
  toggle() {
@@ -9770,6 +9786,10 @@ class SelectBox extends Lifecycle {
9770
9786
  if (superThis.is(LifecycleState.MOUNTED)) {
9771
9787
  superThis.update();
9772
9788
  }
9789
+ if (superThis.pluginContext && superThis.optionModelManager) {
9790
+ const resources = superThis.optionModelManager.getResources();
9791
+ superThis.runPluginHook("onChange", (plugin) => plugin.onChange?.(this.value, resources.modelList, resources.adapter, superThis.pluginContext));
9792
+ }
9773
9793
  },
9774
9794
  refreshMask() {
9775
9795
  let mask = bindedOptions.placeholder;
@@ -9902,6 +9922,27 @@ class SelectBox extends Lifecycle {
9902
9922
  }
9903
9923
  return flatOptions;
9904
9924
  }
9925
+ /**
9926
+ * Safely runs a hook across all registered plugins.
9927
+ *
9928
+ * Any plugin failure is isolated to prevent breaking the current flow.
9929
+ *
9930
+ * @param hook - Hook name for logging context.
9931
+ * @param runner - Hook invocation handler.
9932
+ * @internal
9933
+ */
9934
+ runPluginHook(hook, runner) {
9935
+ if (!this.plugins.length)
9936
+ return;
9937
+ this.plugins.forEach((plugin) => {
9938
+ try {
9939
+ runner(plugin);
9940
+ }
9941
+ catch (error) {
9942
+ console.error(`Plugin "${plugin.id}" ${hook} error:`, error);
9943
+ }
9944
+ });
9945
+ }
9905
9946
  }
9906
9947
 
9907
9948
  /**
@@ -10128,6 +10169,15 @@ class Selective extends Lifecycle {
10128
10169
  * @private
10129
10170
  */
10130
10171
  this.bindedQueries = new Map();
10172
+ /**
10173
+ * Registry of Selective plugins keyed by plugin ID.
10174
+ *
10175
+ * - Managed via {@link registerPlugin}, {@link unregisterPlugin}, and {@link getPlugin}.
10176
+ * - Cleared during {@link destroyAll} after invoking plugin teardown hooks.
10177
+ *
10178
+ * @private
10179
+ */
10180
+ this.plugins = new Map();
10131
10181
  this.init();
10132
10182
  }
10133
10183
  /**
@@ -10136,6 +10186,7 @@ class Selective extends Lifecycle {
10136
10186
  * Behavior:
10137
10187
  * - No-op if not in {@link LifecycleState.NEW} (idempotent guard).
10138
10188
  * - Initializes {@link bindedQueries} as empty `Map`.
10189
+ * - Initializes {@link plugins} as empty `Map`.
10139
10190
  * - Transitions `NEW → INITIALIZED` via `super.init()`.
10140
10191
  *
10141
10192
  * Notes:
@@ -10151,6 +10202,7 @@ class Selective extends Lifecycle {
10151
10202
  return;
10152
10203
  // Initialize core properties
10153
10204
  this.bindedQueries = new Map();
10205
+ this.plugins = new Map();
10154
10206
  super.init();
10155
10207
  }
10156
10208
  /**
@@ -10318,6 +10370,14 @@ class Selective extends Lifecycle {
10318
10370
  }
10319
10371
  return response;
10320
10372
  }
10373
+ /**
10374
+ * Returns all registered Selective plugins.
10375
+ *
10376
+ * @returns The list of plugins in registration order.
10377
+ */
10378
+ getPlugins() {
10379
+ return Array.from(this.plugins.values());
10380
+ }
10321
10381
  /**
10322
10382
  * Activates auto-binding for newly added `<select>` elements.
10323
10383
  *
@@ -10389,14 +10449,51 @@ class Selective extends Lifecycle {
10389
10449
  this.update();
10390
10450
  }
10391
10451
  }
10452
+ /**
10453
+ * Registers a plugin for Selective lifecycle integration.
10454
+ *
10455
+ * @public
10456
+ * @param {SelectivePlugin} plugin - Plugin instance to register.
10457
+ * @returns {void}
10458
+ */
10459
+ registerPlugin(plugin) {
10460
+ if (!plugin?.id)
10461
+ return;
10462
+ this.plugins.set(plugin.id, plugin);
10463
+ }
10464
+ /**
10465
+ * Unregisters a plugin by ID.
10466
+ *
10467
+ * @public
10468
+ * @param {string} id - Plugin ID to remove.
10469
+ * @returns {void}
10470
+ */
10471
+ unregisterPlugin(id) {
10472
+ if (!id)
10473
+ return;
10474
+ this.plugins.delete(id);
10475
+ }
10476
+ /**
10477
+ * Retrieves a plugin by ID.
10478
+ *
10479
+ * @public
10480
+ * @param {string} id - Plugin ID to retrieve.
10481
+ * @returns {SelectivePlugin | undefined} Plugin instance if found.
10482
+ */
10483
+ getPlugin(id) {
10484
+ if (!id)
10485
+ return undefined;
10486
+ return this.plugins.get(id);
10487
+ }
10392
10488
  /**
10393
10489
  * Destroys all bound Selective instances and releases global resources.
10394
10490
  *
10395
10491
  * Teardown flow:
10396
10492
  * 1. Iterates all registered queries and calls {@link destroyByQuery}.
10397
10493
  * 2. Clears {@link bindedQueries} and {@link Libs.getBindedCommand}.
10398
- * 3. Disconnects {@link EAObserver} (stops auto-binding).
10399
- * 4. Transitions to {@link LifecycleState.DESTROYED} via `super.destroy()`.
10494
+ * 3. Invokes plugin teardown hooks and clears {@link plugins}.
10495
+ * 4. Disconnects {@link EAObserver} (stops auto-binding).
10496
+ * 5. Transitions to {@link LifecycleState.DESTROYED} via `super.destroy()`.
10400
10497
  *
10401
10498
  * Idempotency:
10402
10499
  * - No-op if already {@link LifecycleState.DESTROYED}.
@@ -10411,7 +10508,13 @@ class Selective extends Lifecycle {
10411
10508
  bindedCommands.forEach((query) => this.destroyByQuery(query));
10412
10509
  this.bindedQueries.clear();
10413
10510
  Libs.getBindedCommand().length = 0;
10511
+ this.plugins.forEach((plugin) => {
10512
+ plugin.destroy?.();
10513
+ plugin.onDestroy?.();
10514
+ });
10515
+ this.plugins.clear();
10414
10516
  this.EAObserver?.disconnect();
10517
+ this.plugins.clear();
10415
10518
  // Call parent lifecycle destroy
10416
10519
  super.destroy();
10417
10520
  }
@@ -10478,12 +10581,8 @@ class Selective extends Lifecycle {
10478
10581
  const wasObserving = !!this.EAObserver;
10479
10582
  if (wasObserving)
10480
10583
  this.EAObserver?.disconnect();
10481
- try {
10482
- bindMap.self?.deInit?.();
10483
- }
10484
- catch (_) { }
10485
- const wrapper = bindMap.container?.element ??
10486
- selectElement.parentElement;
10584
+ bindMap.self?.deInit?.();
10585
+ const wrapper = (bindMap.container?.element) ?? selectElement.parentElement;
10487
10586
  selectElement.style.display = "";
10488
10587
  selectElement.style.visibility = "";
10489
10588
  selectElement.disabled = false;
@@ -10703,7 +10802,7 @@ const SECLASS = new Selective();
10703
10802
  *
10704
10803
  * Declared as `const` literal type to enable strict typing and easy tree-shaking.
10705
10804
  */
10706
- const version = "1.2.5";
10805
+ const version = "1.2.6";
10707
10806
  /**
10708
10807
  * Library name identifier.
10709
10808
  *
@@ -10782,6 +10881,22 @@ function rebind(query, options = {}) {
10782
10881
  function effector(element) {
10783
10882
  return Effector(element);
10784
10883
  }
10884
+ /**
10885
+ * Register a Selective plugin globally.
10886
+ *
10887
+ * @param plugin - Plugin to register.
10888
+ */
10889
+ function registerPlugin(plugin) {
10890
+ SECLASS.registerPlugin(plugin);
10891
+ }
10892
+ /**
10893
+ * Unregister a Selective plugin by id.
10894
+ *
10895
+ * @param id - Plugin id.
10896
+ */
10897
+ function unregisterPlugin(id) {
10898
+ SECLASS.unregisterPlugin(id);
10899
+ }
10785
10900
  let domInitialized = false;
10786
10901
  function init() {
10787
10902
  if (domInitialized)
@@ -10806,5 +10921,5 @@ if (typeof document !== "undefined") {
10806
10921
  }
10807
10922
  }
10808
10923
 
10809
- export { bind, destroy, effector, find, name, rebind, version };
10924
+ export { bind, destroy, effector, find, name, rebind, registerPlugin, unregisterPlugin, version };
10810
10925
  //# sourceMappingURL=selective-ui.esm.js.map