selective-ui 1.0.2
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/LICENSE +21 -0
- package/README.md +2 -0
- package/dist/selective-ui.css +569 -0
- package/dist/selective-ui.css.map +1 -0
- package/dist/selective-ui.esm.js +6101 -0
- package/dist/selective-ui.esm.js.map +1 -0
- package/dist/selective-ui.esm.min.js +1 -0
- package/dist/selective-ui.esm.min.js.br +0 -0
- package/dist/selective-ui.min.css +1 -0
- package/dist/selective-ui.min.css.br +0 -0
- package/dist/selective-ui.min.js +2 -0
- package/dist/selective-ui.min.js.br +0 -0
- package/dist/selective-ui.umd.js +6115 -0
- package/dist/selective-ui.umd.js.map +1 -0
- package/package.json +68 -0
- package/src/css/components/accessorybox.css +64 -0
- package/src/css/components/directive.css +20 -0
- package/src/css/components/empty-state.css +26 -0
- package/src/css/components/loading-state.css +26 -0
- package/src/css/components/optgroup.css +62 -0
- package/src/css/components/option-handle.css +34 -0
- package/src/css/components/option.css +130 -0
- package/src/css/components/placeholder.css +15 -0
- package/src/css/components/popup.css +39 -0
- package/src/css/components/searchbox.css +29 -0
- package/src/css/components/selectbox.css +54 -0
- package/src/css/index.css +75 -0
- package/src/js/adapter/mixed-adapter.js +435 -0
- package/src/js/components/accessorybox.js +125 -0
- package/src/js/components/directive.js +38 -0
- package/src/js/components/empty-state.js +68 -0
- package/src/js/components/loading-state.js +60 -0
- package/src/js/components/option-handle.js +114 -0
- package/src/js/components/placeholder.js +57 -0
- package/src/js/components/popup.js +471 -0
- package/src/js/components/searchbox.js +168 -0
- package/src/js/components/selectbox.js +693 -0
- package/src/js/core/base/adapter.js +163 -0
- package/src/js/core/base/model.js +59 -0
- package/src/js/core/base/recyclerview.js +83 -0
- package/src/js/core/base/view.js +62 -0
- package/src/js/core/model-manager.js +286 -0
- package/src/js/core/search-controller.js +522 -0
- package/src/js/index.js +137 -0
- package/src/js/models/group-model.js +143 -0
- package/src/js/models/option-model.js +237 -0
- package/src/js/services/dataset-observer.js +73 -0
- package/src/js/services/ea-observer.js +88 -0
- package/src/js/services/effector.js +404 -0
- package/src/js/services/refresher.js +40 -0
- package/src/js/services/resize-observer.js +152 -0
- package/src/js/services/select-observer.js +61 -0
- package/src/js/types/adapter.type.js +33 -0
- package/src/js/types/effector.type.js +24 -0
- package/src/js/types/ievents.type.js +11 -0
- package/src/js/types/libs.type.js +28 -0
- package/src/js/types/model.type.js +11 -0
- package/src/js/types/recyclerview.type.js +12 -0
- package/src/js/types/resize-observer.type.js +19 -0
- package/src/js/types/view.group.type.js +13 -0
- package/src/js/types/view.option.type.js +15 -0
- package/src/js/types/view.type.js +11 -0
- package/src/js/utils/guard.js +47 -0
- package/src/js/utils/ievents.js +83 -0
- package/src/js/utils/istorage.js +61 -0
- package/src/js/utils/libs.js +619 -0
- package/src/js/utils/selective.js +386 -0
- package/src/js/views/group-view.js +103 -0
- package/src/js/views/option-view.js +153 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { Adapter } from "../core/base/adapter";
|
|
2
|
+
import { GroupModel } from "../models/group-model";
|
|
3
|
+
import { OptionModel } from "../models/option-model";
|
|
4
|
+
import { GroupView } from "../views/group-view";
|
|
5
|
+
import { OptionView } from "../views/option-view";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @extends {Adapter<GroupModel|OptionModel>}
|
|
9
|
+
*/
|
|
10
|
+
export class MixedAdapter extends Adapter {
|
|
11
|
+
isMultiple = false;
|
|
12
|
+
#visibilityChangedCallbacks = [];
|
|
13
|
+
#currentHighlightIndex = -1;
|
|
14
|
+
/** @type {OptionModel} */
|
|
15
|
+
#selectedItemSingle = null;
|
|
16
|
+
|
|
17
|
+
/** @type {GroupModel[]} */
|
|
18
|
+
groups = [];
|
|
19
|
+
|
|
20
|
+
/** @type {OptionModel[]} */
|
|
21
|
+
flatOptions = [];
|
|
22
|
+
|
|
23
|
+
constructor(items = []) {
|
|
24
|
+
super(items);
|
|
25
|
+
this.#buildFlatStructure();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build flat list of all options for navigation
|
|
30
|
+
*/
|
|
31
|
+
#buildFlatStructure() {
|
|
32
|
+
this.flatOptions = [];
|
|
33
|
+
this.groups = [];
|
|
34
|
+
|
|
35
|
+
this.items.forEach(item => {
|
|
36
|
+
if (item instanceof GroupModel) {
|
|
37
|
+
this.groups.push(item);
|
|
38
|
+
this.flatOptions.push(...item.items);
|
|
39
|
+
} else if (item instanceof OptionModel) {
|
|
40
|
+
this.flatOptions.push(item);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Creates and returns the appropriate view instance for the given item type.
|
|
47
|
+
*
|
|
48
|
+
* @param {HTMLElement} parent - The parent container element where the view will be attached.
|
|
49
|
+
* @param {GroupModel|OptionModel} item - The data model representing either a group or an option.
|
|
50
|
+
* @returns {GroupView|OptionView} - A new view instance corresponding to the item type.
|
|
51
|
+
*/
|
|
52
|
+
viewHolder(parent, item) {
|
|
53
|
+
if (item instanceof GroupModel) {
|
|
54
|
+
return new GroupView(parent);
|
|
55
|
+
} else {
|
|
56
|
+
return new OptionView(parent);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Binds a data model (GroupModel or OptionModel) to its corresponding view
|
|
62
|
+
* and delegates rendering logic based on the type of item.
|
|
63
|
+
*
|
|
64
|
+
* @param {GroupModel|OptionModel} item - The data model representing either a group or an option.
|
|
65
|
+
* @param {GroupView|OptionView} viewer - The view instance that displays the item in the UI.
|
|
66
|
+
* @param {number} position - The position (index) of the item within its parent list.
|
|
67
|
+
*/
|
|
68
|
+
onViewHolder(item, viewer, position) {
|
|
69
|
+
item.position = position;
|
|
70
|
+
|
|
71
|
+
if (item instanceof GroupModel) {
|
|
72
|
+
this.#handleGroupView(item, /** @type {GroupView} */ (viewer), position);
|
|
73
|
+
} else if (item instanceof OptionModel) {
|
|
74
|
+
this.#handleOptionView(item, /** @type {OptionView} */ (viewer), position);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
item.isInit = true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Handles binding and rendering logic for a group view, including header behavior,
|
|
82
|
+
* collapse/expand state, and initialization of option items.
|
|
83
|
+
*
|
|
84
|
+
* @param {GroupModel} groupModel - The data model representing the group and its items.
|
|
85
|
+
* @param {GroupView} groupView - The view instance that renders the group in the UI.
|
|
86
|
+
* @param {number} position - The position (index) of the group within a list.
|
|
87
|
+
*/
|
|
88
|
+
#handleGroupView(groupModel, groupView, position) {
|
|
89
|
+
super.onViewHolder(groupModel, groupView, position);
|
|
90
|
+
groupModel.view = groupView;
|
|
91
|
+
|
|
92
|
+
const header = groupView.getTag("GroupHeader");
|
|
93
|
+
header.textContent = groupModel.label;
|
|
94
|
+
|
|
95
|
+
if (!groupModel.isInit) {
|
|
96
|
+
header.style.cursor = "pointer";
|
|
97
|
+
header.addEventListener("click", () => {
|
|
98
|
+
groupModel.toggleCollapse();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
groupModel.onCollapsedChanged((evtToken, model, collapsed) => {
|
|
102
|
+
model.items.forEach(optItem => {
|
|
103
|
+
const optView = optItem.view?.getView();
|
|
104
|
+
if (optView) {
|
|
105
|
+
optView.style.display = collapsed ? "none" : "";
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
this.onCollapsedChange(model, collapsed);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const itemsContainer = groupView.getItemsContainer();
|
|
113
|
+
groupModel.items.forEach((optionModel, idx) => {
|
|
114
|
+
let optionViewer = optionModel.view;
|
|
115
|
+
if (!optionModel.isInit) {
|
|
116
|
+
optionViewer = new OptionView(itemsContainer);
|
|
117
|
+
}
|
|
118
|
+
this.#handleOptionView(optionModel, optionViewer, idx);
|
|
119
|
+
optionModel.isInit = true;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
groupView.setCollapsed(groupModel.collapsed);
|
|
123
|
+
groupView.updateVisibility();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Handles binding and rendering logic for an option item, including image setup,
|
|
128
|
+
* label rendering, event wiring, and selection state synchronization.
|
|
129
|
+
*
|
|
130
|
+
* @param {OptionModel} optionModel - The data model representing a single option.
|
|
131
|
+
* @param {OptionView} optionViewer - The view instance that renders the option in the UI.
|
|
132
|
+
* @param {number} position - The index of this option within its group's item list.
|
|
133
|
+
*/
|
|
134
|
+
#handleOptionView(optionModel, optionViewer, position) {
|
|
135
|
+
optionViewer.isMultiple = this.isMultiple;
|
|
136
|
+
optionViewer.hasImage = optionModel.hasImage;
|
|
137
|
+
optionViewer.optionConfig = {
|
|
138
|
+
imageWidth: optionModel.options.imageWidth,
|
|
139
|
+
imageHeight: optionModel.options.imageHeight,
|
|
140
|
+
imageBorderRadius: optionModel.options.imageBorderRadius,
|
|
141
|
+
imagePosition: optionModel.options.imagePosition,
|
|
142
|
+
labelValign: optionModel.options.labelValign,
|
|
143
|
+
labelHalign: optionModel.options.labelHalign
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (!optionModel.isInit) {
|
|
147
|
+
super.onViewHolder(optionModel, optionViewer, position);
|
|
148
|
+
} else {
|
|
149
|
+
optionViewer.update();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
optionModel.view = optionViewer;
|
|
153
|
+
|
|
154
|
+
if (optionModel.hasImage) {
|
|
155
|
+
const imageTag = optionViewer.getTag("OptionImage");
|
|
156
|
+
if (imageTag) {
|
|
157
|
+
imageTag.src = optionModel.imageSrc;
|
|
158
|
+
imageTag.alt = optionModel.text;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
optionViewer.getTag("LabelContent").innerHTML = optionModel.text;
|
|
163
|
+
|
|
164
|
+
if (!optionModel.isInit) {
|
|
165
|
+
optionViewer.getTag("OptionView").addEventListener("click", (ev) => {
|
|
166
|
+
ev.stopPropagation();
|
|
167
|
+
ev.preventDefault();
|
|
168
|
+
|
|
169
|
+
if (this.isSkipEvent) return;
|
|
170
|
+
|
|
171
|
+
if (this.isMultiple) {
|
|
172
|
+
this.changingProp("select");
|
|
173
|
+
setTimeout(() => {
|
|
174
|
+
optionModel.selected = !optionModel.selected;
|
|
175
|
+
}, 5);
|
|
176
|
+
} else if (optionModel.selected !== true) {
|
|
177
|
+
this.changingProp("select");
|
|
178
|
+
setTimeout(() => {
|
|
179
|
+
if (this.#selectedItemSingle) {
|
|
180
|
+
this.#selectedItemSingle.selected = false;
|
|
181
|
+
}
|
|
182
|
+
optionModel.selected = true;
|
|
183
|
+
}, 5);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
optionViewer.getTag("OptionView").title = optionModel.textContent;
|
|
188
|
+
|
|
189
|
+
optionViewer.getTag("OptionView").addEventListener("mouseenter", () => {
|
|
190
|
+
if (this.isSkipEvent) return;
|
|
191
|
+
this.setHighlight(this.flatOptions.indexOf(optionModel), false);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
optionModel.onSelected((evtToken, el, selected) => {
|
|
195
|
+
this.changeProp("selected");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
optionModel.onInternalSelected((evtToken, el, selected) => {
|
|
199
|
+
if (selected) {
|
|
200
|
+
this.#selectedItemSingle = optionModel;
|
|
201
|
+
}
|
|
202
|
+
this.changeProp("selected_internal");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
optionModel.onVisibilityChanged((evtToken, model, visible) => {
|
|
206
|
+
if (model.group) {
|
|
207
|
+
model.group.updateVisibility();
|
|
208
|
+
}
|
|
209
|
+
this.#notifyVisibilityChanged();
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (optionModel.selected) {
|
|
214
|
+
this.#selectedItemSingle = optionModel;
|
|
215
|
+
optionModel.selectedNonTrigger = true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Updates the list of items in the component and rebuilds its internal flat structure.
|
|
221
|
+
*
|
|
222
|
+
* @param {Array<GroupModel|OptionModel>} items - The new collection of items to be displayed.
|
|
223
|
+
*/
|
|
224
|
+
setItems(items) {
|
|
225
|
+
this.changingProp("items", items);
|
|
226
|
+
this.items = items;
|
|
227
|
+
this.#buildFlatStructure();
|
|
228
|
+
this.changeProp("items", items);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Synchronizes the component's items from an external source by delegating to setItems().
|
|
233
|
+
*
|
|
234
|
+
* @param {Array<GroupModel|OptionModel>} items - The new collection of items to sync.
|
|
235
|
+
*/
|
|
236
|
+
syncFromSource(items) {
|
|
237
|
+
this.setItems(items);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Updates the component's data items and rebuilds the internal flat structure
|
|
242
|
+
* without triggering change notifications.
|
|
243
|
+
*
|
|
244
|
+
* @param {Array<GroupModel|OptionModel>} items - The new collection of items to update.
|
|
245
|
+
*/
|
|
246
|
+
updateData(items) {
|
|
247
|
+
this.items = items;
|
|
248
|
+
this.#buildFlatStructure();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Returns all option items that are currently selected.
|
|
253
|
+
*
|
|
254
|
+
* @returns {OptionModel[]} - An array of selected option items from the flat list.
|
|
255
|
+
*/
|
|
256
|
+
getSelectedItems() {
|
|
257
|
+
return this.flatOptions.filter(item => item.selected);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Returns the first selected option item, if any.
|
|
262
|
+
*
|
|
263
|
+
* @returns {OptionModel|undefined} - The first selected option or undefined if none are selected.
|
|
264
|
+
*/
|
|
265
|
+
getSelectedItem() {
|
|
266
|
+
return this.flatOptions.find(item => item.selected);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Checks or unchecks all options when in multiple selection mode.
|
|
271
|
+
*
|
|
272
|
+
* @param {boolean} isChecked - If true, select all; if false, deselect all.
|
|
273
|
+
*/
|
|
274
|
+
checkAll(isChecked) {
|
|
275
|
+
if (this.isMultiple) {
|
|
276
|
+
this.flatOptions.forEach(item => {
|
|
277
|
+
item.selected = isChecked;
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Subscribes a callback to visibility changes across options.
|
|
284
|
+
*
|
|
285
|
+
* @param {(stats: {visibleCount:number,totalCount:number,hasVisible:boolean,isEmpty:boolean}) => void} callback
|
|
286
|
+
* - Function to invoke when visibility stats change.
|
|
287
|
+
*/
|
|
288
|
+
onVisibilityChanged(callback) {
|
|
289
|
+
this.#visibilityChangedCallbacks.push(callback);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Notifies all registered visibility-change callbacks with up-to-date statistics.
|
|
294
|
+
* Computes visible and total counts, then emits aggregated state.
|
|
295
|
+
*/
|
|
296
|
+
#notifyVisibilityChanged() {
|
|
297
|
+
const visibleCount = this.flatOptions.filter(item => item.visible).length;
|
|
298
|
+
const totalCount = this.flatOptions.length;
|
|
299
|
+
|
|
300
|
+
this.#visibilityChangedCallbacks.forEach(callback => {
|
|
301
|
+
callback({
|
|
302
|
+
visibleCount,
|
|
303
|
+
totalCount,
|
|
304
|
+
hasVisible: visibleCount > 0,
|
|
305
|
+
isEmpty: totalCount === 0
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Computes and returns current visibility statistics for options.
|
|
312
|
+
*
|
|
313
|
+
* @returns {{visibleCount:number,totalCount:number,hasVisible:boolean,isEmpty:boolean}}
|
|
314
|
+
* - Aggregated visibility information.
|
|
315
|
+
*/
|
|
316
|
+
getVisibilityStats() {
|
|
317
|
+
const visibleCount = this.flatOptions.filter(item => item.visible).length;
|
|
318
|
+
const totalCount = this.flatOptions.length;
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
visibleCount,
|
|
322
|
+
totalCount,
|
|
323
|
+
hasVisible: visibleCount > 0,
|
|
324
|
+
isEmpty: totalCount === 0
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Resets the highlight to the first visible option (index 0).
|
|
330
|
+
*/
|
|
331
|
+
resetHighlight() {
|
|
332
|
+
this.setHighlight(0);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Moves the highlight forward/backward among visible options and optionally scrolls into view.
|
|
337
|
+
*
|
|
338
|
+
* @param {number} direction - Increment (+1) or decrement (-1) of the current visible index.
|
|
339
|
+
* @param {boolean} [isScrollToView=true] - Whether to scroll the highlighted item into view.
|
|
340
|
+
*/
|
|
341
|
+
navigate(direction, isScrollToView = true) {
|
|
342
|
+
const visibleOptions = this.flatOptions.filter(opt => opt.visible);
|
|
343
|
+
if (visibleOptions.length === 0) return;
|
|
344
|
+
|
|
345
|
+
let currentVisibleIndex = visibleOptions.findIndex(
|
|
346
|
+
opt => opt === this.flatOptions[this.#currentHighlightIndex]
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
if (currentVisibleIndex === -1) currentVisibleIndex = -1;
|
|
350
|
+
|
|
351
|
+
let nextVisibleIndex = currentVisibleIndex + direction;
|
|
352
|
+
|
|
353
|
+
if (nextVisibleIndex >= visibleOptions.length) nextVisibleIndex = 0;
|
|
354
|
+
if (nextVisibleIndex < 0) nextVisibleIndex = visibleOptions.length - 1;
|
|
355
|
+
|
|
356
|
+
const nextOption = visibleOptions[nextVisibleIndex];
|
|
357
|
+
const flatIndex = this.flatOptions.indexOf(nextOption);
|
|
358
|
+
|
|
359
|
+
this.setHighlight(flatIndex, isScrollToView);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Triggers a click on the currently highlighted and visible option to select it.
|
|
364
|
+
* No-op if nothing is highlighted or the highlighted item is not visible.
|
|
365
|
+
*/
|
|
366
|
+
selectHighlighted() {
|
|
367
|
+
if (this.#currentHighlightIndex > -1 && this.flatOptions[this.#currentHighlightIndex]) {
|
|
368
|
+
const item = this.flatOptions[this.#currentHighlightIndex];
|
|
369
|
+
if (item.visible) {
|
|
370
|
+
const viewEl = item.view?.getView();
|
|
371
|
+
if (viewEl) viewEl.click();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Highlights a target option by flat index or model instance, skipping invisible items,
|
|
378
|
+
* and optionally scrolls the highlighted element into view.
|
|
379
|
+
*
|
|
380
|
+
* @param {number|OptionModel} target - Flat index or the specific OptionModel to highlight.
|
|
381
|
+
* @param {boolean} [isScrollToView=true] - Whether to scroll the highlighted item into view.
|
|
382
|
+
*/
|
|
383
|
+
setHighlight(target, isScrollToView = true) {
|
|
384
|
+
let index = 0;
|
|
385
|
+
if (typeof target === "number") {
|
|
386
|
+
index = target;
|
|
387
|
+
} else if (target instanceof OptionModel) {
|
|
388
|
+
const fi = this.flatOptions.indexOf(target);
|
|
389
|
+
index = fi > -1 ? fi : 0;
|
|
390
|
+
} else {
|
|
391
|
+
index = 0;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (this.#currentHighlightIndex > -1 && this.flatOptions[this.#currentHighlightIndex]) {
|
|
395
|
+
this.flatOptions[this.#currentHighlightIndex].highlighted = false;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
for (let i = index; i < this.flatOptions.length; i++) {
|
|
399
|
+
const item = this.flatOptions[i];
|
|
400
|
+
if (item.visible) {
|
|
401
|
+
item.highlighted = true;
|
|
402
|
+
this.#currentHighlightIndex = i;
|
|
403
|
+
|
|
404
|
+
if (isScrollToView) {
|
|
405
|
+
const el = item.view?.getView();
|
|
406
|
+
if (el) {
|
|
407
|
+
el.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
this.onHighlightChange(i, item.view?.getView()?.id);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Hook invoked whenever the highlight changes.
|
|
419
|
+
* Override to handle UI side effects (e.g., ARIA announcement, focus sync).
|
|
420
|
+
*
|
|
421
|
+
* @param {number} index - The flat index of the newly highlighted item.
|
|
422
|
+
* @param {string|undefined} id - The DOM id of the highlighted item's view, if available.
|
|
423
|
+
*/
|
|
424
|
+
onHighlightChange(index, id) {}
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Hook invoked when a group's collapsed state changes.
|
|
429
|
+
* Override to handle side effects like analytics or layout adjustments.
|
|
430
|
+
*
|
|
431
|
+
* @param {GroupModel} model - The group whose collapsed state changed.
|
|
432
|
+
* @param {boolean} collapsed - The new collapsed state.
|
|
433
|
+
*/
|
|
434
|
+
onCollapsedChange(model, collapsed) {}
|
|
435
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { ModelManager } from "../core/model-manager";
|
|
2
|
+
import { OptionModel } from "../models/option-model";
|
|
3
|
+
import { iEvents } from "../utils/ievents";
|
|
4
|
+
import { Libs } from "../utils/libs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @class
|
|
8
|
+
*/
|
|
9
|
+
export class AccessoryBox {
|
|
10
|
+
/**
|
|
11
|
+
* @type {MountViewResult<any>}
|
|
12
|
+
*/
|
|
13
|
+
nodeMounted = null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @type {HTMLDivElement}
|
|
17
|
+
*/
|
|
18
|
+
node = null;
|
|
19
|
+
|
|
20
|
+
options = null;
|
|
21
|
+
|
|
22
|
+
/** @type {HTMLDivElement} */
|
|
23
|
+
selectUIMask;
|
|
24
|
+
|
|
25
|
+
/** @type {HTMLDivElement} */
|
|
26
|
+
parentMask;
|
|
27
|
+
|
|
28
|
+
/** @type {ModelManager} */
|
|
29
|
+
modelManager;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Initializes the accessory box with optional configuration and immediately calls init() if provided.
|
|
33
|
+
*
|
|
34
|
+
* @param {object|null} options - Configuration options for the accessory box (e.g., layout and behavior).
|
|
35
|
+
*/
|
|
36
|
+
constructor(options = null) {
|
|
37
|
+
options && this.init(options);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Creates the accessory box DOM node and stores the provided options.
|
|
42
|
+
* The node is initially hidden and stops mouseup events from bubbling.
|
|
43
|
+
*
|
|
44
|
+
* @param {object} options - Configuration object for the accessory box.
|
|
45
|
+
*/
|
|
46
|
+
init(options) {
|
|
47
|
+
this.nodeMounted = Libs.mountNode({
|
|
48
|
+
AccessoryBox: {
|
|
49
|
+
tag: {node: "div", classList: ["selective-ui-accessorybox", "hide"], onmouseup: (evt) => {
|
|
50
|
+
evt.stopPropagation();
|
|
51
|
+
}}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
this.node = /** @type {HTMLDivElement} */ (this.nodeMounted.view);
|
|
55
|
+
this.options = options;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Sets the root references for the accessory box (mask elements) and refreshes its location in the DOM.
|
|
60
|
+
*
|
|
61
|
+
* @param {HTMLDivElement} selectUIMask - The overlay/mask element of the main Select UI.
|
|
62
|
+
*/
|
|
63
|
+
setRoot(selectUIMask) {
|
|
64
|
+
this.selectUIMask = selectUIMask;
|
|
65
|
+
this.parentMask = /** @type {HTMLDivElement} */ (selectUIMask.parentElement);
|
|
66
|
+
|
|
67
|
+
this.refreshLocation();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Inserts the accessory box before or after the Select UI mask depending on the configured accessoryStyle.
|
|
72
|
+
* Keeps the accessory box aligned relative to the parent mask.
|
|
73
|
+
*/
|
|
74
|
+
refreshLocation() {
|
|
75
|
+
this.parentMask.insertBefore(this.node, (this.options.accessoryStyle == "top" ? this.selectUIMask : this.selectUIMask.nextSibling));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Assigns a ModelManager instance used to trigger and manage selection state changes.
|
|
80
|
+
*
|
|
81
|
+
* @param {ModelManager} modelManager - The model manager controlling option state.
|
|
82
|
+
*/
|
|
83
|
+
setModelManager(modelManager) {
|
|
84
|
+
this.modelManager = modelManager;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Renders accessory items for the currently selected options in multiple-select mode.
|
|
89
|
+
* Shows the accessory box when there are items; otherwise hides it. Triggers a window resize event.
|
|
90
|
+
*
|
|
91
|
+
* @param {OptionModel[]} modelDatas - List of option models to render as accessory items.
|
|
92
|
+
*/
|
|
93
|
+
setModelData(modelDatas) {
|
|
94
|
+
this.node.replaceChildren();
|
|
95
|
+
|
|
96
|
+
if (modelDatas.length > 0 && this.options.multiple) {
|
|
97
|
+
this.node.classList.remove("hide");
|
|
98
|
+
|
|
99
|
+
modelDatas.forEach(modelData => {
|
|
100
|
+
Libs.mountNode({
|
|
101
|
+
AccessoryItem: {
|
|
102
|
+
tag: {node: "div", classList: ["accessory-item"]},
|
|
103
|
+
child: {
|
|
104
|
+
Button: {
|
|
105
|
+
tag: {node: "span", classList: ["accessory-item-button"], role: "button", ariaLabel: `${this.options.textAccessoryDeselect}${modelData.textContent}`, title: `${this.options.textAccessoryDeselect}${modelData.textContent}`, onclick: (evt) => {
|
|
106
|
+
this.modelManager.triggerChanging("select");
|
|
107
|
+
setTimeout(() => {
|
|
108
|
+
modelData.selected = false;
|
|
109
|
+
}, 10);
|
|
110
|
+
}}
|
|
111
|
+
},
|
|
112
|
+
Content: {
|
|
113
|
+
tag: {node: "span", classList: ["accessory-item-content"], innerHTML: modelData.text}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}, this.node);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
this.node.classList.add("hide");
|
|
122
|
+
}
|
|
123
|
+
iEvents.trigger(window, "resize");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {Libs} from "../utils/libs.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @class
|
|
5
|
+
*/
|
|
6
|
+
export class Directive {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.#init();
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* @type {Element}
|
|
12
|
+
*/
|
|
13
|
+
node = null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Represents a directive button element used to toggle dropdown state.
|
|
17
|
+
* Initializes a clickable node with appropriate ARIA attributes for accessibility.
|
|
18
|
+
*/
|
|
19
|
+
#init() {
|
|
20
|
+
this.node = Libs.nodeCreator({
|
|
21
|
+
node: "div", classList: "selective-ui-directive", role: "button", ariaLabel: "Toggle dropdown"
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sets the dropdown state by toggling the "drop-down" CSS class on the directive node.
|
|
27
|
+
*
|
|
28
|
+
* @param {boolean} value - If true, adds the "drop-down" class; otherwise removes it.
|
|
29
|
+
*/
|
|
30
|
+
setDropdown(value) {
|
|
31
|
+
if (value) {
|
|
32
|
+
this.node.classList.add("drop-down");
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
this.node.classList.remove("drop-down");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {Libs} from "../utils/libs.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @class
|
|
5
|
+
*/
|
|
6
|
+
export class EmptyState {
|
|
7
|
+
/**
|
|
8
|
+
* @type {HTMLDivElement}
|
|
9
|
+
*/
|
|
10
|
+
node = null;
|
|
11
|
+
|
|
12
|
+
options = null;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Represents an empty state component that displays a message when no data or search results are available.
|
|
16
|
+
* Provides methods to show/hide the state and check its visibility.
|
|
17
|
+
*/
|
|
18
|
+
constructor(options = null) {
|
|
19
|
+
options && this.init(options);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Initializes the empty state element with ARIA attributes for accessibility and stores configuration options.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} options - Configuration object containing text for "no data" and "not found" states.
|
|
26
|
+
*/
|
|
27
|
+
init(options) {
|
|
28
|
+
this.options = options;
|
|
29
|
+
|
|
30
|
+
this.node = /** @type {HTMLDivElement} */ (Libs.nodeCreator({
|
|
31
|
+
node: "div",
|
|
32
|
+
classList: ["selective-ui-empty-state", "hide"],
|
|
33
|
+
role: "status",
|
|
34
|
+
ariaLive: "polite"
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Displays the empty state message based on the provided type.
|
|
40
|
+
*
|
|
41
|
+
* @param {"notfound" | "nodata"} [type="nodata"] - Determines which message to show:
|
|
42
|
+
* "notfound" for search results not found, "nodata" for no available data.
|
|
43
|
+
*/
|
|
44
|
+
show(type = "nodata") {
|
|
45
|
+
const text = type === "notfound"
|
|
46
|
+
? this.options.textNotFound
|
|
47
|
+
: this.options.textNoData;
|
|
48
|
+
|
|
49
|
+
this.node.textContent = text;
|
|
50
|
+
this.node.classList.remove("hide");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Hides the empty state element by adding the "hide" class.
|
|
55
|
+
*/
|
|
56
|
+
hide() {
|
|
57
|
+
this.node.classList.add("hide");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Indicates whether the empty state is currently visible.
|
|
62
|
+
*
|
|
63
|
+
* @returns {boolean} - True if visible, false otherwise.
|
|
64
|
+
*/
|
|
65
|
+
get isVisible() {
|
|
66
|
+
return !this.node.classList.contains("hide");
|
|
67
|
+
}
|
|
68
|
+
}
|