selective-ui 1.0.2 → 1.0.3

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 (67) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +7 -2
  3. package/dist/selective-ui.css +567 -567
  4. package/dist/selective-ui.css.map +1 -1
  5. package/dist/selective-ui.esm.js +6046 -6046
  6. package/dist/selective-ui.esm.js.map +1 -1
  7. package/dist/selective-ui.esm.min.js +1 -1
  8. package/dist/selective-ui.esm.min.js.br +0 -0
  9. package/dist/selective-ui.min.js +1 -1
  10. package/dist/selective-ui.min.js.br +0 -0
  11. package/dist/selective-ui.umd.js +6046 -6046
  12. package/dist/selective-ui.umd.js.map +1 -1
  13. package/package.json +68 -68
  14. package/src/css/components/accessorybox.css +63 -63
  15. package/src/css/components/directive.css +19 -19
  16. package/src/css/components/empty-state.css +25 -25
  17. package/src/css/components/loading-state.css +25 -25
  18. package/src/css/components/optgroup.css +61 -61
  19. package/src/css/components/option-handle.css +33 -33
  20. package/src/css/components/option.css +129 -129
  21. package/src/css/components/placeholder.css +14 -14
  22. package/src/css/components/popup.css +38 -38
  23. package/src/css/components/searchbox.css +28 -28
  24. package/src/css/components/selectbox.css +53 -53
  25. package/src/css/index.css +74 -74
  26. package/src/js/adapter/mixed-adapter.js +434 -434
  27. package/src/js/components/accessorybox.js +124 -124
  28. package/src/js/components/directive.js +37 -37
  29. package/src/js/components/empty-state.js +67 -67
  30. package/src/js/components/loading-state.js +59 -59
  31. package/src/js/components/option-handle.js +113 -113
  32. package/src/js/components/placeholder.js +56 -56
  33. package/src/js/components/popup.js +470 -470
  34. package/src/js/components/searchbox.js +167 -167
  35. package/src/js/components/selectbox.js +692 -692
  36. package/src/js/core/base/adapter.js +162 -162
  37. package/src/js/core/base/model.js +59 -59
  38. package/src/js/core/base/recyclerview.js +82 -82
  39. package/src/js/core/base/view.js +62 -62
  40. package/src/js/core/model-manager.js +286 -286
  41. package/src/js/core/search-controller.js +521 -521
  42. package/src/js/index.js +136 -136
  43. package/src/js/models/group-model.js +142 -142
  44. package/src/js/models/option-model.js +236 -236
  45. package/src/js/services/dataset-observer.js +73 -73
  46. package/src/js/services/ea-observer.js +87 -87
  47. package/src/js/services/effector.js +403 -403
  48. package/src/js/services/refresher.js +39 -39
  49. package/src/js/services/resize-observer.js +151 -151
  50. package/src/js/services/select-observer.js +60 -60
  51. package/src/js/types/adapter.type.js +32 -32
  52. package/src/js/types/effector.type.js +23 -23
  53. package/src/js/types/ievents.type.js +10 -10
  54. package/src/js/types/libs.type.js +27 -27
  55. package/src/js/types/model.type.js +11 -11
  56. package/src/js/types/recyclerview.type.js +11 -11
  57. package/src/js/types/resize-observer.type.js +18 -18
  58. package/src/js/types/view.group.type.js +12 -12
  59. package/src/js/types/view.option.type.js +14 -14
  60. package/src/js/types/view.type.js +10 -10
  61. package/src/js/utils/guard.js +46 -46
  62. package/src/js/utils/ievents.js +83 -83
  63. package/src/js/utils/istorage.js +60 -60
  64. package/src/js/utils/libs.js +618 -618
  65. package/src/js/utils/selective.js +385 -385
  66. package/src/js/views/group-view.js +102 -102
  67. package/src/js/views/option-view.js +152 -152
@@ -1,386 +1,386 @@
1
- import {Libs} from "./libs.js";
2
- import {iEvents} from "./ievents.js";
3
- import {SelectBox} from "../components/selectbox.js";
4
- import { ElementAdditionObserver } from "../services/ea-observer.js";
5
-
6
- /**
7
- * @class
8
- */
9
- export class Selective {
10
- /** @type {ElementAdditionObserver} */
11
- EAObserver = null;
12
-
13
- static bindedQueries = new Map();
14
-
15
- /**
16
- * Binds Selective UI to all <select> elements matching the query.
17
- * Merges provided options with defaults, schedules `on.load` callbacks,
18
- * initializes each matching select, and records the binding for auto-rebinding.
19
- *
20
- * @param {string} query - CSS selector for target <select> elements.
21
- * @param {object} options - Configuration overrides merged with defaults.
22
- */
23
- static bind(query, options) {
24
- options = Libs.mergeConfig(Libs.getDefaultConfig(), options);
25
-
26
- this.bindedQueries.set(query, options);
27
-
28
- const superThis = this;
29
- const doneToken = Libs.randomString();
30
- Libs.timerProcess.setExecute(doneToken, () => {
31
- iEvents.callEvent([superThis.find(query)], ...options.on.load);
32
- Libs.timerProcess.clearExecute(doneToken);
33
- options.on.load = [];
34
- });
35
-
36
- /** @type {HTMLSelectElement[]} */
37
- const selectElements = /** @type {HTMLSelectElement[]} */ (Libs.getElements(query));
38
-
39
- selectElements.forEach(item => {
40
- (async() => {
41
- if ("SELECT" == item.tagName) {
42
- Libs.removeUnbinderMap(item);
43
- if (this.applySelectBox(item, options)) {
44
- Libs.timerProcess.run(doneToken);
45
- }
46
- }
47
- })();
48
- });
49
-
50
- if (!Libs.getBindedCommand().includes(query)) {
51
- Libs.getBindedCommand().push(query);
52
- }
53
- }
54
-
55
-
56
- /**
57
- * A dynamic action API object produced by `find()`.
58
- * Contains `isEmpty` plus dynamically attached properties (get/set or functions).
59
- *
60
- * @typedef {Record<string, object> & { isEmpty: boolean }} ActionApi
61
- *
62
- * Finds the first bound SelectBox actions for a given query (or all bound queries if "*").
63
- * Returns an API object with methods assembled from the bound action definitions.
64
- *
65
- * @param {string} [query="*"] - CSS selector or "*" to search all bound instances.
66
- * @returns {ActionApi} - Aggregated actions; {isEmpty:true} if none found.
67
- */
68
- static find(query = "*") {
69
- let actions = { isEmpty: true };
70
-
71
- if (query == "*") {
72
- query = Libs.getBindedCommand().join(", ");
73
- if (query == "") {
74
- return actions;
75
- }
76
- }
77
-
78
- const sels = /** @type {HTMLElement[]} */ (Libs.getElements(query));
79
- if (sels.length == 0) {
80
- return actions;
81
- }
82
-
83
- const binded = Libs.getBinderMap(sels[0]);
84
- if (!binded) {
85
- return actions;
86
- }
87
-
88
- for (let actionName in binded.action) {
89
- actions[actionName] = this.#getProperties(actionName, binded.action)
90
- }
91
- Object.keys(binded.action);
92
-
93
- if (actions) {
94
- /** @type {ActionApi} */
95
- let response = {};
96
- for (let actionKey in actions) {
97
-
98
- /** @type {IPropertiesType} */
99
- const action = actions[actionKey];
100
-
101
- switch (action.type) {
102
- case "get-set":
103
- this.#buildGetSetAction(response, action.name, sels);
104
- break;
105
-
106
- case "func":
107
- this.#buildFuntionAction(response, action.name, sels);
108
- break;
109
-
110
- default:
111
- break;
112
- }
113
- }
114
-
115
- response.isEmpty = false;
116
- return response;
117
- }
118
- }
119
-
120
- /**
121
- * Starts observing the document for newly added <select> elements and applies
122
- * Selective bindings automatically when they match previously bound queries.
123
- */
124
- static Observer() {
125
- this.EAObserver = new ElementAdditionObserver();
126
- this.EAObserver.onDetect((selectElement) => {
127
- this.bindedQueries.forEach((options, query) => {
128
- try {
129
- if (selectElement.matches(query)) {
130
- this.applySelectBox(selectElement, options);
131
- }
132
- } catch (error) {
133
- console.warn(`Invalid selector: ${query}`, error);
134
- }
135
- });
136
- });
137
- this.EAObserver.start("select");
138
- }
139
-
140
- /**
141
- * Destroys Selective instances. Supports:
142
- * - destroyAll(): when target is null,
143
- * - destroyByQuery(): when target is a selector string,
144
- * - destroyElement(): when target is an HTMLSelectElement.
145
- *
146
- * @param {null|string|HTMLSelectElement} target - Target to destroy.
147
- */
148
- static destroy(target = null) {
149
- if (target === null) {
150
- this.destroyAll();
151
- } else if (typeof target === "string") {
152
- this.destroyByQuery(target);
153
- } else if (target instanceof HTMLSelectElement) {
154
- this.destroyElement(target);
155
- }
156
- }
157
-
158
- /**
159
- * Destroys all bound Selective instances and clears bindings/state.
160
- * Stops the ElementAdditionObserver.
161
- */
162
- static destroyAll() {
163
- const bindedCommands = Libs.getBindedCommand();
164
-
165
- bindedCommands.forEach(query => {
166
- this.destroyByQuery(query);
167
- });
168
-
169
- this.bindedQueries.clear();
170
- Libs.getBindedCommand().length = 0;
171
-
172
- this.EAObserver.stop();
173
- }
174
-
175
- /**
176
- * Destroys Selective instances bound to the specified query and removes
177
- * the query from the binding registry.
178
- *
179
- * @param {string} query - CSS selector whose Selective instances should be destroyed.
180
- */
181
- static destroyByQuery(query) {
182
- const selectElements = /** @type {HTMLSelectElement[]} */ (Libs.getElements(query));
183
-
184
- selectElements.forEach(element => {
185
- if (element.tagName === "SELECT") {
186
- this.destroyElement(element);
187
- }
188
- });
189
-
190
- this.bindedQueries.delete(query);
191
- const commands = Libs.getBindedCommand();
192
- const index = commands.indexOf(query);
193
- if (index > -1) {
194
- commands.splice(index, 1);
195
- }
196
- }
197
-
198
- /**
199
- * Destroys a single Selective instance attached to the given <select> element,
200
- * detaches UI, restores original select state, and removes binder map entry.
201
- *
202
- * @param {HTMLSelectElement} selectElement - The target <select> element to clean up.
203
- */
204
- static destroyElement(selectElement) {
205
- const bindMap = Libs.getBinderMap(selectElement);
206
- if (!bindMap) return;
207
-
208
- Libs.setUnbinderMap(selectElement, bindMap);
209
-
210
- const wasObserving = !!this.EAObserver;
211
- if (wasObserving) this.EAObserver.stop();
212
-
213
- try { bindMap.self.deInit?.(); } catch (_) {}
214
-
215
- const wrapper = bindMap.container?.element || selectElement.parentElement;
216
-
217
- selectElement.style.display = "";
218
- selectElement.style.visibility = "";
219
- selectElement.disabled = false;
220
- delete selectElement.dataset.selectiveId;
221
-
222
- if (wrapper && wrapper.parentNode) {
223
- wrapper.parentNode.replaceChild(selectElement, wrapper);
224
- } else {
225
- document.body.appendChild(selectElement);
226
- }
227
-
228
- Libs.removeBinderMap(selectElement);
229
-
230
- if (wasObserving && this.bindedQueries.size > 0) {
231
- this.EAObserver.start("select");
232
- }
233
- }
234
-
235
- /**
236
- * Unbinds a previously bound query from auto-apply and auto-observe lists.
237
- * Stops the observer when no bound queries remain.
238
- *
239
- * @param {string} query - The CSS selector to unbind.
240
- */
241
- // static unbind(query) {
242
- // this.bindedQueries.delete(query);
243
-
244
- // const commands = Libs.getBindedCommand();
245
- // const index = commands.indexOf(query);
246
- // if (index > -1) {
247
- // commands.splice(index, 1);
248
- // }
249
-
250
- // if (this.bindedQueries.size === 0) {
251
- // this.EAObserver.stop();
252
- // }
253
- // }
254
-
255
- /**
256
- * Rebinds a query by destroying existing instances and binding anew
257
- * with the provided options.
258
- *
259
- * @param {string} query - CSS selector to rebind.
260
- * @param {object} options - Configuration for the new binding.
261
- */
262
- static rebind(query, options) {
263
- this.destroyByQuery(query);
264
- this.bind(query, options);
265
- }
266
-
267
- /**
268
- * Applies SelectBox enhancement to a single <select> element:
269
- * builds per-instance IDs, merges element dataset into options,
270
- * creates SelectBox, stores binder map with action API, and wires toggle on mouseup.
271
- *
272
- * @param {HTMLSelectElement} selectElement - The native <select> to enhance.
273
- * @param {object} options - Configuration used for this instance.
274
- * @returns {boolean} - False if already bound; true if successfully applied.
275
- */
276
- static applySelectBox(selectElement, options) {
277
- if (Libs.getBinderMap(selectElement) || Libs.getUnbinderMap(selectElement)) {
278
- return false;
279
- }
280
-
281
- const SEID = Libs.randomString(8);
282
- const options_cfg = Libs.buildConfig(selectElement, options);
283
- options_cfg.SEID = SEID
284
- options_cfg.SEID_LIST = `seui-${SEID}-optionlist`;
285
- options_cfg.SEID_HOLDER = `seui-${SEID}-placeholder`;
286
- const bindMap = {options: options_cfg};
287
-
288
- Libs.setBinderMap(selectElement, bindMap);
289
- const selectBox = new SelectBox(selectElement, this);
290
- bindMap.container = selectBox.container;
291
- bindMap.action = selectBox.getAction();
292
- bindMap.self = selectBox;
293
-
294
- selectBox.container.view.addEventListener("mouseup", () => {
295
- bindMap.action.toggle();
296
- });
297
-
298
- return true;
299
- }
300
-
301
- /**
302
- * Determines the property type for an action name on the provided object.
303
- * Classifies as:
304
- * - "get-set" when a getter exists (or a setter with non-function value),
305
- * - "func" when the property is a function,
306
- * - "variable" otherwise.
307
- *
308
- * @typedef {Object} IPropertiesType
309
- * @property {string} type - One of "variable" | "get-set" | "func".
310
- * @property {string} name - The original action name.
311
- *
312
- * @param {string} actionName - The property key to inspect.
313
- * @param {*} action - The object containing the property.
314
- * @returns {IPropertiesType} - The derived property type and name.
315
- */
316
- static #getProperties(actionName, action) {
317
- const descriptor = Object.getOwnPropertyDescriptor(action, actionName);
318
- let type = "variable";
319
-
320
- if (descriptor.get || (descriptor.set && typeof action[actionName] !== "function")) {
321
- type = "get-set";
322
- }
323
- else if (typeof action[actionName] === "function") {
324
- type = "func";
325
- }
326
-
327
- return {
328
- type,
329
- name: actionName
330
- }
331
- }
332
-
333
- /**
334
- * Defines a get/set property on the target object that proxies to each bound element's action API.
335
- * Getter reads from the first element; setter writes the value to all elements.
336
- *
337
- * @param {Object} object - The target object to define the property on.
338
- * @param {string} name - The property name to expose.
339
- * @param {HTMLElement[]} els - The list of bound elements to proxy.
340
- */
341
- static #buildGetSetAction(object, name, els) {
342
- Object.defineProperty(object, name, {
343
- get() {
344
- const binded = Libs.getBinderMap(els[0]);
345
- return binded.action[name];
346
- },
347
- set(value) {
348
- els.forEach(el => {
349
- const binded = Libs.getBinderMap(el);
350
- if (binded) {
351
- binded.action[name] = value;
352
- }
353
- });
354
- },
355
- enumerable: true,
356
- configurable: true
357
- });
358
- }
359
-
360
- /**
361
- * Creates a function on the target object that invokes the corresponding action
362
- * across all bound elements in order, respecting the event token flow control.
363
- * Stops iteration if the token indicates propagation should not continue.
364
- *
365
- * @param {Object} object - The target object to attach the function to.
366
- * @param {string} name - The function name to expose.
367
- * @param {HTMLElement[]} els - The list of bound elements to invoke against.
368
- */
369
- static #buildFuntionAction(object, name, els) {
370
- object[name] = (...params) => {
371
- for (let index = 0; index < els.length; index++) {
372
- const el = els[index];
373
- const binded = Libs.getBinderMap(el);
374
- if (!binded) {
375
- continue;
376
- }
377
- const evtToken = iEvents.buildEventToken();
378
- binded.action[name](evtToken.callback, ...params)
379
- if (!evtToken.token.isContinue) {
380
- break;
381
- }
382
- }
383
- return object;
384
- }
385
- }
1
+ import {Libs} from "./libs.js";
2
+ import {iEvents} from "./ievents.js";
3
+ import {SelectBox} from "../components/selectbox.js";
4
+ import { ElementAdditionObserver } from "../services/ea-observer.js";
5
+
6
+ /**
7
+ * @class
8
+ */
9
+ export class Selective {
10
+ /** @type {ElementAdditionObserver} */
11
+ EAObserver = null;
12
+
13
+ static bindedQueries = new Map();
14
+
15
+ /**
16
+ * Binds Selective UI to all <select> elements matching the query.
17
+ * Merges provided options with defaults, schedules `on.load` callbacks,
18
+ * initializes each matching select, and records the binding for auto-rebinding.
19
+ *
20
+ * @param {string} query - CSS selector for target <select> elements.
21
+ * @param {object} options - Configuration overrides merged with defaults.
22
+ */
23
+ static bind(query, options) {
24
+ options = Libs.mergeConfig(Libs.getDefaultConfig(), options);
25
+
26
+ this.bindedQueries.set(query, options);
27
+
28
+ const superThis = this;
29
+ const doneToken = Libs.randomString();
30
+ Libs.timerProcess.setExecute(doneToken, () => {
31
+ iEvents.callEvent([superThis.find(query)], ...options.on.load);
32
+ Libs.timerProcess.clearExecute(doneToken);
33
+ options.on.load = [];
34
+ });
35
+
36
+ /** @type {HTMLSelectElement[]} */
37
+ const selectElements = /** @type {HTMLSelectElement[]} */ (Libs.getElements(query));
38
+
39
+ selectElements.forEach(item => {
40
+ (async() => {
41
+ if ("SELECT" == item.tagName) {
42
+ Libs.removeUnbinderMap(item);
43
+ if (this.applySelectBox(item, options)) {
44
+ Libs.timerProcess.run(doneToken);
45
+ }
46
+ }
47
+ })();
48
+ });
49
+
50
+ if (!Libs.getBindedCommand().includes(query)) {
51
+ Libs.getBindedCommand().push(query);
52
+ }
53
+ }
54
+
55
+
56
+ /**
57
+ * A dynamic action API object produced by `find()`.
58
+ * Contains `isEmpty` plus dynamically attached properties (get/set or functions).
59
+ *
60
+ * @typedef {Record<string, object> & { isEmpty: boolean }} ActionApi
61
+ *
62
+ * Finds the first bound SelectBox actions for a given query (or all bound queries if "*").
63
+ * Returns an API object with methods assembled from the bound action definitions.
64
+ *
65
+ * @param {string} [query="*"] - CSS selector or "*" to search all bound instances.
66
+ * @returns {ActionApi} - Aggregated actions; {isEmpty:true} if none found.
67
+ */
68
+ static find(query = "*") {
69
+ let actions = { isEmpty: true };
70
+
71
+ if (query == "*") {
72
+ query = Libs.getBindedCommand().join(", ");
73
+ if (query == "") {
74
+ return actions;
75
+ }
76
+ }
77
+
78
+ const sels = /** @type {HTMLElement[]} */ (Libs.getElements(query));
79
+ if (sels.length == 0) {
80
+ return actions;
81
+ }
82
+
83
+ const binded = Libs.getBinderMap(sels[0]);
84
+ if (!binded) {
85
+ return actions;
86
+ }
87
+
88
+ for (let actionName in binded.action) {
89
+ actions[actionName] = this.#getProperties(actionName, binded.action)
90
+ }
91
+ Object.keys(binded.action);
92
+
93
+ if (actions) {
94
+ /** @type {ActionApi} */
95
+ let response = {};
96
+ for (let actionKey in actions) {
97
+
98
+ /** @type {IPropertiesType} */
99
+ const action = actions[actionKey];
100
+
101
+ switch (action.type) {
102
+ case "get-set":
103
+ this.#buildGetSetAction(response, action.name, sels);
104
+ break;
105
+
106
+ case "func":
107
+ this.#buildFuntionAction(response, action.name, sels);
108
+ break;
109
+
110
+ default:
111
+ break;
112
+ }
113
+ }
114
+
115
+ response.isEmpty = false;
116
+ return response;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Starts observing the document for newly added <select> elements and applies
122
+ * Selective bindings automatically when they match previously bound queries.
123
+ */
124
+ static Observer() {
125
+ this.EAObserver = new ElementAdditionObserver();
126
+ this.EAObserver.onDetect((selectElement) => {
127
+ this.bindedQueries.forEach((options, query) => {
128
+ try {
129
+ if (selectElement.matches(query)) {
130
+ this.applySelectBox(selectElement, options);
131
+ }
132
+ } catch (error) {
133
+ console.warn(`Invalid selector: ${query}`, error);
134
+ }
135
+ });
136
+ });
137
+ this.EAObserver.start("select");
138
+ }
139
+
140
+ /**
141
+ * Destroys Selective instances. Supports:
142
+ * - destroyAll(): when target is null,
143
+ * - destroyByQuery(): when target is a selector string,
144
+ * - destroyElement(): when target is an HTMLSelectElement.
145
+ *
146
+ * @param {null|string|HTMLSelectElement} target - Target to destroy.
147
+ */
148
+ static destroy(target = null) {
149
+ if (target === null) {
150
+ this.destroyAll();
151
+ } else if (typeof target === "string") {
152
+ this.destroyByQuery(target);
153
+ } else if (target instanceof HTMLSelectElement) {
154
+ this.destroyElement(target);
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Destroys all bound Selective instances and clears bindings/state.
160
+ * Stops the ElementAdditionObserver.
161
+ */
162
+ static destroyAll() {
163
+ const bindedCommands = Libs.getBindedCommand();
164
+
165
+ bindedCommands.forEach(query => {
166
+ this.destroyByQuery(query);
167
+ });
168
+
169
+ this.bindedQueries.clear();
170
+ Libs.getBindedCommand().length = 0;
171
+
172
+ this.EAObserver.stop();
173
+ }
174
+
175
+ /**
176
+ * Destroys Selective instances bound to the specified query and removes
177
+ * the query from the binding registry.
178
+ *
179
+ * @param {string} query - CSS selector whose Selective instances should be destroyed.
180
+ */
181
+ static destroyByQuery(query) {
182
+ const selectElements = /** @type {HTMLSelectElement[]} */ (Libs.getElements(query));
183
+
184
+ selectElements.forEach(element => {
185
+ if (element.tagName === "SELECT") {
186
+ this.destroyElement(element);
187
+ }
188
+ });
189
+
190
+ this.bindedQueries.delete(query);
191
+ const commands = Libs.getBindedCommand();
192
+ const index = commands.indexOf(query);
193
+ if (index > -1) {
194
+ commands.splice(index, 1);
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Destroys a single Selective instance attached to the given <select> element,
200
+ * detaches UI, restores original select state, and removes binder map entry.
201
+ *
202
+ * @param {HTMLSelectElement} selectElement - The target <select> element to clean up.
203
+ */
204
+ static destroyElement(selectElement) {
205
+ const bindMap = Libs.getBinderMap(selectElement);
206
+ if (!bindMap) return;
207
+
208
+ Libs.setUnbinderMap(selectElement, bindMap);
209
+
210
+ const wasObserving = !!this.EAObserver;
211
+ if (wasObserving) this.EAObserver.stop();
212
+
213
+ try { bindMap.self.deInit?.(); } catch (_) {}
214
+
215
+ const wrapper = bindMap.container?.element || selectElement.parentElement;
216
+
217
+ selectElement.style.display = "";
218
+ selectElement.style.visibility = "";
219
+ selectElement.disabled = false;
220
+ delete selectElement.dataset.selectiveId;
221
+
222
+ if (wrapper && wrapper.parentNode) {
223
+ wrapper.parentNode.replaceChild(selectElement, wrapper);
224
+ } else {
225
+ document.body.appendChild(selectElement);
226
+ }
227
+
228
+ Libs.removeBinderMap(selectElement);
229
+
230
+ if (wasObserving && this.bindedQueries.size > 0) {
231
+ this.EAObserver.start("select");
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Unbinds a previously bound query from auto-apply and auto-observe lists.
237
+ * Stops the observer when no bound queries remain.
238
+ *
239
+ * @param {string} query - The CSS selector to unbind.
240
+ */
241
+ // static unbind(query) {
242
+ // this.bindedQueries.delete(query);
243
+
244
+ // const commands = Libs.getBindedCommand();
245
+ // const index = commands.indexOf(query);
246
+ // if (index > -1) {
247
+ // commands.splice(index, 1);
248
+ // }
249
+
250
+ // if (this.bindedQueries.size === 0) {
251
+ // this.EAObserver.stop();
252
+ // }
253
+ // }
254
+
255
+ /**
256
+ * Rebinds a query by destroying existing instances and binding anew
257
+ * with the provided options.
258
+ *
259
+ * @param {string} query - CSS selector to rebind.
260
+ * @param {object} options - Configuration for the new binding.
261
+ */
262
+ static rebind(query, options) {
263
+ this.destroyByQuery(query);
264
+ this.bind(query, options);
265
+ }
266
+
267
+ /**
268
+ * Applies SelectBox enhancement to a single <select> element:
269
+ * builds per-instance IDs, merges element dataset into options,
270
+ * creates SelectBox, stores binder map with action API, and wires toggle on mouseup.
271
+ *
272
+ * @param {HTMLSelectElement} selectElement - The native <select> to enhance.
273
+ * @param {object} options - Configuration used for this instance.
274
+ * @returns {boolean} - False if already bound; true if successfully applied.
275
+ */
276
+ static applySelectBox(selectElement, options) {
277
+ if (Libs.getBinderMap(selectElement) || Libs.getUnbinderMap(selectElement)) {
278
+ return false;
279
+ }
280
+
281
+ const SEID = Libs.randomString(8);
282
+ const options_cfg = Libs.buildConfig(selectElement, options);
283
+ options_cfg.SEID = SEID
284
+ options_cfg.SEID_LIST = `seui-${SEID}-optionlist`;
285
+ options_cfg.SEID_HOLDER = `seui-${SEID}-placeholder`;
286
+ const bindMap = {options: options_cfg};
287
+
288
+ Libs.setBinderMap(selectElement, bindMap);
289
+ const selectBox = new SelectBox(selectElement, this);
290
+ bindMap.container = selectBox.container;
291
+ bindMap.action = selectBox.getAction();
292
+ bindMap.self = selectBox;
293
+
294
+ selectBox.container.view.addEventListener("mouseup", () => {
295
+ bindMap.action.toggle();
296
+ });
297
+
298
+ return true;
299
+ }
300
+
301
+ /**
302
+ * Determines the property type for an action name on the provided object.
303
+ * Classifies as:
304
+ * - "get-set" when a getter exists (or a setter with non-function value),
305
+ * - "func" when the property is a function,
306
+ * - "variable" otherwise.
307
+ *
308
+ * @typedef {Object} IPropertiesType
309
+ * @property {string} type - One of "variable" | "get-set" | "func".
310
+ * @property {string} name - The original action name.
311
+ *
312
+ * @param {string} actionName - The property key to inspect.
313
+ * @param {*} action - The object containing the property.
314
+ * @returns {IPropertiesType} - The derived property type and name.
315
+ */
316
+ static #getProperties(actionName, action) {
317
+ const descriptor = Object.getOwnPropertyDescriptor(action, actionName);
318
+ let type = "variable";
319
+
320
+ if (descriptor.get || (descriptor.set && typeof action[actionName] !== "function")) {
321
+ type = "get-set";
322
+ }
323
+ else if (typeof action[actionName] === "function") {
324
+ type = "func";
325
+ }
326
+
327
+ return {
328
+ type,
329
+ name: actionName
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Defines a get/set property on the target object that proxies to each bound element's action API.
335
+ * Getter reads from the first element; setter writes the value to all elements.
336
+ *
337
+ * @param {Object} object - The target object to define the property on.
338
+ * @param {string} name - The property name to expose.
339
+ * @param {HTMLElement[]} els - The list of bound elements to proxy.
340
+ */
341
+ static #buildGetSetAction(object, name, els) {
342
+ Object.defineProperty(object, name, {
343
+ get() {
344
+ const binded = Libs.getBinderMap(els[0]);
345
+ return binded.action[name];
346
+ },
347
+ set(value) {
348
+ els.forEach(el => {
349
+ const binded = Libs.getBinderMap(el);
350
+ if (binded) {
351
+ binded.action[name] = value;
352
+ }
353
+ });
354
+ },
355
+ enumerable: true,
356
+ configurable: true
357
+ });
358
+ }
359
+
360
+ /**
361
+ * Creates a function on the target object that invokes the corresponding action
362
+ * across all bound elements in order, respecting the event token flow control.
363
+ * Stops iteration if the token indicates propagation should not continue.
364
+ *
365
+ * @param {Object} object - The target object to attach the function to.
366
+ * @param {string} name - The function name to expose.
367
+ * @param {HTMLElement[]} els - The list of bound elements to invoke against.
368
+ */
369
+ static #buildFuntionAction(object, name, els) {
370
+ object[name] = (...params) => {
371
+ for (let index = 0; index < els.length; index++) {
372
+ const el = els[index];
373
+ const binded = Libs.getBinderMap(el);
374
+ if (!binded) {
375
+ continue;
376
+ }
377
+ const evtToken = iEvents.buildEventToken();
378
+ binded.action[name](evtToken.callback, ...params)
379
+ if (!evtToken.token.isContinue) {
380
+ break;
381
+ }
382
+ }
383
+ return object;
384
+ }
385
+ }
386
386
  }