selective-ui 1.2.3 → 1.2.5
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/dist/selective-ui.css.map +1 -1
- package/dist/selective-ui.esm.js +5462 -1043
- 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.js +2 -2
- package/dist/selective-ui.min.js.br +0 -0
- package/dist/selective-ui.umd.js +5463 -1044
- package/dist/selective-ui.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/ts/adapter/mixed-adapter.ts +312 -65
- package/src/ts/components/accessorybox.ts +248 -28
- package/src/ts/components/directive.ts +91 -11
- package/src/ts/components/option-handle.ts +191 -28
- package/src/ts/components/placeholder.ts +111 -16
- package/src/ts/components/popup/empty-state.ts +162 -0
- package/src/ts/components/popup/loading-state.ts +160 -0
- package/src/ts/components/{popup.ts → popup/popup.ts} +167 -71
- package/src/ts/components/searchbox.ts +225 -20
- package/src/ts/components/selectbox.ts +498 -120
- package/src/ts/core/base/adapter.ts +200 -53
- package/src/ts/core/base/fenwick.ts +147 -0
- package/src/ts/core/base/lifecycle.ts +258 -0
- package/src/ts/core/base/model.ts +120 -31
- package/src/ts/core/base/recyclerview.ts +55 -18
- package/src/ts/core/base/view.ts +87 -19
- package/src/ts/core/base/virtual-recyclerview.ts +475 -202
- package/src/ts/core/model-manager.ts +166 -85
- package/src/ts/core/search-controller.ts +236 -38
- package/src/ts/global.ts +6 -6
- package/src/ts/index.ts +6 -6
- package/src/ts/models/group-model.ts +159 -32
- package/src/ts/models/option-model.ts +213 -54
- package/src/ts/services/dataset-observer.ts +72 -10
- package/src/ts/services/ea-observer.ts +92 -15
- package/src/ts/services/effector.ts +181 -32
- package/src/ts/services/refresher.ts +30 -6
- package/src/ts/services/resize-observer.ts +132 -15
- package/src/ts/services/select-observer.ts +115 -50
- package/src/ts/types/components/searchbox.type.ts +1 -1
- package/src/ts/types/core/base/adapter.type.ts +2 -1
- package/src/ts/types/core/base/lifecycle.type.ts +62 -0
- package/src/ts/types/core/base/model.type.ts +3 -1
- package/src/ts/types/core/base/recyclerview.type.ts +2 -8
- package/src/ts/types/core/base/view.type.ts +36 -24
- package/src/ts/types/utils/ievents.type.ts +6 -1
- package/src/ts/utils/callback-scheduler.ts +112 -34
- package/src/ts/utils/ievents.ts +91 -29
- package/src/ts/utils/istorage.ts +1 -1
- package/src/ts/utils/selective.ts +474 -88
- package/src/ts/views/group-view.ts +170 -21
- package/src/ts/views/option-view.ts +349 -68
- package/src/ts/components/empty-state.ts +0 -68
- package/src/ts/components/loading-state.ts +0 -66
- /package/src/css/components/{empty-state.css → popup/empty-state.css} +0 -0
- /package/src/css/components/{loading-state.css → popup/loading-state.css} +0 -0
- /package/src/css/components/{popup.css → popup/popup.css} +0 -0
- /package/src/css/{components/optgroup.css → views/group-view.css} +0 -0
- /package/src/css/{components/option.css → views/option-view.css} +0 -0
|
@@ -2,42 +2,180 @@ import { Libs } from "../utils/libs";
|
|
|
2
2
|
import { MountViewResult } from "../types/utils/libs.type";
|
|
3
3
|
import { NavigateHandler, SearchBoxTags, SearchHandler } from "../types/components/searchbox.type";
|
|
4
4
|
import { SelectiveOptions } from "../types/utils/selective.type";
|
|
5
|
+
import { Lifecycle } from "../core/base/lifecycle";
|
|
6
|
+
import { LifecycleState } from "../types/core/base/lifecycle.type";
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
/**
|
|
9
|
+
* SearchBox
|
|
10
|
+
*
|
|
11
|
+
* DOM-driven, headless-friendly search input used by the Select UI to filter and
|
|
12
|
+
* navigate option lists. This component owns a small DOM subtree and exposes
|
|
13
|
+
* callback hooks for the host/controller layer to implement filtering, highlight,
|
|
14
|
+
* and commit/cancel behaviors.
|
|
15
|
+
*
|
|
16
|
+
* ### Responsibility
|
|
17
|
+
* - Render a `<input type="search">` wrapped by a container element.
|
|
18
|
+
* - Apply ARIA attributes used by the surrounding listbox/popup integration.
|
|
19
|
+
* - Convert DOM events into typed callbacks:
|
|
20
|
+
* - text input changes → {@link onSearch}
|
|
21
|
+
* - keyboard navigation (ArrowUp/ArrowDown/Tab) → {@link onNavigate}
|
|
22
|
+
* - commit (Enter) → {@link onEnter}
|
|
23
|
+
* - cancel (Escape) → {@link onEsc}
|
|
24
|
+
* - Provide imperative UI helpers:
|
|
25
|
+
* - {@link show}/{@link hide} (visibility + focus/readOnly behavior)
|
|
26
|
+
* - {@link clear} (reset query and optionally trigger the search hook)
|
|
27
|
+
* - {@link setPlaceHolder} (safe placeholder update)
|
|
28
|
+
* - {@link setActiveDescendant} (ARIA highlight binding)
|
|
29
|
+
*
|
|
30
|
+
* ### Lifecycle (Strict FSM)
|
|
31
|
+
* - Constructed in `NEW`.
|
|
32
|
+
* - If options are provided, {@link initialize} creates DOM and calls `init()`
|
|
33
|
+
* → transitions to `INITIALIZED`.
|
|
34
|
+
* - This class does not override `update()`: runtime changes are performed via
|
|
35
|
+
* its imperative methods (e.g., {@link show}, {@link clear}, {@link setPlaceHolder}).
|
|
36
|
+
* - {@link destroy} is terminal: removes DOM references and ends lifecycle.
|
|
37
|
+
* Subsequent calls become no-ops once {@link LifecycleState.DESTROYED}.
|
|
38
|
+
*
|
|
39
|
+
* ### Event Model / Ownership
|
|
40
|
+
* - This component does **not** own filtering logic or selection state.
|
|
41
|
+
* - All "meaningful actions" are emitted outward through callbacks (external events).
|
|
42
|
+
* - It also performs event containment (`stopPropagation`) to avoid parent-level
|
|
43
|
+
* handlers (e.g., popup/list container) from intercepting interactions.
|
|
44
|
+
*
|
|
45
|
+
* ### a11y / DOM Side Effects
|
|
46
|
+
* - Writes ARIA attributes such as `aria-controls`, `aria-autocomplete`, and
|
|
47
|
+
* `aria-activedescendant` onto the input element.
|
|
48
|
+
* - Intercepts keyboard events and may call `preventDefault()` for navigation keys.
|
|
49
|
+
*
|
|
50
|
+
* @extends Lifecycle
|
|
51
|
+
*/
|
|
52
|
+
export class SearchBox extends Lifecycle {
|
|
7
53
|
/**
|
|
8
|
-
* Creates a
|
|
9
|
-
* and initializes it if options are provided.
|
|
54
|
+
* Creates a new {@link SearchBox}.
|
|
10
55
|
*
|
|
11
|
-
*
|
|
56
|
+
* If `options` is provided, initialization is performed immediately (DOM is created
|
|
57
|
+
* and `init()` is called). If `options` is `null`, the instance stays in `NEW` until
|
|
58
|
+
* initialized elsewhere.
|
|
59
|
+
*
|
|
60
|
+
* @param options - Configuration such as placeholder, accessibility IDs, and flags.
|
|
12
61
|
*/
|
|
13
62
|
constructor(options: SelectiveOptions | null = null) {
|
|
63
|
+
super();
|
|
14
64
|
this.options = options;
|
|
15
|
-
if (options) this.
|
|
65
|
+
if (options) this.initialize(options);
|
|
16
66
|
}
|
|
17
67
|
|
|
68
|
+
/**
|
|
69
|
+
* The mount result returned by {@link Libs.mountNode}.
|
|
70
|
+
*
|
|
71
|
+
* Provides typed access to created DOM tags (e.g., `SearchInput`) and the root view.
|
|
72
|
+
* `null` before initialization and after destruction.
|
|
73
|
+
*
|
|
74
|
+
* @internal
|
|
75
|
+
*/
|
|
18
76
|
private nodeMounted: MountViewResult<SearchBoxTags> | null = null;
|
|
19
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Root container node of this component.
|
|
80
|
+
*
|
|
81
|
+
* Created during {@link initialize} and removed during {@link destroy}.
|
|
82
|
+
* Visibility is controlled by adding/removing the `hide` class.
|
|
83
|
+
*/
|
|
20
84
|
public node: HTMLDivElement | null = null;
|
|
21
85
|
|
|
86
|
+
/**
|
|
87
|
+
* The `<input type="search">` element used to capture user queries.
|
|
88
|
+
*
|
|
89
|
+
* Cached for imperative operations (focus, placeholder updates, ARIA updates).
|
|
90
|
+
* `null` before initialization and after destruction.
|
|
91
|
+
*
|
|
92
|
+
* @internal
|
|
93
|
+
*/
|
|
22
94
|
private SearchInput: HTMLInputElement | null = null;
|
|
23
95
|
|
|
96
|
+
/**
|
|
97
|
+
* External "search changed" hook.
|
|
98
|
+
*
|
|
99
|
+
* Invoked when the user edits text (via the `input` event) and the edit is not
|
|
100
|
+
* part of a handled control-key sequence (e.g., ArrowUp/Down/Tab/Enter/Escape).
|
|
101
|
+
*
|
|
102
|
+
* Ownership:
|
|
103
|
+
* - Implementations typically filter adapter/model state and refresh the list.
|
|
104
|
+
*/
|
|
24
105
|
public onSearch: SearchHandler | null = null;
|
|
25
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Options snapshot used for behavior toggles and attributes.
|
|
109
|
+
*
|
|
110
|
+
* Key fields typically consumed here:
|
|
111
|
+
* - `placeholder`: initial placeholder string
|
|
112
|
+
* - `searchable`: toggles readOnly + focus behavior on {@link show}
|
|
113
|
+
* - `SEID_LIST`: used as `aria-controls` value to bind to listbox container
|
|
114
|
+
*
|
|
115
|
+
* Cleared during {@link destroy}.
|
|
116
|
+
*
|
|
117
|
+
* @internal
|
|
118
|
+
*/
|
|
26
119
|
private options: SelectiveOptions | null = null;
|
|
27
120
|
|
|
121
|
+
/**
|
|
122
|
+
* External navigation hook for list traversal.
|
|
123
|
+
*
|
|
124
|
+
* Called with:
|
|
125
|
+
* - `+1` for forward (ArrowDown / Tab)
|
|
126
|
+
* - `-1` for backward (ArrowUp)
|
|
127
|
+
*
|
|
128
|
+
* Typical consumers update highlight/active option in Adapter/RecyclerView.
|
|
129
|
+
*/
|
|
28
130
|
public onNavigate: NavigateHandler | null = null;
|
|
29
131
|
|
|
132
|
+
/**
|
|
133
|
+
* External "commit" hook (Enter key).
|
|
134
|
+
*
|
|
135
|
+
* Typical consumers confirm selection of the highlighted option or submit the current state.
|
|
136
|
+
*/
|
|
30
137
|
public onEnter: (() => void) | null = null;
|
|
31
138
|
|
|
139
|
+
/**
|
|
140
|
+
* External "cancel" hook (Escape key).
|
|
141
|
+
*
|
|
142
|
+
* Typical consumers close the popup, clear highlight, or reset interaction mode.
|
|
143
|
+
*/
|
|
32
144
|
public onEsc: (() => void) | null = null;
|
|
33
145
|
|
|
34
146
|
/**
|
|
35
|
-
* Initializes
|
|
36
|
-
*
|
|
147
|
+
* Initializes DOM, ARIA attributes, and interaction listeners.
|
|
148
|
+
*
|
|
149
|
+
* DOM structure (conceptually):
|
|
150
|
+
* - Root: `div.selective-ui-searchbox.hide`
|
|
151
|
+
* - Child: `input[type="search"].selective-ui-searchbox-input`
|
|
152
|
+
*
|
|
153
|
+
* Accessibility attributes set on the input:
|
|
154
|
+
* - `role="searchbox"`: announces search field semantics
|
|
155
|
+
* - `aria-controls=options.SEID_LIST`: points to the list container (listbox)
|
|
156
|
+
* - `aria-autocomplete="list"`: indicates suggestion results are list-driven
|
|
157
|
+
*
|
|
158
|
+
* Interaction model:
|
|
159
|
+
* - Mouse down/up: stops propagation to prevent container/popup listeners from interfering.
|
|
160
|
+
* - Keydown:
|
|
161
|
+
* - ArrowDown / Tab → emits {@link onNavigate}(+1)
|
|
162
|
+
* - ArrowUp → emits {@link onNavigate}(-1)
|
|
163
|
+
* - Enter → emits {@link onEnter}()
|
|
164
|
+
* - Escape → emits {@link onEsc}()
|
|
165
|
+
* Control keys are treated as "internal control events" and do not produce {@link onSearch}
|
|
166
|
+
* via the `input` listener (guarded by `isControlKey`).
|
|
167
|
+
* - Input:
|
|
168
|
+
* - Emits {@link onSearch}(value, true) for text edits that are not control-key sequences.
|
|
169
|
+
*
|
|
170
|
+
* Side effects:
|
|
171
|
+
* - Creates DOM nodes via {@link Libs.mountNode}.
|
|
172
|
+
* - Attaches event listeners to the input element.
|
|
173
|
+
* - Transitions lifecycle via `init()` at the end.
|
|
37
174
|
*
|
|
38
|
-
* @param
|
|
175
|
+
* @param options - Configuration including placeholder and listbox id used by `aria-controls`.
|
|
176
|
+
* @internal
|
|
39
177
|
*/
|
|
40
|
-
private
|
|
178
|
+
private initialize(options: SelectiveOptions): void {
|
|
41
179
|
this.nodeMounted = Libs.mountNode({
|
|
42
180
|
SearchBox: {
|
|
43
181
|
tag: { node: "div", classList: ["selective-ui-searchbox", "hide"] },
|
|
@@ -56,7 +194,7 @@ export class SearchBox {
|
|
|
56
194
|
},
|
|
57
195
|
},
|
|
58
196
|
},
|
|
59
|
-
}) as
|
|
197
|
+
}) as MountViewResult<SearchBoxTags>;
|
|
60
198
|
|
|
61
199
|
this.node = this.nodeMounted.view as HTMLDivElement;
|
|
62
200
|
this.SearchInput = this.nodeMounted.tags.SearchInput;
|
|
@@ -64,6 +202,7 @@ export class SearchBox {
|
|
|
64
202
|
let isControlKey = false;
|
|
65
203
|
const inputEl = this.nodeMounted.tags.SearchInput;
|
|
66
204
|
|
|
205
|
+
// Prevent parent listeners (e.g., popup container) from intercepting mouse interactions.
|
|
67
206
|
inputEl.addEventListener("mousedown", (e: MouseEvent) => {
|
|
68
207
|
e.stopPropagation();
|
|
69
208
|
});
|
|
@@ -72,6 +211,8 @@ export class SearchBox {
|
|
|
72
211
|
e.stopPropagation();
|
|
73
212
|
});
|
|
74
213
|
|
|
214
|
+
// Keyboard handling: navigation, commit, and cancel.
|
|
215
|
+
// Control-key sequences are tracked to avoid emitting onSearch from the subsequent input event.
|
|
75
216
|
inputEl.addEventListener("keydown", (e: KeyboardEvent) => {
|
|
76
217
|
isControlKey = false;
|
|
77
218
|
|
|
@@ -96,19 +237,33 @@ export class SearchBox {
|
|
|
96
237
|
isControlKey = true;
|
|
97
238
|
this.onEsc?.();
|
|
98
239
|
}
|
|
99
|
-
|
|
240
|
+
|
|
241
|
+
// Ensure events don't bubble to container-level listeners.
|
|
100
242
|
e.stopPropagation();
|
|
101
243
|
});
|
|
102
244
|
|
|
245
|
+
// Text edits (ignore those attributable to control-key flows).
|
|
103
246
|
inputEl.addEventListener("input", () => {
|
|
104
247
|
if (isControlKey) return;
|
|
105
248
|
this.onSearch?.(inputEl.value, true);
|
|
106
249
|
});
|
|
250
|
+
|
|
251
|
+
this.init();
|
|
107
252
|
}
|
|
108
253
|
|
|
109
254
|
/**
|
|
110
|
-
* Shows the search box
|
|
111
|
-
*
|
|
255
|
+
* Shows the search box and prepares the input for interaction.
|
|
256
|
+
*
|
|
257
|
+
* Behavior:
|
|
258
|
+
* - Removes the `hide` class from the root node.
|
|
259
|
+
* - Toggles `readOnly` according to `options.searchable`.
|
|
260
|
+
* - When searchable, schedules a focus on the next animation frame.
|
|
261
|
+
*
|
|
262
|
+
* No-ops if not initialized (missing {@link node}, {@link SearchInput}, or {@link options}).
|
|
263
|
+
*
|
|
264
|
+
* DOM side effects:
|
|
265
|
+
* - May change focus.
|
|
266
|
+
* - Mutates `readOnly` on the input element.
|
|
112
267
|
*/
|
|
113
268
|
public show(): void {
|
|
114
269
|
if (!this.node || !this.SearchInput || !this.options) return;
|
|
@@ -124,7 +279,9 @@ export class SearchBox {
|
|
|
124
279
|
}
|
|
125
280
|
|
|
126
281
|
/**
|
|
127
|
-
* Hides the search box by adding the
|
|
282
|
+
* Hides the search box by adding the `hide` class to the root node.
|
|
283
|
+
*
|
|
284
|
+
* No-ops if {@link node} is `null`.
|
|
128
285
|
*/
|
|
129
286
|
public hide(): void {
|
|
130
287
|
if (!this.node) return;
|
|
@@ -132,9 +289,15 @@ export class SearchBox {
|
|
|
132
289
|
}
|
|
133
290
|
|
|
134
291
|
/**
|
|
135
|
-
* Clears the current
|
|
292
|
+
* Clears the current query and optionally notifies the host via {@link onSearch}.
|
|
293
|
+
*
|
|
294
|
+
* This method always resets the input's value to an empty string.
|
|
295
|
+
* The `isTrigger` flag is forwarded to {@link onSearch} and can be used by the host
|
|
296
|
+
* to differentiate external (programmatic) clearing from user-driven changes.
|
|
136
297
|
*
|
|
137
|
-
*
|
|
298
|
+
* No-ops if the component has not been initialized ({@link nodeMounted} is `null`).
|
|
299
|
+
*
|
|
300
|
+
* @param isTrigger - Whether to invoke {@link onSearch} with an empty string. Defaults to `true`.
|
|
138
301
|
*/
|
|
139
302
|
public clear(isTrigger: boolean = true): void {
|
|
140
303
|
if (!this.nodeMounted) return;
|
|
@@ -143,9 +306,14 @@ export class SearchBox {
|
|
|
143
306
|
}
|
|
144
307
|
|
|
145
308
|
/**
|
|
146
|
-
* Updates the input's placeholder text
|
|
309
|
+
* Updates the input's placeholder text.
|
|
310
|
+
*
|
|
311
|
+
* Safety:
|
|
312
|
+
* - HTML is stripped via {@link Libs.stripHtml} to avoid rendering markup in an attribute.
|
|
147
313
|
*
|
|
148
|
-
* @
|
|
314
|
+
* No-ops if {@link SearchInput} is `null`.
|
|
315
|
+
*
|
|
316
|
+
* @param value - New placeholder text (may contain markup, which will be stripped).
|
|
149
317
|
*/
|
|
150
318
|
public setPlaceHolder(value: string): void {
|
|
151
319
|
if (!this.SearchInput) return;
|
|
@@ -153,12 +321,49 @@ export class SearchBox {
|
|
|
153
321
|
}
|
|
154
322
|
|
|
155
323
|
/**
|
|
156
|
-
* Sets
|
|
324
|
+
* Sets `aria-activedescendant` to reflect the currently highlighted option in the list.
|
|
325
|
+
*
|
|
326
|
+
* This is typically used in conjunction with keyboard navigation to keep assistive
|
|
327
|
+
* technologies informed about the active/highlighted item without moving DOM focus away
|
|
328
|
+
* from the search input.
|
|
157
329
|
*
|
|
158
|
-
*
|
|
330
|
+
* No-ops if {@link SearchInput} is `null`.
|
|
331
|
+
*
|
|
332
|
+
* @param id - DOM id of the active option element.
|
|
159
333
|
*/
|
|
160
334
|
public setActiveDescendant(id: string): void {
|
|
161
335
|
if (!this.SearchInput) return;
|
|
162
336
|
this.SearchInput.setAttribute("aria-activedescendant", id);
|
|
163
337
|
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Disposes DOM resources and terminates the lifecycle.
|
|
341
|
+
*
|
|
342
|
+
* Strict FSM / idempotency:
|
|
343
|
+
* - If already {@link LifecycleState.DESTROYED}, returns immediately.
|
|
344
|
+
*
|
|
345
|
+
* Side effects:
|
|
346
|
+
* - Removes the root DOM node from the document (if present).
|
|
347
|
+
* - Clears references to DOM nodes and callbacks to enable garbage collection.
|
|
348
|
+
* - Delegates to `super.destroy()` to finalize lifecycle transition.
|
|
349
|
+
*
|
|
350
|
+
* @override
|
|
351
|
+
*/
|
|
352
|
+
public override destroy(): void {
|
|
353
|
+
if (this.is(LifecycleState.DESTROYED)) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
this.node?.remove();
|
|
358
|
+
this.nodeMounted = null;
|
|
359
|
+
this.node = null;
|
|
360
|
+
this.SearchInput = null;
|
|
361
|
+
this.onSearch = null;
|
|
362
|
+
this.options = null;
|
|
363
|
+
this.onNavigate = null;
|
|
364
|
+
this.onEnter = null;
|
|
365
|
+
this.onEsc = null;
|
|
366
|
+
|
|
367
|
+
super.destroy();
|
|
368
|
+
}
|
|
164
369
|
}
|