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,386 @@
|
|
|
1
|
+
import {Libs} from "./libs.js";
|
|
2
|
+
import {iEvents} from "./ievents.js";
|
|
3
|
+
import {SelectBox} from "../components/selectbox.js";
|
|
4
|
+
import { ElementAdditionObserver } from "../services/ea-observer.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @class
|
|
8
|
+
*/
|
|
9
|
+
export class Selective {
|
|
10
|
+
/** @type {ElementAdditionObserver} */
|
|
11
|
+
EAObserver = null;
|
|
12
|
+
|
|
13
|
+
static bindedQueries = new Map();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Binds Selective UI to all <select> elements matching the query.
|
|
17
|
+
* Merges provided options with defaults, schedules `on.load` callbacks,
|
|
18
|
+
* initializes each matching select, and records the binding for auto-rebinding.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} query - CSS selector for target <select> elements.
|
|
21
|
+
* @param {object} options - Configuration overrides merged with defaults.
|
|
22
|
+
*/
|
|
23
|
+
static bind(query, options) {
|
|
24
|
+
options = Libs.mergeConfig(Libs.getDefaultConfig(), options);
|
|
25
|
+
|
|
26
|
+
this.bindedQueries.set(query, options);
|
|
27
|
+
|
|
28
|
+
const superThis = this;
|
|
29
|
+
const doneToken = Libs.randomString();
|
|
30
|
+
Libs.timerProcess.setExecute(doneToken, () => {
|
|
31
|
+
iEvents.callEvent([superThis.find(query)], ...options.on.load);
|
|
32
|
+
Libs.timerProcess.clearExecute(doneToken);
|
|
33
|
+
options.on.load = [];
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/** @type {HTMLSelectElement[]} */
|
|
37
|
+
const selectElements = /** @type {HTMLSelectElement[]} */ (Libs.getElements(query));
|
|
38
|
+
|
|
39
|
+
selectElements.forEach(item => {
|
|
40
|
+
(async() => {
|
|
41
|
+
if ("SELECT" == item.tagName) {
|
|
42
|
+
Libs.removeUnbinderMap(item);
|
|
43
|
+
if (this.applySelectBox(item, options)) {
|
|
44
|
+
Libs.timerProcess.run(doneToken);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
})();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (!Libs.getBindedCommand().includes(query)) {
|
|
51
|
+
Libs.getBindedCommand().push(query);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A dynamic action API object produced by `find()`.
|
|
58
|
+
* Contains `isEmpty` plus dynamically attached properties (get/set or functions).
|
|
59
|
+
*
|
|
60
|
+
* @typedef {Record<string, object> & { isEmpty: boolean }} ActionApi
|
|
61
|
+
*
|
|
62
|
+
* Finds the first bound SelectBox actions for a given query (or all bound queries if "*").
|
|
63
|
+
* Returns an API object with methods assembled from the bound action definitions.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} [query="*"] - CSS selector or "*" to search all bound instances.
|
|
66
|
+
* @returns {ActionApi} - Aggregated actions; {isEmpty:true} if none found.
|
|
67
|
+
*/
|
|
68
|
+
static find(query = "*") {
|
|
69
|
+
let actions = { isEmpty: true };
|
|
70
|
+
|
|
71
|
+
if (query == "*") {
|
|
72
|
+
query = Libs.getBindedCommand().join(", ");
|
|
73
|
+
if (query == "") {
|
|
74
|
+
return actions;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const sels = /** @type {HTMLElement[]} */ (Libs.getElements(query));
|
|
79
|
+
if (sels.length == 0) {
|
|
80
|
+
return actions;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const binded = Libs.getBinderMap(sels[0]);
|
|
84
|
+
if (!binded) {
|
|
85
|
+
return actions;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (let actionName in binded.action) {
|
|
89
|
+
actions[actionName] = this.#getProperties(actionName, binded.action)
|
|
90
|
+
}
|
|
91
|
+
Object.keys(binded.action);
|
|
92
|
+
|
|
93
|
+
if (actions) {
|
|
94
|
+
/** @type {ActionApi} */
|
|
95
|
+
let response = {};
|
|
96
|
+
for (let actionKey in actions) {
|
|
97
|
+
|
|
98
|
+
/** @type {IPropertiesType} */
|
|
99
|
+
const action = actions[actionKey];
|
|
100
|
+
|
|
101
|
+
switch (action.type) {
|
|
102
|
+
case "get-set":
|
|
103
|
+
this.#buildGetSetAction(response, action.name, sels);
|
|
104
|
+
break;
|
|
105
|
+
|
|
106
|
+
case "func":
|
|
107
|
+
this.#buildFuntionAction(response, action.name, sels);
|
|
108
|
+
break;
|
|
109
|
+
|
|
110
|
+
default:
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
response.isEmpty = false;
|
|
116
|
+
return response;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Starts observing the document for newly added <select> elements and applies
|
|
122
|
+
* Selective bindings automatically when they match previously bound queries.
|
|
123
|
+
*/
|
|
124
|
+
static Observer() {
|
|
125
|
+
this.EAObserver = new ElementAdditionObserver();
|
|
126
|
+
this.EAObserver.onDetect((selectElement) => {
|
|
127
|
+
this.bindedQueries.forEach((options, query) => {
|
|
128
|
+
try {
|
|
129
|
+
if (selectElement.matches(query)) {
|
|
130
|
+
this.applySelectBox(selectElement, options);
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.warn(`Invalid selector: ${query}`, error);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
this.EAObserver.start("select");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Destroys Selective instances. Supports:
|
|
142
|
+
* - destroyAll(): when target is null,
|
|
143
|
+
* - destroyByQuery(): when target is a selector string,
|
|
144
|
+
* - destroyElement(): when target is an HTMLSelectElement.
|
|
145
|
+
*
|
|
146
|
+
* @param {null|string|HTMLSelectElement} target - Target to destroy.
|
|
147
|
+
*/
|
|
148
|
+
static destroy(target = null) {
|
|
149
|
+
if (target === null) {
|
|
150
|
+
this.destroyAll();
|
|
151
|
+
} else if (typeof target === "string") {
|
|
152
|
+
this.destroyByQuery(target);
|
|
153
|
+
} else if (target instanceof HTMLSelectElement) {
|
|
154
|
+
this.destroyElement(target);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Destroys all bound Selective instances and clears bindings/state.
|
|
160
|
+
* Stops the ElementAdditionObserver.
|
|
161
|
+
*/
|
|
162
|
+
static destroyAll() {
|
|
163
|
+
const bindedCommands = Libs.getBindedCommand();
|
|
164
|
+
|
|
165
|
+
bindedCommands.forEach(query => {
|
|
166
|
+
this.destroyByQuery(query);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
this.bindedQueries.clear();
|
|
170
|
+
Libs.getBindedCommand().length = 0;
|
|
171
|
+
|
|
172
|
+
this.EAObserver.stop();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Destroys Selective instances bound to the specified query and removes
|
|
177
|
+
* the query from the binding registry.
|
|
178
|
+
*
|
|
179
|
+
* @param {string} query - CSS selector whose Selective instances should be destroyed.
|
|
180
|
+
*/
|
|
181
|
+
static destroyByQuery(query) {
|
|
182
|
+
const selectElements = /** @type {HTMLSelectElement[]} */ (Libs.getElements(query));
|
|
183
|
+
|
|
184
|
+
selectElements.forEach(element => {
|
|
185
|
+
if (element.tagName === "SELECT") {
|
|
186
|
+
this.destroyElement(element);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
this.bindedQueries.delete(query);
|
|
191
|
+
const commands = Libs.getBindedCommand();
|
|
192
|
+
const index = commands.indexOf(query);
|
|
193
|
+
if (index > -1) {
|
|
194
|
+
commands.splice(index, 1);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Destroys a single Selective instance attached to the given <select> element,
|
|
200
|
+
* detaches UI, restores original select state, and removes binder map entry.
|
|
201
|
+
*
|
|
202
|
+
* @param {HTMLSelectElement} selectElement - The target <select> element to clean up.
|
|
203
|
+
*/
|
|
204
|
+
static destroyElement(selectElement) {
|
|
205
|
+
const bindMap = Libs.getBinderMap(selectElement);
|
|
206
|
+
if (!bindMap) return;
|
|
207
|
+
|
|
208
|
+
Libs.setUnbinderMap(selectElement, bindMap);
|
|
209
|
+
|
|
210
|
+
const wasObserving = !!this.EAObserver;
|
|
211
|
+
if (wasObserving) this.EAObserver.stop();
|
|
212
|
+
|
|
213
|
+
try { bindMap.self.deInit?.(); } catch (_) {}
|
|
214
|
+
|
|
215
|
+
const wrapper = bindMap.container?.element || selectElement.parentElement;
|
|
216
|
+
|
|
217
|
+
selectElement.style.display = "";
|
|
218
|
+
selectElement.style.visibility = "";
|
|
219
|
+
selectElement.disabled = false;
|
|
220
|
+
delete selectElement.dataset.selectiveId;
|
|
221
|
+
|
|
222
|
+
if (wrapper && wrapper.parentNode) {
|
|
223
|
+
wrapper.parentNode.replaceChild(selectElement, wrapper);
|
|
224
|
+
} else {
|
|
225
|
+
document.body.appendChild(selectElement);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
Libs.removeBinderMap(selectElement);
|
|
229
|
+
|
|
230
|
+
if (wasObserving && this.bindedQueries.size > 0) {
|
|
231
|
+
this.EAObserver.start("select");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Unbinds a previously bound query from auto-apply and auto-observe lists.
|
|
237
|
+
* Stops the observer when no bound queries remain.
|
|
238
|
+
*
|
|
239
|
+
* @param {string} query - The CSS selector to unbind.
|
|
240
|
+
*/
|
|
241
|
+
// static unbind(query) {
|
|
242
|
+
// this.bindedQueries.delete(query);
|
|
243
|
+
|
|
244
|
+
// const commands = Libs.getBindedCommand();
|
|
245
|
+
// const index = commands.indexOf(query);
|
|
246
|
+
// if (index > -1) {
|
|
247
|
+
// commands.splice(index, 1);
|
|
248
|
+
// }
|
|
249
|
+
|
|
250
|
+
// if (this.bindedQueries.size === 0) {
|
|
251
|
+
// this.EAObserver.stop();
|
|
252
|
+
// }
|
|
253
|
+
// }
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Rebinds a query by destroying existing instances and binding anew
|
|
257
|
+
* with the provided options.
|
|
258
|
+
*
|
|
259
|
+
* @param {string} query - CSS selector to rebind.
|
|
260
|
+
* @param {object} options - Configuration for the new binding.
|
|
261
|
+
*/
|
|
262
|
+
static rebind(query, options) {
|
|
263
|
+
this.destroyByQuery(query);
|
|
264
|
+
this.bind(query, options);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Applies SelectBox enhancement to a single <select> element:
|
|
269
|
+
* builds per-instance IDs, merges element dataset into options,
|
|
270
|
+
* creates SelectBox, stores binder map with action API, and wires toggle on mouseup.
|
|
271
|
+
*
|
|
272
|
+
* @param {HTMLSelectElement} selectElement - The native <select> to enhance.
|
|
273
|
+
* @param {object} options - Configuration used for this instance.
|
|
274
|
+
* @returns {boolean} - False if already bound; true if successfully applied.
|
|
275
|
+
*/
|
|
276
|
+
static applySelectBox(selectElement, options) {
|
|
277
|
+
if (Libs.getBinderMap(selectElement) || Libs.getUnbinderMap(selectElement)) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const SEID = Libs.randomString(8);
|
|
282
|
+
const options_cfg = Libs.buildConfig(selectElement, options);
|
|
283
|
+
options_cfg.SEID = SEID
|
|
284
|
+
options_cfg.SEID_LIST = `seui-${SEID}-optionlist`;
|
|
285
|
+
options_cfg.SEID_HOLDER = `seui-${SEID}-placeholder`;
|
|
286
|
+
const bindMap = {options: options_cfg};
|
|
287
|
+
|
|
288
|
+
Libs.setBinderMap(selectElement, bindMap);
|
|
289
|
+
const selectBox = new SelectBox(selectElement, this);
|
|
290
|
+
bindMap.container = selectBox.container;
|
|
291
|
+
bindMap.action = selectBox.getAction();
|
|
292
|
+
bindMap.self = selectBox;
|
|
293
|
+
|
|
294
|
+
selectBox.container.view.addEventListener("mouseup", () => {
|
|
295
|
+
bindMap.action.toggle();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Determines the property type for an action name on the provided object.
|
|
303
|
+
* Classifies as:
|
|
304
|
+
* - "get-set" when a getter exists (or a setter with non-function value),
|
|
305
|
+
* - "func" when the property is a function,
|
|
306
|
+
* - "variable" otherwise.
|
|
307
|
+
*
|
|
308
|
+
* @typedef {Object} IPropertiesType
|
|
309
|
+
* @property {string} type - One of "variable" | "get-set" | "func".
|
|
310
|
+
* @property {string} name - The original action name.
|
|
311
|
+
*
|
|
312
|
+
* @param {string} actionName - The property key to inspect.
|
|
313
|
+
* @param {*} action - The object containing the property.
|
|
314
|
+
* @returns {IPropertiesType} - The derived property type and name.
|
|
315
|
+
*/
|
|
316
|
+
static #getProperties(actionName, action) {
|
|
317
|
+
const descriptor = Object.getOwnPropertyDescriptor(action, actionName);
|
|
318
|
+
let type = "variable";
|
|
319
|
+
|
|
320
|
+
if (descriptor.get || (descriptor.set && typeof action[actionName] !== "function")) {
|
|
321
|
+
type = "get-set";
|
|
322
|
+
}
|
|
323
|
+
else if (typeof action[actionName] === "function") {
|
|
324
|
+
type = "func";
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
type,
|
|
329
|
+
name: actionName
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Defines a get/set property on the target object that proxies to each bound element's action API.
|
|
335
|
+
* Getter reads from the first element; setter writes the value to all elements.
|
|
336
|
+
*
|
|
337
|
+
* @param {Object} object - The target object to define the property on.
|
|
338
|
+
* @param {string} name - The property name to expose.
|
|
339
|
+
* @param {HTMLElement[]} els - The list of bound elements to proxy.
|
|
340
|
+
*/
|
|
341
|
+
static #buildGetSetAction(object, name, els) {
|
|
342
|
+
Object.defineProperty(object, name, {
|
|
343
|
+
get() {
|
|
344
|
+
const binded = Libs.getBinderMap(els[0]);
|
|
345
|
+
return binded.action[name];
|
|
346
|
+
},
|
|
347
|
+
set(value) {
|
|
348
|
+
els.forEach(el => {
|
|
349
|
+
const binded = Libs.getBinderMap(el);
|
|
350
|
+
if (binded) {
|
|
351
|
+
binded.action[name] = value;
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
},
|
|
355
|
+
enumerable: true,
|
|
356
|
+
configurable: true
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Creates a function on the target object that invokes the corresponding action
|
|
362
|
+
* across all bound elements in order, respecting the event token flow control.
|
|
363
|
+
* Stops iteration if the token indicates propagation should not continue.
|
|
364
|
+
*
|
|
365
|
+
* @param {Object} object - The target object to attach the function to.
|
|
366
|
+
* @param {string} name - The function name to expose.
|
|
367
|
+
* @param {HTMLElement[]} els - The list of bound elements to invoke against.
|
|
368
|
+
*/
|
|
369
|
+
static #buildFuntionAction(object, name, els) {
|
|
370
|
+
object[name] = (...params) => {
|
|
371
|
+
for (let index = 0; index < els.length; index++) {
|
|
372
|
+
const el = els[index];
|
|
373
|
+
const binded = Libs.getBinderMap(el);
|
|
374
|
+
if (!binded) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const evtToken = iEvents.buildEventToken();
|
|
378
|
+
binded.action[name](evtToken.callback, ...params)
|
|
379
|
+
if (!evtToken.token.isContinue) {
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return object;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { View } from "../core/base/view";
|
|
2
|
+
import { Libs } from "../utils/libs";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @extends {View<GroupViewTags>}
|
|
6
|
+
*/
|
|
7
|
+
export class GroupView extends View {
|
|
8
|
+
/** @type {GroupViewResult} */
|
|
9
|
+
view;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Renders the group view structure (header + items container), sets ARIA attributes,
|
|
13
|
+
* and appends the root element to the parent container.
|
|
14
|
+
*/
|
|
15
|
+
render() {
|
|
16
|
+
const group_id = Libs.randomString(7);
|
|
17
|
+
|
|
18
|
+
this.view = Libs.mountView({
|
|
19
|
+
GroupView: {
|
|
20
|
+
tag: {
|
|
21
|
+
node: "div",
|
|
22
|
+
classList: ["selective-ui-group"],
|
|
23
|
+
role: "group",
|
|
24
|
+
ariaLabelledby: `seui-${group_id}-header`,
|
|
25
|
+
id: `seui-${group_id}-group`
|
|
26
|
+
},
|
|
27
|
+
child: {
|
|
28
|
+
GroupHeader: {
|
|
29
|
+
tag: {
|
|
30
|
+
node: "div",
|
|
31
|
+
classList: ["selective-ui-group-header"],
|
|
32
|
+
role: "presentation",
|
|
33
|
+
id: `seui-${group_id}-header`
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
GroupItems: {
|
|
37
|
+
tag: {
|
|
38
|
+
node: "div",
|
|
39
|
+
classList: ["selective-ui-group-items"],
|
|
40
|
+
role: "group"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
this.parent.appendChild(this.view.view);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Performs a lightweight refresh of the view (currently updates the header label).
|
|
52
|
+
*/
|
|
53
|
+
update() {
|
|
54
|
+
this.updateLabel();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Updates the group header text content if a label is provided.
|
|
59
|
+
*
|
|
60
|
+
* @param {string|null} [label=null] - The new label to display; if null, keeps current.
|
|
61
|
+
*/
|
|
62
|
+
updateLabel(label = null) {
|
|
63
|
+
const headerEl = this.view.tags.GroupHeader;
|
|
64
|
+
if (label !== null) {
|
|
65
|
+
headerEl.textContent = label;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Returns the container element that holds all option/item views in this group.
|
|
71
|
+
*
|
|
72
|
+
* @returns {HTMLDivElement} - The items container element.
|
|
73
|
+
*/
|
|
74
|
+
getItemsContainer() {
|
|
75
|
+
return this.view.tags.GroupItems;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Toggles the group's visibility based on whether any child item is visible.
|
|
80
|
+
* Hides the entire group when all children are hidden.
|
|
81
|
+
*/
|
|
82
|
+
updateVisibility() {
|
|
83
|
+
const items = this.view.tags.GroupItems;
|
|
84
|
+
const visibleItems = Array.from(items.children)
|
|
85
|
+
.filter(child => !child.classList.contains("hide"));
|
|
86
|
+
|
|
87
|
+
const isVisible = visibleItems.length > 0;
|
|
88
|
+
this.view.view.classList.toggle("hide", !isVisible);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Sets the collapsed state on the group and updates ARIA attributes accordingly.
|
|
93
|
+
*
|
|
94
|
+
* @param {boolean} collapsed - True to collapse; false to expand.
|
|
95
|
+
*/
|
|
96
|
+
setCollapsed(collapsed) {
|
|
97
|
+
this.view.view.classList.toggle("collapsed", collapsed);
|
|
98
|
+
this.view.tags.GroupHeader.setAttribute(
|
|
99
|
+
"aria-expanded",
|
|
100
|
+
collapsed ? "false" : "true"
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
|
|
2
|
+
import { View } from "../core/base/view";
|
|
3
|
+
import { Libs } from "../utils/libs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @extends {View<OptionViewTags>}
|
|
7
|
+
*/
|
|
8
|
+
export class OptionView extends View {
|
|
9
|
+
/** @type {OptionViewResult} */
|
|
10
|
+
view;
|
|
11
|
+
|
|
12
|
+
isMultiple = false;
|
|
13
|
+
hasImage = false;
|
|
14
|
+
optionConfig = null;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Renders the option view DOM structure (input, optional image, label),
|
|
18
|
+
* sets ARIA attributes/IDs, mounts into parent, and applies initial config.
|
|
19
|
+
*/
|
|
20
|
+
render() {
|
|
21
|
+
const viewClass = ["selective-ui-option-view"];
|
|
22
|
+
const opt_id = Libs.randomString(7);
|
|
23
|
+
const inputID = `option_${opt_id}`;
|
|
24
|
+
|
|
25
|
+
if (this.isMultiple) {
|
|
26
|
+
viewClass.push("multiple");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (this.hasImage) {
|
|
30
|
+
viewClass.push("has-image");
|
|
31
|
+
viewClass.push(`image-${this.optionConfig?.imagePosition}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const childStructure = {
|
|
35
|
+
OptionInput: {
|
|
36
|
+
tag: {
|
|
37
|
+
node: "input",
|
|
38
|
+
type: this.isMultiple ? "checkbox" : "radio",
|
|
39
|
+
classList: "allow-choice",
|
|
40
|
+
id: inputID
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
...(this.hasImage && {
|
|
44
|
+
OptionImage: {
|
|
45
|
+
tag: {
|
|
46
|
+
node: "img",
|
|
47
|
+
classList: "option-image",
|
|
48
|
+
style: {
|
|
49
|
+
width: this.optionConfig?.imageWidth || "60px",
|
|
50
|
+
height: this.optionConfig?.imageHeight || "60px",
|
|
51
|
+
borderRadius: this.optionConfig?.imageBorderRadius || "4px"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}),
|
|
56
|
+
OptionLabel: {
|
|
57
|
+
tag: {
|
|
58
|
+
node: "label",
|
|
59
|
+
htmlFor: inputID,
|
|
60
|
+
classList: [
|
|
61
|
+
`align-vertical-${this.optionConfig?.labelValign}`,
|
|
62
|
+
`align-horizontal-${this.optionConfig?.labelHalign}`
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
child: {
|
|
66
|
+
LabelContent: { tag: { node: "div" } }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
this.view = Libs.mountView({
|
|
72
|
+
OptionView: {
|
|
73
|
+
tag: {
|
|
74
|
+
node: "div",
|
|
75
|
+
id: `seui-${opt_id}-option`,
|
|
76
|
+
classList: viewClass,
|
|
77
|
+
role: "option",
|
|
78
|
+
ariaSelected: "false",
|
|
79
|
+
tabIndex: "-1"
|
|
80
|
+
},
|
|
81
|
+
child: childStructure
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
this.parent.appendChild(this.view.view);
|
|
86
|
+
|
|
87
|
+
this.applyConfigToDOM();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Refreshes the option view by reapplying configuration (classes, alignments, image styles).
|
|
92
|
+
*/
|
|
93
|
+
update() {
|
|
94
|
+
this.applyConfigToDOM();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Applies current configuration to the DOM in a minimal, fast way:
|
|
99
|
+
* - Set root/label classes in a single assignment (less DOM churn),
|
|
100
|
+
* - Ensure input type matches selection mode,
|
|
101
|
+
* - Create/remove image element only when needed, update its styles.
|
|
102
|
+
*/
|
|
103
|
+
applyConfigToDOM() {
|
|
104
|
+
const v = this.view;
|
|
105
|
+
if (!v || !v.view) return;
|
|
106
|
+
|
|
107
|
+
const root = v.view;
|
|
108
|
+
const input = v.tags?.OptionInput;
|
|
109
|
+
const label = v.tags?.OptionLabel;
|
|
110
|
+
const isMultiple = !!this.isMultiple;
|
|
111
|
+
const hasImage = !!this.hasImage;
|
|
112
|
+
const imagePos = this.optionConfig?.imagePosition || 'right';
|
|
113
|
+
const imageWidth = this.optionConfig?.imageWidth || '60px';
|
|
114
|
+
const imageHeight = this.optionConfig?.imageHeight || '60px';
|
|
115
|
+
const imageRadius = this.optionConfig?.imageBorderRadius || '4px';
|
|
116
|
+
const vAlign = this.optionConfig?.labelValign || 'center';
|
|
117
|
+
const hAlign = this.optionConfig?.labelHalign || 'left';
|
|
118
|
+
|
|
119
|
+
const rootClasses = ['selective-ui-option-view'];
|
|
120
|
+
if (isMultiple) rootClasses.push('multiple');
|
|
121
|
+
if (hasImage) {
|
|
122
|
+
rootClasses.push('has-image', `image-${imagePos}`);
|
|
123
|
+
}
|
|
124
|
+
root.className = rootClasses.join(' ');
|
|
125
|
+
|
|
126
|
+
if (input) {
|
|
127
|
+
const desiredType = isMultiple ? 'checkbox' : 'radio';
|
|
128
|
+
if (input.type !== desiredType) input.type = desiredType;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (label) {
|
|
132
|
+
label.className = `align-vertical-${vAlign} align-horizontal-${hAlign}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let image = v.tags?.OptionImage;
|
|
136
|
+
if (hasImage) {
|
|
137
|
+
if (!image) {
|
|
138
|
+
image = document.createElement('img');
|
|
139
|
+
image.className = 'option-image';
|
|
140
|
+
if (label && label.parentElement) root.insertBefore(image, label);
|
|
141
|
+
else root.appendChild(image);
|
|
142
|
+
v.tags.OptionImage = image;
|
|
143
|
+
}
|
|
144
|
+
const style = image.style;
|
|
145
|
+
style.width = imageWidth;
|
|
146
|
+
style.height = imageHeight;
|
|
147
|
+
style.borderRadius = imageRadius;
|
|
148
|
+
} else if (image) {
|
|
149
|
+
image.remove();
|
|
150
|
+
v.tags.OptionImage = null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|