selective-ui 1.1.0 → 1.1.2

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 (42) hide show
  1. package/dist/selective-ui.esm.js +225 -200
  2. package/dist/selective-ui.esm.js.map +1 -1
  3. package/dist/selective-ui.esm.min.js +2 -1
  4. package/dist/selective-ui.esm.min.js.br +0 -0
  5. package/dist/selective-ui.min.js +2 -2
  6. package/dist/selective-ui.min.js.br +0 -0
  7. package/dist/selective-ui.umd.js +225 -200
  8. package/dist/selective-ui.umd.js.map +1 -1
  9. package/package.json +16 -15
  10. package/src/ts/adapter/mixed-adapter.ts +6 -7
  11. package/src/ts/components/accessorybox.ts +0 -10
  12. package/src/ts/components/directive.ts +0 -3
  13. package/src/ts/components/empty-state.ts +0 -3
  14. package/src/ts/components/loading-state.ts +3 -1
  15. package/src/ts/components/option-handle.ts +0 -5
  16. package/src/ts/components/placeholder.ts +0 -3
  17. package/src/ts/components/popup.ts +70 -11
  18. package/src/ts/components/searchbox.ts +2 -12
  19. package/src/ts/components/selectbox.ts +4 -2
  20. package/src/ts/core/base/adapter.ts +4 -5
  21. package/src/ts/core/base/model.ts +0 -1
  22. package/src/ts/core/base/recyclerview.ts +0 -6
  23. package/src/ts/core/base/view.ts +0 -24
  24. package/src/ts/core/model-manager.ts +3 -24
  25. package/src/ts/index.ts +10 -9
  26. package/src/ts/models/group-model.ts +0 -1
  27. package/src/ts/models/option-model.ts +3 -3
  28. package/src/ts/services/ea-observer.ts +0 -2
  29. package/src/ts/services/effector.ts +3 -4
  30. package/src/ts/services/refresher.ts +1 -1
  31. package/src/ts/services/resize-observer.ts +0 -1
  32. package/src/ts/services/select-observer.ts +0 -2
  33. package/src/ts/types/core/base/view.type.ts +0 -18
  34. package/src/ts/types/services/effector.type.ts +14 -0
  35. package/src/ts/types/utils/callback-scheduler.type.ts +69 -0
  36. package/src/ts/types/utils/libs.type.ts +0 -46
  37. package/src/ts/utils/callback-scheduler.ts +135 -0
  38. package/src/ts/utils/guard.ts +4 -10
  39. package/src/ts/utils/libs.ts +6 -77
  40. package/src/ts/utils/selective.ts +7 -5
  41. package/src/ts/views/group-view.ts +0 -1
  42. package/src/ts/views/option-view.ts +2 -0
@@ -1,4 +1,4 @@
1
- /*! Selective UI v1.1.0 | MIT License */
1
+ /*! Selective UI v1.1.2 | MIT License */
2
2
  /**
3
3
  * @class
4
4
  */
@@ -66,6 +66,126 @@ class iStorage {
66
66
  }
67
67
  }
68
68
 
69
+ class CallbackScheduler {
70
+ constructor() {
71
+ /**
72
+ * Stores callbacks by key in registration order.
73
+ *
74
+ * Notes:
75
+ * - Entries may become `undefined` after execution when `once` is enabled.
76
+ * This preserves indices so timer bookkeeping remains stable.
77
+ */
78
+ this.executeStored = new Map();
79
+ /**
80
+ * Per-key timer registry.
81
+ *
82
+ * - Outer Map: groups timers by `TimerKey`
83
+ * - Inner Map: maps callback index -> active timeout handle
84
+ *
85
+ * Each callback index has its own debounce timer, allowing independent scheduling.
86
+ */
87
+ this.timerRunner = new Map();
88
+ }
89
+ /**
90
+ * Registers a callback under a key.
91
+ *
92
+ * @param key - Group identifier for callbacks.
93
+ * @param callback - Function to execute after the debounce timeout.
94
+ * @param options - Scheduling options.
95
+ * @returns The index of the registered callback within its key bucket.
96
+ *
97
+ * Behavior:
98
+ * - Callbacks are stored in registration order.
99
+ * - `options.debounce` is treated as a per-callback delay (milliseconds).
100
+ * - `options.once` removes the entry after its first execution (index is preserved).
101
+ */
102
+ on(key, callback, options = {}) {
103
+ const timeout = options.debounce ?? 50;
104
+ const once = options.once ?? false;
105
+ if (!this.executeStored.has(key))
106
+ this.executeStored.set(key, []);
107
+ const bucket = this.executeStored.get(key);
108
+ bucket.push({ callback, timeout, once });
109
+ }
110
+ /**
111
+ * Removes all callbacks associated with a key and clears any active timers.
112
+ *
113
+ * @param key - Key whose callbacks and timers will be removed.
114
+ */
115
+ off(key) {
116
+ const runner = this.timerRunner.get(key);
117
+ if (runner) {
118
+ for (const t of runner.values())
119
+ clearTimeout(t);
120
+ runner.clear();
121
+ this.timerRunner.delete(key);
122
+ }
123
+ this.executeStored.delete(key);
124
+ }
125
+ /**
126
+ * Schedules execution for all callbacks registered under a key.
127
+ *
128
+ * @param key - Key whose callbacks will be scheduled.
129
+ * @param params - Parameters collected and passed as a single payload.
130
+ *
131
+ * Payload rules:
132
+ * - If `run(key)` is called without params, callbacks receive `null`.
133
+ * - If `run(key, ...params)` is called with params, callbacks receive `params` as an array.
134
+ *
135
+ * Debounce rules:
136
+ * - Each callback has its own timer (by index).
137
+ * - Calling `run()` again before the timeout clears the previous timer for that callback.
138
+ *
139
+ * Once rules:
140
+ * - If an entry has `once = true`, it is removed after execution by setting its slot to `undefined`.
141
+ * (The list is not spliced to preserve indices.)
142
+ */
143
+ run(key, ...params) {
144
+ const executes = this.executeStored.get(key);
145
+ if (!executes)
146
+ return;
147
+ if (!this.timerRunner.has(key))
148
+ this.timerRunner.set(key, new Map());
149
+ const runner = this.timerRunner.get(key);
150
+ for (let i = 0; i < executes.length; i++) {
151
+ const entry = executes[i];
152
+ if (!entry)
153
+ continue;
154
+ const prev = runner.get(i);
155
+ if (prev)
156
+ clearTimeout(prev);
157
+ const timer = setTimeout(() => {
158
+ entry.callback(params.length > 0 ? params : null);
159
+ if (entry.once) {
160
+ // Preserve index stability by leaving an empty slot.
161
+ executes[i] = undefined;
162
+ // Cleanup the timer handle for this index.
163
+ const current = runner.get(i);
164
+ if (current)
165
+ clearTimeout(current);
166
+ runner.delete(i);
167
+ }
168
+ }, entry.timeout);
169
+ runner.set(i, timer);
170
+ }
171
+ }
172
+ /**
173
+ * Clears callbacks and timers.
174
+ *
175
+ * @param key - When provided, clears only that key; otherwise clears all keys.
176
+ */
177
+ clear(key) {
178
+ if (key !== undefined) {
179
+ this.off(key);
180
+ return;
181
+ }
182
+ // Iterate over a snapshot of keys because `off()` mutates the maps.
183
+ for (const k of Array.from(this.executeStored.keys())) {
184
+ this.off(k);
185
+ }
186
+ }
187
+ }
188
+
69
189
  /**
70
190
  * @class
71
191
  */
@@ -80,18 +200,6 @@ class Libs {
80
200
  this._iStorage = new iStorage();
81
201
  return this._iStorage;
82
202
  }
83
- /**
84
- * Checks whether a value is null/undefined/empty-string/"0"/0.
85
- * Booleans are always considered non-empty.
86
- *
87
- * @param {unknown} value - The value to test.
88
- * @returns {boolean} - True if considered empty; otherwise false.
89
- */
90
- static isNullOrEmpty(value) {
91
- if (typeof value === "boolean")
92
- return false;
93
- return value == null || value === "" || value === 0 || value === "0";
94
- }
95
203
  /**
96
204
  * Deep-copies plain objects/arrays recursively. Returns primitives as-is.
97
205
  *
@@ -289,23 +397,6 @@ class Libs {
289
397
  }
290
398
  return recursiveTemp;
291
399
  }
292
- /**
293
- * Applies inline CSS styles to all matched elements. Accepts either a style
294
- * object or a single property + value pair.
295
- *
296
- * @param {string|NodeListOf<HTMLElement>|HTMLElement} queryCommon - Selector or element(s).
297
- * @param {Record<string, string>|string} styles - Style object or a single property name.
298
- * @param {string|null} [value=null] - Value for the single property form.
299
- */
300
- static setStyle(queryCommon, styles, value = null) {
301
- const apply_styles = typeof styles === "string" ? { [styles]: value } : { ...styles };
302
- const queryItems = this.getElements(queryCommon);
303
- for (let i = 0; i < queryItems.length; i++) {
304
- const item = queryItems[i];
305
- if (item)
306
- Object.assign(item.style, apply_styles);
307
- }
308
- }
309
400
  /**
310
401
  * Builds a configuration object by copying defaults and then overriding with
311
402
  * matching element properties or data-* attributes when present.
@@ -590,41 +681,7 @@ Libs._iStorage = null;
590
681
  * Schedules and batches function executions keyed by name, with debounced timers.
591
682
  * Provides setExecute(), clearExecute(), and run() to manage deferred callbacks.
592
683
  */
593
- Libs.timerProcess = {
594
- executeStored: {},
595
- timerRunner: {},
596
- setExecute(keyExecute, execute, timeout = 50, once = false) {
597
- if (!this.executeStored[keyExecute])
598
- this.executeStored[keyExecute] = [];
599
- this.executeStored[keyExecute].push({ execute, timeout, once });
600
- },
601
- clearExecute(keyExecute) {
602
- delete this.executeStored[keyExecute];
603
- },
604
- run(keyExecute, ...params) {
605
- const executes = this.executeStored[keyExecute];
606
- if (!executes)
607
- return;
608
- if (!this.timerRunner[keyExecute])
609
- this.timerRunner[keyExecute] = {};
610
- for (const key in executes) {
611
- const entry = executes[Number(key)];
612
- if (!entry)
613
- continue;
614
- if (!this.timerRunner[keyExecute][key]) {
615
- // placeholder, will be overwritten by setTimeout
616
- this.timerRunner[keyExecute][key] = setTimeout(() => { }, 0);
617
- clearTimeout(this.timerRunner[keyExecute][key]);
618
- }
619
- clearTimeout(this.timerRunner[keyExecute][key]);
620
- this.timerRunner[keyExecute][key] = setTimeout(() => {
621
- entry.execute(params.length > 0 ? params : null);
622
- if (entry.once)
623
- delete this.executeStored[keyExecute][Number(key)];
624
- }, entry.timeout);
625
- }
626
- },
627
- };
684
+ Libs.callbackScheduler = new CallbackScheduler();
628
685
 
629
686
  /**
630
687
  * Provides a lightweight event utility with cancel/continue control:
@@ -743,7 +800,7 @@ class Refresher {
743
800
  width = options.width;
744
801
  if (cfgHeight > 0)
745
802
  height = options.height;
746
- Libs.setStyle(view, { width, height, minWidth, minHeight });
803
+ Object.assign(view.style, { width, height, minWidth, minHeight });
747
804
  }
748
805
  }
749
806
 
@@ -756,9 +813,6 @@ class PlaceHolder {
756
813
  * Supports HTML content based on configuration and provides methods to get or set the placeholder value.
757
814
  */
758
815
  constructor(options) {
759
- /**
760
- * @type {HTMLElement | null}
761
- */
762
816
  this.node = null;
763
817
  this._options = null;
764
818
  if (options)
@@ -843,14 +897,9 @@ class OptionHandle {
843
897
  */
844
898
  constructor(options = null) {
845
899
  this.nodeMounted = null;
846
- /**
847
- * @type {HTMLDivElement | null}
848
- */
849
900
  this.node = null;
850
901
  this.options = null;
851
- /** @type {Function[]} */
852
902
  this._ActionOnSelectAll = [];
853
- /** @type {Function[]} */
854
903
  this._ActionOnDeSelectAll = [];
855
904
  if (options)
856
905
  this.init(options);
@@ -959,9 +1008,6 @@ class EmptyState {
959
1008
  * Provides methods to show/hide the state and check its visibility.
960
1009
  */
961
1010
  constructor(options = null) {
962
- /**
963
- * @type {HTMLDivElement | null}
964
- */
965
1011
  this.node = null;
966
1012
  this.options = null;
967
1013
  if (options)
@@ -1012,13 +1058,15 @@ class EmptyState {
1012
1058
  }
1013
1059
  }
1014
1060
 
1061
+ /**
1062
+ * @class
1063
+ */
1015
1064
  class LoadingState {
1016
1065
  /**
1017
1066
  * Represents a loading state component that displays a loading message during data fetch or processing.
1018
1067
  * Provides methods to show/hide the state and check its visibility.
1019
1068
  */
1020
1069
  constructor(options = null) {
1021
- /** @type {HTMLDivElement | null} */
1022
1070
  this.node = null;
1023
1071
  this.options = null;
1024
1072
  if (options)
@@ -1080,7 +1128,6 @@ class ResizeObserverService {
1080
1128
  constructor() {
1081
1129
  this.isInit = false;
1082
1130
  this.element = null;
1083
- /** @type {ResizeObserver|null} */
1084
1131
  this._resizeObserver = null;
1085
1132
  this._mutationObserver = null;
1086
1133
  this.isInit = true;
@@ -1210,27 +1257,17 @@ class Popup {
1210
1257
  constructor(select = null, options = null, modelManager = null) {
1211
1258
  this.options = null;
1212
1259
  this.isCreated = false;
1213
- /** @type {MixedAdapter | null} */
1214
1260
  this.optionAdapter = null;
1215
- /** @type {HTMLDivElement | null} */
1216
1261
  this.node = null;
1217
- /** @type {EffectorInterface | null} */
1218
1262
  this._effSvc = null;
1219
- /** @type {ResizeObserverService | null} */
1220
1263
  this._resizeObser = null;
1221
1264
  this._parent = null;
1222
- /** @type {OptionHandle | null} */
1223
1265
  this.optionHandle = null;
1224
- /** @type {EmptyState | null} */
1225
1266
  this.emptyState = null;
1226
- /** @type {LoadingState | null} */
1227
1267
  this.loadingState = null;
1228
- /** @type {RecyclerViewContract<MixedAdapter> | null} */
1229
1268
  this.recyclerView = null;
1230
- /** @type {HTMLDivElement | null} */
1231
1269
  this._optionsContainer = null;
1232
1270
  this._scrollListener = null;
1233
- /** @type {ReturnType<typeof setTimeout> | null} */
1234
1271
  this._hideLoadHandle = null;
1235
1272
  this._modelManager = modelManager;
1236
1273
  if (select && options) {
@@ -1449,6 +1486,9 @@ class Popup {
1449
1486
  /**
1450
1487
  * Enables infinite scroll by listening to container scroll events and loading more data
1451
1488
  * when nearing the bottom, respecting pagination state (enabled/loading/hasMore).
1489
+ *
1490
+ * @param searchController - Provides pagination state and a method to load more items.
1491
+ * @param _options - Optional SelectiveOptions (reserved for future behavior tuning).
1452
1492
  */
1453
1493
  setupInfiniteScroll(searchController, _options) {
1454
1494
  if (!this.node)
@@ -1473,6 +1513,65 @@ class Popup {
1473
1513
  };
1474
1514
  this.node.addEventListener("scroll", this._scrollListener);
1475
1515
  }
1516
+ /**
1517
+ * Completely tear down the popup instance and release all resources.
1518
+ *
1519
+ * Responsibilities:
1520
+ * - Clear any pending timeouts and cancel animations/effects.
1521
+ * - Remove event listeners (scroll, mousedown) and disconnect ResizeObserver.
1522
+ * - Unmount and remove the DOM node; sever references to Effector/ModelManager.
1523
+ * - Dispose adapter/recycler and child components (OptionHandle, EmptyState, LoadingState).
1524
+ * - Reset flags and null out references to avoid memory leaks.
1525
+ *
1526
+ * Safe to call multiple times; all operations are guarded via optional chaining.
1527
+ */
1528
+ detroy() {
1529
+ if (this._hideLoadHandle) {
1530
+ clearTimeout(this._hideLoadHandle);
1531
+ this._hideLoadHandle = null;
1532
+ }
1533
+ if (this.node && this._scrollListener) {
1534
+ this.node.removeEventListener("scroll", this._scrollListener);
1535
+ this._scrollListener = null;
1536
+ }
1537
+ try {
1538
+ this._resizeObser?.disconnect();
1539
+ }
1540
+ catch (_) { }
1541
+ this._resizeObser = null;
1542
+ try {
1543
+ this._effSvc?.setElement?.(null);
1544
+ }
1545
+ catch (_) { }
1546
+ this._effSvc = null;
1547
+ if (this.node) {
1548
+ try {
1549
+ const clone = this.node.cloneNode(true);
1550
+ this.node.replaceWith(clone);
1551
+ clone.remove();
1552
+ }
1553
+ catch (_) {
1554
+ this.node.remove();
1555
+ }
1556
+ }
1557
+ this.node = null;
1558
+ this._optionsContainer = null;
1559
+ try {
1560
+ this._modelManager?.skipEvent?.(false);
1561
+ this.recyclerView?.clear?.();
1562
+ this.recyclerView = null;
1563
+ this.optionAdapter = null;
1564
+ this.node.remove();
1565
+ }
1566
+ catch (_) { }
1567
+ this._modelManager = null;
1568
+ this.optionHandle = null;
1569
+ this.emptyState = null;
1570
+ this.loadingState = null;
1571
+ this._parent = null;
1572
+ this.options = null;
1573
+ this.isCreated = false;
1574
+ }
1476
1575
  /**
1477
1576
  * Computes the parent panel's location and box metrics, including size, position,
1478
1577
  * padding, and border, accounting for iOS visual viewport offsets.
@@ -1578,21 +1677,9 @@ class SearchBox {
1578
1677
  * @param {object|null} [options=null] - Configuration (e.g., placeholder, accessibility IDs).
1579
1678
  */
1580
1679
  constructor(options = null) {
1581
- /**
1582
- * @type {MountViewResult<any> | null}
1583
- */
1584
1680
  this.nodeMounted = null;
1585
- /**
1586
- * @type {HTMLDivElement | null}
1587
- */
1588
1681
  this.node = null;
1589
- /**
1590
- * @type {HTMLInputElement | null}
1591
- */
1592
1682
  this.SearchInput = null;
1593
- /**
1594
- * @type {Function|null}
1595
- */
1596
1683
  this.onSearch = null;
1597
1684
  this.options = null;
1598
1685
  this.onNavigate = null;
@@ -1999,7 +2086,9 @@ class EffectorImpl {
1999
2086
  }
2000
2087
  else {
2001
2088
  this._resizeTimeout = setTimeout(() => {
2002
- this.element.style.transition = "none";
2089
+ if (this.element?.style) {
2090
+ this.element.style.transition = "none";
2091
+ }
2003
2092
  }, duration);
2004
2093
  }
2005
2094
  Object.assign(this.element.style, styles);
@@ -2053,7 +2142,6 @@ class Model {
2053
2142
  constructor(options, targetElement = null, view = null) {
2054
2143
  /** @type {TTarget | null} */
2055
2144
  this.targetElement = null;
2056
- /** @type {TView | null} */
2057
2145
  this.view = null;
2058
2146
  this.position = -1;
2059
2147
  this.isInit = false;
@@ -2091,7 +2179,6 @@ class GroupModel extends Model {
2091
2179
  constructor(options, targetElement) {
2092
2180
  super(options, targetElement ?? null, null);
2093
2181
  this.label = "";
2094
- /** @type {OptionModel[]} */
2095
2182
  this.items = [];
2096
2183
  this.collapsed = false;
2097
2184
  this._privOnCollapsedChanged = [];
@@ -2289,7 +2376,7 @@ class OptionModel extends Model {
2289
2376
  * @type {boolean}
2290
2377
  */
2291
2378
  set selectedNonTrigger(value) {
2292
- const input = this.view?.getTag?.("OptionInput");
2379
+ const input = this.view?.view?.tags?.OptionInput;
2293
2380
  const viewEl = this.view?.getView?.();
2294
2381
  if (input)
2295
2382
  input.checked = value;
@@ -2384,7 +2471,7 @@ class OptionModel extends Model {
2384
2471
  onTargetChanged() {
2385
2472
  if (!this.view)
2386
2473
  return;
2387
- const labelContent = this.view.getTag("LabelContent");
2474
+ const labelContent = this.view.view.tags.LabelContent;
2388
2475
  if (labelContent) {
2389
2476
  if (this.options.allowHtml) {
2390
2477
  labelContent.innerHTML = this.text;
@@ -2393,7 +2480,7 @@ class OptionModel extends Model {
2393
2480
  labelContent.textContent = this.textContent;
2394
2481
  }
2395
2482
  }
2396
- const imageTag = this.view.getTag("OptionImage");
2483
+ const imageTag = this.view.view.tags.OptionImage;
2397
2484
  if (imageTag && this.hasImage) {
2398
2485
  imageTag.src = this.imageSrc;
2399
2486
  imageTag.alt = this.text;
@@ -2570,7 +2657,6 @@ class ModelManager {
2570
2657
  });
2571
2658
  let currentGroup = null;
2572
2659
  let position = 0;
2573
- const changesToApply = [];
2574
2660
  modelData.forEach((data) => {
2575
2661
  if (data.tagName === "OPTGROUP") {
2576
2662
  const dataVset = data;
@@ -2579,7 +2665,7 @@ class ModelManager {
2579
2665
  // Label is used as key; keep original behavior.
2580
2666
  const hasLabelChange = existingGroup.label !== dataVset.label;
2581
2667
  if (hasLabelChange) {
2582
- changesToApply.push(() => existingGroup.update(dataVset));
2668
+ existingGroup.update(dataVset);
2583
2669
  }
2584
2670
  existingGroup.position = position;
2585
2671
  existingGroup.items = [];
@@ -2599,17 +2685,8 @@ class ModelManager {
2599
2685
  const key = `${dataVset.value}::${dataVset.text}`;
2600
2686
  const existingOption = oldOptionMap.get(key);
2601
2687
  if (existingOption) {
2602
- const hasSelectedChange = existingOption.selected !== dataVset.selected;
2603
- const hasPositionChange = existingOption.position !== position;
2604
- if (hasSelectedChange || hasPositionChange) {
2605
- changesToApply.push(() => {
2606
- existingOption.update(dataVset);
2607
- existingOption.position = position;
2608
- });
2609
- }
2610
- else {
2611
- existingOption.position = position;
2612
- }
2688
+ existingOption.update(dataVset);
2689
+ existingOption.position = position;
2613
2690
  const parentGroup = dataVset["__parentGroup"];
2614
2691
  if (parentGroup && currentGroup) {
2615
2692
  currentGroup.addItem(existingOption);
@@ -2636,11 +2713,6 @@ class ModelManager {
2636
2713
  position++;
2637
2714
  }
2638
2715
  });
2639
- if (changesToApply.length > 0) {
2640
- requestAnimationFrame(() => {
2641
- changesToApply.forEach((change) => change());
2642
- });
2643
- }
2644
2716
  oldGroupMap.forEach((removedGroup) => {
2645
2717
  removedGroup.view?.getView?.()?.remove?.();
2646
2718
  });
@@ -2683,9 +2755,6 @@ class ModelManager {
2683
2755
  * adapter instance, and recycler view instance.
2684
2756
  */
2685
2757
  getResources() {
2686
- if (!this._privAdapterHandle || !this._privRecyclerViewHandle) {
2687
- throw new Error("ModelManager resources not loaded. Call load() first.");
2688
- }
2689
2758
  return {
2690
2759
  modelList: this._privModelList,
2691
2760
  adapter: this._privAdapterHandle,
@@ -2720,13 +2789,7 @@ class RecyclerView {
2720
2789
  * @param {HTMLDivElement|null} [viewElement=null] - The root element where the adapter will render items.
2721
2790
  */
2722
2791
  constructor(viewElement = null) {
2723
- /**
2724
- * @type {HTMLDivElement|null}
2725
- */
2726
2792
  this.viewElement = null;
2727
- /**
2728
- * @type {TAdapter|null}
2729
- */
2730
2793
  this.adapter = null;
2731
2794
  this.viewElement = viewElement;
2732
2795
  }
@@ -2793,20 +2856,11 @@ class AccessoryBox {
2793
2856
  * @param {object|null} options - Configuration options for the accessory box (e.g., layout and behavior).
2794
2857
  */
2795
2858
  constructor(options = null) {
2796
- /**
2797
- * @type {MountViewResult<any> | null}
2798
- */
2799
2859
  this.nodeMounted = null;
2800
- /**
2801
- * @type {HTMLDivElement | null}
2802
- */
2803
2860
  this.node = null;
2804
2861
  this.options = null;
2805
- /** @type {HTMLDivElement | null} */
2806
2862
  this.selectUIMask = null;
2807
- /** @type {HTMLDivElement | null} */
2808
2863
  this.parentMask = null;
2809
- /** @type {ModelManager<MixedItem> | null} */
2810
2864
  this.modelManager = null;
2811
2865
  if (options)
2812
2866
  this.init(options);
@@ -3507,7 +3561,6 @@ class Adapter {
3507
3561
  * @param {TItem[]} [items=[]] - Initial items to be managed by the adapter.
3508
3562
  */
3509
3563
  constructor(items = []) {
3510
- /** @type {TItem[]} */
3511
3564
  this.items = [];
3512
3565
  this.adapterKey = Libs.randomString(12);
3513
3566
  this.isSkipEvent = false;
@@ -3544,7 +3597,7 @@ class Adapter {
3544
3597
  * @param {Function} callback - Function to execute before the property changes.
3545
3598
  */
3546
3599
  onPropChanging(propName, callback) {
3547
- Libs.timerProcess.setExecute(`${propName}ing_${this.adapterKey}`, callback, 1);
3600
+ Libs.callbackScheduler.on(`${propName}ing_${this.adapterKey}`, callback, { debounce: 1 });
3548
3601
  }
3549
3602
  /**
3550
3603
  * Registers a post-change callback for a property change pipeline.
@@ -3554,7 +3607,7 @@ class Adapter {
3554
3607
  * @param {Function} callback - Function to execute after the property changes.
3555
3608
  */
3556
3609
  onPropChanged(propName, callback) {
3557
- Libs.timerProcess.setExecute(`${propName}_${this.adapterKey}`, callback);
3610
+ Libs.callbackScheduler.on(`${propName}_${this.adapterKey}`, callback);
3558
3611
  }
3559
3612
  /**
3560
3613
  * Triggers the post-change pipeline for a given property, passing optional parameters
@@ -3564,7 +3617,7 @@ class Adapter {
3564
3617
  * @param {...any} params - Parameters forwarded to the callbacks.
3565
3618
  */
3566
3619
  changeProp(propName, ...params) {
3567
- Libs.timerProcess.run(`${propName}_${this.adapterKey}`, ...params);
3620
+ Libs.callbackScheduler.run(`${propName}_${this.adapterKey}`, ...params);
3568
3621
  }
3569
3622
  /**
3570
3623
  * Triggers the pre-change pipeline for a given property, passing optional parameters
@@ -3574,7 +3627,7 @@ class Adapter {
3574
3627
  * @param {...any} params - Parameters forwarded to the callbacks.
3575
3628
  */
3576
3629
  changingProp(propName, ...params) {
3577
- Libs.timerProcess.run(`${propName}ing_${this.adapterKey}`, ...params);
3630
+ Libs.callbackScheduler.run(`${propName}ing_${this.adapterKey}`, ...params);
3578
3631
  }
3579
3632
  /**
3580
3633
  * Creates and returns a viewer instance for the given item within the specified parent container.
@@ -3654,9 +3707,7 @@ class View {
3654
3707
  * @param {HTMLElement} parent - The parent element into which this view will render.
3655
3708
  */
3656
3709
  constructor(parent) {
3657
- /** @type {HTMLElement|null} */
3658
3710
  this.parent = null;
3659
- /** @type {MountViewResult<TTags> | null} */
3660
3711
  this.view = null;
3661
3712
  this.parent = parent;
3662
3713
  }
@@ -3680,28 +3731,6 @@ class View {
3680
3731
  throw new Error("View is not mounted. Did you forget to set this.view?");
3681
3732
  return this.view.view;
3682
3733
  }
3683
- /**
3684
- * Retrieves a single tagged element from the mounted view.
3685
- *
3686
- * @template K
3687
- * @param {K} tag - The tag key corresponding to the desired element.
3688
- * @returns {TTags[K]} - The element associated with the provided tag key.
3689
- */
3690
- getTag(tag) {
3691
- if (!this.view)
3692
- throw new Error("View is not mounted. Did you forget to set this.view?");
3693
- return this.view.tags[tag];
3694
- }
3695
- /**
3696
- * Retrieves the full tag map for the mounted view.
3697
- *
3698
- * @returns {TTags} - An object map of all tagged elements.
3699
- */
3700
- getTags() {
3701
- if (!this.view)
3702
- throw new Error("View is not mounted. Did you forget to set this.view?");
3703
- return this.view.tags;
3704
- }
3705
3734
  }
3706
3735
 
3707
3736
  /**
@@ -3710,7 +3739,6 @@ class View {
3710
3739
  class GroupView extends View {
3711
3740
  constructor() {
3712
3741
  super(...arguments);
3713
- /** @type {GroupViewResult} */
3714
3742
  this.view = null;
3715
3743
  }
3716
3744
  /**
@@ -4158,7 +4186,7 @@ class MixedAdapter extends Adapter {
4158
4186
  _handleGroupView(groupModel, groupView, position) {
4159
4187
  super.onViewHolder(groupModel, groupView, position);
4160
4188
  groupModel.view = groupView;
4161
- const header = groupView.getTag("GroupHeader");
4189
+ const header = groupView.view.tags.GroupHeader;
4162
4190
  header.textContent = groupModel.label;
4163
4191
  if (!groupModel.isInit) {
4164
4192
  header.style.cursor = "pointer";
@@ -4210,7 +4238,7 @@ class MixedAdapter extends Adapter {
4210
4238
  }
4211
4239
  optionModel.view = optionViewer;
4212
4240
  if (optionModel.hasImage) {
4213
- const imageTag = optionViewer.getTag("OptionImage");
4241
+ const imageTag = optionViewer.view.tags.OptionImage;
4214
4242
  if (imageTag) {
4215
4243
  if (imageTag.src !== optionModel.imageSrc)
4216
4244
  imageTag.src = optionModel.imageSrc;
@@ -4218,9 +4246,9 @@ class MixedAdapter extends Adapter {
4218
4246
  imageTag.alt = optionModel.text;
4219
4247
  }
4220
4248
  }
4221
- optionViewer.getTag("LabelContent").innerHTML = optionModel.text;
4249
+ optionViewer.view.tags.LabelContent.innerHTML = optionModel.text;
4222
4250
  if (!optionModel.isInit) {
4223
- optionViewer.getTag("OptionView").addEventListener("click", (ev) => {
4251
+ optionViewer.view.tags.OptionView.addEventListener("click", (ev) => {
4224
4252
  ev.stopPropagation();
4225
4253
  ev.preventDefault();
4226
4254
  if (this.isSkipEvent)
@@ -4240,8 +4268,8 @@ class MixedAdapter extends Adapter {
4240
4268
  }, 5);
4241
4269
  }
4242
4270
  });
4243
- optionViewer.getTag("OptionView").title = optionModel.textContent;
4244
- optionViewer.getTag("OptionView").addEventListener("mouseenter", () => {
4271
+ optionViewer.view.tags.OptionView.title = optionModel.textContent;
4272
+ optionViewer.view.tags.OptionView.addEventListener("mouseenter", () => {
4245
4273
  if (this.isSkipEvent)
4246
4274
  return;
4247
4275
  this.setHighlight(this.flatOptions.indexOf(optionModel), false);
@@ -4464,10 +4492,8 @@ class SelectBox {
4464
4492
  constructor(select = null, Selective = null) {
4465
4493
  this.container = {};
4466
4494
  this.oldValue = null;
4467
- /** @type {HTMLDivElement|null} */
4468
4495
  this.node = null;
4469
4496
  this.options = null;
4470
- /** @type {ModelManager<MixedItem, MixedAdapter> | null} */
4471
4497
  this.optionModelManager = null;
4472
4498
  this.isOpen = false;
4473
4499
  this.hasLoadedOnce = false;
@@ -5044,6 +5070,9 @@ class SelectBox {
5044
5070
  }
5045
5071
  return flatOptions;
5046
5072
  }
5073
+ detroy() {
5074
+ this.container.popup.detroy();
5075
+ }
5047
5076
  }
5048
5077
 
5049
5078
  /**
@@ -5052,9 +5081,7 @@ class SelectBox {
5052
5081
  class ElementAdditionObserver {
5053
5082
  constructor() {
5054
5083
  this._isActive = false;
5055
- /** @type {MutationObserver|null} */
5056
5084
  this._observer = null;
5057
- /** @type {Array<(el: T) => void>} */
5058
5085
  this._actions = [];
5059
5086
  }
5060
5087
  /**
@@ -5139,9 +5166,9 @@ class Selective {
5139
5166
  merged.on.load = (merged.on.load ?? []);
5140
5167
  this.bindedQueries.set(query, merged);
5141
5168
  const doneToken = Libs.randomString();
5142
- Libs.timerProcess.setExecute(doneToken, () => {
5169
+ Libs.callbackScheduler.on(doneToken, () => {
5143
5170
  iEvents.callEvent([this.find(query)], ...merged.on.load);
5144
- Libs.timerProcess.clearExecute(doneToken);
5171
+ Libs.callbackScheduler.clear(doneToken);
5145
5172
  merged.on.load = [];
5146
5173
  });
5147
5174
  const selectElements = Libs.getElements(query);
@@ -5150,7 +5177,7 @@ class Selective {
5150
5177
  if (item.tagName === "SELECT") {
5151
5178
  Libs.removeUnbinderMap(item);
5152
5179
  if (this.applySelectBox(item, merged)) {
5153
- Libs.timerProcess.run(doneToken);
5180
+ Libs.callbackScheduler.run(doneToken);
5154
5181
  }
5155
5182
  }
5156
5183
  })();
@@ -5275,6 +5302,8 @@ class Selective {
5275
5302
  const bindMap = Libs.getBinderMap(selectElement);
5276
5303
  if (!bindMap)
5277
5304
  return;
5305
+ const popup = bindMap.container?.popup;
5306
+ popup?.detroy();
5278
5307
  Libs.setUnbinderMap(selectElement, bindMap);
5279
5308
  const wasObserving = !!this.EAObserver;
5280
5309
  if (wasObserving)
@@ -5415,7 +5444,6 @@ Selective.bindedQueries = new Map();
5415
5444
 
5416
5445
  /**
5417
5446
  * Checks for a previously loaded global library instance by name.
5418
- * If found (with `__loaded` flag), logs a warning and returns true; otherwise
5419
5447
  * initializes a loading placeholder on `window[name]` and returns false.
5420
5448
  *
5421
5449
  * @param {string} LIB_NAME - The global namespace key to check on `window`.
@@ -5425,13 +5453,12 @@ function checkDuplicate(LIB_NAME) {
5425
5453
  if (typeof window === "undefined")
5426
5454
  return false;
5427
5455
  const existing = window[LIB_NAME];
5428
- if (existing && existing.__loaded) {
5429
- console.warn(`[${LIB_NAME}] Already loaded (v${existing.__version}). ` +
5456
+ if (existing) {
5457
+ console.warn(`[${LIB_NAME}] Already loaded (v${existing.version}). ` +
5430
5458
  `Using existing instance. Please remove duplicate <script> tags.`);
5431
5459
  return true;
5432
5460
  }
5433
5461
  const base = existing ?? {};
5434
- base.__loading = true;
5435
5462
  window[LIB_NAME] = base;
5436
5463
  return false;
5437
5464
  }
@@ -5449,16 +5476,14 @@ function markLoaded(name, version, api) {
5449
5476
  if (typeof window === "undefined")
5450
5477
  return;
5451
5478
  const ns = (window[name] ?? {});
5452
- ns.__loaded = true;
5453
- ns.__loading = false;
5454
- ns.__version = version;
5479
+ ns.version = version;
5455
5480
  Object.assign(ns, api);
5456
5481
  Object.freeze(ns);
5457
5482
  window[name] = ns;
5458
5483
  console.log(`[${name}] v${version} loaded successfully`);
5459
5484
  }
5460
5485
 
5461
- const version = "1.1.0";
5486
+ const version = "1.1.2";
5462
5487
  const name = "SelectiveUI";
5463
5488
  const alreadyLoaded = checkDuplicate(name);
5464
5489
  function getGlobal() {
@@ -5490,7 +5515,7 @@ function find(query) {
5490
5515
  * Destroys Selective instances associated with the given query.
5491
5516
  * Proxies to a global loaded instance if available; otherwise uses local Selective.destroy.
5492
5517
  */
5493
- function destroy(query) {
5518
+ function destroy(query = null) {
5494
5519
  const global = getGlobal();
5495
5520
  if (alreadyLoaded && global)
5496
5521
  return global.destroy(query);
@@ -5517,11 +5542,13 @@ function effector(element) {
5517
5542
  return Effector(element);
5518
5543
  }
5519
5544
  if (!alreadyLoaded) {
5520
- let initialized = false;
5545
+ const api = { bind, find, destroy, rebind, effector, version };
5546
+ markLoaded(name, version, api);
5547
+ let domInitialized = false;
5521
5548
  function init() {
5522
- if (initialized)
5549
+ if (domInitialized)
5523
5550
  return;
5524
- initialized = true;
5551
+ domInitialized = true;
5525
5552
  document.addEventListener("mousedown", () => {
5526
5553
  const sels = Libs.getBindedCommand();
5527
5554
  if (sels.length > 0) {
@@ -5531,8 +5558,6 @@ if (!alreadyLoaded) {
5531
5558
  }
5532
5559
  });
5533
5560
  Selective.Observer();
5534
- const api = { bind, find, destroy, rebind, effector, version };
5535
- markLoaded(name, version, api);
5536
5561
  }
5537
5562
  if (document.readyState === "loading") {
5538
5563
  document.addEventListener("DOMContentLoaded", init);