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.
Files changed (59) hide show
  1. package/dist/selective-ui.css.map +1 -1
  2. package/dist/selective-ui.esm.js +5462 -1043
  3. package/dist/selective-ui.esm.js.map +1 -1
  4. package/dist/selective-ui.esm.min.js +2 -2
  5. package/dist/selective-ui.esm.min.js.br +0 -0
  6. package/dist/selective-ui.min.js +2 -2
  7. package/dist/selective-ui.min.js.br +0 -0
  8. package/dist/selective-ui.umd.js +5463 -1044
  9. package/dist/selective-ui.umd.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/ts/adapter/mixed-adapter.ts +312 -65
  12. package/src/ts/components/accessorybox.ts +248 -28
  13. package/src/ts/components/directive.ts +91 -11
  14. package/src/ts/components/option-handle.ts +191 -28
  15. package/src/ts/components/placeholder.ts +111 -16
  16. package/src/ts/components/popup/empty-state.ts +162 -0
  17. package/src/ts/components/popup/loading-state.ts +160 -0
  18. package/src/ts/components/{popup.ts → popup/popup.ts} +167 -71
  19. package/src/ts/components/searchbox.ts +225 -20
  20. package/src/ts/components/selectbox.ts +498 -120
  21. package/src/ts/core/base/adapter.ts +200 -53
  22. package/src/ts/core/base/fenwick.ts +147 -0
  23. package/src/ts/core/base/lifecycle.ts +258 -0
  24. package/src/ts/core/base/model.ts +120 -31
  25. package/src/ts/core/base/recyclerview.ts +55 -18
  26. package/src/ts/core/base/view.ts +87 -19
  27. package/src/ts/core/base/virtual-recyclerview.ts +475 -202
  28. package/src/ts/core/model-manager.ts +166 -85
  29. package/src/ts/core/search-controller.ts +236 -38
  30. package/src/ts/global.ts +6 -6
  31. package/src/ts/index.ts +6 -6
  32. package/src/ts/models/group-model.ts +159 -32
  33. package/src/ts/models/option-model.ts +213 -54
  34. package/src/ts/services/dataset-observer.ts +72 -10
  35. package/src/ts/services/ea-observer.ts +92 -15
  36. package/src/ts/services/effector.ts +181 -32
  37. package/src/ts/services/refresher.ts +30 -6
  38. package/src/ts/services/resize-observer.ts +132 -15
  39. package/src/ts/services/select-observer.ts +115 -50
  40. package/src/ts/types/components/searchbox.type.ts +1 -1
  41. package/src/ts/types/core/base/adapter.type.ts +2 -1
  42. package/src/ts/types/core/base/lifecycle.type.ts +62 -0
  43. package/src/ts/types/core/base/model.type.ts +3 -1
  44. package/src/ts/types/core/base/recyclerview.type.ts +2 -8
  45. package/src/ts/types/core/base/view.type.ts +36 -24
  46. package/src/ts/types/utils/ievents.type.ts +6 -1
  47. package/src/ts/utils/callback-scheduler.ts +112 -34
  48. package/src/ts/utils/ievents.ts +91 -29
  49. package/src/ts/utils/istorage.ts +1 -1
  50. package/src/ts/utils/selective.ts +474 -88
  51. package/src/ts/views/group-view.ts +170 -21
  52. package/src/ts/views/option-view.ts +349 -68
  53. package/src/ts/components/empty-state.ts +0 -68
  54. package/src/ts/components/loading-state.ts +0 -66
  55. /package/src/css/components/{empty-state.css → popup/empty-state.css} +0 -0
  56. /package/src/css/components/{loading-state.css → popup/loading-state.css} +0 -0
  57. /package/src/css/components/{popup.css → popup/popup.css} +0 -0
  58. /package/src/css/{components/optgroup.css → views/group-view.css} +0 -0
  59. /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
- export class SearchBox {
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 searchable input box component with optional configuration
9
- * and initializes it if options are provided.
54
+ * Creates a new {@link SearchBox}.
10
55
  *
11
- * @param {object|null} [options=null] - Configuration (e.g., placeholder, accessibility IDs).
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.init(options);
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 the search box DOM, sets ARIA attributes, and wires keyboard/mouse/input events.
36
- * Supports navigation (ArrowUp/ArrowDown/Tab), Enter, and Escape through callbacks.
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 {object} options - Configuration including placeholder and SEID_LIST for aria-controls.
175
+ * @param options - Configuration including placeholder and listbox id used by `aria-controls`.
176
+ * @internal
39
177
  */
40
- private init(options: SelectiveOptions): void {
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 unknown as MountViewResult<SearchBoxTags>;
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, toggles read-only based on `options.searchable`,
111
- * and focuses the input when searchable.
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 "hide" class.
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 search value and optionally triggers the onSearch callback.
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
- * @param {boolean} [isTrigger=true] - Whether to invoke onSearch with an empty string.
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, stripping any HTML for safety.
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
- * @param {string} value - The new placeholder text.
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 the active descendant for ARIA to indicate which option is currently highlighted.
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
- * @param {string} id - The DOM id of the active option element.
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
  }