selective-ui 1.2.4 → 1.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/selective-ui.css +64 -58
- package/dist/selective-ui.css.map +1 -1
- package/dist/selective-ui.esm.js +4396 -1344
- package/dist/selective-ui.esm.js.map +1 -1
- package/dist/selective-ui.esm.min.js +2 -2
- package/dist/selective-ui.esm.min.js.br +0 -0
- package/dist/selective-ui.min.css +1 -1
- package/dist/selective-ui.min.css.br +0 -0
- package/dist/selective-ui.min.js +2 -2
- package/dist/selective-ui.min.js.br +0 -0
- package/dist/selective-ui.umd.js +4401 -1345
- package/dist/selective-ui.umd.js.map +1 -1
- package/package.json +3 -3
- package/src/css/components/accessorybox.css +1 -1
- package/src/css/components/directive.css +2 -2
- package/src/css/components/option-handle.css +4 -4
- package/src/css/components/placeholder.css +1 -1
- package/src/css/components/popup/empty-state.css +3 -3
- package/src/css/components/popup/loading-state.css +3 -3
- package/src/css/components/popup/popup.css +5 -5
- package/src/css/components/searchbox.css +2 -2
- package/src/css/components/selectbox.css +7 -7
- package/src/css/views/group-view.css +8 -8
- package/src/css/views/option-view.css +22 -22
- package/src/ts/adapter/mixed-adapter.ts +248 -92
- package/src/ts/components/accessorybox.ts +170 -73
- package/src/ts/components/directive.ts +55 -26
- package/src/ts/components/option-handle.ts +127 -60
- package/src/ts/components/placeholder.ts +73 -35
- package/src/ts/components/popup/empty-state.ts +71 -35
- package/src/ts/components/popup/loading-state.ts +73 -33
- package/src/ts/components/popup/popup.ts +19 -39
- package/src/ts/components/searchbox.ts +189 -50
- package/src/ts/components/selectbox.ts +401 -40
- package/src/ts/core/base/adapter.ts +160 -79
- package/src/ts/core/base/fenwick.ts +147 -0
- package/src/ts/core/base/lifecycle.ts +118 -35
- package/src/ts/core/base/model.ts +94 -36
- package/src/ts/core/base/recyclerview.ts +0 -1
- package/src/ts/core/base/view.ts +54 -23
- package/src/ts/core/base/virtual-recyclerview.ts +365 -283
- package/src/ts/core/model-manager.ts +172 -92
- package/src/ts/core/search-controller.ts +166 -93
- package/src/ts/global.ts +26 -5
- package/src/ts/index.ts +22 -3
- package/src/ts/models/group-model.ts +138 -32
- package/src/ts/models/option-model.ts +197 -53
- package/src/ts/services/dataset-observer.ts +72 -10
- package/src/ts/services/ea-observer.ts +87 -10
- package/src/ts/services/effector.ts +181 -32
- package/src/ts/services/refresher.ts +32 -7
- package/src/ts/services/resize-observer.ts +136 -19
- package/src/ts/services/select-observer.ts +115 -50
- package/src/ts/types/core/base/view.type.ts +3 -3
- package/src/ts/types/core/base/virtual-recyclerview.type.ts +1 -1
- package/src/ts/types/plugins/plugin.type.ts +46 -0
- package/src/ts/types/utils/ievents.type.ts +6 -1
- package/src/ts/types/utils/istorage.type.ts +8 -4
- package/src/ts/types/utils/libs.type.ts +2 -2
- package/src/ts/types/utils/selective.type.ts +14 -1
- package/src/ts/utils/callback-scheduler.ts +115 -37
- package/src/ts/utils/ievents.ts +91 -29
- package/src/ts/utils/libs.ts +41 -65
- package/src/ts/utils/selective.ts +412 -79
- package/src/ts/views/group-view.ts +142 -31
- package/src/ts/views/option-view.ts +272 -60
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
|
|
1
2
|
import { Libs } from "../utils/libs";
|
|
2
3
|
import { Refresher } from "../services/refresher";
|
|
3
4
|
import { PlaceHolder } from "./placeholder";
|
|
@@ -22,49 +23,175 @@ import type { SelectiveOptions } from "../types/utils/selective.type";
|
|
|
22
23
|
import { IEventToken, IEventCallback } from "../types/utils/ievents.type";
|
|
23
24
|
import { MixedItem } from "../types/core/base/mixed-adapter.type";
|
|
24
25
|
import { BinderMap } from "../types/utils/istorage.type";
|
|
25
|
-
import { ContainerRuntime, SelectBoxAction } from "../types/components/searchbox.type";
|
|
26
|
+
import { ContainerRuntime, SelectBoxAction, SelectBoxTags } from "../types/components/searchbox.type";
|
|
26
27
|
import { AjaxConfig } from "../types/core/search-controller.type";
|
|
27
28
|
import { Selective } from "../utils/selective";
|
|
28
29
|
import { VirtualRecyclerView } from "../core/base/virtual-recyclerview";
|
|
30
|
+
import type { PluginContext, SelectivePlugin } from "../types/plugins/plugin.type";
|
|
29
31
|
|
|
30
32
|
/**
|
|
31
|
-
*
|
|
33
|
+
* SelectBox
|
|
34
|
+
*
|
|
35
|
+
* Root coordinator component that enhances a native `<select>` element into the library's
|
|
36
|
+
* DOM-driven Select UI. `SelectBox` composes and wires together the major runtime pieces:
|
|
37
|
+
*
|
|
38
|
+
* - **View layer**: {@link PlaceHolder}, {@link Directive}, {@link SearchBox}, {@link Popup}, {@link AccessoryBox}
|
|
39
|
+
* - **Model layer**: {@link ModelManager} with {@link MixedAdapter} resources (groups/options/navigation/visibility)
|
|
40
|
+
* - **Rendering layer**: {@link RecyclerView} or {@link VirtualRecyclerView} (virtual scroll)
|
|
41
|
+
* - **Controllers / services**: {@link SearchController}, {@link Effector}, {@link Refresher}
|
|
42
|
+
* - **Observers**: {@link SelectObserver} and {@link DatasetObserver} for keeping DOM/source-of-truth in sync
|
|
43
|
+
*
|
|
44
|
+
* ### Architecture / Relationships
|
|
45
|
+
* - The native `<select>` remains the canonical form element and is moved into the SelectBox DOM wrapper.
|
|
46
|
+
* - `ModelManager` owns adapter + recyclerview instances and exposes a resource model list.
|
|
47
|
+
* - `Popup` hosts the list UI (adapter ↔ recycler/view) and emits adapter property changes.
|
|
48
|
+
* - `SearchBox` emits external events (search/navigation/enter/esc), which drive adapter navigation and search.
|
|
49
|
+
*
|
|
50
|
+
* ### Lifecycle (Strict FSM)
|
|
51
|
+
* This class uses explicit state guards (`this.state !== ...`) to enforce a strict sequence:
|
|
52
|
+
* - `NEW` → {@link init} (creates subcomponents and runtime wiring) → `INITIALIZED`
|
|
53
|
+
* - {@link mount} (inserts wrapper and relocates `<select>` in DOM) → `MOUNTED`
|
|
54
|
+
* - {@link update} (resize / reactive refresh) → `UPDATED`
|
|
55
|
+
* - {@link destroy} (disconnect observers, destroy children, remove DOM) → `DESTROYED`
|
|
56
|
+
*
|
|
57
|
+
* Each lifecycle entry point is designed to be **idempotent/no-op** when called from an
|
|
58
|
+
* unexpected state.
|
|
59
|
+
*
|
|
60
|
+
* ### External vs Internal Events (Selection)
|
|
61
|
+
* Selection changes can be routed through two different adapter property channels:
|
|
62
|
+
* - `"selected"`: treated as **external** selection (user-triggered) → calls `change(..., true)`
|
|
63
|
+
* - `"selected_internal"`: treated as **internal** selection (non-trigger) → calls `change(..., false)`
|
|
64
|
+
*
|
|
65
|
+
* This separation allows the framework to distinguish “notify observers / emit events”
|
|
66
|
+
* from “silent state sync” (e.g., restoring selection, programmatic updates).
|
|
67
|
+
*
|
|
68
|
+
* ### DOM / a11y Side Effects
|
|
69
|
+
* - Creates a focusable `ViewPanel` and applies listbox-related ARIA attributes on open/close
|
|
70
|
+
* (`aria-expanded`, `aria-controls`, `aria-haspopup`, `aria-labelledby`, `aria-multiselectable`).
|
|
71
|
+
* - Stops `mousedown` propagation on the view panel to avoid outer click handlers capturing interaction.
|
|
72
|
+
*
|
|
73
|
+
* @extends Lifecycle
|
|
32
74
|
*/
|
|
33
75
|
export class SelectBox extends Lifecycle {
|
|
76
|
+
/**
|
|
77
|
+
* Runtime container holding:
|
|
78
|
+
* - `view/tags` from {@link Libs.mountNode}
|
|
79
|
+
* - composed child components (placeholder, searchbox, popup, etc.)
|
|
80
|
+
* - runtime services/controllers and observers
|
|
81
|
+
*
|
|
82
|
+
* Declared as a `Partial` because it is progressively populated during {@link init}.
|
|
83
|
+
*/
|
|
34
84
|
public container: Partial<ContainerRuntime> = {};
|
|
35
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Snapshot of the previous selection value used for rollback in `beforeChange` cancellation
|
|
88
|
+
* and max-selection enforcement.
|
|
89
|
+
*
|
|
90
|
+
* @internal
|
|
91
|
+
*/
|
|
36
92
|
private oldValue: unknown = null;
|
|
37
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Root wrapper DOM node for the enhanced UI.
|
|
96
|
+
*
|
|
97
|
+
* Created during {@link init} via {@link Libs.mountNode}, inserted into the DOM during {@link mount},
|
|
98
|
+
* and removed during {@link destroy}.
|
|
99
|
+
*/
|
|
38
100
|
private node: HTMLDivElement | null = null;
|
|
39
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Parsed configuration (bound from the `<select>` element via binder map).
|
|
104
|
+
*
|
|
105
|
+
* Provides feature flags (multiple/disabled/readonly/visible/virtualScroll/ajax/autoclose…),
|
|
106
|
+
* a11y ids (e.g. `SEID_LIST`, `SEID_HOLDER`) and user callbacks under `options.on`.
|
|
107
|
+
*
|
|
108
|
+
* @internal
|
|
109
|
+
*/
|
|
40
110
|
private options: SelectiveOptions | null = null;
|
|
41
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Manager that owns model resources and bridges the Adapter ↔ RecyclerView pipeline.
|
|
114
|
+
*
|
|
115
|
+
* The configured adapter is {@link MixedAdapter}. The recyclerview implementation is chosen
|
|
116
|
+
* based on `options.virtualScroll` (standard {@link RecyclerView} vs {@link VirtualRecyclerView}).
|
|
117
|
+
*
|
|
118
|
+
* @internal
|
|
119
|
+
*/
|
|
42
120
|
private optionModelManager: ModelManager<MixedItem, MixedAdapter> | null = null;
|
|
43
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Whether the popup/list UI is currently open.
|
|
124
|
+
*
|
|
125
|
+
* This is authoritative for the action API (`getAction().isOpen`) and open/close guards.
|
|
126
|
+
*
|
|
127
|
+
* @internal
|
|
128
|
+
*/
|
|
44
129
|
private isOpen = false;
|
|
45
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Tracks whether an initial AJAX load has been performed at least once.
|
|
133
|
+
* Used to avoid redundant initial fetches on open.
|
|
134
|
+
*
|
|
135
|
+
* @internal
|
|
136
|
+
*/
|
|
46
137
|
private hasLoadedOnce = false;
|
|
47
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Tracks whether the instance is in "pre-search" mode (a search is about to happen).
|
|
141
|
+
* Used as a hint to perform AJAX refresh on open.
|
|
142
|
+
*
|
|
143
|
+
* @internal
|
|
144
|
+
*/
|
|
48
145
|
private isBeforeSearch = false;
|
|
49
146
|
|
|
50
|
-
/**
|
|
147
|
+
/**
|
|
148
|
+
* Tracks whether {@link deInit} has already run.
|
|
149
|
+
*
|
|
150
|
+
* This guards teardown work (including plugin lifecycle hooks) from running more than once
|
|
151
|
+
* when {@link deInit} is called separately before {@link destroy}.
|
|
152
|
+
*
|
|
153
|
+
* @internal
|
|
154
|
+
*/
|
|
155
|
+
private hasDeInitialized = false;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Selective context (global helper / registry).
|
|
159
|
+
*
|
|
160
|
+
* Used to locate the instance wrapper via `Selective.find(...)` and to close other open instances.
|
|
161
|
+
*/
|
|
51
162
|
public Selective: Selective | null = null;
|
|
52
163
|
|
|
53
164
|
/**
|
|
54
|
-
*
|
|
55
|
-
|
|
165
|
+
* Registered plugins for this SelectBox instance.
|
|
166
|
+
*/
|
|
167
|
+
private plugins: SelectivePlugin[] = [];
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Cached plugin context for this SelectBox instance.
|
|
171
|
+
*/
|
|
172
|
+
private pluginContext: PluginContext<SelectBoxTags> | null = null;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Creates a {@link SelectBox} bound to a native `<select>` element.
|
|
176
|
+
*
|
|
177
|
+
* When both `select` and `Selective` are provided, the instance initializes immediately
|
|
178
|
+
* (bind options from dataset/binder map and enters the lifecycle via {@link init}).
|
|
56
179
|
*
|
|
57
|
-
* @param
|
|
58
|
-
* @param
|
|
180
|
+
* @param select - The native select element to enhance.
|
|
181
|
+
* @param Selective - The Selective framework context used for registry/services.
|
|
59
182
|
*/
|
|
60
|
-
public constructor(select: HTMLSelectElement
|
|
183
|
+
public constructor(select: HTMLSelectElement, Selective: Selective) {
|
|
61
184
|
super();
|
|
62
185
|
if (select && Selective) this.initialize(select, Selective);
|
|
63
186
|
}
|
|
64
187
|
|
|
65
188
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
189
|
+
* Disabled state mirror for both runtime behavior and DOM/a11y representation.
|
|
190
|
+
*
|
|
191
|
+
* Side effects when set:
|
|
192
|
+
* - Updates `options.disabled`
|
|
193
|
+
* - Toggles `.disabled` on the root wrapper
|
|
194
|
+
* - Sets `aria-disabled` on wrapper and view panel
|
|
68
195
|
*/
|
|
69
196
|
public get isDisabled(): boolean {
|
|
70
197
|
return !!this.options?.disabled;
|
|
@@ -78,8 +205,11 @@ export class SelectBox extends Lifecycle {
|
|
|
78
205
|
}
|
|
79
206
|
|
|
80
207
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
208
|
+
* Read-only state mirror.
|
|
209
|
+
*
|
|
210
|
+
* Side effects when set:
|
|
211
|
+
* - Updates `options.readonly`
|
|
212
|
+
* - Toggles `.readonly` on the root wrapper to prevent user interaction in UI layer
|
|
83
213
|
*/
|
|
84
214
|
public get isReadOnly(): boolean {
|
|
85
215
|
return !!this.options?.readonly;
|
|
@@ -91,8 +221,11 @@ export class SelectBox extends Lifecycle {
|
|
|
91
221
|
}
|
|
92
222
|
|
|
93
223
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
224
|
+
* Visibility state mirror.
|
|
225
|
+
*
|
|
226
|
+
* Side effects when set:
|
|
227
|
+
* - Updates `options.visible`
|
|
228
|
+
* - Toggles `.invisible` class on the root wrapper
|
|
96
229
|
*/
|
|
97
230
|
public get isVisible(): boolean {
|
|
98
231
|
return !!this.options?.visible;
|
|
@@ -104,18 +237,47 @@ export class SelectBox extends Lifecycle {
|
|
|
104
237
|
}
|
|
105
238
|
|
|
106
239
|
/**
|
|
107
|
-
*
|
|
240
|
+
* Binds configuration and Selective context, then enters lifecycle initialization.
|
|
241
|
+
*
|
|
242
|
+
* Sources configuration from the select element binder map:
|
|
243
|
+
* - {@link Libs.getBinderMap} → {@link BinderMap.options} → {@link SelectiveOptions}
|
|
244
|
+
*
|
|
245
|
+
* @param select - Native select element being enhanced.
|
|
246
|
+
* @param Selective - Selective runtime context.
|
|
247
|
+
* @internal
|
|
108
248
|
*/
|
|
109
|
-
private initialize(select: HTMLSelectElement, Selective:
|
|
110
|
-
const bindedMap = Libs.getBinderMap(select)
|
|
111
|
-
this.options = bindedMap.options
|
|
249
|
+
private initialize(select: HTMLSelectElement, Selective: Selective): void {
|
|
250
|
+
const bindedMap = Libs.getBinderMap<BinderMap>(select);
|
|
251
|
+
this.options = bindedMap.options;
|
|
112
252
|
this.Selective = Selective;
|
|
113
253
|
|
|
114
254
|
this.init(select);
|
|
115
255
|
}
|
|
116
256
|
|
|
117
257
|
/**
|
|
118
|
-
*
|
|
258
|
+
* Lifecycle: `init` (composition / wiring stage).
|
|
259
|
+
*
|
|
260
|
+
* Strict FSM:
|
|
261
|
+
* - No-ops unless `state === NEW`.
|
|
262
|
+
*
|
|
263
|
+
* Responsibilities:
|
|
264
|
+
* - Instantiate view subcomponents (placeholder/directive/searchbox/accessory/popup).
|
|
265
|
+
* - Create and mount the container DOM structure (but does not insert into document yet).
|
|
266
|
+
* - Configure {@link ModelManager} with {@link MixedAdapter} and a RecyclerView implementation
|
|
267
|
+
* ({@link VirtualRecyclerView} when `options.virtualScroll`).
|
|
268
|
+
* - Create initial model resources by parsing the source `<select>`.
|
|
269
|
+
* - Wire controller/service flows:
|
|
270
|
+
* - search events → {@link SearchController} → adapter updates → popup resize/highlight resets
|
|
271
|
+
* - adapter selection changes → action API {@link SelectBoxAction.change} with trigger rules
|
|
272
|
+
* - Connect observers for two-way synchronization:
|
|
273
|
+
* - {@link SelectObserver} for option changes in `<select>`
|
|
274
|
+
* - {@link DatasetObserver} for runtime flags (disabled/readonly/visible) from dataset
|
|
275
|
+
*
|
|
276
|
+
* DOM/a11y:
|
|
277
|
+
* - Ensures placeholder node has an id for `aria-labelledby` usage.
|
|
278
|
+
* - Adds a keydown handler on `ViewPanel` to open on Enter/Space/ArrowDown.
|
|
279
|
+
*
|
|
280
|
+
* @param select - Native select element used as source of truth for options/value.
|
|
119
281
|
*/
|
|
120
282
|
public init(select?: HTMLSelectElement): void {
|
|
121
283
|
if (this.state !== LifecycleState.NEW) return;
|
|
@@ -138,15 +300,15 @@ export class SelectBox extends Lifecycle {
|
|
|
138
300
|
// ensure placeholder has id for aria-labelledby usage
|
|
139
301
|
if (placeholder.node) placeholder.node.id = String(options.SEID_HOLDER ?? "");
|
|
140
302
|
|
|
141
|
-
const container = Libs.mountNode(
|
|
303
|
+
const container = Libs.mountNode<ContainerRuntime>(
|
|
142
304
|
{
|
|
143
305
|
Container: {
|
|
144
|
-
tag: { node: "div", classList: "
|
|
306
|
+
tag: { node: "div", classList: "seui-MAIN" },
|
|
145
307
|
child: {
|
|
146
308
|
ViewPanel: {
|
|
147
309
|
tag: {
|
|
148
310
|
node: "div",
|
|
149
|
-
classList: "
|
|
311
|
+
classList: "seui-view",
|
|
150
312
|
tabIndex: 0,
|
|
151
313
|
onkeydown: (e: KeyboardEvent) => {
|
|
152
314
|
if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
|
|
@@ -165,7 +327,7 @@ export class SelectBox extends Lifecycle {
|
|
|
165
327
|
},
|
|
166
328
|
},
|
|
167
329
|
null
|
|
168
|
-
)
|
|
330
|
+
);
|
|
169
331
|
|
|
170
332
|
this.container = container;
|
|
171
333
|
this.node = container.view as HTMLDivElement;
|
|
@@ -190,9 +352,9 @@ export class SelectBox extends Lifecycle {
|
|
|
190
352
|
}
|
|
191
353
|
optionModelManager.createModelResources(Libs.parseSelectToArray(select));
|
|
192
354
|
|
|
193
|
-
optionModelManager.
|
|
355
|
+
optionModelManager.on("onUpdate", () => {
|
|
194
356
|
container.popup?.triggerResize?.();
|
|
195
|
-
};
|
|
357
|
+
});
|
|
196
358
|
|
|
197
359
|
this.optionModelManager = optionModelManager;
|
|
198
360
|
|
|
@@ -217,6 +379,21 @@ export class SelectBox extends Lifecycle {
|
|
|
217
379
|
this.setupEventHandlers(select, container, options, searchController, searchbox);
|
|
218
380
|
this.setupObservers(selectObserver, datasetObserver, select, optionModelManager);
|
|
219
381
|
|
|
382
|
+
this.plugins = this.Selective?.getPlugins?.() ?? [];
|
|
383
|
+
if (this.plugins.length) {
|
|
384
|
+
const resources = optionModelManager.getResources();
|
|
385
|
+
const pluginContext: PluginContext<SelectBoxTags> = {
|
|
386
|
+
selectBox: this,
|
|
387
|
+
options,
|
|
388
|
+
adapter: resources.adapter,
|
|
389
|
+
recycler: resources.recyclerView,
|
|
390
|
+
viewTags: container.tags,
|
|
391
|
+
actions: this.getAction(),
|
|
392
|
+
};
|
|
393
|
+
this.pluginContext = pluginContext;
|
|
394
|
+
this.runPluginHook("init", (plugin) => plugin.init?.(pluginContext));
|
|
395
|
+
}
|
|
396
|
+
|
|
220
397
|
// Initial states
|
|
221
398
|
this.isDisabled = Libs.string2Boolean(options.disabled);
|
|
222
399
|
this.isReadOnly = Libs.string2Boolean(options.readonly);
|
|
@@ -226,14 +403,24 @@ export class SelectBox extends Lifecycle {
|
|
|
226
403
|
}
|
|
227
404
|
|
|
228
405
|
/**
|
|
229
|
-
*
|
|
406
|
+
* Lifecycle: `mount` (DOM insertion stage).
|
|
407
|
+
*
|
|
408
|
+
* Strict FSM:
|
|
409
|
+
* - No-ops unless `state === INITIALIZED`.
|
|
410
|
+
*
|
|
411
|
+
* DOM operations:
|
|
412
|
+
* - Inserts the SelectBox wrapper before the original `<select>`.
|
|
413
|
+
* - Moves the `<select>` inside the wrapper (before `ViewPanel`) to preserve form behavior.
|
|
414
|
+
* - Adds a `mousedown` handler to `ViewPanel` to contain interactions and prevent outer handlers.
|
|
415
|
+
* - Applies initial sizing (`Refresher.resizeBox`) and marks the select as initialized (`.init`).
|
|
416
|
+
* - Applies an initial "mask" refresh via `change(null, false)` without emitting external triggers.
|
|
230
417
|
*/
|
|
231
418
|
public mount(): void {
|
|
232
419
|
if (this.state !== LifecycleState.INITIALIZED) return;
|
|
233
420
|
if (!this.node || !this.container.targetElement) return;
|
|
234
421
|
|
|
235
422
|
const select = this.container.targetElement;
|
|
236
|
-
const container = this.container
|
|
423
|
+
const container = this.container;
|
|
237
424
|
|
|
238
425
|
// Mount into DOM: wrapper before select, then move select inside
|
|
239
426
|
select.parentNode?.insertBefore(this.node, select);
|
|
@@ -255,7 +442,18 @@ export class SelectBox extends Lifecycle {
|
|
|
255
442
|
}
|
|
256
443
|
|
|
257
444
|
/**
|
|
258
|
-
*
|
|
445
|
+
* Lifecycle: `update` (reactive refresh stage).
|
|
446
|
+
*
|
|
447
|
+
* Strict FSM:
|
|
448
|
+
* - No-ops unless `state === MOUNTED`.
|
|
449
|
+
*
|
|
450
|
+
* Behavior:
|
|
451
|
+
* - Triggers popup resize recalculation to keep layout consistent with content changes
|
|
452
|
+
* (e.g. filtering results, collapses/expands, accessory changes).
|
|
453
|
+
*
|
|
454
|
+
* Note:
|
|
455
|
+
* - Actual data mutations are driven by adapter/model updates and action API methods,
|
|
456
|
+
* not by this method directly.
|
|
259
457
|
*/
|
|
260
458
|
public update(): void {
|
|
261
459
|
if (this.state !== LifecycleState.MOUNTED) return;
|
|
@@ -267,7 +465,24 @@ export class SelectBox extends Lifecycle {
|
|
|
267
465
|
}
|
|
268
466
|
|
|
269
467
|
/**
|
|
270
|
-
*
|
|
468
|
+
* Wires event handlers between UI components, controller, and adapter.
|
|
469
|
+
*
|
|
470
|
+
* Key flows:
|
|
471
|
+
* - SearchBox input → SearchController.search/clear → Popup resize + adapter highlight reset
|
|
472
|
+
* - SearchBox navigation/enter/esc → MixedAdapter.navigate/selectHighlighted + close + focus restore
|
|
473
|
+
* - Adapter highlight changes → SearchBox `aria-activedescendant`
|
|
474
|
+
* - Adapter collapsed changes → Popup resize
|
|
475
|
+
*
|
|
476
|
+
* Trigger semantics:
|
|
477
|
+
* - The `isTrigger` boolean from SearchBox is used to distinguish user-driven vs programmatic clears.
|
|
478
|
+
* - AJAX searches optionally show/hide loading UI and respect `delaysearchtime`.
|
|
479
|
+
*
|
|
480
|
+
* @param select - The enhanced native select element.
|
|
481
|
+
* @param container - The assembled runtime container.
|
|
482
|
+
* @param options - Bound configuration flags and callbacks.
|
|
483
|
+
* @param searchController - Controller responsible for local/AJAX searches and pagination.
|
|
484
|
+
* @param searchbox - Search input component emitting search/navigation intents.
|
|
485
|
+
* @internal
|
|
271
486
|
*/
|
|
272
487
|
private setupEventHandlers(
|
|
273
488
|
select: HTMLSelectElement,
|
|
@@ -276,7 +491,7 @@ export class SelectBox extends Lifecycle {
|
|
|
276
491
|
searchController: SearchController,
|
|
277
492
|
searchbox: SearchBox
|
|
278
493
|
): void {
|
|
279
|
-
const optionAdapter = container.popup!.optionAdapter
|
|
494
|
+
const optionAdapter = container.popup!.optionAdapter;
|
|
280
495
|
let hightlightTimer: ReturnType<typeof setTimeout> | null = null;
|
|
281
496
|
|
|
282
497
|
const searchHandle = (keyword: string, isTrigger: boolean) => {
|
|
@@ -352,12 +567,28 @@ export class SelectBox extends Lifecycle {
|
|
|
352
567
|
|
|
353
568
|
// AJAX setup (if provided)
|
|
354
569
|
if (options.ajax) {
|
|
570
|
+
if (options.ajax?.keepSelected == undefined) {
|
|
571
|
+
options.ajax.keepSelected = options.keepSelected;
|
|
572
|
+
}
|
|
355
573
|
searchController.setAjax(options.ajax);
|
|
356
574
|
}
|
|
357
575
|
}
|
|
358
576
|
|
|
359
577
|
/**
|
|
360
|
-
*
|
|
578
|
+
* Connects and wires observers that synchronize the enhanced UI with the source `<select>`
|
|
579
|
+
* element and its dataset-based runtime flags.
|
|
580
|
+
*
|
|
581
|
+
* - {@link SelectObserver}:
|
|
582
|
+
* - On change, re-parses the select into resources and refreshes the selection mask.
|
|
583
|
+
* - {@link DatasetObserver}:
|
|
584
|
+
* - On change, mirrors dataset flags into runtime properties:
|
|
585
|
+
* `disabled` / `readonly` / `visible`
|
|
586
|
+
*
|
|
587
|
+
* @param selectObserver - Observer tracking select option/value mutations.
|
|
588
|
+
* @param datasetObserver - Observer tracking dataset attribute changes.
|
|
589
|
+
* @param select - The enhanced native select element.
|
|
590
|
+
* @param optionModelManager - Model manager to update from parsed select.
|
|
591
|
+
* @internal
|
|
361
592
|
*/
|
|
362
593
|
private setupObservers(
|
|
363
594
|
selectObserver: SelectObserver,
|
|
@@ -367,7 +598,7 @@ export class SelectBox extends Lifecycle {
|
|
|
367
598
|
): void {
|
|
368
599
|
selectObserver.connect();
|
|
369
600
|
selectObserver.onChanged = (sel) => {
|
|
370
|
-
optionModelManager.
|
|
601
|
+
optionModelManager.updateModel(Libs.parseSelectToArray(sel));
|
|
371
602
|
this.getAction()?.refreshMask();
|
|
372
603
|
};
|
|
373
604
|
|
|
@@ -386,18 +617,44 @@ export class SelectBox extends Lifecycle {
|
|
|
386
617
|
}
|
|
387
618
|
|
|
388
619
|
/**
|
|
389
|
-
* Disconnects observers associated with
|
|
620
|
+
* Disconnects observers associated with this instance.
|
|
621
|
+
*
|
|
622
|
+
* This is used during {@link destroy} to ensure external DOM observers are stopped,
|
|
623
|
+
* preventing memory leaks and unintended background updates.
|
|
390
624
|
*/
|
|
391
625
|
public deInit(): void {
|
|
626
|
+
if (this.hasDeInitialized) {
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
|
|
392
630
|
const c: any = this.container ?? {};
|
|
393
631
|
const { selectObserver, datasetObserver } = c;
|
|
394
632
|
|
|
633
|
+
if (this.plugins.length) {
|
|
634
|
+
this.runPluginHook("destroy", (plugin) => plugin.destroy?.());
|
|
635
|
+
}
|
|
636
|
+
this.plugins = [];
|
|
637
|
+
this.pluginContext = null;
|
|
638
|
+
|
|
395
639
|
if (selectObserver?.disconnect) selectObserver.disconnect();
|
|
396
640
|
if (datasetObserver?.disconnect) datasetObserver.disconnect();
|
|
641
|
+
|
|
642
|
+
this.hasDeInitialized = true;
|
|
397
643
|
}
|
|
398
644
|
|
|
399
645
|
/**
|
|
400
|
-
*
|
|
646
|
+
* Lifecycle: `destroy` (teardown stage).
|
|
647
|
+
*
|
|
648
|
+
* Strict FSM / idempotency:
|
|
649
|
+
* - No-ops when already in {@link LifecycleState.DESTROYED}.
|
|
650
|
+
*
|
|
651
|
+
* Responsibilities:
|
|
652
|
+
* - Disconnect observers.
|
|
653
|
+
* - Destroy composed child components/controllers.
|
|
654
|
+
* - Remove wrapper DOM from the document.
|
|
655
|
+
* - Clear references to enable garbage collection.
|
|
656
|
+
*
|
|
657
|
+
* @override
|
|
401
658
|
*/
|
|
402
659
|
public override destroy(): void {
|
|
403
660
|
if (this.is(LifecycleState.DESTROYED)) {
|
|
@@ -415,6 +672,7 @@ export class SelectBox extends Lifecycle {
|
|
|
415
672
|
container.accessorybox.destroy();
|
|
416
673
|
container.placeholder.destroy();
|
|
417
674
|
container.searchbox.destroy();
|
|
675
|
+
this.optionModelManager.destroy();
|
|
418
676
|
|
|
419
677
|
// Remove from DOM
|
|
420
678
|
this.node?.remove();
|
|
@@ -435,7 +693,33 @@ export class SelectBox extends Lifecycle {
|
|
|
435
693
|
}
|
|
436
694
|
|
|
437
695
|
/**
|
|
438
|
-
*
|
|
696
|
+
* Builds and returns an imperative action API for controlling this SelectBox instance.
|
|
697
|
+
*
|
|
698
|
+
* The returned object is a "facade" used by external consumers (and internal wiring) to:
|
|
699
|
+
* - read/write selection values (`value`, `valueArray`, `setValue`, `selectAll`, `deSelectAll`)
|
|
700
|
+
* - control popup visibility (`open`, `close`, `toggle`)
|
|
701
|
+
* - refresh mask/placeholder (`refreshMask`)
|
|
702
|
+
* - attach event callbacks (`on`)
|
|
703
|
+
* - configure AJAX (`ajax`, `loadAjax`)
|
|
704
|
+
*
|
|
705
|
+
* ### Triggering contract (external vs internal)
|
|
706
|
+
* Many methods accept a `trigger`/`canTrigger` boolean which controls whether:
|
|
707
|
+
* - `beforeChange` / `change` callbacks are invoked via {@link iEvents.callEvent}
|
|
708
|
+
* - native DOM `"change"` is fired on the underlying select
|
|
709
|
+
*
|
|
710
|
+
* This mirrors the library convention of distinguishing user-visible change events from
|
|
711
|
+
* internal/non-trigger state synchronization.
|
|
712
|
+
*
|
|
713
|
+
* ### Side effects
|
|
714
|
+
* - Mutates `OptionModel.selectedNonTrigger` flags to update selection.
|
|
715
|
+
* - Writes to the native select value for single-select mode.
|
|
716
|
+
* - Updates UI mask and accessory box, and requests popup resizing where needed.
|
|
717
|
+
* - Applies a11y attributes to `ViewPanel` on open/close.
|
|
718
|
+
*
|
|
719
|
+
* No-ops:
|
|
720
|
+
* - Returns `null` when the binder map is missing for the current target element.
|
|
721
|
+
*
|
|
722
|
+
* @returns An action facade for controlling this instance, or `null` if not bound.
|
|
439
723
|
*/
|
|
440
724
|
public getAction(): SelectBoxAction | null {
|
|
441
725
|
const container = this.container;
|
|
@@ -444,7 +728,7 @@ export class SelectBox extends Lifecycle {
|
|
|
444
728
|
return this.Selective.find(container.targetElement);
|
|
445
729
|
};
|
|
446
730
|
|
|
447
|
-
const bindedMap = Libs.getBinderMap(container.targetElement)
|
|
731
|
+
const bindedMap = Libs.getBinderMap<BinderMap>(container.targetElement);
|
|
448
732
|
if (!bindedMap) return null;
|
|
449
733
|
|
|
450
734
|
const bindedOptions = bindedMap.options;
|
|
@@ -702,6 +986,9 @@ export class SelectBox extends Lifecycle {
|
|
|
702
986
|
if (bindedOptions.multiple) ViewPanel.setAttribute("aria-multiselectable", "true");
|
|
703
987
|
|
|
704
988
|
iEvents.callEvent([getInstance()], ...bindedOptions.on.show);
|
|
989
|
+
if (superThis.pluginContext) {
|
|
990
|
+
superThis.runPluginHook("onOpen", (plugin) => plugin.onOpen?.(superThis.pluginContext));
|
|
991
|
+
}
|
|
705
992
|
return;
|
|
706
993
|
},
|
|
707
994
|
|
|
@@ -722,6 +1009,9 @@ export class SelectBox extends Lifecycle {
|
|
|
722
1009
|
container.tags.ViewPanel.setAttribute("aria-expanded", "false");
|
|
723
1010
|
|
|
724
1011
|
iEvents.callEvent([getInstance()], ...bindedOptions.on.close);
|
|
1012
|
+
if (superThis.pluginContext) {
|
|
1013
|
+
superThis.runPluginHook("onClose", (plugin) => plugin.onClose?.(superThis.pluginContext));
|
|
1014
|
+
}
|
|
725
1015
|
return;
|
|
726
1016
|
},
|
|
727
1017
|
|
|
@@ -764,6 +1054,13 @@ export class SelectBox extends Lifecycle {
|
|
|
764
1054
|
if (superThis.is(LifecycleState.MOUNTED)) {
|
|
765
1055
|
superThis.update();
|
|
766
1056
|
}
|
|
1057
|
+
|
|
1058
|
+
if (superThis.pluginContext && superThis.optionModelManager) {
|
|
1059
|
+
const resources = superThis.optionModelManager.getResources();
|
|
1060
|
+
superThis.runPluginHook("onChange", (plugin) =>
|
|
1061
|
+
plugin.onChange?.(this.value, resources.modelList, resources.adapter, superThis.pluginContext)
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
767
1064
|
},
|
|
768
1065
|
|
|
769
1066
|
refreshMask() {
|
|
@@ -785,6 +1082,9 @@ export class SelectBox extends Lifecycle {
|
|
|
785
1082
|
},
|
|
786
1083
|
|
|
787
1084
|
ajax(_evtToken: IEventCallback, obj: AjaxConfig) {
|
|
1085
|
+
if (obj.keepSelected == undefined) {
|
|
1086
|
+
obj.keepSelected = superThis.options.keepSelected;
|
|
1087
|
+
}
|
|
788
1088
|
container.searchController.setAjax(obj);
|
|
789
1089
|
},
|
|
790
1090
|
|
|
@@ -822,7 +1122,29 @@ export class SelectBox extends Lifecycle {
|
|
|
822
1122
|
}
|
|
823
1123
|
|
|
824
1124
|
/**
|
|
825
|
-
*
|
|
1125
|
+
* Defines a mirrored facade property on an arbitrary object.
|
|
1126
|
+
*
|
|
1127
|
+
* This helper is used when building the {@link SelectBoxAction} facade to expose
|
|
1128
|
+
* `disabled` / `readonly` / `visible` as ergonomic properties while keeping them
|
|
1129
|
+
* synchronized with the underlying {@link SelectBox} runtime state.
|
|
1130
|
+
*
|
|
1131
|
+
* ### Behavior
|
|
1132
|
+
* - Getter proxies the current runtime value from `this[privateProp]`.
|
|
1133
|
+
* - Setter coerces the incoming value to boolean and writes it to `this[privateProp]`.
|
|
1134
|
+
* - Additionally reflects the value onto `targetElement.dataset[prop]` when available,
|
|
1135
|
+
* allowing external dataset observers (and DOM tooling) to observe state changes.
|
|
1136
|
+
*
|
|
1137
|
+
* ### Side effects
|
|
1138
|
+
* - Mutates the action facade object via `Object.defineProperty`.
|
|
1139
|
+
* - Mutates DOM dataset on the underlying `<select>` element (if present).
|
|
1140
|
+
*
|
|
1141
|
+
* No-ops:
|
|
1142
|
+
* - Dataset reflection is skipped when `container.targetElement.dataset` is unavailable.
|
|
1143
|
+
*
|
|
1144
|
+
* @param obj - The facade object to define the property on.
|
|
1145
|
+
* @param prop - The public facade property name (`disabled` | `readonly` | `visible`).
|
|
1146
|
+
* @param privateProp - The backing SelectBox property name (`isDisabled` | `isReadOnly` | `isVisible`).
|
|
1147
|
+
* @internal
|
|
826
1148
|
*/
|
|
827
1149
|
private createSymProp(
|
|
828
1150
|
obj: Record<string, any>,
|
|
@@ -847,7 +1169,25 @@ export class SelectBox extends Lifecycle {
|
|
|
847
1169
|
}
|
|
848
1170
|
|
|
849
1171
|
/**
|
|
850
|
-
*
|
|
1172
|
+
* Returns a flat list of {@link OptionModel} items from current model resources.
|
|
1173
|
+
*
|
|
1174
|
+
* The underlying resource list may contain a mix of:
|
|
1175
|
+
* - {@link OptionModel} (standalone options)
|
|
1176
|
+
* - {@link GroupModel} (group headers with nested `items`)
|
|
1177
|
+
*
|
|
1178
|
+
* This method flattens the structure into a single array of options, optionally
|
|
1179
|
+
* filtered by the *current* selection state.
|
|
1180
|
+
*
|
|
1181
|
+
* ### Filtering
|
|
1182
|
+
* - When `isSelected` is `true` or `false`, filters by `OptionModel.selected`.
|
|
1183
|
+
* - When `isSelected` is `null`, returns all available options.
|
|
1184
|
+
*
|
|
1185
|
+
* No-ops:
|
|
1186
|
+
* - Returns an empty array if the {@link optionModelManager} is not available.
|
|
1187
|
+
*
|
|
1188
|
+
* @param isSelected - Optional selection filter (`true` | `false` | `null`). Defaults to `null`.
|
|
1189
|
+
* @returns A flat array of option models (possibly filtered).
|
|
1190
|
+
* @internal
|
|
851
1191
|
*/
|
|
852
1192
|
private getModelOption(isSelected: boolean | null = null): OptionModel[] {
|
|
853
1193
|
if (!this.optionModelManager) return [];
|
|
@@ -855,7 +1195,7 @@ export class SelectBox extends Lifecycle {
|
|
|
855
1195
|
const { modelList } = this.optionModelManager.getResources();
|
|
856
1196
|
const flatOptions: OptionModel[] = [];
|
|
857
1197
|
|
|
858
|
-
for (const m of modelList
|
|
1198
|
+
for (const m of modelList) {
|
|
859
1199
|
if (m instanceof OptionModel) {
|
|
860
1200
|
flatOptions.push(m);
|
|
861
1201
|
} else if (m instanceof GroupModel) {
|
|
@@ -869,4 +1209,25 @@ export class SelectBox extends Lifecycle {
|
|
|
869
1209
|
|
|
870
1210
|
return flatOptions;
|
|
871
1211
|
}
|
|
872
|
-
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* Safely runs a hook across all registered plugins.
|
|
1215
|
+
*
|
|
1216
|
+
* Any plugin failure is isolated to prevent breaking the current flow.
|
|
1217
|
+
*
|
|
1218
|
+
* @param hook - Hook name for logging context.
|
|
1219
|
+
* @param runner - Hook invocation handler.
|
|
1220
|
+
* @internal
|
|
1221
|
+
*/
|
|
1222
|
+
private runPluginHook(hook: string, runner: (plugin: SelectivePlugin) => void): void {
|
|
1223
|
+
if (!this.plugins.length) return;
|
|
1224
|
+
|
|
1225
|
+
this.plugins.forEach((plugin) => {
|
|
1226
|
+
try {
|
|
1227
|
+
runner(plugin);
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
console.error(`Plugin "${plugin.id}" ${hook} error:`, error);
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
}
|