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
|
@@ -4,35 +4,60 @@ import { SelectiveOptions } from "../../types/utils/selective.type";
|
|
|
4
4
|
import { Libs } from "../../utils/libs";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* UI
|
|
8
|
-
*
|
|
9
|
-
* The loading state is displayed while data is being fetched,
|
|
7
|
+
* Lightweight UI state box that renders a "loading" indicator while data is being fetched,
|
|
10
8
|
* processed, or updated asynchronously.
|
|
11
9
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* ### Responsibility
|
|
11
|
+
* - Owns a single DOM node representing the loading state.
|
|
12
|
+
* - Exposes an imperative API (`show`, `hide`, `isVisible`) to be driven by higher-level
|
|
13
|
+
* controllers (e.g., AJAX search / pagination) and containers (e.g., Popup).
|
|
14
|
+
*
|
|
15
|
+
* ### Lifecycle (Strict FSM)
|
|
16
|
+
* - Constructed in `NEW`. When `options` are provided, {@link initialize} is invoked and the
|
|
17
|
+
* instance transitions to `INITIALIZED` via {@link Lifecycle.init}.
|
|
18
|
+
* - This component does **not** attach itself to the DOM; consumers append {@link node} to the
|
|
19
|
+
* desired container.
|
|
20
|
+
* - {@link destroy} removes the node, clears references, and transitions to `DESTROYED`.
|
|
21
|
+
*
|
|
22
|
+
* ### Idempotency / No-ops
|
|
23
|
+
* - {@link show} and {@link hide} are **no-ops** until {@link node} exists.
|
|
24
|
+
* - {@link destroy} is idempotent once in {@link LifecycleState.DESTROYED}.
|
|
25
|
+
*
|
|
26
|
+
* ### Accessibility / DOM side effects
|
|
27
|
+
* - Uses `role="status"` and `aria-live="polite"` so assistive technologies announce changes
|
|
28
|
+
* without interrupting the user.
|
|
29
|
+
* - Visibility is controlled via the `"hide"` CSS class; hiding does not remove the element.
|
|
30
|
+
* - The `"small"` CSS class is toggled by {@link show} to support a compact loading indicator
|
|
31
|
+
* when items are already present.
|
|
14
32
|
*
|
|
15
33
|
* @extends Lifecycle
|
|
34
|
+
* @see {@link LifecycleState}
|
|
16
35
|
*/
|
|
17
36
|
export class LoadingState extends Lifecycle {
|
|
18
|
-
|
|
19
37
|
/**
|
|
20
|
-
* Root DOM element
|
|
21
|
-
*
|
|
38
|
+
* Root DOM element for the loading state UI.
|
|
39
|
+
*
|
|
40
|
+
* - Created during {@link initialize}.
|
|
41
|
+
* - Intended to be appended by the parent container (component does not auto-attach).
|
|
42
|
+
* - Removed from DOM during {@link destroy}.
|
|
22
43
|
*/
|
|
23
44
|
public node: HTMLDivElement | null = null;
|
|
24
45
|
|
|
25
46
|
/**
|
|
26
|
-
* Configuration
|
|
47
|
+
* Configuration source for loading message text.
|
|
48
|
+
*
|
|
49
|
+
* Expected to provide:
|
|
50
|
+
* - `textLoading` (displayed while loading is active)
|
|
27
51
|
*/
|
|
28
52
|
public options: SelectiveOptions | null = null;
|
|
29
53
|
|
|
30
54
|
/**
|
|
31
|
-
* Creates a new LoadingState
|
|
55
|
+
* Creates a new {@link LoadingState}.
|
|
32
56
|
*
|
|
33
|
-
* If options are provided,
|
|
57
|
+
* If `options` are provided, initialization runs immediately (creates {@link node} and
|
|
58
|
+
* transitions to `INITIALIZED`).
|
|
34
59
|
*
|
|
35
|
-
* @param options - Configuration
|
|
60
|
+
* @param {SelectiveOptions | null} [options=null] - Configuration containing the loading message text.
|
|
36
61
|
*/
|
|
37
62
|
public constructor(options: SelectiveOptions | null = null) {
|
|
38
63
|
super();
|
|
@@ -40,36 +65,43 @@ export class LoadingState extends Lifecycle {
|
|
|
40
65
|
}
|
|
41
66
|
|
|
42
67
|
/**
|
|
43
|
-
* Initializes
|
|
68
|
+
* Initializes internal resources for this component.
|
|
44
69
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
70
|
+
* Side effects:
|
|
71
|
+
* - Creates the root `div` node with base CSS classes: `"seui-loading-state"` and `"hide"`.
|
|
72
|
+
* - Sets initial text to `options.textLoading`.
|
|
73
|
+
* - Applies `role="status"` and `aria-live="polite"`.
|
|
74
|
+
* - Stores the options reference and calls {@link Lifecycle.init}.
|
|
48
75
|
*
|
|
49
|
-
* @param options - Configuration object containing loading text.
|
|
76
|
+
* @param {SelectiveOptions} options - Configuration object containing loading text.
|
|
77
|
+
* @returns {void}
|
|
50
78
|
*/
|
|
51
79
|
private initialize(options: SelectiveOptions): void {
|
|
52
80
|
this.options = options;
|
|
53
81
|
|
|
54
|
-
this.node = Libs.nodeCreator({
|
|
82
|
+
this.node = Libs.nodeCreator<HTMLDivElement>({
|
|
55
83
|
node: "div",
|
|
56
|
-
classList: ["
|
|
84
|
+
classList: ["seui-loading-state", "hide"],
|
|
57
85
|
textContent: options.textLoading,
|
|
58
86
|
role: "status",
|
|
59
87
|
ariaLive: "polite",
|
|
60
|
-
})
|
|
88
|
+
});
|
|
61
89
|
|
|
62
90
|
this.init();
|
|
63
91
|
}
|
|
64
92
|
|
|
65
93
|
/**
|
|
66
|
-
*
|
|
94
|
+
* Shows the loading indicator.
|
|
95
|
+
*
|
|
96
|
+
* Behavior:
|
|
97
|
+
* - Updates the text to the latest `options.textLoading` (in case options changed).
|
|
98
|
+
* - Toggles the `"small"` CSS class when `hasItems` is true to display a compact variant.
|
|
99
|
+
* - Removes the `"hide"` class to make the node visible.
|
|
67
100
|
*
|
|
68
|
-
*
|
|
69
|
-
* in a compact form by applying a reduced ("small") style.
|
|
101
|
+
* No-op if {@link node} or {@link options} are not initialized.
|
|
70
102
|
*
|
|
71
|
-
* @param hasItems -
|
|
72
|
-
*
|
|
103
|
+
* @param {boolean} hasItems - Whether existing items are already present (enables compact loading style).
|
|
104
|
+
* @returns {void}
|
|
73
105
|
*/
|
|
74
106
|
public show(hasItems: boolean): void {
|
|
75
107
|
if (!this.node || !this.options) return;
|
|
@@ -80,10 +112,12 @@ export class LoadingState extends Lifecycle {
|
|
|
80
112
|
}
|
|
81
113
|
|
|
82
114
|
/**
|
|
83
|
-
* Hides the loading
|
|
115
|
+
* Hides the loading indicator by applying the `"hide"` CSS class.
|
|
116
|
+
*
|
|
117
|
+
* This does not remove the element from the DOM.
|
|
118
|
+
* No-op if {@link node} is not initialized.
|
|
84
119
|
*
|
|
85
|
-
*
|
|
86
|
-
* remove the element from the DOM.
|
|
120
|
+
* @returns {void}
|
|
87
121
|
*/
|
|
88
122
|
public hide(): void {
|
|
89
123
|
if (!this.node) return;
|
|
@@ -91,19 +125,25 @@ export class LoadingState extends Lifecycle {
|
|
|
91
125
|
}
|
|
92
126
|
|
|
93
127
|
/**
|
|
94
|
-
*
|
|
128
|
+
* Whether the loading indicator is currently visible.
|
|
95
129
|
*
|
|
96
|
-
* @returns
|
|
130
|
+
* @returns {boolean} `true` when {@link node} exists and does not have the `"hide"` class.
|
|
97
131
|
*/
|
|
98
132
|
public get isVisible(): boolean {
|
|
99
133
|
return !!this.node && !this.node.classList.contains("hide");
|
|
100
134
|
}
|
|
101
135
|
|
|
102
136
|
/**
|
|
103
|
-
*
|
|
137
|
+
* Releases resources owned by this component.
|
|
138
|
+
*
|
|
139
|
+
* - Removes the root DOM node (if present).
|
|
140
|
+
* - Clears stored options and internal references.
|
|
141
|
+
* - Transitions to `DESTROYED`.
|
|
142
|
+
*
|
|
143
|
+
* Idempotent: returns early if already in {@link LifecycleState.DESTROYED}.
|
|
104
144
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
145
|
+
* @returns {void}
|
|
146
|
+
* @override
|
|
107
147
|
*/
|
|
108
148
|
public override destroy(): void {
|
|
109
149
|
if (this.is(LifecycleState.DESTROYED)) {
|
|
@@ -13,6 +13,7 @@ import { SelectiveOptions } from "../../types/utils/selective.type";
|
|
|
13
13
|
import { ParentBinderMapLike, VirtualRecyclerOptions } from "../../types/components/popup.type";
|
|
14
14
|
import { Lifecycle } from "../../core/base/lifecycle";
|
|
15
15
|
import { LifecycleState } from "../../types/core/base/lifecycle.type";
|
|
16
|
+
import { MountViewResult } from "src/ts/types/utils/libs.type";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Popup panel that renders and manages the dropdown surface.
|
|
@@ -129,12 +130,12 @@ export class Popup extends Lifecycle {
|
|
|
129
130
|
this.emptyState = new EmptyState(options);
|
|
130
131
|
this.loadingState = new LoadingState(options);
|
|
131
132
|
|
|
132
|
-
const nodeMounted = Libs.mountNode(
|
|
133
|
+
const nodeMounted = Libs.mountNode<MountViewResult>(
|
|
133
134
|
{
|
|
134
135
|
PopupContainer: {
|
|
135
136
|
tag: {
|
|
136
137
|
node: "div",
|
|
137
|
-
classList: "
|
|
138
|
+
classList: "seui-popup",
|
|
138
139
|
style: { maxHeight: options.panelHeight },
|
|
139
140
|
},
|
|
140
141
|
child: {
|
|
@@ -143,7 +144,7 @@ export class Popup extends Lifecycle {
|
|
|
143
144
|
tag: {
|
|
144
145
|
id: options.SEID_LIST,
|
|
145
146
|
node: "div",
|
|
146
|
-
classList: "
|
|
147
|
+
classList: "seui-options-container",
|
|
147
148
|
role: "listbox",
|
|
148
149
|
},
|
|
149
150
|
},
|
|
@@ -158,7 +159,7 @@ export class Popup extends Lifecycle {
|
|
|
158
159
|
this.node = nodeMounted.view as HTMLDivElement;
|
|
159
160
|
this.optionsContainer = nodeMounted.tags.OptionsContainer as HTMLDivElement;
|
|
160
161
|
|
|
161
|
-
this.parent = Libs.getBinderMap(select)
|
|
162
|
+
this.parent = Libs.getBinderMap<ParentBinderMapLike>(select);
|
|
162
163
|
this.options = options;
|
|
163
164
|
this.init();
|
|
164
165
|
|
|
@@ -174,10 +175,7 @@ export class Popup extends Lifecycle {
|
|
|
174
175
|
// Load ModelManager resources into the list container
|
|
175
176
|
this.modelManager.load<VirtualRecyclerOptions>(this.optionsContainer, { isMultiple: options.multiple }, recyclerViewOpt);
|
|
176
177
|
|
|
177
|
-
const MMResources = this.modelManager.getResources()
|
|
178
|
-
adapter: MixedAdapter;
|
|
179
|
-
recyclerView: RecyclerViewContract<MixedAdapter>;
|
|
180
|
-
};
|
|
178
|
+
const MMResources = this.modelManager.getResources();
|
|
181
179
|
|
|
182
180
|
this.optionAdapter = MMResources.adapter;
|
|
183
181
|
this.recyclerView = MMResources.recyclerView;
|
|
@@ -471,10 +469,8 @@ export class Popup extends Lifecycle {
|
|
|
471
469
|
return;
|
|
472
470
|
}
|
|
473
471
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
this.hideLoadHandle = null;
|
|
477
|
-
}
|
|
472
|
+
clearTimeout(this.hideLoadHandle!);
|
|
473
|
+
this.hideLoadHandle = null;
|
|
478
474
|
|
|
479
475
|
if (this.node && this.scrollListener) {
|
|
480
476
|
this.node.removeEventListener("scroll", this.scrollListener);
|
|
@@ -484,51 +480,35 @@ export class Popup extends Lifecycle {
|
|
|
484
480
|
this.emptyState.destroy();
|
|
485
481
|
this.loadingState.destroy();
|
|
486
482
|
this.optionHandle.destroy();
|
|
487
|
-
this.
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
this.resizeObser = null;
|
|
493
|
-
|
|
494
|
-
try {
|
|
495
|
-
this.effSvc?.setElement?.(null);
|
|
496
|
-
} catch (_) {}
|
|
497
|
-
this.effSvc = null;
|
|
483
|
+
this.resizeObser?.disconnect?.();
|
|
484
|
+
this.effSvc?.setElement?.(null);
|
|
485
|
+
this.modelManager?.skipEvent?.(false);
|
|
486
|
+
this.recyclerView?.clear?.();
|
|
487
|
+
this.node?.remove?.();
|
|
498
488
|
|
|
499
489
|
if (this.node) {
|
|
500
490
|
try {
|
|
501
|
-
const clone = this.node
|
|
491
|
+
const clone = Libs.nodeCloner<HTMLDivElement>(this.node);
|
|
502
492
|
this.node.replaceWith(clone);
|
|
503
493
|
clone.remove();
|
|
504
494
|
} catch (_) {
|
|
505
495
|
this.node.remove();
|
|
506
496
|
}
|
|
507
497
|
}
|
|
498
|
+
|
|
508
499
|
this.node = null;
|
|
509
500
|
this.optionsContainer = null;
|
|
510
|
-
|
|
511
|
-
try {
|
|
512
|
-
this.modelManager?.skipEvent?.(false);
|
|
513
|
-
|
|
514
|
-
this.recyclerView?.clear?.();
|
|
515
|
-
this.recyclerView = null;
|
|
516
|
-
|
|
517
|
-
this.optionAdapter = null;
|
|
518
|
-
|
|
519
|
-
// Original behavior kept intentionally.
|
|
520
|
-
this.node.remove();
|
|
521
|
-
} catch (_) {}
|
|
522
|
-
|
|
523
501
|
this.modelManager = null;
|
|
524
502
|
this.optionHandle = null;
|
|
525
503
|
this.emptyState = null;
|
|
526
504
|
this.loadingState = null;
|
|
527
|
-
|
|
528
505
|
this.parent = null;
|
|
529
506
|
this.options = null;
|
|
530
|
-
|
|
531
507
|
this.isCreated = false;
|
|
508
|
+
this.effSvc = null;
|
|
509
|
+
this.resizeObser = null;
|
|
510
|
+
this.recyclerView = null;
|
|
511
|
+
this.optionAdapter = null;
|
|
532
512
|
|
|
533
513
|
super.destroy();
|
|
534
514
|
}
|
|
@@ -6,25 +6,58 @@ import { Lifecycle } from "../core/base/lifecycle";
|
|
|
6
6
|
import { LifecycleState } from "../types/core/base/lifecycle.type";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* SearchBox
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
15
|
*
|
|
16
|
-
*
|
|
17
|
-
* -
|
|
18
|
-
* -
|
|
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.
|
|
19
49
|
*
|
|
20
50
|
* @extends Lifecycle
|
|
21
51
|
*/
|
|
22
52
|
export class SearchBox extends Lifecycle {
|
|
23
53
|
/**
|
|
24
|
-
* Creates a
|
|
25
|
-
*
|
|
54
|
+
* Creates a new {@link SearchBox}.
|
|
55
|
+
*
|
|
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.
|
|
26
59
|
*
|
|
27
|
-
* @param options - Configuration
|
|
60
|
+
* @param options - Configuration such as placeholder, accessibility IDs, and flags.
|
|
28
61
|
*/
|
|
29
62
|
constructor(options: SelectiveOptions | null = null) {
|
|
30
63
|
super();
|
|
@@ -32,57 +65,127 @@ export class SearchBox extends Lifecycle {
|
|
|
32
65
|
if (options) this.initialize(options);
|
|
33
66
|
}
|
|
34
67
|
|
|
35
|
-
/**
|
|
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
|
+
*/
|
|
36
76
|
private nodeMounted: MountViewResult<SearchBoxTags> | null = null;
|
|
37
77
|
|
|
38
|
-
/**
|
|
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
|
+
*/
|
|
39
84
|
public node: HTMLDivElement | null = null;
|
|
40
85
|
|
|
41
|
-
/**
|
|
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
|
+
*/
|
|
42
94
|
private SearchInput: HTMLInputElement | null = null;
|
|
43
95
|
|
|
44
|
-
/**
|
|
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
|
+
*/
|
|
45
105
|
public onSearch: SearchHandler | null = null;
|
|
46
106
|
|
|
47
|
-
/**
|
|
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
|
+
*/
|
|
48
119
|
private options: SelectiveOptions | null = null;
|
|
49
120
|
|
|
50
|
-
/**
|
|
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
|
+
*/
|
|
51
130
|
public onNavigate: NavigateHandler | null = null;
|
|
52
131
|
|
|
53
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* External "commit" hook (Enter key).
|
|
134
|
+
*
|
|
135
|
+
* Typical consumers confirm selection of the highlighted option or submit the current state.
|
|
136
|
+
*/
|
|
54
137
|
public onEnter: (() => void) | null = null;
|
|
55
138
|
|
|
56
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* External "cancel" hook (Escape key).
|
|
141
|
+
*
|
|
142
|
+
* Typical consumers close the popup, clear highlight, or reset interaction mode.
|
|
143
|
+
*/
|
|
57
144
|
public onEsc: (() => void) | null = null;
|
|
58
145
|
|
|
59
146
|
/**
|
|
60
|
-
* Initializes
|
|
147
|
+
* Initializes DOM, ARIA attributes, and interaction listeners.
|
|
61
148
|
*
|
|
62
|
-
*
|
|
63
|
-
* - `
|
|
64
|
-
* - `
|
|
65
|
-
* - `aria-autocomplete="list"` indicates list-based suggestions/results
|
|
149
|
+
* DOM structure (conceptually):
|
|
150
|
+
* - Root: `div.seui-searchbox.hide`
|
|
151
|
+
* - Child: `input[type="search"].seui-searchbox-input`
|
|
66
152
|
*
|
|
67
|
-
*
|
|
68
|
-
* -
|
|
69
|
-
* -
|
|
70
|
-
* -
|
|
71
|
-
* - Escape → cancel/close action
|
|
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
|
|
72
157
|
*
|
|
73
|
-
*
|
|
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.
|
|
174
|
+
*
|
|
175
|
+
* @param options - Configuration including placeholder and listbox id used by `aria-controls`.
|
|
176
|
+
* @internal
|
|
74
177
|
*/
|
|
75
178
|
private initialize(options: SelectiveOptions): void {
|
|
76
|
-
this.nodeMounted = Libs.mountNode({
|
|
179
|
+
this.nodeMounted = Libs.mountNode<MountViewResult<SearchBoxTags>>({
|
|
77
180
|
SearchBox: {
|
|
78
|
-
tag: { node: "div", classList: ["
|
|
181
|
+
tag: { node: "div", classList: ["seui-searchbox", "hide"] },
|
|
79
182
|
child: {
|
|
80
183
|
SearchInput: {
|
|
81
184
|
tag: {
|
|
82
185
|
id: Libs.randomString(),
|
|
83
186
|
node: "input",
|
|
84
187
|
type: "search",
|
|
85
|
-
classList: ["
|
|
188
|
+
classList: ["seui-searchbox-input"],
|
|
86
189
|
placeholder: options.placeholder,
|
|
87
190
|
role: "searchbox",
|
|
88
191
|
ariaControls: options.SEID_LIST,
|
|
@@ -91,7 +194,7 @@ export class SearchBox extends Lifecycle {
|
|
|
91
194
|
},
|
|
92
195
|
},
|
|
93
196
|
},
|
|
94
|
-
})
|
|
197
|
+
});
|
|
95
198
|
|
|
96
199
|
this.node = this.nodeMounted.view as HTMLDivElement;
|
|
97
200
|
this.SearchInput = this.nodeMounted.tags.SearchInput;
|
|
@@ -99,7 +202,7 @@ export class SearchBox extends Lifecycle {
|
|
|
99
202
|
let isControlKey = false;
|
|
100
203
|
const inputEl = this.nodeMounted.tags.SearchInput;
|
|
101
204
|
|
|
102
|
-
// Prevent parent listeners from intercepting mouse interactions.
|
|
205
|
+
// Prevent parent listeners (e.g., popup container) from intercepting mouse interactions.
|
|
103
206
|
inputEl.addEventListener("mousedown", (e: MouseEvent) => {
|
|
104
207
|
e.stopPropagation();
|
|
105
208
|
});
|
|
@@ -108,7 +211,8 @@ export class SearchBox extends Lifecycle {
|
|
|
108
211
|
e.stopPropagation();
|
|
109
212
|
});
|
|
110
213
|
|
|
111
|
-
// Keyboard handling: navigation,
|
|
214
|
+
// Keyboard handling: navigation, commit, and cancel.
|
|
215
|
+
// Control-key sequences are tracked to avoid emitting onSearch from the subsequent input event.
|
|
112
216
|
inputEl.addEventListener("keydown", (e: KeyboardEvent) => {
|
|
113
217
|
isControlKey = false;
|
|
114
218
|
|
|
@@ -138,7 +242,7 @@ export class SearchBox extends Lifecycle {
|
|
|
138
242
|
e.stopPropagation();
|
|
139
243
|
});
|
|
140
244
|
|
|
141
|
-
// Text
|
|
245
|
+
// Text edits (ignore those attributable to control-key flows).
|
|
142
246
|
inputEl.addEventListener("input", () => {
|
|
143
247
|
if (isControlKey) return;
|
|
144
248
|
this.onSearch?.(inputEl.value, true);
|
|
@@ -148,8 +252,18 @@ export class SearchBox extends Lifecycle {
|
|
|
148
252
|
}
|
|
149
253
|
|
|
150
254
|
/**
|
|
151
|
-
* Shows the search box
|
|
152
|
-
*
|
|
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.
|
|
153
267
|
*/
|
|
154
268
|
public show(): void {
|
|
155
269
|
if (!this.node || !this.SearchInput || !this.options) return;
|
|
@@ -165,7 +279,9 @@ export class SearchBox extends Lifecycle {
|
|
|
165
279
|
}
|
|
166
280
|
|
|
167
281
|
/**
|
|
168
|
-
* 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`.
|
|
169
285
|
*/
|
|
170
286
|
public hide(): void {
|
|
171
287
|
if (!this.node) return;
|
|
@@ -173,9 +289,15 @@ export class SearchBox extends Lifecycle {
|
|
|
173
289
|
}
|
|
174
290
|
|
|
175
291
|
/**
|
|
176
|
-
* 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.
|
|
297
|
+
*
|
|
298
|
+
* No-ops if the component has not been initialized ({@link nodeMounted} is `null`).
|
|
177
299
|
*
|
|
178
|
-
* @param isTrigger - Whether to invoke
|
|
300
|
+
* @param isTrigger - Whether to invoke {@link onSearch} with an empty string. Defaults to `true`.
|
|
179
301
|
*/
|
|
180
302
|
public clear(isTrigger: boolean = true): void {
|
|
181
303
|
if (!this.nodeMounted) return;
|
|
@@ -184,9 +306,14 @@ export class SearchBox extends Lifecycle {
|
|
|
184
306
|
}
|
|
185
307
|
|
|
186
308
|
/**
|
|
187
|
-
* Updates the input's placeholder text
|
|
309
|
+
* Updates the input's placeholder text.
|
|
188
310
|
*
|
|
189
|
-
*
|
|
311
|
+
* Safety:
|
|
312
|
+
* - HTML is stripped via {@link Libs.stripHtml} to avoid rendering markup in an attribute.
|
|
313
|
+
*
|
|
314
|
+
* No-ops if {@link SearchInput} is `null`.
|
|
315
|
+
*
|
|
316
|
+
* @param value - New placeholder text (may contain markup, which will be stripped).
|
|
190
317
|
*/
|
|
191
318
|
public setPlaceHolder(value: string): void {
|
|
192
319
|
if (!this.SearchInput) return;
|
|
@@ -194,9 +321,15 @@ export class SearchBox extends Lifecycle {
|
|
|
194
321
|
}
|
|
195
322
|
|
|
196
323
|
/**
|
|
197
|
-
* 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.
|
|
198
329
|
*
|
|
199
|
-
*
|
|
330
|
+
* No-ops if {@link SearchInput} is `null`.
|
|
331
|
+
*
|
|
332
|
+
* @param id - DOM id of the active option element.
|
|
200
333
|
*/
|
|
201
334
|
public setActiveDescendant(id: string): void {
|
|
202
335
|
if (!this.SearchInput) return;
|
|
@@ -204,11 +337,17 @@ export class SearchBox extends Lifecycle {
|
|
|
204
337
|
}
|
|
205
338
|
|
|
206
339
|
/**
|
|
207
|
-
*
|
|
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.
|
|
208
349
|
*
|
|
209
|
-
*
|
|
210
|
-
* - Clears references to DOM and callbacks
|
|
211
|
-
* - Ends the lifecycle
|
|
350
|
+
* @override
|
|
212
351
|
*/
|
|
213
352
|
public override destroy(): void {
|
|
214
353
|
if (this.is(LifecycleState.DESTROYED)) {
|