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
|
@@ -10,33 +10,109 @@ import { SelectiveOptions } from "../types/utils/selective.type";
|
|
|
10
10
|
import { LifecycleState } from "../types/core/base/lifecycle.type";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
+
* Domain model for a native `<option>` element.
|
|
14
|
+
*
|
|
15
|
+
* This is the core selectable row model consumed by adapters/recyclers. It mirrors the backing
|
|
16
|
+
* `<option>` node while also carrying UI-only state used by the headless+DOM-driven layer
|
|
17
|
+
* (visibility, highlight, precomputed search key).
|
|
18
|
+
*
|
|
19
|
+
* ### Responsibility
|
|
20
|
+
* - Mirror and synchronize state with the backing `<option>` element:
|
|
21
|
+
* - `value`, `selected`, `dataset`, and display label (with optional tag translation / HTML policy).
|
|
22
|
+
* - Provide derived properties for rendering:
|
|
23
|
+
* - image resolution (`imageSrc` / `hasImage`),
|
|
24
|
+
* - rich/stripped label (`text` / `textContent`),
|
|
25
|
+
* - normalized search key (`textToFind`).
|
|
26
|
+
* - Maintain UI-only flags:
|
|
27
|
+
* - `visible` for filtering/search,
|
|
28
|
+
* - `highlighted` for keyboard navigation/hover.
|
|
29
|
+
* - Publish change notifications:
|
|
30
|
+
* - **External** selection (`selected`) vs **internal** selection sync (`selectedNonTrigger`),
|
|
31
|
+
* - visibility changes (`visible`).
|
|
32
|
+
*
|
|
33
|
+
* ### Lifecycle (Strict FSM)
|
|
34
|
+
* - Base {@link Model} calls `init()` during construction (`NEW → INITIALIZED`), and this subclass
|
|
35
|
+
* overrides {@link init} to precompute {@link textToFind} before delegating to `super.init()`.
|
|
36
|
+
* - {@link init} then transitions to `MOUNTED` via `mount()` for first render readiness.
|
|
37
|
+
* - {@link update} recomputes derived text/search fields and pushes state into the {@link OptionView}
|
|
38
|
+
* if attached, then emits lifecycle update.
|
|
39
|
+
* - {@link destroy} clears listeners/references and transitions to `DESTROYED` (idempotent).
|
|
40
|
+
*
|
|
41
|
+
* ### External vs internal selection semantics
|
|
42
|
+
* - `selected` is the **external** user-facing signal:
|
|
43
|
+
* updates state (via {@link selectedNonTrigger}) and then notifies {@link onSelected} listeners.
|
|
44
|
+
* - `selectedNonTrigger` is the **internal** sync signal:
|
|
45
|
+
* updates view/DOM/backing `<option>` and then notifies {@link onInternalSelected} listeners
|
|
46
|
+
* **without** implying user intent.
|
|
47
|
+
*
|
|
48
|
+
* ### DOM & a11y side effects (when a view is attached)
|
|
49
|
+
* - Toggles CSS classes: `"hide"`, `"highlight"`, `"checked"`.
|
|
50
|
+
* - Updates `aria-selected` on the option row root element.
|
|
51
|
+
* - Updates label content (either `innerHTML` or `textContent` depending on `allowHtml`).
|
|
52
|
+
* - Mirrors selection state to the backing `<option>` (property + attribute).
|
|
53
|
+
*
|
|
13
54
|
* @extends {Model<HTMLOptionElement, OptionViewTags, OptionView, SelectiveOptions>}
|
|
55
|
+
* @see {@link GroupModel}
|
|
56
|
+
* @see {@link OptionView}
|
|
14
57
|
*/
|
|
15
58
|
export class OptionModel extends Model<HTMLOptionElement, OptionViewTags, OptionView, SelectiveOptions> {
|
|
59
|
+
/**
|
|
60
|
+
* External selection subscribers (emitted by the {@link selected} setter).
|
|
61
|
+
* Use this for user-facing selection flows.
|
|
62
|
+
*/
|
|
16
63
|
private privOnSelected: Array<(evtToken: IEventCallback, el: OptionModel, selected: boolean) => void> = [];
|
|
17
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Internal selection subscribers (emitted by the {@link selectedNonTrigger} setter).
|
|
67
|
+
* Use this for silent synchronization flows.
|
|
68
|
+
*/
|
|
18
69
|
private privOnInternalSelected: Array<(evtToken: IEventCallback, el: OptionModel, selected: boolean) => void> = [];
|
|
19
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Visibility subscribers (emitted by the {@link visible} setter).
|
|
73
|
+
* Commonly used to recompute group visibility and update aggregated visibility stats.
|
|
74
|
+
*/
|
|
20
75
|
private privOnVisibilityChanged: Array<(evtToken: IEventCallback, model: OptionModel, visible: boolean) => void> = [];
|
|
21
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Visibility flag used for filtering/search.
|
|
79
|
+
* When `false`, adapters/recyclers may treat this item as non-renderable.
|
|
80
|
+
*/
|
|
22
81
|
private _visible = true;
|
|
23
82
|
|
|
83
|
+
/** Highlight flag used for keyboard navigation / hover. */
|
|
24
84
|
private _highlighted = false;
|
|
25
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Parent group model (if this option belongs to a group).
|
|
88
|
+
* Assigned by grouping logic (e.g., GroupModel/MixedAdapter).
|
|
89
|
+
*/
|
|
26
90
|
public group: GroupModel | null = null;
|
|
27
91
|
|
|
28
92
|
/**
|
|
29
|
-
*
|
|
30
|
-
* Stores references for later updates and rendering.
|
|
93
|
+
* Creates an option model.
|
|
31
94
|
*
|
|
32
|
-
* @param {SelectiveOptions} options -
|
|
33
|
-
* @param {HTMLOptionElement|null} [targetElement=null] -
|
|
34
|
-
* @param {OptionView|null} [view=null] -
|
|
95
|
+
* @param {SelectiveOptions} options - Shared configuration for models/views.
|
|
96
|
+
* @param {HTMLOptionElement | null} [targetElement=null] - Backing `<option>` element.
|
|
97
|
+
* @param {OptionView | null} [view=null] - Optional view used to render this model.
|
|
35
98
|
*/
|
|
36
|
-
public constructor(
|
|
99
|
+
public constructor(
|
|
100
|
+
options: SelectiveOptions,
|
|
101
|
+
targetElement: HTMLOptionElement | null = null,
|
|
102
|
+
view: OptionView | null = null
|
|
103
|
+
) {
|
|
37
104
|
super(options, targetElement, view);
|
|
38
105
|
}
|
|
39
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Initializes the model and precomputes the search key.
|
|
109
|
+
*
|
|
110
|
+
* - Computes {@link textToFind} from {@link textContent} (lowercased + normalized).
|
|
111
|
+
* - Delegates to `super.init()` and then transitions to `MOUNTED` via `mount()`.
|
|
112
|
+
*
|
|
113
|
+
* @returns {void}
|
|
114
|
+
* @override
|
|
115
|
+
*/
|
|
40
116
|
public override init(): void {
|
|
41
117
|
this.textToFind = Libs.string2normalize(this.textContent.toLowerCase());
|
|
42
118
|
|
|
@@ -45,46 +121,50 @@ export class OptionModel extends Model<HTMLOptionElement, OptionViewTags, Option
|
|
|
45
121
|
}
|
|
46
122
|
|
|
47
123
|
/**
|
|
48
|
-
*
|
|
124
|
+
* Image source resolved from dataset (`imgsrc` or `image`), or empty string if absent.
|
|
49
125
|
*
|
|
50
|
-
* @
|
|
126
|
+
* @returns {string}
|
|
51
127
|
*/
|
|
52
128
|
public get imageSrc(): string {
|
|
53
129
|
return this.dataset?.imgsrc || this.dataset?.image || "";
|
|
54
130
|
}
|
|
55
131
|
|
|
56
132
|
/**
|
|
57
|
-
*
|
|
133
|
+
* Whether this option has an image associated with it.
|
|
58
134
|
*
|
|
59
|
-
* @
|
|
135
|
+
* @returns {boolean}
|
|
60
136
|
*/
|
|
61
137
|
public get hasImage(): boolean {
|
|
62
138
|
return !!this.imageSrc;
|
|
63
139
|
}
|
|
64
140
|
|
|
65
141
|
/**
|
|
66
|
-
*
|
|
142
|
+
* Current value of the backing `<option>`.
|
|
67
143
|
*
|
|
68
|
-
* @
|
|
144
|
+
* @returns {string}
|
|
69
145
|
*/
|
|
70
146
|
public get value(): string {
|
|
71
147
|
return this.targetElement?.value ?? "";
|
|
72
148
|
}
|
|
73
149
|
|
|
74
150
|
/**
|
|
75
|
-
*
|
|
151
|
+
* Whether the backing `<option>` is selected.
|
|
76
152
|
*
|
|
77
|
-
* @
|
|
153
|
+
* @returns {boolean}
|
|
78
154
|
*/
|
|
79
155
|
public get selected(): boolean {
|
|
80
156
|
return !!this.targetElement?.selected;
|
|
81
157
|
}
|
|
82
158
|
|
|
83
159
|
/**
|
|
84
|
-
* Sets
|
|
85
|
-
* Uses selectedNonTrigger internally to update DOM/ARIA without firing external side effects first.
|
|
160
|
+
* Sets selected state and emits **external** selection listeners.
|
|
86
161
|
*
|
|
87
|
-
*
|
|
162
|
+
* Flow:
|
|
163
|
+
* - Delegates to {@link selectedNonTrigger} to synchronize view/DOM/backing element.
|
|
164
|
+
* - Notifies {@link onSelected} subscribers via {@link iEvents.callEvent}.
|
|
165
|
+
*
|
|
166
|
+
* @param {boolean} value - New selection state.
|
|
167
|
+
* @returns {void}
|
|
88
168
|
*/
|
|
89
169
|
public set selected(value: boolean) {
|
|
90
170
|
this.selectedNonTrigger = value;
|
|
@@ -92,18 +172,25 @@ export class OptionModel extends Model<HTMLOptionElement, OptionViewTags, Option
|
|
|
92
172
|
}
|
|
93
173
|
|
|
94
174
|
/**
|
|
95
|
-
*
|
|
175
|
+
* Whether this option is visible (used for filtering/search).
|
|
96
176
|
*
|
|
97
|
-
* @
|
|
177
|
+
* @returns {boolean}
|
|
98
178
|
*/
|
|
99
179
|
public get visible(): boolean {
|
|
100
180
|
return this._visible;
|
|
101
181
|
}
|
|
102
182
|
|
|
103
183
|
/**
|
|
104
|
-
* Sets
|
|
184
|
+
* Sets visibility and synchronizes the view (if attached), then emits visibility listeners.
|
|
185
|
+
*
|
|
186
|
+
* Side effects (when view attached):
|
|
187
|
+
* - Toggles `"hide"` CSS class on the view root element.
|
|
105
188
|
*
|
|
106
|
-
*
|
|
189
|
+
* Idempotent:
|
|
190
|
+
* - No-op if the new value equals the current state.
|
|
191
|
+
*
|
|
192
|
+
* @param {boolean} value - New visibility state.
|
|
193
|
+
* @returns {void}
|
|
107
194
|
*/
|
|
108
195
|
public set visible(value: boolean) {
|
|
109
196
|
if (this._visible === value) return;
|
|
@@ -116,25 +203,35 @@ export class OptionModel extends Model<HTMLOptionElement, OptionViewTags, Option
|
|
|
116
203
|
}
|
|
117
204
|
|
|
118
205
|
/**
|
|
119
|
-
*
|
|
206
|
+
* Reads selected state without emitting external selection listeners.
|
|
120
207
|
*
|
|
121
|
-
* @
|
|
208
|
+
* @returns {boolean}
|
|
122
209
|
*/
|
|
123
210
|
public get selectedNonTrigger(): boolean {
|
|
124
211
|
return this.selected;
|
|
125
212
|
}
|
|
126
213
|
|
|
127
214
|
/**
|
|
128
|
-
* Sets
|
|
129
|
-
*
|
|
215
|
+
* Sets selected state **silently** (internal sync), updates view/a11y/backing DOM, then emits internal listeners.
|
|
216
|
+
*
|
|
217
|
+
* Side effects (when view/backing element exist):
|
|
218
|
+
* - Updates the input checked state (`OptionInput`) if present.
|
|
219
|
+
* - Toggles `"checked"` class on the root element.
|
|
220
|
+
* - Sets `aria-selected`.
|
|
221
|
+
* - Mirrors to backing `<option>`:
|
|
222
|
+
* - toggles `selected` attribute,
|
|
223
|
+
* - sets `targetElement.selected`.
|
|
130
224
|
*
|
|
131
|
-
* @
|
|
225
|
+
* @param {boolean} value - New selection state.
|
|
226
|
+
* @returns {void}
|
|
132
227
|
*/
|
|
133
228
|
public set selectedNonTrigger(value: boolean) {
|
|
134
229
|
const input = this.view?.view?.tags?.OptionInput;
|
|
135
230
|
const viewEl = this.view?.getView?.();
|
|
136
231
|
|
|
137
|
-
if (input)
|
|
232
|
+
if (input) {
|
|
233
|
+
input.checked = value;
|
|
234
|
+
}
|
|
138
235
|
|
|
139
236
|
if (viewEl && this.targetElement) {
|
|
140
237
|
viewEl.classList.toggle("checked", !!value);
|
|
@@ -142,16 +239,24 @@ export class OptionModel extends Model<HTMLOptionElement, OptionViewTags, Option
|
|
|
142
239
|
this.targetElement.toggleAttribute("selected", !!value);
|
|
143
240
|
}
|
|
144
241
|
|
|
145
|
-
if (this.targetElement)
|
|
242
|
+
if (this.targetElement) {
|
|
243
|
+
this.targetElement.selected = value;
|
|
244
|
+
}
|
|
146
245
|
|
|
147
246
|
iEvents.callEvent<[OptionModel, boolean]>([this, value], ...this.privOnInternalSelected);
|
|
148
247
|
}
|
|
149
248
|
|
|
150
249
|
/**
|
|
151
|
-
*
|
|
152
|
-
*
|
|
250
|
+
* Display label for rendering (with tag translation and HTML policy).
|
|
251
|
+
*
|
|
252
|
+
* Source precedence:
|
|
253
|
+
* - `dataset.mask` if present, otherwise `targetElement.text`.
|
|
153
254
|
*
|
|
154
|
-
*
|
|
255
|
+
* Policy:
|
|
256
|
+
* - When `options.allowHtml === true`, returns translated HTML.
|
|
257
|
+
* - When `options.allowHtml === false`, returns plain text (HTML stripped).
|
|
258
|
+
*
|
|
259
|
+
* @returns {string}
|
|
155
260
|
*/
|
|
156
261
|
public get text(): string {
|
|
157
262
|
const raw = this.dataset?.mask ?? this.targetElement?.text ?? "";
|
|
@@ -160,40 +265,49 @@ export class OptionModel extends Model<HTMLOptionElement, OptionViewTags, Option
|
|
|
160
265
|
}
|
|
161
266
|
|
|
162
267
|
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
268
|
+
* Plain-text version of the display label, trimmed.
|
|
269
|
+
*
|
|
270
|
+
* - If `allowHtml` is enabled, strips HTML from {@link text}.
|
|
271
|
+
* - Otherwise returns {@link text} directly (already plain).
|
|
165
272
|
*
|
|
166
|
-
* @
|
|
273
|
+
* @returns {string}
|
|
167
274
|
*/
|
|
168
275
|
public get textContent(): string {
|
|
169
276
|
return this.options.allowHtml ? Libs.stripHtml(this.text).trim() : this.text.trim();
|
|
170
277
|
}
|
|
171
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Normalized, lowercase search key used for filtering/search.
|
|
281
|
+
* Computed during {@link init} and recomputed in {@link update}.
|
|
282
|
+
*/
|
|
172
283
|
public textToFind: string;
|
|
173
284
|
|
|
174
285
|
/**
|
|
175
|
-
*
|
|
286
|
+
* Dataset object of the backing `<option>` element.
|
|
176
287
|
*
|
|
177
|
-
* @
|
|
288
|
+
* @returns {DOMStringMap}
|
|
178
289
|
*/
|
|
179
290
|
public get dataset(): DOMStringMap {
|
|
180
|
-
return this.targetElement?.dataset ??
|
|
291
|
+
return this.targetElement?.dataset ?? {};
|
|
181
292
|
}
|
|
182
293
|
|
|
183
294
|
/**
|
|
184
|
-
*
|
|
295
|
+
* Whether this option is currently highlighted (navigation/hover).
|
|
185
296
|
*
|
|
186
|
-
* @
|
|
297
|
+
* @returns {boolean}
|
|
187
298
|
*/
|
|
188
299
|
public get highlighted(): boolean {
|
|
189
300
|
return this._highlighted;
|
|
190
301
|
}
|
|
191
302
|
|
|
192
303
|
/**
|
|
193
|
-
* Sets
|
|
194
|
-
* Always syncs the DOM class even if the state is unchanged.
|
|
304
|
+
* Sets highlight state and synchronizes the view (if attached).
|
|
195
305
|
*
|
|
196
|
-
*
|
|
306
|
+
* Side effects:
|
|
307
|
+
* - Toggles `"highlight"` CSS class on the view root element.
|
|
308
|
+
*
|
|
309
|
+
* @param {boolean} value - New highlight state.
|
|
310
|
+
* @returns {void}
|
|
197
311
|
*/
|
|
198
312
|
public set highlighted(value: boolean) {
|
|
199
313
|
const val = !!value;
|
|
@@ -204,36 +318,50 @@ export class OptionModel extends Model<HTMLOptionElement, OptionViewTags, Option
|
|
|
204
318
|
}
|
|
205
319
|
|
|
206
320
|
/**
|
|
207
|
-
*
|
|
321
|
+
* Subscribes to **external** selection changes (emitted by {@link selected}).
|
|
208
322
|
*
|
|
209
|
-
* @param {(evtToken: IEventCallback, el: OptionModel, selected: boolean) => void} callback -
|
|
323
|
+
* @param {(evtToken: IEventCallback, el: OptionModel, selected: boolean) => void} callback - Listener callback.
|
|
324
|
+
* @returns {void}
|
|
210
325
|
*/
|
|
211
326
|
public onSelected(callback: (evtToken: IEventCallback, el: OptionModel, selected: boolean) => void): void {
|
|
212
327
|
this.privOnSelected.push(callback);
|
|
213
328
|
}
|
|
214
329
|
|
|
215
330
|
/**
|
|
216
|
-
*
|
|
331
|
+
* Subscribes to **internal** selection changes (emitted by {@link selectedNonTrigger}).
|
|
217
332
|
*
|
|
218
|
-
* @param {(evtToken: IEventCallback, el: OptionModel, selected: boolean) => void} callback -
|
|
333
|
+
* @param {(evtToken: IEventCallback, el: OptionModel, selected: boolean) => void} callback - Listener callback.
|
|
334
|
+
* @returns {void}
|
|
219
335
|
*/
|
|
220
336
|
public onInternalSelected(callback: (evtToken: IEventCallback, el: OptionModel, selected: boolean) => void): void {
|
|
221
337
|
this.privOnInternalSelected.push(callback);
|
|
222
338
|
}
|
|
223
339
|
|
|
224
340
|
/**
|
|
225
|
-
*
|
|
341
|
+
* Subscribes to visibility changes (emitted by {@link visible}).
|
|
226
342
|
*
|
|
227
|
-
* @param {(evtToken: IEventCallback, model: OptionModel, visible: boolean) => void} callback -
|
|
343
|
+
* @param {(evtToken: IEventCallback, model: OptionModel, visible: boolean) => void} callback - Listener callback.
|
|
344
|
+
* @returns {void}
|
|
228
345
|
*/
|
|
229
346
|
public onVisibilityChanged(callback: (evtToken: IEventCallback, model: OptionModel, visible: boolean) => void): void {
|
|
230
347
|
this.privOnVisibilityChanged.push(callback);
|
|
231
348
|
}
|
|
232
349
|
|
|
233
350
|
/**
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
351
|
+
* Synchronizes derived fields and the attached view from the current backing element/options.
|
|
352
|
+
*
|
|
353
|
+
* Syncs:
|
|
354
|
+
* - {@link textToFind} (normalized search key)
|
|
355
|
+
* - Label content:
|
|
356
|
+
* - `innerHTML` when `allowHtml` is enabled,
|
|
357
|
+
* - otherwise `textContent`
|
|
358
|
+
* - Image attributes (`src`/`alt`) when present
|
|
359
|
+
* - Selected state from `targetElement.selected` via {@link selectedNonTrigger}
|
|
360
|
+
*
|
|
361
|
+
* No-op for view updates when no view is attached; still emits lifecycle update via `super.update()`.
|
|
362
|
+
*
|
|
363
|
+
* @returns {void}
|
|
364
|
+
* @override
|
|
237
365
|
*/
|
|
238
366
|
public override update(): void {
|
|
239
367
|
this.textToFind = Libs.string2normalize(this.textContent.toLowerCase());
|
|
@@ -253,8 +381,12 @@ export class OptionModel extends Model<HTMLOptionElement, OptionViewTags, Option
|
|
|
253
381
|
|
|
254
382
|
const imageTag = this.view.view.tags.OptionImage;
|
|
255
383
|
if (imageTag && this.hasImage) {
|
|
256
|
-
(imageTag
|
|
257
|
-
|
|
384
|
+
if (imageTag.src != this.imageSrc) {
|
|
385
|
+
imageTag.src = this.imageSrc;
|
|
386
|
+
}
|
|
387
|
+
if (imageTag.alt != this.text) {
|
|
388
|
+
imageTag.alt = this.text;
|
|
389
|
+
}
|
|
258
390
|
}
|
|
259
391
|
|
|
260
392
|
if (this.targetElement) this.selectedNonTrigger = this.targetElement.selected;
|
|
@@ -262,6 +394,18 @@ export class OptionModel extends Model<HTMLOptionElement, OptionViewTags, Option
|
|
|
262
394
|
super.update();
|
|
263
395
|
}
|
|
264
396
|
|
|
397
|
+
/**
|
|
398
|
+
* Destroys the model and releases listener references.
|
|
399
|
+
*
|
|
400
|
+
* Behavior:
|
|
401
|
+
* - Idempotent once lifecycle is {@link LifecycleState.DESTROYED}.
|
|
402
|
+
* - Clears external/internal selection listeners and visibility listeners.
|
|
403
|
+
* - Detaches from parent group and clears cached search key.
|
|
404
|
+
* - Completes teardown via `super.destroy()` (base {@link Model} also destroys the view if present).
|
|
405
|
+
*
|
|
406
|
+
* @returns {void}
|
|
407
|
+
* @override
|
|
408
|
+
*/
|
|
265
409
|
public override destroy(): void {
|
|
266
410
|
if (this.is(LifecycleState.DESTROYED)) {
|
|
267
411
|
return;
|
|
@@ -1,18 +1,59 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* DatasetObserver
|
|
3
|
+
*
|
|
4
|
+
* Lightweight observer that watches `data-*` attribute mutations on a target element
|
|
5
|
+
* and emits a debounced snapshot of `element.dataset`.
|
|
6
|
+
*
|
|
7
|
+
* ### Responsibility
|
|
8
|
+
* - Detect changes to `data-*` attributes using a {@link MutationObserver}.
|
|
9
|
+
* - Debounce rapid attribute mutations into a single callback invocation.
|
|
10
|
+
* - Provide a secondary/manual notification path via a custom `"dataset:changed"` event.
|
|
11
|
+
*
|
|
12
|
+
* ### Event Model (External vs. Internal)
|
|
13
|
+
* - **External changes**: DOM attribute mutations (e.g., `el.dataset.disabled = "1"`) are detected
|
|
14
|
+
* by {@link MutationObserver} and delivered after the debounce window.
|
|
15
|
+
* - **Internal/manual signal**: dispatching `"dataset:changed"` on the element forces an immediate
|
|
16
|
+
* snapshot emission (not debounced here), useful when dataset-like state is updated through
|
|
17
|
+
* non-attribute paths or when consumers want an explicit refresh signal.
|
|
18
|
+
*
|
|
19
|
+
* ### Debounce Semantics
|
|
20
|
+
* - Multiple attribute changes within ~50ms are coalesced into a single {@link onChanged} call.
|
|
21
|
+
* - The callback receives a shallow copy of the current dataset (`{ ...element.dataset }`),
|
|
22
|
+
* ensuring callers do not hold a live reference.
|
|
23
|
+
*
|
|
24
|
+
* ### Usage
|
|
25
|
+
* - Create instance with a target element.
|
|
26
|
+
* - Call {@link connect} to start observing.
|
|
27
|
+
* - Implement/assign {@link onChanged} to react to updates.
|
|
28
|
+
* - Call {@link disconnect} during teardown to prevent leaks.
|
|
3
29
|
*/
|
|
4
30
|
export class DatasetObserver {
|
|
31
|
+
/** Underlying MutationObserver instance used to detect `data-*` attribute mutations. */
|
|
5
32
|
private observer: MutationObserver;
|
|
6
33
|
|
|
34
|
+
/** Target element whose dataset (`data-*` attributes) is observed. */
|
|
7
35
|
private element: HTMLElement;
|
|
8
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Debounce timer handle for coalescing rapid attribute mutations.
|
|
39
|
+
* Cleared/replaced whenever a new relevant mutation arrives within the debounce window.
|
|
40
|
+
*/
|
|
9
41
|
private debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
10
42
|
|
|
11
43
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
44
|
+
* Creates a {@link DatasetObserver} for the given element.
|
|
45
|
+
*
|
|
46
|
+
* Side effects:
|
|
47
|
+
* - Instantiates a {@link MutationObserver} that filters for `attributes` mutations
|
|
48
|
+
* where `attributeName` starts with `"data-"`.
|
|
49
|
+
* - Registers a `"dataset:changed"` event listener on the element to allow manual
|
|
50
|
+
* emission of dataset snapshots.
|
|
14
51
|
*
|
|
15
|
-
*
|
|
52
|
+
* Notes:
|
|
53
|
+
* - Observation does not begin until {@link connect} is called.
|
|
54
|
+
* - The `"dataset:changed"` listener is always active after construction.
|
|
55
|
+
*
|
|
56
|
+
* @param element - The element whose `data-*` attributes will be observed.
|
|
16
57
|
*/
|
|
17
58
|
public constructor(element: HTMLElement) {
|
|
18
59
|
this.element = element;
|
|
@@ -41,8 +82,17 @@ export class DatasetObserver {
|
|
|
41
82
|
}
|
|
42
83
|
|
|
43
84
|
/**
|
|
44
|
-
* Starts observing the element for attribute changes
|
|
45
|
-
*
|
|
85
|
+
* Starts observing the target element for attribute changes.
|
|
86
|
+
*
|
|
87
|
+
* - Observes all attribute mutations and relies on the mutation callback to filter
|
|
88
|
+
* down to `data-*` changes.
|
|
89
|
+
* - `attributeOldValue` is enabled to allow future diagnostics; the current implementation
|
|
90
|
+
* does not consume old values directly.
|
|
91
|
+
*
|
|
92
|
+
* No-op behavior:
|
|
93
|
+
* - Calling `connect()` multiple times will register multiple observations on the same
|
|
94
|
+
* element in standard DOM APIs. Consumers should treat this as "call once" unless the
|
|
95
|
+
* implementation is extended to guard idempotency.
|
|
46
96
|
*/
|
|
47
97
|
public connect(): void {
|
|
48
98
|
this.observer.observe(this.element, {
|
|
@@ -52,10 +102,15 @@ export class DatasetObserver {
|
|
|
52
102
|
}
|
|
53
103
|
|
|
54
104
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
105
|
+
* Hook invoked when the element's dataset changes.
|
|
106
|
+
*
|
|
107
|
+
* Consumers typically override this method (or assign to it) to react to changes such as:
|
|
108
|
+
* - disabled / readonly / visible flags
|
|
109
|
+
* - feature toggles exposed via `data-*` attributes
|
|
110
|
+
*
|
|
111
|
+
* The `dataset` argument is a shallow copy of the *current* dataset at the time of emission.
|
|
57
112
|
*
|
|
58
|
-
* @param
|
|
113
|
+
* @param dataset - Snapshot of `element.dataset` (string values).
|
|
59
114
|
*/
|
|
60
115
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
61
116
|
public onChanged(dataset: Record<string, string>): void {
|
|
@@ -63,7 +118,14 @@ export class DatasetObserver {
|
|
|
63
118
|
}
|
|
64
119
|
|
|
65
120
|
/**
|
|
66
|
-
* Stops observing
|
|
121
|
+
* Stops observing and clears pending debounce work.
|
|
122
|
+
*
|
|
123
|
+
* Side effects:
|
|
124
|
+
* - Cancels the pending debounce timer (if any).
|
|
125
|
+
* - Disconnects the underlying {@link MutationObserver}.
|
|
126
|
+
*
|
|
127
|
+
* Idempotency:
|
|
128
|
+
* - Safe to call multiple times; subsequent calls will be effectively no-ops after disconnect.
|
|
67
129
|
*/
|
|
68
130
|
public disconnect(): void {
|
|
69
131
|
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|