selective-ui 1.0.2 → 1.0.3
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 -21
- package/README.md +7 -2
- package/dist/selective-ui.css +567 -567
- package/dist/selective-ui.css.map +1 -1
- package/dist/selective-ui.esm.js +6046 -6046
- package/dist/selective-ui.esm.js.map +1 -1
- package/dist/selective-ui.esm.min.js +1 -1
- package/dist/selective-ui.esm.min.js.br +0 -0
- package/dist/selective-ui.min.js +1 -1
- package/dist/selective-ui.min.js.br +0 -0
- package/dist/selective-ui.umd.js +6046 -6046
- package/dist/selective-ui.umd.js.map +1 -1
- package/package.json +68 -68
- package/src/css/components/accessorybox.css +63 -63
- package/src/css/components/directive.css +19 -19
- package/src/css/components/empty-state.css +25 -25
- package/src/css/components/loading-state.css +25 -25
- package/src/css/components/optgroup.css +61 -61
- package/src/css/components/option-handle.css +33 -33
- package/src/css/components/option.css +129 -129
- package/src/css/components/placeholder.css +14 -14
- package/src/css/components/popup.css +38 -38
- package/src/css/components/searchbox.css +28 -28
- package/src/css/components/selectbox.css +53 -53
- package/src/css/index.css +74 -74
- package/src/js/adapter/mixed-adapter.js +434 -434
- package/src/js/components/accessorybox.js +124 -124
- package/src/js/components/directive.js +37 -37
- package/src/js/components/empty-state.js +67 -67
- package/src/js/components/loading-state.js +59 -59
- package/src/js/components/option-handle.js +113 -113
- package/src/js/components/placeholder.js +56 -56
- package/src/js/components/popup.js +470 -470
- package/src/js/components/searchbox.js +167 -167
- package/src/js/components/selectbox.js +692 -692
- package/src/js/core/base/adapter.js +162 -162
- package/src/js/core/base/model.js +59 -59
- package/src/js/core/base/recyclerview.js +82 -82
- package/src/js/core/base/view.js +62 -62
- package/src/js/core/model-manager.js +286 -286
- package/src/js/core/search-controller.js +521 -521
- package/src/js/index.js +136 -136
- package/src/js/models/group-model.js +142 -142
- package/src/js/models/option-model.js +236 -236
- package/src/js/services/dataset-observer.js +73 -73
- package/src/js/services/ea-observer.js +87 -87
- package/src/js/services/effector.js +403 -403
- package/src/js/services/refresher.js +39 -39
- package/src/js/services/resize-observer.js +151 -151
- package/src/js/services/select-observer.js +60 -60
- package/src/js/types/adapter.type.js +32 -32
- package/src/js/types/effector.type.js +23 -23
- package/src/js/types/ievents.type.js +10 -10
- package/src/js/types/libs.type.js +27 -27
- package/src/js/types/model.type.js +11 -11
- package/src/js/types/recyclerview.type.js +11 -11
- package/src/js/types/resize-observer.type.js +18 -18
- package/src/js/types/view.group.type.js +12 -12
- package/src/js/types/view.option.type.js +14 -14
- package/src/js/types/view.type.js +10 -10
- package/src/js/utils/guard.js +46 -46
- package/src/js/utils/ievents.js +83 -83
- package/src/js/utils/istorage.js +60 -60
- package/src/js/utils/libs.js +618 -618
- package/src/js/utils/selective.js +385 -385
- package/src/js/views/group-view.js +102 -102
- package/src/js/views/option-view.js +152 -152
package/src/js/utils/libs.js
CHANGED
|
@@ -1,619 +1,619 @@
|
|
|
1
|
-
import {iStorage} from "./istorage.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @class
|
|
5
|
-
*/
|
|
6
|
-
export class Libs {
|
|
7
|
-
/** @type {iStorage} */
|
|
8
|
-
static #iStorage = null;
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Retrieves the shared iStorage instance (lazy-initialized singleton).
|
|
12
|
-
*
|
|
13
|
-
* @returns {iStorage} - The global storage utility used by Libs.
|
|
14
|
-
*/
|
|
15
|
-
static get iStorage() {
|
|
16
|
-
!this.#iStorage && (this.#iStorage = new iStorage());
|
|
17
|
-
return this.#iStorage;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Schedules and batches function executions keyed by name, with debounced timers.
|
|
22
|
-
* Provides setExecute(), clearExecute(), and run() to manage deferred callbacks.
|
|
23
|
-
*/
|
|
24
|
-
static timerProcess = {
|
|
25
|
-
executeStored: {},
|
|
26
|
-
setExecute: function(keyExecute, execute, timeout = 50, once = false) {
|
|
27
|
-
if (!this.executeStored[keyExecute]) {
|
|
28
|
-
this.executeStored[keyExecute] = [];
|
|
29
|
-
}
|
|
30
|
-
this.executeStored[keyExecute].push({execute: execute, timeout: timeout, once: once});
|
|
31
|
-
}, clearExecute: function(keyExecute) {
|
|
32
|
-
delete this.executeStored[keyExecute];
|
|
33
|
-
}, run: function(keyExecute, ...params) {
|
|
34
|
-
let executes = this.executeStored[keyExecute];
|
|
35
|
-
|
|
36
|
-
if (!this.timerRunner[keyExecute]) {
|
|
37
|
-
this.timerRunner[keyExecute] = {};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
for (const key in executes) {
|
|
41
|
-
const execute = executes[key];
|
|
42
|
-
|
|
43
|
-
if (!this.timerRunner[keyExecute][key]) {
|
|
44
|
-
this.timerRunner[keyExecute][key] = {};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (execute) {
|
|
48
|
-
clearTimeout(this.timerRunner[keyExecute][key]);
|
|
49
|
-
this.timerRunner[keyExecute][key] = setTimeout(() => {
|
|
50
|
-
execute && execute.execute(params.length > 0 ? params : null);
|
|
51
|
-
execute.once && (delete this.executeStored[keyExecute][key]);
|
|
52
|
-
}, execute.timeout);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}, timerRunner: {}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Checks whether a value is null/undefined/empty-string/"0"/0.
|
|
60
|
-
* Booleans are always considered non-empty.
|
|
61
|
-
*
|
|
62
|
-
* @param {any} value - The value to test.
|
|
63
|
-
* @returns {boolean} - True if considered empty; otherwise false.
|
|
64
|
-
*/
|
|
65
|
-
static isNullOrEmpty(value) {
|
|
66
|
-
if (typeof value === "boolean") return false;
|
|
67
|
-
return value == null || value === "" || value === 0 || value === "0";
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Deep-copies plain objects/arrays recursively. Returns primitives as-is.
|
|
72
|
-
*
|
|
73
|
-
* @param {any} obj - The source object or array.
|
|
74
|
-
* @returns {any} - A deep-cloned copy.
|
|
75
|
-
*/
|
|
76
|
-
static jsCopyObject(obj) {
|
|
77
|
-
if (obj === null || typeof obj !== "object") {
|
|
78
|
-
return obj;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
let copy = Array.isArray(obj) ? [] : {};
|
|
82
|
-
|
|
83
|
-
for (let key in obj) {
|
|
84
|
-
if (obj.hasOwnProperty(key)) {
|
|
85
|
-
copy[key] = this.jsCopyObject(obj[key]);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return copy;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Generates a random alphanumeric string of given length.
|
|
94
|
-
*
|
|
95
|
-
* @param {number} [length=6] - Desired length.
|
|
96
|
-
* @returns {string} - The generated string.
|
|
97
|
-
*/
|
|
98
|
-
static randomString(length = 6) {
|
|
99
|
-
let result = "";
|
|
100
|
-
let characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
101
|
-
let charactersLength = characters.length;
|
|
102
|
-
for ( let i = 0; i < length; i++ ) {
|
|
103
|
-
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
|
104
|
-
}
|
|
105
|
-
return result;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Resolves a selector, NodeList, or single Element into an array of elements.
|
|
110
|
-
* Returns an empty array if nothing is found.
|
|
111
|
-
*
|
|
112
|
-
* @param {string|NodeList|Element|HTMLElement|null} queryCommon - CSS selector, NodeList, or Element.
|
|
113
|
-
* @returns {Element[]|HTMLElement[]|Node[]} - Array of matched elements (empty if none).
|
|
114
|
-
*/
|
|
115
|
-
static getElements(queryCommon) {
|
|
116
|
-
if (!queryCommon) return [];
|
|
117
|
-
|
|
118
|
-
if (typeof queryCommon === "string") {
|
|
119
|
-
const nodeList = document.querySelectorAll(queryCommon);
|
|
120
|
-
return Array.from(nodeList);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (queryCommon instanceof Element) {
|
|
124
|
-
return [queryCommon];
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (queryCommon instanceof NodeList || Array.isArray(queryCommon)) {
|
|
128
|
-
return Array.from(queryCommon);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return [];
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Creates a new Element based on a NodeSpec and applies attributes, classes, styles, dataset, and events.
|
|
136
|
-
*
|
|
137
|
-
* @param {NodeSpec} data - Specification describing the element to create.
|
|
138
|
-
* @returns {Element} - The created element.
|
|
139
|
-
*/
|
|
140
|
-
static nodeCreator(data = {}) {
|
|
141
|
-
return this.nodeCloner(document.createElement(data.node), data, true);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Clones an element (or converts a Node to Element) and applies NodeSpec options.
|
|
146
|
-
* When systemNodeCreate=true, uses the provided node as-is.
|
|
147
|
-
*
|
|
148
|
-
* @param {Element} node - The element to clone or use.
|
|
149
|
-
* @param {NodeSpec} _nodeOption - Options (classList, style, dataset, event, other props).
|
|
150
|
-
* @param {boolean} systemNodeCreate - If true, do not clone; use original node.
|
|
151
|
-
* @returns {Element} - The processed element.
|
|
152
|
-
*/
|
|
153
|
-
static nodeCloner(node = document.documentElement, _nodeOption = null, systemNodeCreate = false) {
|
|
154
|
-
const nodeOption = { ..._nodeOption };
|
|
155
|
-
|
|
156
|
-
/** @type {Element} */
|
|
157
|
-
const element_creation = systemNodeCreate ? node : this.nodeToElement(node.cloneNode(true));
|
|
158
|
-
|
|
159
|
-
if (typeof nodeOption.classList === "string") {
|
|
160
|
-
element_creation.classList.add(nodeOption.classList);
|
|
161
|
-
} else if (Array.isArray(nodeOption.classList)) {
|
|
162
|
-
element_creation.classList.add(...nodeOption.classList);
|
|
163
|
-
}
|
|
164
|
-
delete nodeOption.classList;
|
|
165
|
-
|
|
166
|
-
["style", "dataset"].forEach(property => {
|
|
167
|
-
Object.assign(element_creation[property], nodeOption[property]);
|
|
168
|
-
delete nodeOption[property];
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
if (nodeOption.role) {
|
|
172
|
-
element_creation.setAttribute("role", nodeOption.role);
|
|
173
|
-
delete nodeOption.role;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (nodeOption.ariaLive) {
|
|
177
|
-
element_creation.setAttribute("aria-live", nodeOption.ariaLive);
|
|
178
|
-
delete nodeOption.ariaLive;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (nodeOption.ariaLabelledby) {
|
|
182
|
-
element_creation.setAttribute("aria-labelledby", nodeOption.ariaLabelledby);
|
|
183
|
-
delete nodeOption.ariaLabelledby;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (nodeOption.ariaControls) {
|
|
187
|
-
element_creation.setAttribute("aria-controls", nodeOption.ariaControls);
|
|
188
|
-
delete nodeOption.ariaControls;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (nodeOption.ariaHaspopup) {
|
|
192
|
-
element_creation.setAttribute("aria-haspopup", nodeOption.ariaHaspopup);
|
|
193
|
-
delete nodeOption.ariaHaspopup;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (nodeOption.ariaMultiselectable) {
|
|
197
|
-
element_creation.setAttribute("aria-multiselectable", nodeOption.ariaMultiselectable);
|
|
198
|
-
delete nodeOption.ariaMultiselectable;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (nodeOption.ariaAutocomplete) {
|
|
202
|
-
element_creation.setAttribute("aria-autocomplete", nodeOption.ariaAutocomplete);
|
|
203
|
-
delete nodeOption.ariaAutocomplete;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (nodeOption.event) {
|
|
207
|
-
Object.entries(nodeOption.event).forEach(([key, value]) => {
|
|
208
|
-
element_creation.addEventListener(key, value);
|
|
209
|
-
});
|
|
210
|
-
delete nodeOption.event;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
Object.entries(nodeOption).forEach(([key, value]) => {
|
|
214
|
-
if (value === null) {
|
|
215
|
-
element_creation.removeAttribute(key);
|
|
216
|
-
} else {
|
|
217
|
-
element_creation[key] = value;
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
return element_creation;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Ensures the given Node is an Element; throws if not.
|
|
226
|
-
*
|
|
227
|
-
* @param {Node} node - The node to validate.
|
|
228
|
-
* @returns {Element} - The element cast.
|
|
229
|
-
* @throws {TypeError} - If node is not an Element.
|
|
230
|
-
*/
|
|
231
|
-
static nodeToElement(node) {
|
|
232
|
-
if (node instanceof Element) {
|
|
233
|
-
return node;
|
|
234
|
-
}
|
|
235
|
-
throw new TypeError("Node is not an Element");
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Mounts a view from a plain object specification and returns a typed result
|
|
241
|
-
* containing the root element and a tag map.
|
|
242
|
-
*
|
|
243
|
-
* @template TTags
|
|
244
|
-
* @param {Object} rawObj - The specification describing elements and tags.
|
|
245
|
-
* @returns {MountViewResult<TTags>} - The mounted view and its tag references.
|
|
246
|
-
*/
|
|
247
|
-
static mountView(rawObj) {
|
|
248
|
-
return /** @type {MountViewResult<TTags>} */ (
|
|
249
|
-
this.mountNode(rawObj)
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Recursively builds DOM nodes from a specification object, appends/prepends them
|
|
255
|
-
* to an optional parent, and returns either a tag map or a full MountViewResult.
|
|
256
|
-
*
|
|
257
|
-
* @template TTags
|
|
258
|
-
* @param {Object<string, any>} rawObj - Node spec (keys -> { tag, child }).
|
|
259
|
-
* @param {Element|null} [parentE=null] - Parent to attach into; if null, returns root.
|
|
260
|
-
* @param {boolean} [isPrepend=false] - If true, prepend; otherwise append.
|
|
261
|
-
* @param {boolean} [isRecusive=false] - Internal flag for recursion control.
|
|
262
|
-
* @param {TTags|Object} [recursiveTemp={}] - Accumulator for tag references.
|
|
263
|
-
* @returns {MountViewResult<TTags>|TTags} - Tag map or the final mount result.
|
|
264
|
-
*/
|
|
265
|
-
static mountNode(rawObj, parentE = null, isPrepend = false, isRecusive = false, recursiveTemp = {}) {
|
|
266
|
-
let view = null;
|
|
267
|
-
for (let key in rawObj) {
|
|
268
|
-
const singleObj = rawObj[key];
|
|
269
|
-
|
|
270
|
-
const tag = (singleObj.tag?.tagName) ? singleObj.tag : this.nodeCreator(singleObj.tag);
|
|
271
|
-
recursiveTemp[key] = tag;
|
|
272
|
-
singleObj.child && this.mountNode(singleObj.child, tag, false, false, recursiveTemp);
|
|
273
|
-
|
|
274
|
-
if (parentE) {
|
|
275
|
-
if (isPrepend) {
|
|
276
|
-
parentE.prepend(tag);
|
|
277
|
-
}
|
|
278
|
-
else {
|
|
279
|
-
parentE.append(tag);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
else if (!isRecusive && !view) {
|
|
283
|
-
view = tag;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
if (!isRecusive) {
|
|
287
|
-
recursiveTemp.id = this.randomString(7);
|
|
288
|
-
|
|
289
|
-
if (!parentE) {
|
|
290
|
-
recursiveTemp = {tags: recursiveTemp, view: view}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
return recursiveTemp;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Applies inline CSS styles to all matched elements. Accepts either a style
|
|
298
|
-
* object or a single property + value pair.
|
|
299
|
-
*
|
|
300
|
-
* @param {string|NodeList|HTMLElement} queryCommon - Selector or element(s).
|
|
301
|
-
* @param {Record<string, string>|string} styles - Style object or a single property name.
|
|
302
|
-
* @param {string|null} [value=null] - Value for the single property form.
|
|
303
|
-
*/
|
|
304
|
-
static setStyle(queryCommon, styles, value = null) {
|
|
305
|
-
const apply_styles = typeof styles === "string" ? { [styles]: value } : { ...styles },
|
|
306
|
-
queryItems = this.getElements(queryCommon);
|
|
307
|
-
|
|
308
|
-
if (queryItems && typeof queryItems == "object"){
|
|
309
|
-
for (let i = 0; i < queryItems.length; i++){
|
|
310
|
-
const item = queryItems[i];
|
|
311
|
-
if (item) {
|
|
312
|
-
Object.assign(item["style"], apply_styles);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Builds a configuration object by copying defaults and then overriding with
|
|
320
|
-
* matching element properties or data-* attributes when present.
|
|
321
|
-
*
|
|
322
|
-
* @param {HTMLElement} element - Source element providing overrides.
|
|
323
|
-
* @param {object} options - Default configuration to be merged.
|
|
324
|
-
* @returns {object} - Final configuration after element overrides.
|
|
325
|
-
*/
|
|
326
|
-
static buildConfig(element, options) {
|
|
327
|
-
let myOptions = this.jsCopyObject(options);
|
|
328
|
-
|
|
329
|
-
for (let optionKey in myOptions) {
|
|
330
|
-
if (element[optionKey]) {
|
|
331
|
-
myOptions[optionKey] = element[optionKey];
|
|
332
|
-
}
|
|
333
|
-
else if (typeof element?.dataset[optionKey] !== "undefined") {
|
|
334
|
-
myOptions[optionKey] = element.dataset[optionKey];
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
return myOptions;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Deep-merges multiple configuration objects. Special-cases the `on` field
|
|
342
|
-
* by concatenating event handler arrays; other keys are overwritten.
|
|
343
|
-
*
|
|
344
|
-
* @param {...object} params - Config objects in priority order (leftmost is base).
|
|
345
|
-
* @returns {object} - Merged configuration object.
|
|
346
|
-
*/
|
|
347
|
-
static mergeConfig(...params) {
|
|
348
|
-
if (params.length == 0) {
|
|
349
|
-
return {};
|
|
350
|
-
}
|
|
351
|
-
if (params.length == 1) {
|
|
352
|
-
return this.jsCopyObject(params[0]);
|
|
353
|
-
}
|
|
354
|
-
else {
|
|
355
|
-
const level0 = this.jsCopyObject(params[0]);
|
|
356
|
-
for (let index = 1; index < params.length; index++) {
|
|
357
|
-
const cfg = params[index];
|
|
358
|
-
|
|
359
|
-
for (let optionKey in cfg) {
|
|
360
|
-
if (optionKey == "on") {
|
|
361
|
-
const cfgVar = cfg[optionKey];
|
|
362
|
-
for (let actKey in cfgVar) {
|
|
363
|
-
level0[optionKey][actKey].push(cfgVar[actKey]);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
else {
|
|
367
|
-
level0[optionKey] = cfg[optionKey];
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
return level0;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Converts strings like "true", "1", "yes", "on" to boolean true; "false", "0",
|
|
377
|
-
* "no", "off" to false. Non-strings are coerced via Boolean().
|
|
378
|
-
*
|
|
379
|
-
* @param {unknown} str - String or any value to convert.
|
|
380
|
-
* @returns {boolean} - The normalized boolean.
|
|
381
|
-
*/
|
|
382
|
-
static string2Boolean(str) {
|
|
383
|
-
if (typeof str === "boolean") return str;
|
|
384
|
-
if (typeof str !== "string") return Boolean(str);
|
|
385
|
-
|
|
386
|
-
switch (str.trim().toLowerCase()) {
|
|
387
|
-
case "true":
|
|
388
|
-
case "1":
|
|
389
|
-
case "yes":
|
|
390
|
-
case "on":
|
|
391
|
-
return true;
|
|
392
|
-
case "false":
|
|
393
|
-
case "0":
|
|
394
|
-
case "no":
|
|
395
|
-
case "off":
|
|
396
|
-
return false;
|
|
397
|
-
default:
|
|
398
|
-
return false;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Removes a binder map entry for the given element from the global storage.
|
|
404
|
-
*
|
|
405
|
-
* @param {HTMLElement} element - Element key to remove from the binder map.
|
|
406
|
-
* @returns {boolean} - True if an entry existed and was removed.
|
|
407
|
-
*/
|
|
408
|
-
static removeBinderMap(element) {
|
|
409
|
-
return this.iStorage.bindedMap.delete(element);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Retrieves the binder map entry associated with the given element.
|
|
414
|
-
*
|
|
415
|
-
* @param {HTMLElement} item - Element key whose binder map is requested.
|
|
416
|
-
* @returns {any} - The stored binder map value or undefined if absent.
|
|
417
|
-
*/
|
|
418
|
-
static getBinderMap(item) {
|
|
419
|
-
return this.iStorage.bindedMap.get(item);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
* Sets or updates the binder map entry for a given element.
|
|
424
|
-
*
|
|
425
|
-
* @param {HTMLElement} item - Element key to associate with the binder map.
|
|
426
|
-
* @param {any} bindMap - Value to store in the binder map.
|
|
427
|
-
*/
|
|
428
|
-
static setBinderMap(item, bindMap) {
|
|
429
|
-
this.iStorage.bindedMap.set(item, bindMap);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* Removes a binder map entry for the given element from the global storage.
|
|
434
|
-
*
|
|
435
|
-
* @param {HTMLElement} element - Element key to remove from the binder map.
|
|
436
|
-
* @returns {boolean} - True if an entry existed and was removed.
|
|
437
|
-
*/
|
|
438
|
-
static removeUnbinderMap(element) {
|
|
439
|
-
return this.iStorage.unbindedMap.delete(element);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* Retrieves the binder map entry associated with the given element.
|
|
444
|
-
*
|
|
445
|
-
* @param {HTMLElement} item - Element key whose binder map is requested.
|
|
446
|
-
* @returns {any} - The stored binder map value or undefined if absent.
|
|
447
|
-
*/
|
|
448
|
-
static getUnbinderMap(item) {
|
|
449
|
-
return this.iStorage.unbindedMap.get(item);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
* Sets or updates the binder map entry for a given element.
|
|
454
|
-
*
|
|
455
|
-
* @param {HTMLElement} item - Element key to associate with the binder map.
|
|
456
|
-
* @param {any} bindMap - Value to store in the binder map.
|
|
457
|
-
*/
|
|
458
|
-
static setUnbinderMap(item, bindMap) {
|
|
459
|
-
this.iStorage.unbindedMap.set(item, bindMap);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
* Returns the global default configuration used by the Selective UI system.
|
|
464
|
-
*
|
|
465
|
-
* @returns {object} - The default config object.
|
|
466
|
-
*/
|
|
467
|
-
static getDefaultConfig() {
|
|
468
|
-
return this.iStorage.defaultConfig;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
/**
|
|
472
|
-
* Returns the global list of bound commands stored in the shared storage.
|
|
473
|
-
*
|
|
474
|
-
* @returns {any[]} - The bound command list.
|
|
475
|
-
*/
|
|
476
|
-
static getBindedCommand() {
|
|
477
|
-
return this.iStorage.bindedCommand;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* Safely translates an HTML-like string to sanitized markup:
|
|
482
|
-
* - decodes custom <`/`> placeholders,
|
|
483
|
-
* - strips <script> tags,
|
|
484
|
-
* - removes event and javascript: attributes from all nodes.
|
|
485
|
-
*
|
|
486
|
-
* @param {string} str_tag - The input string to sanitize/translate.
|
|
487
|
-
* @returns {string} - Safe innerHTML string.
|
|
488
|
-
*/
|
|
489
|
-
static tagTranslate(str_tag) {
|
|
490
|
-
if (str_tag == null) return "";
|
|
491
|
-
|
|
492
|
-
str_tag = String(str_tag).replace(/<`/g, "<").replace(/`>/g, ">").replace(/<`/g, "<").replace(/`>/g, ">").trim();
|
|
493
|
-
|
|
494
|
-
const doc = globalThis?.document;
|
|
495
|
-
|
|
496
|
-
if (!doc || typeof doc.createElement !== "function") {
|
|
497
|
-
str_tag = str_tag
|
|
498
|
-
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "")
|
|
499
|
-
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "")
|
|
500
|
-
.replace(/<iframe\b[^>]*>[\s\S]*?<\/iframe>/gi, "")
|
|
501
|
-
.replace(/<(object|embed|link)\b[^>]*>[\s\S]*?<\/\1>/gi, "");
|
|
502
|
-
str_tag = str_tag.replace(/\son[a-z]+\s*=\s*(['"]).*?\1/gi, "");
|
|
503
|
-
str_tag = str_tag.replace(/\s([a-z-:]+)\s*=\s*(['"])\s*javascript:[^'"]*\2/gi, "");
|
|
504
|
-
return str_tag;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
const tmp = doc.createElement("div");
|
|
508
|
-
tmp.innerHTML = str_tag;
|
|
509
|
-
|
|
510
|
-
tmp.querySelectorAll("script, style, iframe, object, embed, link").forEach(n => n.remove());
|
|
511
|
-
|
|
512
|
-
tmp.querySelectorAll("*").forEach(n => {
|
|
513
|
-
[...n.attributes].forEach(a => {
|
|
514
|
-
const name = a.name || "";
|
|
515
|
-
const value = a.value || "";
|
|
516
|
-
if (/^on/i.test(name)) {
|
|
517
|
-
n.removeAttribute(name);
|
|
518
|
-
return;
|
|
519
|
-
}
|
|
520
|
-
if (/^(href|src|xlink:href)$/i.test(name) && /^javascript:/i.test(value)) {
|
|
521
|
-
n.removeAttribute(name);
|
|
522
|
-
}
|
|
523
|
-
});
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
return (tmp.innerHTML || "").trim();
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
/**
|
|
530
|
-
* Strips all HTML from a string and returns trimmed plain text.
|
|
531
|
-
*
|
|
532
|
-
* @param {string} html - The HTML string to strip.
|
|
533
|
-
* @returns {string} - The extracted plain text.
|
|
534
|
-
*/
|
|
535
|
-
static stripHtml(html) {
|
|
536
|
-
let tmp = document.createElement("DIV");
|
|
537
|
-
tmp.innerHTML = html;
|
|
538
|
-
let text_tmp = tmp.textContent || tmp.innerText || "";
|
|
539
|
-
tmp.remove();
|
|
540
|
-
return text_tmp.trim();
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
/**
|
|
544
|
-
* Normalizes a Vietnamese string by removing diacritics and special combining marks,
|
|
545
|
-
* returning a lowercase non-accent version for searching/matching.
|
|
546
|
-
*
|
|
547
|
-
* @param {string} str - The input text.
|
|
548
|
-
* @returns {string} - The diacritic-free lowercase string.
|
|
549
|
-
*/
|
|
550
|
-
static string2normalize(str) {
|
|
551
|
-
if (str == null) return '';
|
|
552
|
-
const s = String(str).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
553
|
-
return s.replace(/đ/g, 'd').replace(/Đ/g, 'd');
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
/**
|
|
557
|
-
* Parse select element to array (including optgroups)
|
|
558
|
-
* @param {HTMLSelectElement} selectElement
|
|
559
|
-
* @returns {Array<HTMLOptGroupElement|HTMLOptionElement>}
|
|
560
|
-
*/
|
|
561
|
-
static parseSelectToArray(selectElement) {
|
|
562
|
-
const result = [];
|
|
563
|
-
const children = Array.from(selectElement.children);
|
|
564
|
-
|
|
565
|
-
children.forEach(child => {
|
|
566
|
-
if (child.tagName === "OPTGROUP") {
|
|
567
|
-
result.push(child);
|
|
568
|
-
|
|
569
|
-
Array.from(child.children).forEach(option => {
|
|
570
|
-
option["__parentGroup"] = child;
|
|
571
|
-
result.push(option);
|
|
572
|
-
});
|
|
573
|
-
} else if (child.tagName === "OPTION") {
|
|
574
|
-
result.push(child);
|
|
575
|
-
}
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
return result;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
/**
|
|
583
|
-
* Detects whether the current environment is iOS (including iPad/iPhone/iPod, iOS browsers,
|
|
584
|
-
* iPadOS on Mac with touch, standalone PWAs, and WebKit mobile heuristics). Caches the result
|
|
585
|
-
* for subsequent calls to avoid re-computation.
|
|
586
|
-
*
|
|
587
|
-
* @returns {boolean} - True if the user agent/platform appears to be iOS; otherwise false.
|
|
588
|
-
*/
|
|
589
|
-
|
|
590
|
-
static IsIOS() {
|
|
591
|
-
const ua = navigator.userAgent;
|
|
592
|
-
return /iP(hone|ad|od)/.test(ua) ||
|
|
593
|
-
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
/**
|
|
597
|
-
* Converts an arbitrary CSS size value into pixel units by measuring a temporary element.
|
|
598
|
-
* Returns the computed pixel height as a string with "px" suffix.
|
|
599
|
-
*
|
|
600
|
-
* @param {string} value - Any valid CSS size (e.g., "2rem", "5vh", "12pt").
|
|
601
|
-
* @returns {string} - The equivalent pixel value (e.g., "32px").
|
|
602
|
-
*/
|
|
603
|
-
|
|
604
|
-
static any2px(value) {
|
|
605
|
-
const v = String(value).trim();
|
|
606
|
-
if (v.endsWith('px')) return v;
|
|
607
|
-
if (v.endsWith('vh')) return (window.innerHeight * parseFloat(v)/100) + 'px';
|
|
608
|
-
if (v.endsWith('vw')) return (window.innerWidth * parseFloat(v)/100) + 'px';
|
|
609
|
-
// ... rem/em: lấy computed font-size thân document
|
|
610
|
-
const fs = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
611
|
-
if (v.endsWith('rem')) return (fs * parseFloat(v)) + 'px';
|
|
612
|
-
// fallback: đo DOM (hiếm)
|
|
613
|
-
const el = /** @type {HTMLElement} */ (this.nodeCreator({node:'div', style:{height:v, opacity:0}}));
|
|
614
|
-
document.body.appendChild(el);
|
|
615
|
-
const px = el.offsetHeight + 'px';
|
|
616
|
-
el.remove();
|
|
617
|
-
return px;
|
|
618
|
-
}
|
|
1
|
+
import {iStorage} from "./istorage.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @class
|
|
5
|
+
*/
|
|
6
|
+
export class Libs {
|
|
7
|
+
/** @type {iStorage} */
|
|
8
|
+
static #iStorage = null;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Retrieves the shared iStorage instance (lazy-initialized singleton).
|
|
12
|
+
*
|
|
13
|
+
* @returns {iStorage} - The global storage utility used by Libs.
|
|
14
|
+
*/
|
|
15
|
+
static get iStorage() {
|
|
16
|
+
!this.#iStorage && (this.#iStorage = new iStorage());
|
|
17
|
+
return this.#iStorage;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Schedules and batches function executions keyed by name, with debounced timers.
|
|
22
|
+
* Provides setExecute(), clearExecute(), and run() to manage deferred callbacks.
|
|
23
|
+
*/
|
|
24
|
+
static timerProcess = {
|
|
25
|
+
executeStored: {},
|
|
26
|
+
setExecute: function(keyExecute, execute, timeout = 50, once = false) {
|
|
27
|
+
if (!this.executeStored[keyExecute]) {
|
|
28
|
+
this.executeStored[keyExecute] = [];
|
|
29
|
+
}
|
|
30
|
+
this.executeStored[keyExecute].push({execute: execute, timeout: timeout, once: once});
|
|
31
|
+
}, clearExecute: function(keyExecute) {
|
|
32
|
+
delete this.executeStored[keyExecute];
|
|
33
|
+
}, run: function(keyExecute, ...params) {
|
|
34
|
+
let executes = this.executeStored[keyExecute];
|
|
35
|
+
|
|
36
|
+
if (!this.timerRunner[keyExecute]) {
|
|
37
|
+
this.timerRunner[keyExecute] = {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const key in executes) {
|
|
41
|
+
const execute = executes[key];
|
|
42
|
+
|
|
43
|
+
if (!this.timerRunner[keyExecute][key]) {
|
|
44
|
+
this.timerRunner[keyExecute][key] = {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (execute) {
|
|
48
|
+
clearTimeout(this.timerRunner[keyExecute][key]);
|
|
49
|
+
this.timerRunner[keyExecute][key] = setTimeout(() => {
|
|
50
|
+
execute && execute.execute(params.length > 0 ? params : null);
|
|
51
|
+
execute.once && (delete this.executeStored[keyExecute][key]);
|
|
52
|
+
}, execute.timeout);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}, timerRunner: {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Checks whether a value is null/undefined/empty-string/"0"/0.
|
|
60
|
+
* Booleans are always considered non-empty.
|
|
61
|
+
*
|
|
62
|
+
* @param {any} value - The value to test.
|
|
63
|
+
* @returns {boolean} - True if considered empty; otherwise false.
|
|
64
|
+
*/
|
|
65
|
+
static isNullOrEmpty(value) {
|
|
66
|
+
if (typeof value === "boolean") return false;
|
|
67
|
+
return value == null || value === "" || value === 0 || value === "0";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Deep-copies plain objects/arrays recursively. Returns primitives as-is.
|
|
72
|
+
*
|
|
73
|
+
* @param {any} obj - The source object or array.
|
|
74
|
+
* @returns {any} - A deep-cloned copy.
|
|
75
|
+
*/
|
|
76
|
+
static jsCopyObject(obj) {
|
|
77
|
+
if (obj === null || typeof obj !== "object") {
|
|
78
|
+
return obj;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let copy = Array.isArray(obj) ? [] : {};
|
|
82
|
+
|
|
83
|
+
for (let key in obj) {
|
|
84
|
+
if (obj.hasOwnProperty(key)) {
|
|
85
|
+
copy[key] = this.jsCopyObject(obj[key]);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return copy;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Generates a random alphanumeric string of given length.
|
|
94
|
+
*
|
|
95
|
+
* @param {number} [length=6] - Desired length.
|
|
96
|
+
* @returns {string} - The generated string.
|
|
97
|
+
*/
|
|
98
|
+
static randomString(length = 6) {
|
|
99
|
+
let result = "";
|
|
100
|
+
let characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
101
|
+
let charactersLength = characters.length;
|
|
102
|
+
for ( let i = 0; i < length; i++ ) {
|
|
103
|
+
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resolves a selector, NodeList, or single Element into an array of elements.
|
|
110
|
+
* Returns an empty array if nothing is found.
|
|
111
|
+
*
|
|
112
|
+
* @param {string|NodeList|Element|HTMLElement|null} queryCommon - CSS selector, NodeList, or Element.
|
|
113
|
+
* @returns {Element[]|HTMLElement[]|Node[]} - Array of matched elements (empty if none).
|
|
114
|
+
*/
|
|
115
|
+
static getElements(queryCommon) {
|
|
116
|
+
if (!queryCommon) return [];
|
|
117
|
+
|
|
118
|
+
if (typeof queryCommon === "string") {
|
|
119
|
+
const nodeList = document.querySelectorAll(queryCommon);
|
|
120
|
+
return Array.from(nodeList);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (queryCommon instanceof Element) {
|
|
124
|
+
return [queryCommon];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (queryCommon instanceof NodeList || Array.isArray(queryCommon)) {
|
|
128
|
+
return Array.from(queryCommon);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Creates a new Element based on a NodeSpec and applies attributes, classes, styles, dataset, and events.
|
|
136
|
+
*
|
|
137
|
+
* @param {NodeSpec} data - Specification describing the element to create.
|
|
138
|
+
* @returns {Element} - The created element.
|
|
139
|
+
*/
|
|
140
|
+
static nodeCreator(data = {}) {
|
|
141
|
+
return this.nodeCloner(document.createElement(data.node), data, true);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Clones an element (or converts a Node to Element) and applies NodeSpec options.
|
|
146
|
+
* When systemNodeCreate=true, uses the provided node as-is.
|
|
147
|
+
*
|
|
148
|
+
* @param {Element} node - The element to clone or use.
|
|
149
|
+
* @param {NodeSpec} _nodeOption - Options (classList, style, dataset, event, other props).
|
|
150
|
+
* @param {boolean} systemNodeCreate - If true, do not clone; use original node.
|
|
151
|
+
* @returns {Element} - The processed element.
|
|
152
|
+
*/
|
|
153
|
+
static nodeCloner(node = document.documentElement, _nodeOption = null, systemNodeCreate = false) {
|
|
154
|
+
const nodeOption = { ..._nodeOption };
|
|
155
|
+
|
|
156
|
+
/** @type {Element} */
|
|
157
|
+
const element_creation = systemNodeCreate ? node : this.nodeToElement(node.cloneNode(true));
|
|
158
|
+
|
|
159
|
+
if (typeof nodeOption.classList === "string") {
|
|
160
|
+
element_creation.classList.add(nodeOption.classList);
|
|
161
|
+
} else if (Array.isArray(nodeOption.classList)) {
|
|
162
|
+
element_creation.classList.add(...nodeOption.classList);
|
|
163
|
+
}
|
|
164
|
+
delete nodeOption.classList;
|
|
165
|
+
|
|
166
|
+
["style", "dataset"].forEach(property => {
|
|
167
|
+
Object.assign(element_creation[property], nodeOption[property]);
|
|
168
|
+
delete nodeOption[property];
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (nodeOption.role) {
|
|
172
|
+
element_creation.setAttribute("role", nodeOption.role);
|
|
173
|
+
delete nodeOption.role;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (nodeOption.ariaLive) {
|
|
177
|
+
element_creation.setAttribute("aria-live", nodeOption.ariaLive);
|
|
178
|
+
delete nodeOption.ariaLive;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (nodeOption.ariaLabelledby) {
|
|
182
|
+
element_creation.setAttribute("aria-labelledby", nodeOption.ariaLabelledby);
|
|
183
|
+
delete nodeOption.ariaLabelledby;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (nodeOption.ariaControls) {
|
|
187
|
+
element_creation.setAttribute("aria-controls", nodeOption.ariaControls);
|
|
188
|
+
delete nodeOption.ariaControls;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (nodeOption.ariaHaspopup) {
|
|
192
|
+
element_creation.setAttribute("aria-haspopup", nodeOption.ariaHaspopup);
|
|
193
|
+
delete nodeOption.ariaHaspopup;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (nodeOption.ariaMultiselectable) {
|
|
197
|
+
element_creation.setAttribute("aria-multiselectable", nodeOption.ariaMultiselectable);
|
|
198
|
+
delete nodeOption.ariaMultiselectable;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (nodeOption.ariaAutocomplete) {
|
|
202
|
+
element_creation.setAttribute("aria-autocomplete", nodeOption.ariaAutocomplete);
|
|
203
|
+
delete nodeOption.ariaAutocomplete;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (nodeOption.event) {
|
|
207
|
+
Object.entries(nodeOption.event).forEach(([key, value]) => {
|
|
208
|
+
element_creation.addEventListener(key, value);
|
|
209
|
+
});
|
|
210
|
+
delete nodeOption.event;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
Object.entries(nodeOption).forEach(([key, value]) => {
|
|
214
|
+
if (value === null) {
|
|
215
|
+
element_creation.removeAttribute(key);
|
|
216
|
+
} else {
|
|
217
|
+
element_creation[key] = value;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return element_creation;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Ensures the given Node is an Element; throws if not.
|
|
226
|
+
*
|
|
227
|
+
* @param {Node} node - The node to validate.
|
|
228
|
+
* @returns {Element} - The element cast.
|
|
229
|
+
* @throws {TypeError} - If node is not an Element.
|
|
230
|
+
*/
|
|
231
|
+
static nodeToElement(node) {
|
|
232
|
+
if (node instanceof Element) {
|
|
233
|
+
return node;
|
|
234
|
+
}
|
|
235
|
+
throw new TypeError("Node is not an Element");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Mounts a view from a plain object specification and returns a typed result
|
|
241
|
+
* containing the root element and a tag map.
|
|
242
|
+
*
|
|
243
|
+
* @template TTags
|
|
244
|
+
* @param {Object} rawObj - The specification describing elements and tags.
|
|
245
|
+
* @returns {MountViewResult<TTags>} - The mounted view and its tag references.
|
|
246
|
+
*/
|
|
247
|
+
static mountView(rawObj) {
|
|
248
|
+
return /** @type {MountViewResult<TTags>} */ (
|
|
249
|
+
this.mountNode(rawObj)
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Recursively builds DOM nodes from a specification object, appends/prepends them
|
|
255
|
+
* to an optional parent, and returns either a tag map or a full MountViewResult.
|
|
256
|
+
*
|
|
257
|
+
* @template TTags
|
|
258
|
+
* @param {Object<string, any>} rawObj - Node spec (keys -> { tag, child }).
|
|
259
|
+
* @param {Element|null} [parentE=null] - Parent to attach into; if null, returns root.
|
|
260
|
+
* @param {boolean} [isPrepend=false] - If true, prepend; otherwise append.
|
|
261
|
+
* @param {boolean} [isRecusive=false] - Internal flag for recursion control.
|
|
262
|
+
* @param {TTags|Object} [recursiveTemp={}] - Accumulator for tag references.
|
|
263
|
+
* @returns {MountViewResult<TTags>|TTags} - Tag map or the final mount result.
|
|
264
|
+
*/
|
|
265
|
+
static mountNode(rawObj, parentE = null, isPrepend = false, isRecusive = false, recursiveTemp = {}) {
|
|
266
|
+
let view = null;
|
|
267
|
+
for (let key in rawObj) {
|
|
268
|
+
const singleObj = rawObj[key];
|
|
269
|
+
|
|
270
|
+
const tag = (singleObj.tag?.tagName) ? singleObj.tag : this.nodeCreator(singleObj.tag);
|
|
271
|
+
recursiveTemp[key] = tag;
|
|
272
|
+
singleObj.child && this.mountNode(singleObj.child, tag, false, false, recursiveTemp);
|
|
273
|
+
|
|
274
|
+
if (parentE) {
|
|
275
|
+
if (isPrepend) {
|
|
276
|
+
parentE.prepend(tag);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
parentE.append(tag);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
else if (!isRecusive && !view) {
|
|
283
|
+
view = tag;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (!isRecusive) {
|
|
287
|
+
recursiveTemp.id = this.randomString(7);
|
|
288
|
+
|
|
289
|
+
if (!parentE) {
|
|
290
|
+
recursiveTemp = {tags: recursiveTemp, view: view}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return recursiveTemp;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Applies inline CSS styles to all matched elements. Accepts either a style
|
|
298
|
+
* object or a single property + value pair.
|
|
299
|
+
*
|
|
300
|
+
* @param {string|NodeList|HTMLElement} queryCommon - Selector or element(s).
|
|
301
|
+
* @param {Record<string, string>|string} styles - Style object or a single property name.
|
|
302
|
+
* @param {string|null} [value=null] - Value for the single property form.
|
|
303
|
+
*/
|
|
304
|
+
static setStyle(queryCommon, styles, value = null) {
|
|
305
|
+
const apply_styles = typeof styles === "string" ? { [styles]: value } : { ...styles },
|
|
306
|
+
queryItems = this.getElements(queryCommon);
|
|
307
|
+
|
|
308
|
+
if (queryItems && typeof queryItems == "object"){
|
|
309
|
+
for (let i = 0; i < queryItems.length; i++){
|
|
310
|
+
const item = queryItems[i];
|
|
311
|
+
if (item) {
|
|
312
|
+
Object.assign(item["style"], apply_styles);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Builds a configuration object by copying defaults and then overriding with
|
|
320
|
+
* matching element properties or data-* attributes when present.
|
|
321
|
+
*
|
|
322
|
+
* @param {HTMLElement} element - Source element providing overrides.
|
|
323
|
+
* @param {object} options - Default configuration to be merged.
|
|
324
|
+
* @returns {object} - Final configuration after element overrides.
|
|
325
|
+
*/
|
|
326
|
+
static buildConfig(element, options) {
|
|
327
|
+
let myOptions = this.jsCopyObject(options);
|
|
328
|
+
|
|
329
|
+
for (let optionKey in myOptions) {
|
|
330
|
+
if (element[optionKey]) {
|
|
331
|
+
myOptions[optionKey] = element[optionKey];
|
|
332
|
+
}
|
|
333
|
+
else if (typeof element?.dataset[optionKey] !== "undefined") {
|
|
334
|
+
myOptions[optionKey] = element.dataset[optionKey];
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return myOptions;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Deep-merges multiple configuration objects. Special-cases the `on` field
|
|
342
|
+
* by concatenating event handler arrays; other keys are overwritten.
|
|
343
|
+
*
|
|
344
|
+
* @param {...object} params - Config objects in priority order (leftmost is base).
|
|
345
|
+
* @returns {object} - Merged configuration object.
|
|
346
|
+
*/
|
|
347
|
+
static mergeConfig(...params) {
|
|
348
|
+
if (params.length == 0) {
|
|
349
|
+
return {};
|
|
350
|
+
}
|
|
351
|
+
if (params.length == 1) {
|
|
352
|
+
return this.jsCopyObject(params[0]);
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
const level0 = this.jsCopyObject(params[0]);
|
|
356
|
+
for (let index = 1; index < params.length; index++) {
|
|
357
|
+
const cfg = params[index];
|
|
358
|
+
|
|
359
|
+
for (let optionKey in cfg) {
|
|
360
|
+
if (optionKey == "on") {
|
|
361
|
+
const cfgVar = cfg[optionKey];
|
|
362
|
+
for (let actKey in cfgVar) {
|
|
363
|
+
level0[optionKey][actKey].push(cfgVar[actKey]);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
level0[optionKey] = cfg[optionKey];
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return level0;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Converts strings like "true", "1", "yes", "on" to boolean true; "false", "0",
|
|
377
|
+
* "no", "off" to false. Non-strings are coerced via Boolean().
|
|
378
|
+
*
|
|
379
|
+
* @param {unknown} str - String or any value to convert.
|
|
380
|
+
* @returns {boolean} - The normalized boolean.
|
|
381
|
+
*/
|
|
382
|
+
static string2Boolean(str) {
|
|
383
|
+
if (typeof str === "boolean") return str;
|
|
384
|
+
if (typeof str !== "string") return Boolean(str);
|
|
385
|
+
|
|
386
|
+
switch (str.trim().toLowerCase()) {
|
|
387
|
+
case "true":
|
|
388
|
+
case "1":
|
|
389
|
+
case "yes":
|
|
390
|
+
case "on":
|
|
391
|
+
return true;
|
|
392
|
+
case "false":
|
|
393
|
+
case "0":
|
|
394
|
+
case "no":
|
|
395
|
+
case "off":
|
|
396
|
+
return false;
|
|
397
|
+
default:
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Removes a binder map entry for the given element from the global storage.
|
|
404
|
+
*
|
|
405
|
+
* @param {HTMLElement} element - Element key to remove from the binder map.
|
|
406
|
+
* @returns {boolean} - True if an entry existed and was removed.
|
|
407
|
+
*/
|
|
408
|
+
static removeBinderMap(element) {
|
|
409
|
+
return this.iStorage.bindedMap.delete(element);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Retrieves the binder map entry associated with the given element.
|
|
414
|
+
*
|
|
415
|
+
* @param {HTMLElement} item - Element key whose binder map is requested.
|
|
416
|
+
* @returns {any} - The stored binder map value or undefined if absent.
|
|
417
|
+
*/
|
|
418
|
+
static getBinderMap(item) {
|
|
419
|
+
return this.iStorage.bindedMap.get(item);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Sets or updates the binder map entry for a given element.
|
|
424
|
+
*
|
|
425
|
+
* @param {HTMLElement} item - Element key to associate with the binder map.
|
|
426
|
+
* @param {any} bindMap - Value to store in the binder map.
|
|
427
|
+
*/
|
|
428
|
+
static setBinderMap(item, bindMap) {
|
|
429
|
+
this.iStorage.bindedMap.set(item, bindMap);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Removes a binder map entry for the given element from the global storage.
|
|
434
|
+
*
|
|
435
|
+
* @param {HTMLElement} element - Element key to remove from the binder map.
|
|
436
|
+
* @returns {boolean} - True if an entry existed and was removed.
|
|
437
|
+
*/
|
|
438
|
+
static removeUnbinderMap(element) {
|
|
439
|
+
return this.iStorage.unbindedMap.delete(element);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Retrieves the binder map entry associated with the given element.
|
|
444
|
+
*
|
|
445
|
+
* @param {HTMLElement} item - Element key whose binder map is requested.
|
|
446
|
+
* @returns {any} - The stored binder map value or undefined if absent.
|
|
447
|
+
*/
|
|
448
|
+
static getUnbinderMap(item) {
|
|
449
|
+
return this.iStorage.unbindedMap.get(item);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Sets or updates the binder map entry for a given element.
|
|
454
|
+
*
|
|
455
|
+
* @param {HTMLElement} item - Element key to associate with the binder map.
|
|
456
|
+
* @param {any} bindMap - Value to store in the binder map.
|
|
457
|
+
*/
|
|
458
|
+
static setUnbinderMap(item, bindMap) {
|
|
459
|
+
this.iStorage.unbindedMap.set(item, bindMap);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Returns the global default configuration used by the Selective UI system.
|
|
464
|
+
*
|
|
465
|
+
* @returns {object} - The default config object.
|
|
466
|
+
*/
|
|
467
|
+
static getDefaultConfig() {
|
|
468
|
+
return this.iStorage.defaultConfig;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Returns the global list of bound commands stored in the shared storage.
|
|
473
|
+
*
|
|
474
|
+
* @returns {any[]} - The bound command list.
|
|
475
|
+
*/
|
|
476
|
+
static getBindedCommand() {
|
|
477
|
+
return this.iStorage.bindedCommand;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Safely translates an HTML-like string to sanitized markup:
|
|
482
|
+
* - decodes custom <`/`> placeholders,
|
|
483
|
+
* - strips <script> tags,
|
|
484
|
+
* - removes event and javascript: attributes from all nodes.
|
|
485
|
+
*
|
|
486
|
+
* @param {string} str_tag - The input string to sanitize/translate.
|
|
487
|
+
* @returns {string} - Safe innerHTML string.
|
|
488
|
+
*/
|
|
489
|
+
static tagTranslate(str_tag) {
|
|
490
|
+
if (str_tag == null) return "";
|
|
491
|
+
|
|
492
|
+
str_tag = String(str_tag).replace(/<`/g, "<").replace(/`>/g, ">").replace(/<`/g, "<").replace(/`>/g, ">").trim();
|
|
493
|
+
|
|
494
|
+
const doc = globalThis?.document;
|
|
495
|
+
|
|
496
|
+
if (!doc || typeof doc.createElement !== "function") {
|
|
497
|
+
str_tag = str_tag
|
|
498
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "")
|
|
499
|
+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "")
|
|
500
|
+
.replace(/<iframe\b[^>]*>[\s\S]*?<\/iframe>/gi, "")
|
|
501
|
+
.replace(/<(object|embed|link)\b[^>]*>[\s\S]*?<\/\1>/gi, "");
|
|
502
|
+
str_tag = str_tag.replace(/\son[a-z]+\s*=\s*(['"]).*?\1/gi, "");
|
|
503
|
+
str_tag = str_tag.replace(/\s([a-z-:]+)\s*=\s*(['"])\s*javascript:[^'"]*\2/gi, "");
|
|
504
|
+
return str_tag;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const tmp = doc.createElement("div");
|
|
508
|
+
tmp.innerHTML = str_tag;
|
|
509
|
+
|
|
510
|
+
tmp.querySelectorAll("script, style, iframe, object, embed, link").forEach(n => n.remove());
|
|
511
|
+
|
|
512
|
+
tmp.querySelectorAll("*").forEach(n => {
|
|
513
|
+
[...n.attributes].forEach(a => {
|
|
514
|
+
const name = a.name || "";
|
|
515
|
+
const value = a.value || "";
|
|
516
|
+
if (/^on/i.test(name)) {
|
|
517
|
+
n.removeAttribute(name);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (/^(href|src|xlink:href)$/i.test(name) && /^javascript:/i.test(value)) {
|
|
521
|
+
n.removeAttribute(name);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
return (tmp.innerHTML || "").trim();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Strips all HTML from a string and returns trimmed plain text.
|
|
531
|
+
*
|
|
532
|
+
* @param {string} html - The HTML string to strip.
|
|
533
|
+
* @returns {string} - The extracted plain text.
|
|
534
|
+
*/
|
|
535
|
+
static stripHtml(html) {
|
|
536
|
+
let tmp = document.createElement("DIV");
|
|
537
|
+
tmp.innerHTML = html;
|
|
538
|
+
let text_tmp = tmp.textContent || tmp.innerText || "";
|
|
539
|
+
tmp.remove();
|
|
540
|
+
return text_tmp.trim();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Normalizes a Vietnamese string by removing diacritics and special combining marks,
|
|
545
|
+
* returning a lowercase non-accent version for searching/matching.
|
|
546
|
+
*
|
|
547
|
+
* @param {string} str - The input text.
|
|
548
|
+
* @returns {string} - The diacritic-free lowercase string.
|
|
549
|
+
*/
|
|
550
|
+
static string2normalize(str) {
|
|
551
|
+
if (str == null) return '';
|
|
552
|
+
const s = String(str).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
553
|
+
return s.replace(/đ/g, 'd').replace(/Đ/g, 'd');
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Parse select element to array (including optgroups)
|
|
558
|
+
* @param {HTMLSelectElement} selectElement
|
|
559
|
+
* @returns {Array<HTMLOptGroupElement|HTMLOptionElement>}
|
|
560
|
+
*/
|
|
561
|
+
static parseSelectToArray(selectElement) {
|
|
562
|
+
const result = [];
|
|
563
|
+
const children = Array.from(selectElement.children);
|
|
564
|
+
|
|
565
|
+
children.forEach(child => {
|
|
566
|
+
if (child.tagName === "OPTGROUP") {
|
|
567
|
+
result.push(child);
|
|
568
|
+
|
|
569
|
+
Array.from(child.children).forEach(option => {
|
|
570
|
+
option["__parentGroup"] = child;
|
|
571
|
+
result.push(option);
|
|
572
|
+
});
|
|
573
|
+
} else if (child.tagName === "OPTION") {
|
|
574
|
+
result.push(child);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
return result;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Detects whether the current environment is iOS (including iPad/iPhone/iPod, iOS browsers,
|
|
584
|
+
* iPadOS on Mac with touch, standalone PWAs, and WebKit mobile heuristics). Caches the result
|
|
585
|
+
* for subsequent calls to avoid re-computation.
|
|
586
|
+
*
|
|
587
|
+
* @returns {boolean} - True if the user agent/platform appears to be iOS; otherwise false.
|
|
588
|
+
*/
|
|
589
|
+
|
|
590
|
+
static IsIOS() {
|
|
591
|
+
const ua = navigator.userAgent;
|
|
592
|
+
return /iP(hone|ad|od)/.test(ua) ||
|
|
593
|
+
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Converts an arbitrary CSS size value into pixel units by measuring a temporary element.
|
|
598
|
+
* Returns the computed pixel height as a string with "px" suffix.
|
|
599
|
+
*
|
|
600
|
+
* @param {string} value - Any valid CSS size (e.g., "2rem", "5vh", "12pt").
|
|
601
|
+
* @returns {string} - The equivalent pixel value (e.g., "32px").
|
|
602
|
+
*/
|
|
603
|
+
|
|
604
|
+
static any2px(value) {
|
|
605
|
+
const v = String(value).trim();
|
|
606
|
+
if (v.endsWith('px')) return v;
|
|
607
|
+
if (v.endsWith('vh')) return (window.innerHeight * parseFloat(v)/100) + 'px';
|
|
608
|
+
if (v.endsWith('vw')) return (window.innerWidth * parseFloat(v)/100) + 'px';
|
|
609
|
+
// ... rem/em: lấy computed font-size thân document
|
|
610
|
+
const fs = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
611
|
+
if (v.endsWith('rem')) return (fs * parseFloat(v)) + 'px';
|
|
612
|
+
// fallback: đo DOM (hiếm)
|
|
613
|
+
const el = /** @type {HTMLElement} */ (this.nodeCreator({node:'div', style:{height:v, opacity:0}}));
|
|
614
|
+
document.body.appendChild(el);
|
|
615
|
+
const px = el.offsetHeight + 'px';
|
|
616
|
+
el.remove();
|
|
617
|
+
return px;
|
|
618
|
+
}
|
|
619
619
|
}
|