selective-ui 1.0.2 → 1.0.4

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 +6186 -6047
  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 +2 -2
  10. package/dist/selective-ui.min.js.br +0 -0
  11. package/dist/selective-ui.umd.js +6186 -6047
  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 +749 -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 +603 -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,693 +1,750 @@
1
- import {Libs} from "../utils/libs.js";
2
- import {Refresher} from "../services/refresher.js";
3
- import {PlaceHolder} from "../components/placeholder.js";
4
- import {Directive} from "../components/directive.js";
5
- import {Popup} from "../components/popup.js";
6
- import {SearchBox} from "../components/searchbox.js";
7
- import * as Effector from "../services/effector.js";
8
- import { iEvents } from "../utils/ievents.js";
9
- import { ModelManager } from "../core/model-manager.js";
10
- import { RecyclerView } from "../core/base/recyclerview.js";
11
- import { AccessoryBox } from "./accessorybox.js";
12
- import { SearchController } from "../core/search-controller.js";
13
- import { SelectObserver } from "../services/select-observer.js";
14
- import { DatasetObserver } from "../services/dataset-observer.js";
15
- import { MixedAdapter } from "../adapter/mixed-adapter.js";
16
- import { GroupModel } from "../models/group-model.js";
17
- import { OptionModel } from "../models/option-model.js";
18
-
19
- /**
20
- * @class
21
- */
22
- export class SelectBox {
23
-
24
- /**
25
- * Initializes a SelectBox instance and, if a source <select> and Selective context are provided,
26
- * immediately calls init() to set up the enhanced UI and behavior.
27
- *
28
- * @param {HTMLSelectElement|null} [select=null] - The native select element to enhance.
29
- * @param {typeof import('../utils/selective.js').Selective} [Selective=null] - The Selective framework/context used for configuration and services.
30
- */
31
- constructor(select = null, Selective = null) {
32
- select && this.init(select, Selective);
33
- }
34
- container = {};
35
- oldValue = null;
36
-
37
- /** @type {HTMLDivElement} */
38
- node = null;
39
-
40
- options = null;
41
-
42
- /** @type {ModelManager} */
43
- optionModelManager = null;
44
-
45
- isOpen = false;
46
-
47
- hasLoadedOnce = false;
48
-
49
- isBeforeSearch = false;
50
-
51
- /** @type {typeof import('../utils/selective.js').Selective} */
52
- Selective = null;
53
-
54
- /**
55
- * Gets or sets the disabled state of the SelectBox.
56
- * When set, updates CSS class and ARIA attributes to reflect the disabled state.
57
- *
58
- * @type {boolean}
59
- */
60
- get isDisabled() {
61
- return this.options.disabled;
62
- }
63
- set isDisabled(value) {
64
- this.options.disabled = value;
65
- this.node.classList.toggle("disabled", value);
66
- this.node.setAttribute("aria-disabled", value.toString());
67
- this.container.tags.ViewPanel.setAttribute("aria-disabled", value.toString());
68
- }
69
-
70
- /**
71
- * Gets or sets the read-only state of the SelectBox.
72
- * When set, toggles the "readonly" CSS class to prevent user interaction.
73
- *
74
- * @type {boolean}
75
- */
76
- get isReadOnly() {
77
- return this.options.readonly;
78
- }
79
- set isReadOnly(value) {
80
- this.options.readonly = value;
81
- this.node.classList.toggle("readonly", value);
82
- }
83
-
84
- /**
85
- * Gets or sets the visibility state of the SelectBox.
86
- * When set, toggles the "invisible" CSS class to show or hide the component.
87
- *
88
- * @type {boolean}
89
- */
90
- get isVisible() {
91
- return this.options.visible;
92
- }
93
- set isVisible(value) {
94
- this.options.visible = value;
95
- this.node.classList.toggle("invisible", !value);
96
- }
97
-
98
-
99
- /**
100
- * Initializes the SelectBox UI and behavior by wiring core components (Placeholder, Directive,
101
- * SearchBox, Popup, AccessoryBox), setting ARIA attributes, mounting DOM structure, and connecting
102
- * observers (SelectObserver, DatasetObserver). Configures ModelManager with MixedAdapter and
103
- * RecyclerView, sets up search (including optional AJAX with debouncing), infinite scroll, and
104
- * event handlers for selection, highlighting, collapsing, and keyboard navigation.
105
- *
106
- * @param {HTMLSelectElement} select - The native <select> element to enhance.
107
- * @param {typeof import('../utils/selective.js').Selective} Selective - The Selective framework/context for services and configuration.
108
- */
109
- init(select, Selective) {
110
- const
111
- bindedMap = Libs.getBinderMap(select),
112
- options = bindedMap.options,
113
- placeholder = new PlaceHolder(options),
114
- directive = new Directive(),
115
- searchbox = new SearchBox(options),
116
- effector = Effector.Effector(),
117
- optionModelManager = new ModelManager(options),
118
- accessoryBox = new AccessoryBox(options),
119
- searchController = new SearchController(
120
- select,
121
- optionModelManager
122
- ),
123
- selectObserver = new SelectObserver(select),
124
- datasetObserver = new DatasetObserver(select)
125
- ;
126
-
127
- this.Selective = Selective;
128
- this.options = options;
129
-
130
- placeholder.node.id = options.SEID_HOLDER;
131
-
132
- const container = Libs.mountNode({
133
- Container: {
134
- tag: {node: "div", classList: "selective-ui-MAIN"},
135
- child: {
136
- ViewPanel: {
137
- tag: {
138
- node: "div",
139
- classList: "selective-ui-view",
140
- tabIndex: 0,
141
- role: "combobox",
142
- ariaExpanded: "false",
143
- ariaLabelledby: options.SEID_HOLDER,
144
- ariaControls: options.SEID_LIST,
145
- ariaHaspopup: "true",
146
- ariaMultiselectable: options.multiple ? "true" : "false",
147
- onkeydown: (e) => {
148
- if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
149
- e.preventDefault();
150
- this.getAction()?.open();
151
- }
152
- }
153
- },
154
- child: {
155
- PlaceHolder: {
156
- tag: placeholder.node
157
- },
158
- Directive: {
159
- tag: directive.node
160
- },
161
- SearchBox: {
162
- tag: searchbox.node
163
- }
164
- }
165
- }
166
- }
167
- }
168
- });
169
-
170
- this.container = container;
171
-
172
- this.node = this.container.view;
173
-
174
- select.parentNode.insertBefore(this.node, select);
175
-
176
- this.node.insertBefore(select, container.tags.ViewPanel);
177
-
178
- accessoryBox.setRoot(container.tags.ViewPanel);
179
- accessoryBox.setModelManager(optionModelManager);
180
-
181
- container.tags.ViewPanel.addEventListener("mousedown", (e) => {
182
- e.stopPropagation();
183
- e.preventDefault();
184
- });
185
-
186
- Refresher.resizeBox(select, container.tags.ViewPanel);
187
-
188
- select.classList.add("init");
189
-
190
- // optionModelManager.setupModel(OptionModel);
191
- optionModelManager.setupAdapter(MixedAdapter);
192
- optionModelManager.setupRecyclerView(RecyclerView);
193
- optionModelManager.createModelResources(Libs.parseSelectToArray(select));
194
- optionModelManager.onUpdated = () => {
195
- container.popup.triggerResize();
196
- };
197
-
198
- this.optionModelManager = optionModelManager;
199
-
200
- container.searchController = searchController;
201
-
202
- container.placeholder = placeholder;
203
- container.directive = directive;
204
- container.popup = new Popup(select, options, optionModelManager);
205
- container.popup.setupEffector(effector);
206
- container.popup.setupInfiniteScroll(searchController, options);
207
- container.popup.onAdapterPropChanged("selected", () => {
208
- this.getAction()?.change(null, true);
209
- });
210
- container.popup.onAdapterPropChanged("selected_internal", () => {
211
- this.getAction()?.change(null, false);
212
- });
213
- container.popup.onAdapterPropChanging("select", () => {
214
- this.oldValue = this.getAction()?.value ?? "";
215
- });
216
- container.searchbox = searchbox;
217
- container.effector = effector;
218
- container.targetElement = select;
219
- container.accessorybox = accessoryBox;
220
- this.getAction()?.change(null, false);
221
-
222
- selectObserver.connect();
223
- selectObserver.onChanged = options => {
224
- optionModelManager.update(Libs.parseSelectToArray(options));
225
- this.getAction()?.refreshMask();
226
- };
227
- container.selectObserver = selectObserver;
228
-
229
- container.datasetObserver = datasetObserver;
230
- datasetObserver.connect();
231
-
232
- select.addEventListener('options:changed', () => {
233
- optionModelManager.update(Libs.parseSelectToArray(select));
234
- this.getAction()?.refreshMask();
235
- container.popup?.triggerResize?.();
236
- });
237
-
238
- datasetObserver.onChanged = dataset => {
239
- if (Libs.string2Boolean(dataset.disabled) != this.isDisabled) {
240
- this.isDisabled = Libs.string2Boolean(dataset.disabled);
241
- }
242
-
243
- if (Libs.string2Boolean(dataset.readonly) != this.isReadOnly) {
244
- this.isReadOnly = Libs.string2Boolean(dataset.readonly);
245
- }
246
-
247
- if (Libs.string2Boolean(dataset.visible) != this.isVisible) {
248
- this.isVisible = Libs.string2Boolean(dataset.visible ?? "1");
249
- }
250
- }
251
-
252
- if (options.ajax) {
253
- searchController.setAjax(options.ajax);
254
- }
255
-
256
- const optionAdapter = container.popup.optionAdapter;
257
-
258
- let searchHandle = (keyword, isTrigger) => {
259
- if (!isTrigger && keyword == "") {
260
- searchController.clear();
261
- }
262
- else {
263
- if (keyword != "") {
264
- this.isBeforeSearch = true;
265
- }
266
- searchController.search(keyword).then((result) => {
267
- container.popup.triggerResize();
268
-
269
- if (result.hasResults) {
270
- setTimeout(() => {
271
- optionAdapter.resetHighlight();
272
- }, options.animationtime);
273
- }
274
- }).catch(error => {
275
- console.error("Search error:", error);
276
- });
277
- }
278
- }
279
- let searchHandleTimer = null;
280
- searchbox.onSearch = (keyword, isTrigger) => {
281
- if (!searchController.compareSearchTrigger(keyword)) {
282
- return;
283
- }
284
-
285
- if (searchController.isAjax()) {
286
- clearTimeout(searchHandleTimer);
287
- container.popup.showLoading();
288
-
289
- searchHandleTimer = setTimeout(() => {
290
- searchHandle(keyword, isTrigger);
291
- }, options.delaysearchtime);
292
- }
293
- else {
294
- searchHandle(keyword, isTrigger);
295
- }
296
- };
297
-
298
- searchController.setPopup(container.popup);
299
- searchbox.onNavigate = (direction) => {
300
- optionAdapter.navigate(direction);
301
- };
302
-
303
- searchbox.onEnter = () => {
304
- optionAdapter.selectHighlighted();
305
- };
306
-
307
- optionAdapter.onHighlightChange = (index, id) => {
308
- searchbox.setActiveDescendant(id);
309
- }
310
-
311
- optionAdapter.onCollapsedChange = () => {
312
- container.popup.triggerResize();
313
- }
314
-
315
- searchbox.onEsc = () => {
316
- this.getAction()?.close();
317
- container.tags.ViewPanel.focus();
318
- };
319
-
320
- this.isDisabled = Libs.string2Boolean(options.disabled);
321
- this.isReadOnly = Libs.string2Boolean(options.readonly);
322
- }
323
-
324
- /**
325
- * Disconnects observers associated with the SelectBox instance,
326
- * including SelectObserver and DatasetObserver, to clean up resources
327
- * and stop monitoring changes.
328
- */
329
- deInit() {
330
- const container = this.container || {};
331
- const { selectObserver, datasetObserver } = container;
332
- if (selectObserver?.disconnect) selectObserver.disconnect();
333
- if (datasetObserver?.disconnect) datasetObserver.disconnect();
334
- }
335
-
336
- /**
337
- * Returns an action API for controlling the SelectBox instance.
338
- * The API exposes getters/setters and operations that synchronize UI, model, and events:
339
- * - placeholder: get/set placeholder (updates Placeholder & SearchBox)
340
- * - oldValue/value/valueArray/valueString/valueOptions/valueText/mask: read current selection(s)
341
- * - disabled/readonly/visible: proxy component state to dataset & CSS/ARIA
342
- * - selectAll/deSelectAll: bulk select/deselect (respects multiple & maxSelected, emits beforeChange/change)
343
- * - setValue: programmatically set selection(s) (array or single), with optional trigger/force
344
- * - open/close/toggle: control popup visibility (runs beforeShow/show and beforeClose/close hooks)
345
- * - change: commit selection changes, refresh mask, update AccessoryBox, fire DOM "change" & custom events, and auto-close if configured
346
- * - refreshMask: recompute displayed label (single-select uses selected text; otherwise placeholder)
347
- * - on: register custom event handlers into options.on
348
- * - ajax: configure AJAX search behavior via SearchController
349
- * Internally uses ModelManager/adapter to highlight, resize, and keep ARIA attributes in sync.
350
- */
351
- getAction() {
352
- const container = this.container;
353
- const superThis = this;
354
- const bindedMap = Libs.getBinderMap(container.targetElement);
355
- if (!bindedMap) {
356
- return null;
357
- }
358
- const bindedOptions = bindedMap.options;
359
- let resp = {
360
- get placeholder() {
361
- return container.placeholder.get();
362
- },
363
- set placeholder(value) {
364
- container.placeholder?.set(value);
365
- container.searchbox?.setPlaceHolder(value);
366
- },
367
- get oldValue() {
368
- return superThis.oldValue;
369
- },
370
- set value(value) {
371
- this.setValue(null, value, true);
372
- },
373
- get value() {
374
- let item_list = this.valueArray;
375
-
376
- const valLength = item_list.length;
377
- return valLength > 1 ? item_list : (valLength == 0 ? "" : item_list[0]);
378
- },
379
- get valueArray() {
380
- let item_list = [];
381
- superThis.getModelOption().forEach(modelElement => {
382
- modelElement["selected"] && (item_list.push(modelElement["value"]));
383
- });
384
- return item_list;
385
- },
386
- get valueString() {
387
- const customDelimiter = bindedOptions.customDelimiter;
388
- let item_list = this.valueArray;
389
-
390
- return item_list.join(customDelimiter);
391
- },
392
- get valueOptions() {
393
- let item_list = [];
394
- superThis.getModelOption().forEach(modelElement => {
395
- modelElement["selected"] && (item_list.push(modelElement));
396
- });
397
- return item_list;
398
- },
399
- get mask() {
400
- let item_list = [];
401
- superThis.getModelOption().forEach(modelOption => {
402
- modelOption["selected"] && (item_list.push(modelOption["text"]));
403
- });
404
- return item_list;
405
- },
406
- get valueText() {
407
- var item_list = [];
408
- superThis.getModelOption().forEach(modelOption => {
409
- modelOption["selected"] && (item_list.push(modelOption["text"]));
410
- });
411
-
412
- const valLength = item_list.length;
413
- return valLength > 1 ? item_list : (valLength == 0 ? "" : item_list[0]);
414
- },
415
- get isOpen() {
416
- return superThis.isOpen;
417
- },
418
- selectAll(evtToken, trigger = true) {
419
- if (bindedOptions.multiple && bindedOptions.maxSelected > 0) {
420
- if (superThis.getModelOption().length > bindedOptions.maxSelected) {
421
- return
422
- }
423
- }
424
-
425
- if (this.disabled || this.readonly || !bindedOptions.multiple) {
426
- return;
427
- }
428
-
429
- if (trigger) {
430
- const beforeChangeToken = iEvents.callEvent([this], ...bindedOptions.on.beforeChange);
431
- if (beforeChangeToken.isCancel) {
432
- return;
433
- }
434
-
435
- superThis.oldValue = this.value;
436
- }
437
-
438
- superThis.getModelOption().forEach(modelOption => {
439
- modelOption["selectedNonTrigger"] = true;
440
- });
441
-
442
- this.change(false, trigger);
443
- },
444
- deSelectAll(evtToken, trigger = true) {
445
- if (this.disabled || this.readonly || !bindedOptions.multiple) {
446
- return;
447
- }
448
-
449
- if (trigger) {
450
- const beforeChangeToken = iEvents.callEvent([this], ...bindedOptions.on.beforeChange);
451
- if (beforeChangeToken.isCancel) {
452
- return;
453
- }
454
-
455
- superThis.oldValue = this.value;
456
- }
457
-
458
- superThis.getModelOption().forEach(modelOption => {
459
- modelOption["selectedNonTrigger"] = false;
460
- });
461
-
462
- this.change(false, trigger);
463
- },
464
- setValue(evtToken = null, value, trigger = true, force = false) {
465
- !Array.isArray(value) && (value = [value]);
466
-
467
- if (bindedOptions.multiple && bindedOptions.maxSelected > 0) {
468
- if (value.length > bindedOptions.maxSelected) {
469
- return
470
- }
471
- }
472
-
473
- if (!force && (this.disabled || this.readonly)) {
474
- return;
475
- }
476
-
477
- if (trigger) {
478
- const beforeChangeToken = iEvents.callEvent([this], ...bindedOptions.on.beforeChange);
479
- if (beforeChangeToken.isCancel) {
480
- return;
481
- }
482
-
483
- superThis.oldValue = this.value;
484
- }
485
-
486
- superThis.getModelOption().forEach(modelOption => {
487
- modelOption["selectedNonTrigger"] = value.some(v => v == modelOption["value"]);
488
- });
489
-
490
- if (!bindedOptions.multiple){
491
- container.targetElement.value = value[0];
492
- }
493
-
494
- this.change(false, trigger);
495
- },
496
- open() {
497
- if (superThis.isOpen) return false;
498
- let findAnother = superThis.Selective.find();
499
- if (!findAnother.isEmpty) {
500
- /** @type {IEventToken} */
501
- const closeToken = findAnother.close();
502
- if (closeToken.isCancel) {
503
- return false;
504
- }
505
- }
506
-
507
- if (this.disabled) {
508
- return false;
509
- }
510
-
511
- const beforeShowToken = iEvents.callEvent([this], ...bindedOptions.on.beforeShow);
512
- if (beforeShowToken.isCancel) {
513
- return false;
514
- }
515
-
516
- superThis.isOpen = true;
517
- container.directive.setDropdown(true);
518
-
519
- const adapter = container.popup.optionAdapter;
520
- const selectedOption = adapter.getSelectedItem();
521
- if (selectedOption) {
522
- adapter.setHighlight(selectedOption, false);
523
- } else {
524
- adapter.resetHighlight();
525
- }
526
-
527
- if ((!superThis.hasLoadedOnce || superThis.isBeforeSearch) && bindedOptions?.ajax) {
528
- container.popup.showLoading();
529
- superThis.hasLoadedOnce = true;
530
- superThis.isBeforeSearch = false;
531
-
532
- setTimeout(() => {
533
- if (!container.popup || !container.searchController) return;
534
- container.searchController.search("")
535
- .then(() => container.popup?.triggerResize?.())
536
- .catch(err => console.error("Initial ajax load error:", err));
537
- }, bindedOptions.animationtime);
538
- }
539
-
540
- container.popup.open();
541
- container.searchbox.show();
542
-
543
- container.tags.ViewPanel.setAttribute("aria-expanded", "true");
544
-
545
- iEvents.callEvent([this], ...bindedOptions.on.show);
546
-
547
- return true;
548
- },
549
- close() {
550
- if (!superThis.isOpen) return false;
551
-
552
- const beforeCloseToken = iEvents.callEvent([this], ...bindedOptions.on.beforeClose);
553
- if (beforeCloseToken.isCancel) {
554
- return false;
555
- }
556
-
557
- superThis.isOpen = false;
558
-
559
- container.directive.setDropdown(false);
560
- container.popup.close(() => {
561
- container.searchbox.clear(false);
562
- });
563
- container.searchbox.hide();
564
- container.tags.ViewPanel.setAttribute("aria-expanded", "false");
565
-
566
- iEvents.callEvent([this], ...bindedOptions.on.close);
567
-
568
- return true;
569
- },
570
- toggle() {
571
- if (superThis.isOpen) {
572
- this.close();
573
- }
574
- else {
575
- this.open();
576
- }
577
- },
578
- change(evtToken = null, canTrigger = true) {
579
- if (canTrigger) {
580
- if (bindedOptions.multiple && bindedOptions.maxSelected > 0) {
581
- if (this.valueArray.length > bindedOptions.maxSelected) {
582
- this.setValue(null, this.oldValue, false, true);
583
- }
584
- }
585
-
586
- if (this.disabled || this.readonly) {
587
- this.setValue(null, this.oldValue, false, true);
588
- return;
589
- }
590
-
591
- const beforeChangeToken = iEvents.callEvent([this, this.value], ...bindedOptions.on.beforeChange);
592
-
593
- if (beforeChangeToken.isCancel) {
594
- this.setValue(null, this.oldValue, false);
595
- return;
596
- }
597
- }
598
-
599
- this.refreshMask();
600
- container.accessorybox.setModelData(this.valueOptions);
601
- if (canTrigger) {
602
- if (container.targetElement) {
603
- iEvents.trigger(container.targetElement, "change");
604
- }
605
- iEvents.callEvent([this, this.value], ...bindedOptions.on.change);
606
-
607
- if (superThis.options.autoclose) {
608
- this.close();
609
- }
610
- }
611
- },
612
- refreshMask() {
613
- let mask = bindedOptions.placeholder;
614
- if (!bindedOptions.multiple && superThis.getModelOption().length > 0) {
615
- mask = this.mask[0];
616
- }
617
- mask ??= bindedOptions.placeholder;
618
-
619
- container.placeholder.set(mask, false);
620
- container.searchbox.setPlaceHolder(mask);
621
- },
622
- on(evtToken, evtName, handle) {
623
- if (!bindedOptions.on[evtName]) {
624
- bindedOptions.on[evtName] = [];
625
- }
626
- bindedOptions.on[evtName].push(handle);
627
- },
628
- ajax(evtToken, obj) {
629
- container.searchController.setAjax(obj);
630
- }
631
- };
632
-
633
- this.createSymProp(resp, "disabled", "isDisabled");
634
- this.createSymProp(resp, "readonly", "isReadOnly");
635
- this.createSymProp(resp, "visible", "isVisible");
636
-
637
- return resp;
638
- }
639
-
640
- /**
641
- * Creates a property on the given object with custom getter and setter behavior.
642
- * The getter returns the value stored in a private property on the current instance (`this`),
643
- * and the setter updates both the private property and a corresponding data-* attribute
644
- * on the instance's `container.targetElement`.
645
- *
646
- * @param {Object} obj - The object on which to define the public property.
647
- * @param {string} prop - The public property name to create on `obj`.
648
- * @param {string} privateProp - The private property name on `this` that stores the actual value.
649
- */
650
- createSymProp(obj, prop, privateProp) {
651
- const superThis = this;
652
-
653
- Object.defineProperty(obj, prop, {
654
- get() { return superThis[privateProp]; },
655
- set(value) {
656
- superThis[privateProp] = value;
657
- superThis.container.targetElement.dataset[prop] = value;
658
- },
659
- enumerable: true,
660
- configurable: true
661
- });
662
- }
663
-
664
- /**
665
- * Flattens and returns all option models from the current resources.
666
- * Collects OptionModel instances directly and items within GroupModel.
667
- * If `isSelected` is a boolean, filters by selection state; otherwise returns all.
668
- *
669
- * @param {boolean|null} [isSelected=null] - Optional filter to return only selected or unselected options.
670
- * @returns {(GroupModel|OptionModel)[]} - A flat array of option models (and/or group items).
671
- */
672
- getModelOption(isSelected = null) {
673
- if (!this.optionModelManager) return [];
674
-
675
- const { modelList } = this.optionModelManager.getResources();
676
-
677
- const flatOptions = [];
678
- for (const m of modelList) {
679
- if (m instanceof OptionModel) {
680
- flatOptions.push(m);
681
- } else if (m instanceof GroupModel) {
682
- if (Array.isArray(m.items) && m.items.length) {
683
- flatOptions.push(...m.items);
684
- }
685
- }
686
- }
687
-
688
- if (typeof isSelected === "boolean") {
689
- return flatOptions.filter(o => o.selected === isSelected);
690
- }
691
- return flatOptions;
692
- }
1
+ import {Libs} from "../utils/libs.js";
2
+ import {Refresher} from "../services/refresher.js";
3
+ import {PlaceHolder} from "../components/placeholder.js";
4
+ import {Directive} from "../components/directive.js";
5
+ import {Popup} from "../components/popup.js";
6
+ import {SearchBox} from "../components/searchbox.js";
7
+ import * as Effector from "../services/effector.js";
8
+ import { iEvents } from "../utils/ievents.js";
9
+ import { ModelManager } from "../core/model-manager.js";
10
+ import { RecyclerView } from "../core/base/recyclerview.js";
11
+ import { AccessoryBox } from "./accessorybox.js";
12
+ import { SearchController } from "../core/search-controller.js";
13
+ import { SelectObserver } from "../services/select-observer.js";
14
+ import { DatasetObserver } from "../services/dataset-observer.js";
15
+ import { MixedAdapter } from "../adapter/mixed-adapter.js";
16
+ import { GroupModel } from "../models/group-model.js";
17
+ import { OptionModel } from "../models/option-model.js";
18
+
19
+ /**
20
+ * @class
21
+ */
22
+ export class SelectBox {
23
+
24
+ /**
25
+ * Initializes a SelectBox instance and, if a source <select> and Selective context are provided,
26
+ * immediately calls init() to set up the enhanced UI and behavior.
27
+ *
28
+ * @param {HTMLSelectElement|null} [select=null] - The native select element to enhance.
29
+ * @param {typeof import('../utils/selective.js').Selective} [Selective=null] - The Selective framework/context used for configuration and services.
30
+ */
31
+ constructor(select = null, Selective = null) {
32
+ select && this.init(select, Selective);
33
+ }
34
+ container = {};
35
+ oldValue = null;
36
+
37
+ /** @type {HTMLDivElement} */
38
+ node = null;
39
+
40
+ options = null;
41
+
42
+ /** @type {ModelManager} */
43
+ optionModelManager = null;
44
+
45
+ isOpen = false;
46
+
47
+ hasLoadedOnce = false;
48
+
49
+ isBeforeSearch = false;
50
+
51
+ /** @type {typeof import('../utils/selective.js').Selective} */
52
+ Selective = null;
53
+
54
+ /**
55
+ * Gets or sets the disabled state of the SelectBox.
56
+ * When set, updates CSS class and ARIA attributes to reflect the disabled state.
57
+ *
58
+ * @type {boolean}
59
+ */
60
+ get isDisabled() {
61
+ return this.options.disabled;
62
+ }
63
+ set isDisabled(value) {
64
+ this.options.disabled = value;
65
+ this.node.classList.toggle("disabled", value);
66
+ this.node.setAttribute("aria-disabled", value.toString());
67
+ this.container.tags.ViewPanel.setAttribute("aria-disabled", value.toString());
68
+ }
69
+
70
+ /**
71
+ * Gets or sets the read-only state of the SelectBox.
72
+ * When set, toggles the "readonly" CSS class to prevent user interaction.
73
+ *
74
+ * @type {boolean}
75
+ */
76
+ get isReadOnly() {
77
+ return this.options.readonly;
78
+ }
79
+ set isReadOnly(value) {
80
+ this.options.readonly = value;
81
+ this.node.classList.toggle("readonly", value);
82
+ }
83
+
84
+ /**
85
+ * Gets or sets the visibility state of the SelectBox.
86
+ * When set, toggles the "invisible" CSS class to show or hide the component.
87
+ *
88
+ * @type {boolean}
89
+ */
90
+ get isVisible() {
91
+ return this.options.visible;
92
+ }
93
+ set isVisible(value) {
94
+ this.options.visible = value;
95
+ this.node.classList.toggle("invisible", !value);
96
+ }
97
+
98
+
99
+ /**
100
+ * Initializes the SelectBox UI and behavior by wiring core components (Placeholder, Directive,
101
+ * SearchBox, Popup, AccessoryBox), setting ARIA attributes, mounting DOM structure, and connecting
102
+ * observers (SelectObserver, DatasetObserver). Configures ModelManager with MixedAdapter and
103
+ * RecyclerView, sets up search (including optional AJAX with debouncing), infinite scroll, and
104
+ * event handlers for selection, highlighting, collapsing, and keyboard navigation.
105
+ *
106
+ * @param {HTMLSelectElement} select - The native <select> element to enhance.
107
+ * @param {typeof import('../utils/selective.js').Selective} Selective - The Selective framework/context for services and configuration.
108
+ */
109
+ init(select, Selective) {
110
+ const
111
+ bindedMap = Libs.getBinderMap(select),
112
+ options = bindedMap.options,
113
+ placeholder = new PlaceHolder(options),
114
+ directive = new Directive(),
115
+ searchbox = new SearchBox(options),
116
+ effector = Effector.Effector(),
117
+ optionModelManager = new ModelManager(options),
118
+ accessoryBox = new AccessoryBox(options),
119
+ searchController = new SearchController(
120
+ select,
121
+ optionModelManager
122
+ ),
123
+ selectObserver = new SelectObserver(select),
124
+ datasetObserver = new DatasetObserver(select)
125
+ ;
126
+
127
+ this.Selective = Selective;
128
+ this.options = options;
129
+
130
+ placeholder.node.id = options.SEID_HOLDER;
131
+
132
+ const container = Libs.mountNode({
133
+ Container: {
134
+ tag: {node: "div", classList: "selective-ui-MAIN"},
135
+ child: {
136
+ ViewPanel: {
137
+ tag: {
138
+ node: "div",
139
+ classList: "selective-ui-view",
140
+ tabIndex: 0,
141
+ onkeydown: (e) => {
142
+ if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
143
+ e.preventDefault();
144
+ this.getAction()?.open();
145
+ }
146
+ }
147
+ },
148
+ child: {
149
+ PlaceHolder: {
150
+ tag: placeholder.node
151
+ },
152
+ Directive: {
153
+ tag: directive.node
154
+ },
155
+ SearchBox: {
156
+ tag: searchbox.node
157
+ }
158
+ }
159
+ }
160
+ }
161
+ }
162
+ });
163
+
164
+ this.container = container;
165
+
166
+ this.node = this.container.view;
167
+
168
+ select.parentNode.insertBefore(this.node, select);
169
+
170
+ this.node.insertBefore(select, container.tags.ViewPanel);
171
+
172
+ accessoryBox.setRoot(container.tags.ViewPanel);
173
+ accessoryBox.setModelManager(optionModelManager);
174
+
175
+ container.tags.ViewPanel.addEventListener("mousedown", (e) => {
176
+ e.stopPropagation();
177
+ e.preventDefault();
178
+ });
179
+
180
+ Refresher.resizeBox(select, container.tags.ViewPanel);
181
+
182
+ select.classList.add("init");
183
+
184
+ // optionModelManager.setupModel(OptionModel);
185
+ optionModelManager.setupAdapter(MixedAdapter);
186
+ optionModelManager.setupRecyclerView(RecyclerView);
187
+ optionModelManager.createModelResources(Libs.parseSelectToArray(select));
188
+ optionModelManager.onUpdated = () => {
189
+ container.popup.triggerResize();
190
+ };
191
+
192
+ this.optionModelManager = optionModelManager;
193
+
194
+ container.searchController = searchController;
195
+
196
+ container.placeholder = placeholder;
197
+ container.directive = directive;
198
+ container.popup = new Popup(select, options, optionModelManager);
199
+ container.popup.setupEffector(effector);
200
+ container.popup.setupInfiniteScroll(searchController, options);
201
+ container.popup.onAdapterPropChanged("selected", () => {
202
+ this.getAction()?.change(null, true);
203
+ });
204
+ container.popup.onAdapterPropChanged("selected_internal", () => {
205
+ this.getAction()?.change(null, false);
206
+ });
207
+ container.popup.onAdapterPropChanging("select", () => {
208
+ this.oldValue = this.getAction()?.value ?? "";
209
+ });
210
+ container.searchbox = searchbox;
211
+ container.effector = effector;
212
+ container.targetElement = select;
213
+ container.accessorybox = accessoryBox;
214
+ this.getAction()?.change(null, false);
215
+
216
+ selectObserver.connect();
217
+ selectObserver.onChanged = options => {
218
+ optionModelManager.update(Libs.parseSelectToArray(options));
219
+ this.getAction()?.refreshMask();
220
+ };
221
+ container.selectObserver = selectObserver;
222
+
223
+ container.datasetObserver = datasetObserver;
224
+ datasetObserver.connect();
225
+
226
+ select.addEventListener('options:changed', () => {
227
+ optionModelManager.update(Libs.parseSelectToArray(select));
228
+ this.getAction()?.refreshMask();
229
+ container.popup?.triggerResize?.();
230
+ });
231
+
232
+ datasetObserver.onChanged = dataset => {
233
+ if (Libs.string2Boolean(dataset.disabled) != this.isDisabled) {
234
+ this.isDisabled = Libs.string2Boolean(dataset.disabled);
235
+ }
236
+
237
+ if (Libs.string2Boolean(dataset.readonly) != this.isReadOnly) {
238
+ this.isReadOnly = Libs.string2Boolean(dataset.readonly);
239
+ }
240
+
241
+ if (Libs.string2Boolean(dataset.visible) != this.isVisible) {
242
+ this.isVisible = Libs.string2Boolean(dataset.visible ?? "1");
243
+ }
244
+ }
245
+
246
+ if (options.ajax) {
247
+ searchController.setAjax(options.ajax);
248
+ }
249
+
250
+ const optionAdapter = container.popup.optionAdapter;
251
+
252
+ let searchHandle = (keyword, isTrigger) => {
253
+ if (!isTrigger && keyword == "") {
254
+ searchController.clear();
255
+ }
256
+ else {
257
+ if (keyword != "") {
258
+ this.isBeforeSearch = true;
259
+ }
260
+ searchController.search(keyword).then((result) => {
261
+ container.popup.triggerResize();
262
+
263
+ if (result.hasResults) {
264
+ setTimeout(() => {
265
+ optionAdapter.resetHighlight();
266
+ }, options.animationtime);
267
+ }
268
+ }).catch(error => {
269
+ console.error("Search error:", error);
270
+ });
271
+ }
272
+ }
273
+ let searchHandleTimer = null;
274
+ searchbox.onSearch = (keyword, isTrigger) => {
275
+ if (!searchController.compareSearchTrigger(keyword)) {
276
+ return;
277
+ }
278
+
279
+ if (searchController.isAjax()) {
280
+ clearTimeout(searchHandleTimer);
281
+ container.popup.showLoading();
282
+
283
+ searchHandleTimer = setTimeout(() => {
284
+ searchHandle(keyword, isTrigger);
285
+ }, options.delaysearchtime);
286
+ }
287
+ else {
288
+ searchHandle(keyword, isTrigger);
289
+ }
290
+ };
291
+
292
+ searchController.setPopup(container.popup);
293
+ searchbox.onNavigate = (direction) => {
294
+ optionAdapter.navigate(direction);
295
+ };
296
+
297
+ searchbox.onEnter = () => {
298
+ optionAdapter.selectHighlighted();
299
+ };
300
+
301
+ optionAdapter.onHighlightChange = (index, id) => {
302
+ searchbox.setActiveDescendant(id);
303
+ }
304
+
305
+ optionAdapter.onCollapsedChange = () => {
306
+ container.popup.triggerResize();
307
+ }
308
+
309
+ searchbox.onEsc = () => {
310
+ this.getAction()?.close();
311
+ container.tags.ViewPanel.focus();
312
+ };
313
+
314
+ this.isDisabled = Libs.string2Boolean(options.disabled);
315
+ this.isReadOnly = Libs.string2Boolean(options.readonly);
316
+ }
317
+
318
+ /**
319
+ * Disconnects observers associated with the SelectBox instance,
320
+ * including SelectObserver and DatasetObserver, to clean up resources
321
+ * and stop monitoring changes.
322
+ */
323
+ deInit() {
324
+ const container = this.container || {};
325
+ const { selectObserver, datasetObserver } = container;
326
+ if (selectObserver?.disconnect) selectObserver.disconnect();
327
+ if (datasetObserver?.disconnect) datasetObserver.disconnect();
328
+ }
329
+
330
+ /**
331
+ * Returns an action API for controlling the SelectBox instance.
332
+ * The API exposes getters/setters and operations that synchronize UI, model, and events:
333
+ * - placeholder: get/set placeholder (updates Placeholder & SearchBox)
334
+ * - oldValue/value/valueArray/valueString/valueOptions/valueText/mask: read current selection(s)
335
+ * - disabled/readonly/visible: proxy component state to dataset & CSS/ARIA
336
+ * - selectAll/deSelectAll: bulk select/deselect (respects multiple & maxSelected, emits beforeChange/change)
337
+ * - setValue: programmatically set selection(s) (array or single), with optional trigger/force
338
+ * - open/close/toggle: control popup visibility (runs beforeShow/show and beforeClose/close hooks)
339
+ * - change: commit selection changes, refresh mask, update AccessoryBox, fire DOM "change" & custom events, and auto-close if configured
340
+ * - refreshMask: recompute displayed label (single-select uses selected text; otherwise placeholder)
341
+ * - on: register custom event handlers into options.on
342
+ * - ajax: configure AJAX search behavior via SearchController
343
+ * Internally uses ModelManager/adapter to highlight, resize, and keep ARIA attributes in sync.
344
+ */
345
+ getAction() {
346
+ const container = this.container;
347
+ const superThis = this;
348
+ const bindedMap = Libs.getBinderMap(container.targetElement);
349
+ if (!bindedMap) {
350
+ return null;
351
+ }
352
+ const bindedOptions = bindedMap.options;
353
+ let resp = {
354
+ get placeholder() {
355
+ return container.placeholder.get();
356
+ },
357
+ set placeholder(value) {
358
+ container.placeholder?.set(value);
359
+ container.searchbox?.setPlaceHolder(value);
360
+ },
361
+ get oldValue() {
362
+ return superThis.oldValue;
363
+ },
364
+ set value(value) {
365
+ this.setValue(null, value, true);
366
+ },
367
+ get value() {
368
+ let item_list = this.valueArray;
369
+
370
+ const valLength = item_list.length;
371
+ return valLength > 1 ? item_list : (valLength == 0 ? "" : item_list[0]);
372
+ },
373
+ get valueArray() {
374
+ let item_list = [];
375
+ superThis.getModelOption().forEach(modelElement => {
376
+ modelElement["selected"] && (item_list.push(modelElement["value"]));
377
+ });
378
+ return item_list;
379
+ },
380
+ get valueString() {
381
+ const customDelimiter = bindedOptions.customDelimiter;
382
+ let item_list = this.valueArray;
383
+
384
+ return item_list.join(customDelimiter);
385
+ },
386
+ get valueOptions() {
387
+ let item_list = [];
388
+ superThis.getModelOption().forEach(modelElement => {
389
+ modelElement["selected"] && (item_list.push(modelElement));
390
+ });
391
+ return item_list;
392
+ },
393
+ get mask() {
394
+ let item_list = [];
395
+ superThis.getModelOption().forEach(modelOption => {
396
+ modelOption["selected"] && (item_list.push(modelOption["text"]));
397
+ });
398
+ return item_list;
399
+ },
400
+ get valueText() {
401
+ var item_list = [];
402
+ superThis.getModelOption().forEach(modelOption => {
403
+ modelOption["selected"] && (item_list.push(modelOption["text"]));
404
+ });
405
+
406
+ const valLength = item_list.length;
407
+ return valLength > 1 ? item_list : (valLength == 0 ? "" : item_list[0]);
408
+ },
409
+ get isOpen() {
410
+ return superThis.isOpen;
411
+ },
412
+ selectAll(evtToken, trigger = true) {
413
+ if (bindedOptions.multiple && bindedOptions.maxSelected > 0) {
414
+ if (superThis.getModelOption().length > bindedOptions.maxSelected) {
415
+ return
416
+ }
417
+ }
418
+
419
+ if (this.disabled || this.readonly || !bindedOptions.multiple) {
420
+ return;
421
+ }
422
+
423
+ if (trigger) {
424
+ const beforeChangeToken = iEvents.callEvent([this], ...bindedOptions.on.beforeChange);
425
+ if (beforeChangeToken.isCancel) {
426
+ return;
427
+ }
428
+
429
+ superThis.oldValue = this.value;
430
+ }
431
+
432
+ superThis.getModelOption().forEach(modelOption => {
433
+ modelOption["selectedNonTrigger"] = true;
434
+ });
435
+
436
+ this.change(false, trigger);
437
+ },
438
+ deSelectAll(evtToken, trigger = true) {
439
+ if (this.disabled || this.readonly || !bindedOptions.multiple) {
440
+ return;
441
+ }
442
+
443
+ if (trigger) {
444
+ const beforeChangeToken = iEvents.callEvent([this], ...bindedOptions.on.beforeChange);
445
+ if (beforeChangeToken.isCancel) {
446
+ return;
447
+ }
448
+
449
+ superThis.oldValue = this.value;
450
+ }
451
+
452
+ superThis.getModelOption().forEach(modelOption => {
453
+ modelOption["selectedNonTrigger"] = false;
454
+ });
455
+
456
+ this.change(false, trigger);
457
+ },
458
+ setValue(evtToken = null, value, trigger = true, force = false) {
459
+ !Array.isArray(value) && (value = [value]);
460
+
461
+ value = value.filter(v => v !== "" && v != null);
462
+
463
+ if (value.length === 0) {
464
+ superThis.getModelOption().forEach(modelOption => {
465
+ modelOption["selectedNonTrigger"] = false;
466
+ });
467
+ this.change(false, trigger);
468
+ return;
469
+ }
470
+
471
+ if (bindedOptions.multiple && bindedOptions.maxSelected > 0) {
472
+ if (value.length > bindedOptions.maxSelected) {
473
+ console.warn(`Cannot select more than ${bindedOptions.maxSelected} items`);
474
+ return;
475
+ }
476
+ }
477
+
478
+ if (!force && (this.disabled || this.readonly)) {
479
+ return;
480
+ }
481
+
482
+ if (container.searchController?.isAjax()) {
483
+ const { existing, missing } = container.searchController.checkMissingValues(value);
484
+
485
+ if (missing.length > 0) {
486
+ console.log(`Loading ${missing.length} missing values from server...`);
487
+
488
+ (async () => {
489
+ if (bindedOptions.loadingfield) {
490
+ container.popup?.showLoading();
491
+ }
492
+
493
+ try {
494
+ const result = await container.searchController.loadByValues(missing);
495
+
496
+ if (result.success && result.items.length > 0) {
497
+ result.items.forEach(item => {
498
+ if (missing.includes(item.value)) {
499
+ item.selected = true;
500
+ }
501
+ });
502
+
503
+ container.searchController['#applyAjaxResult'](
504
+ result.items,
505
+ true,
506
+ true
507
+ );
508
+
509
+ setTimeout(() => {
510
+ superThis.getModelOption().forEach(modelOption => {
511
+ modelOption["selectedNonTrigger"] = value.some(v => v == modelOption["value"]);
512
+ });
513
+ this.change(false, false);
514
+ }, 100);
515
+ } else if (missing.length > 0) {
516
+ console.warn(`Could not load ${missing.length} values:`, missing);
517
+ }
518
+ } catch (error) {
519
+ console.error("Error loading missing values:", error);
520
+ } finally {
521
+ if (bindedOptions.loadingfield) {
522
+ container.popup?.hideLoading();
523
+ }
524
+ }
525
+ })();
526
+ }
527
+ }
528
+
529
+ if (trigger) {
530
+ const beforeChangeToken = iEvents.callEvent([this], ...bindedOptions.on.beforeChange);
531
+ if (beforeChangeToken.isCancel) {
532
+ return;
533
+ }
534
+ superThis.oldValue = this.value;
535
+ }
536
+
537
+ superThis.getModelOption().forEach(modelOption => {
538
+ modelOption["selectedNonTrigger"] = value.some(v => v == modelOption["value"]);
539
+ });
540
+
541
+ if (!bindedOptions.multiple && value.length > 0) {
542
+ container.targetElement.value = value[0];
543
+ }
544
+
545
+ this.change(false, trigger);
546
+ },
547
+ open() {
548
+ if (superThis.isOpen) return false;
549
+ let findAnother = superThis.Selective.find();
550
+ if (!findAnother.isEmpty) {
551
+ /** @type {IEventToken} */
552
+ const closeToken = findAnother.close();
553
+ if (closeToken.isCancel) {
554
+ return false;
555
+ }
556
+ }
557
+
558
+ if (this.disabled) {
559
+ return false;
560
+ }
561
+
562
+ const beforeShowToken = iEvents.callEvent([this], ...bindedOptions.on.beforeShow);
563
+ if (beforeShowToken.isCancel) {
564
+ return false;
565
+ }
566
+
567
+ superThis.isOpen = true;
568
+ container.directive.setDropdown(true);
569
+
570
+ const adapter = container.popup.optionAdapter;
571
+ const selectedOption = adapter.getSelectedItem();
572
+ if (selectedOption) {
573
+ adapter.setHighlight(selectedOption, false);
574
+ } else {
575
+ adapter.resetHighlight();
576
+ }
577
+
578
+ if ((!superThis.hasLoadedOnce || superThis.isBeforeSearch) && bindedOptions?.ajax) {
579
+ container.popup.showLoading();
580
+ superThis.hasLoadedOnce = true;
581
+ superThis.isBeforeSearch = false;
582
+
583
+ setTimeout(() => {
584
+ if (!container.popup || !container.searchController) return;
585
+ container.searchController.search("")
586
+ .then(() => container.popup?.triggerResize?.())
587
+ .catch(err => console.error("Initial ajax load error:", err));
588
+ }, bindedOptions.animationtime);
589
+ }
590
+
591
+ container.popup.open();
592
+ container.searchbox.show();
593
+ const ViewPanel = /** @type {HTMLElement} */ (container.tags.ViewPanel);
594
+ ViewPanel.setAttribute("aria-expanded", "true");
595
+ ViewPanel.setAttribute("aria-controls", bindedOptions.SEID_LIST);
596
+ ViewPanel.setAttribute("aria-haspopup", "listbox");
597
+ ViewPanel.setAttribute("aria-labelledby", bindedOptions.SEID_HOLDER);
598
+ if (bindedOptions.multiple) {
599
+ ViewPanel.setAttribute("aria-multiselectable", "true");
600
+ }
601
+
602
+ iEvents.callEvent([this], ...bindedOptions.on.show);
603
+
604
+ return true;
605
+ },
606
+ close() {
607
+ if (!superThis.isOpen) return false;
608
+
609
+ const beforeCloseToken = iEvents.callEvent([this], ...bindedOptions.on.beforeClose);
610
+ if (beforeCloseToken.isCancel) {
611
+ return false;
612
+ }
613
+
614
+ superThis.isOpen = false;
615
+
616
+ container.directive.setDropdown(false);
617
+ container.popup.close(() => {
618
+ container.searchbox.clear(false);
619
+ });
620
+ container.searchbox.hide();
621
+ container.tags.ViewPanel.setAttribute("aria-expanded", "false");
622
+
623
+ iEvents.callEvent([this], ...bindedOptions.on.close);
624
+
625
+ return true;
626
+ },
627
+ toggle() {
628
+ if (superThis.isOpen) {
629
+ this.close();
630
+ }
631
+ else {
632
+ this.open();
633
+ }
634
+ },
635
+ change(evtToken = null, canTrigger = true) {
636
+ if (canTrigger) {
637
+ if (bindedOptions.multiple && bindedOptions.maxSelected > 0) {
638
+ if (this.valueArray.length > bindedOptions.maxSelected) {
639
+ this.setValue(null, this.oldValue, false, true);
640
+ }
641
+ }
642
+
643
+ if (this.disabled || this.readonly) {
644
+ this.setValue(null, this.oldValue, false, true);
645
+ return;
646
+ }
647
+
648
+ const beforeChangeToken = iEvents.callEvent([this, this.value], ...bindedOptions.on.beforeChange);
649
+
650
+ if (beforeChangeToken.isCancel) {
651
+ this.setValue(null, this.oldValue, false);
652
+ return;
653
+ }
654
+ }
655
+
656
+ this.refreshMask();
657
+ container.accessorybox.setModelData(this.valueOptions);
658
+ if (canTrigger) {
659
+ if (container.targetElement) {
660
+ iEvents.trigger(container.targetElement, "change");
661
+ }
662
+ iEvents.callEvent([this, this.value], ...bindedOptions.on.change);
663
+
664
+ if (superThis.options.autoclose) {
665
+ this.close();
666
+ }
667
+ }
668
+ },
669
+ refreshMask() {
670
+ let mask = bindedOptions.placeholder;
671
+ if (!bindedOptions.multiple && superThis.getModelOption().length > 0) {
672
+ mask = this.mask[0];
673
+ }
674
+ mask ??= bindedOptions.placeholder;
675
+
676
+ container.placeholder.set(mask, false);
677
+ container.searchbox.setPlaceHolder(mask);
678
+ },
679
+ on(evtToken, evtName, handle) {
680
+ if (!bindedOptions.on[evtName]) {
681
+ bindedOptions.on[evtName] = [];
682
+ }
683
+ bindedOptions.on[evtName].push(handle);
684
+ },
685
+ ajax(evtToken, obj) {
686
+ container.searchController.setAjax(obj);
687
+ }
688
+ };
689
+
690
+ this.createSymProp(resp, "disabled", "isDisabled");
691
+ this.createSymProp(resp, "readonly", "isReadOnly");
692
+ this.createSymProp(resp, "visible", "isVisible");
693
+
694
+ return resp;
695
+ }
696
+
697
+ /**
698
+ * Creates a property on the given object with custom getter and setter behavior.
699
+ * The getter returns the value stored in a private property on the current instance (`this`),
700
+ * and the setter updates both the private property and a corresponding data-* attribute
701
+ * on the instance's `container.targetElement`.
702
+ *
703
+ * @param {Object} obj - The object on which to define the public property.
704
+ * @param {string} prop - The public property name to create on `obj`.
705
+ * @param {string} privateProp - The private property name on `this` that stores the actual value.
706
+ */
707
+ createSymProp(obj, prop, privateProp) {
708
+ const superThis = this;
709
+
710
+ Object.defineProperty(obj, prop, {
711
+ get() { return superThis[privateProp]; },
712
+ set(value) {
713
+ superThis[privateProp] = value;
714
+ superThis.container.targetElement.dataset[prop] = value;
715
+ },
716
+ enumerable: true,
717
+ configurable: true
718
+ });
719
+ }
720
+
721
+ /**
722
+ * Flattens and returns all option models from the current resources.
723
+ * Collects OptionModel instances directly and items within GroupModel.
724
+ * If `isSelected` is a boolean, filters by selection state; otherwise returns all.
725
+ *
726
+ * @param {boolean|null} [isSelected=null] - Optional filter to return only selected or unselected options.
727
+ * @returns {(GroupModel|OptionModel)[]} - A flat array of option models (and/or group items).
728
+ */
729
+ getModelOption(isSelected = null) {
730
+ if (!this.optionModelManager) return [];
731
+
732
+ const { modelList } = this.optionModelManager.getResources();
733
+
734
+ const flatOptions = [];
735
+ for (const m of modelList) {
736
+ if (m instanceof OptionModel) {
737
+ flatOptions.push(m);
738
+ } else if (m instanceof GroupModel) {
739
+ if (Array.isArray(m.items) && m.items.length) {
740
+ flatOptions.push(...m.items);
741
+ }
742
+ }
743
+ }
744
+
745
+ if (typeof isSelected === "boolean") {
746
+ return flatOptions.filter(o => o.selected === isSelected);
747
+ }
748
+ return flatOptions;
749
+ }
693
750
  }