objs-core 1.1.1 → 2.0.1
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/EXAMPLES.md +1637 -0
- package/README.md +346 -75
- package/SKILL.md +500 -0
- package/objs.built.js +2657 -0
- package/objs.built.min.js +67 -0
- package/objs.d.ts +455 -0
- package/objs.js +3805 -0
- package/package.json +70 -37
- package/objs.1.1.js +0 -1205
- package/objs.1.1.js.zip +0 -0
- package/objs.1.1.min.js +0 -2
- package/objs.1.1.min.js.zip +0 -0
- package/objs.npm.1.1.js +0 -16
package/objs.js
ADDED
|
@@ -0,0 +1,3805 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Objs-core library
|
|
3
|
+
* @version 2.0
|
|
4
|
+
* @author Roman Torshin
|
|
5
|
+
* @license Apache-2.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** @type {boolean} When true, enables debug flag and debug logging (o.debug, result.debug, console.log in returner/o.first). */
|
|
9
|
+
const __DEV__ = true;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Main Objs function for DOM manipulation and state control
|
|
13
|
+
* @function Objs
|
|
14
|
+
* @param {any} query - Selector, DOM element to use, an array of elements, inited ID or nothing for creating an element
|
|
15
|
+
* @returns {Object} Objs instance with DOM manipulation methods
|
|
16
|
+
*/
|
|
17
|
+
const o = (query) => {
|
|
18
|
+
let result = {
|
|
19
|
+
els: [],
|
|
20
|
+
ie: {},
|
|
21
|
+
delegated: {},
|
|
22
|
+
parented: {},
|
|
23
|
+
store: {},
|
|
24
|
+
refs: {},
|
|
25
|
+
states: [],
|
|
26
|
+
isDebug: false,
|
|
27
|
+
currentState: "",
|
|
28
|
+
savedStates: {},
|
|
29
|
+
isRoot: false,
|
|
30
|
+
_parent: null,
|
|
31
|
+
},
|
|
32
|
+
ONE = 1,
|
|
33
|
+
TWO = 2,
|
|
34
|
+
THREE = 3,
|
|
35
|
+
booleanType = "boolean",
|
|
36
|
+
objectType = "object",
|
|
37
|
+
functionType = "function",
|
|
38
|
+
stringType = "string",
|
|
39
|
+
numberType = "number",
|
|
40
|
+
notEmptyStringType = "notEmptyString",
|
|
41
|
+
undefinedType = "undefined",
|
|
42
|
+
_reactProp = "dangerouslySetInnerHTML",
|
|
43
|
+
u,
|
|
44
|
+
D = o.D,
|
|
45
|
+
start = -1,
|
|
46
|
+
finish = 0,
|
|
47
|
+
select = 0,
|
|
48
|
+
ssr = typeof process !== "undefined" || o.D === o.DocumentMVP,
|
|
49
|
+
i = 0,
|
|
50
|
+
j = 0;
|
|
51
|
+
const self = result; // capture so connect() always passes this instance to loader
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Shortcut for typeof operator
|
|
55
|
+
* @param {any} obj - Object to check type of
|
|
56
|
+
* @returns {string} Type of the object
|
|
57
|
+
*/
|
|
58
|
+
const type = (obj) => typeof obj;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Iterate through object properties
|
|
62
|
+
* @param {Object} obj - Object to iterate through
|
|
63
|
+
* @param {Function} func - Function to execute for each property
|
|
64
|
+
*/
|
|
65
|
+
const cycleObj = (obj, func) => {
|
|
66
|
+
for (const item in obj) if (Object.hasOwn(obj, item)) func(item, obj);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Error handling function
|
|
71
|
+
* @type {Function}
|
|
72
|
+
*/
|
|
73
|
+
const error = o.onError;
|
|
74
|
+
const typeVerify = (pairs) => o.verify(pairs);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Creates a function that returns values or the result object
|
|
78
|
+
* @param {Function} f - Function to wrap
|
|
79
|
+
* @returns {Function} Wrapped function that handles errors and returns
|
|
80
|
+
*/
|
|
81
|
+
const returner = (f, name = "") => {
|
|
82
|
+
return (...a) => {
|
|
83
|
+
if (__DEV__ && (o.debug || result.isDebug)) {
|
|
84
|
+
console.log(
|
|
85
|
+
name ? `${name}()` : f,
|
|
86
|
+
a.length ? "with " + a.join(", ") : "without parameters",
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const res = f(a[0], a[ONE], a[TWO], a[THREE]);
|
|
91
|
+
return res !== u ? res : result;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
error(err, name);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Iterate through selected elements
|
|
100
|
+
* @param {Function} f - Function to execute for each element
|
|
101
|
+
*/
|
|
102
|
+
const iterator = (f) => {
|
|
103
|
+
for (i = finish; i <= start; i++) f();
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Convert query to DOM element
|
|
108
|
+
* @param {any} el - Element to convert
|
|
109
|
+
* @returns {Element} DOM element
|
|
110
|
+
*/
|
|
111
|
+
const toEl = (el) => {
|
|
112
|
+
if (el?.els) return el.el; // ObjsInstance → first DOM element
|
|
113
|
+
if (type(el) !== objectType) el = o.first(el).el;
|
|
114
|
+
return el;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Set result object properties
|
|
119
|
+
* @param {boolean} clearStates - Whether to clear states
|
|
120
|
+
* @param {Array} els - Elements to set properties for
|
|
121
|
+
*/
|
|
122
|
+
const setResultVals = (clearStates = true, els = result.els) => {
|
|
123
|
+
const ln = els.length;
|
|
124
|
+
result.length = ln;
|
|
125
|
+
start = ln - ONE;
|
|
126
|
+
finish = 0;
|
|
127
|
+
result.el = ln ? els[0] : u;
|
|
128
|
+
result.last = ln ? els[start] : u;
|
|
129
|
+
if (clearStates) {
|
|
130
|
+
cycleObj(result.states, (i, state) => {
|
|
131
|
+
delete result[state[i]];
|
|
132
|
+
});
|
|
133
|
+
result.states = [];
|
|
134
|
+
result.ie = {};
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
// sets new objects to operate
|
|
138
|
+
result.reset = o;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Transform DOM elements based on state and props
|
|
142
|
+
* @param {Element} el - DOM element to transform
|
|
143
|
+
* @param {Object} state - State data
|
|
144
|
+
* @param {Object} props - Additional props and dynamic content
|
|
145
|
+
*/
|
|
146
|
+
const transform = (el, state, props) => {
|
|
147
|
+
// filter state vs current state
|
|
148
|
+
cycleObj(state, (s) => {
|
|
149
|
+
let value = state[s];
|
|
150
|
+
|
|
151
|
+
// eval functions in attributes
|
|
152
|
+
if (type(value) === functionType) {
|
|
153
|
+
value = value(props);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// prepare objs to append
|
|
157
|
+
if (s === "append" && type(value) === objectType) {
|
|
158
|
+
if (value.els) {
|
|
159
|
+
value = [value];
|
|
160
|
+
}
|
|
161
|
+
if (value[0]?.els) {
|
|
162
|
+
valueBuff = [];
|
|
163
|
+
cycleObj(value, (i) => {
|
|
164
|
+
valueBuff.push(...value[i].els);
|
|
165
|
+
});
|
|
166
|
+
value = valueBuff;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (
|
|
171
|
+
value !== u &&
|
|
172
|
+
el.getAttribute(s) !== value &&
|
|
173
|
+
![
|
|
174
|
+
"tag",
|
|
175
|
+
"tagName",
|
|
176
|
+
"name",
|
|
177
|
+
"sample",
|
|
178
|
+
"state",
|
|
179
|
+
"events",
|
|
180
|
+
"ssr",
|
|
181
|
+
"nodeName",
|
|
182
|
+
"revertChildren",
|
|
183
|
+
"root",
|
|
184
|
+
"ref",
|
|
185
|
+
].includes(s)
|
|
186
|
+
) {
|
|
187
|
+
// insert html
|
|
188
|
+
["html", "innerHTML"].includes(s)
|
|
189
|
+
? (el.innerHTML = value)
|
|
190
|
+
: // className alias
|
|
191
|
+
s === "className"
|
|
192
|
+
? el.setAttribute("class", value)
|
|
193
|
+
: // attach dataset
|
|
194
|
+
s === "dataset" && type(value) === objectType
|
|
195
|
+
? cycleObj(value, (data) => {
|
|
196
|
+
el.dataset[data] = value[data];
|
|
197
|
+
})
|
|
198
|
+
: // classes
|
|
199
|
+
s === "toggleClass"
|
|
200
|
+
? el.classList.toggle(value)
|
|
201
|
+
: s === "addClass"
|
|
202
|
+
? type(value) === objectType
|
|
203
|
+
? el.classList.add(...value)
|
|
204
|
+
: el.classList.add(value)
|
|
205
|
+
: s === "removeClass"
|
|
206
|
+
? el.classList.remove(value)
|
|
207
|
+
: // style attribute
|
|
208
|
+
s === "style" && type(value) === objectType
|
|
209
|
+
? cycleObj(value, (data) => {
|
|
210
|
+
el.style[data] = value[data];
|
|
211
|
+
})
|
|
212
|
+
: // append DOM objects
|
|
213
|
+
(s === "append" || s === "children" || s === "childNodes") &&
|
|
214
|
+
type(value) === objectType
|
|
215
|
+
? cycleObj(value.length ? value : [value], (j) => {
|
|
216
|
+
if (s === "append" || !el.childNodes[j]) {
|
|
217
|
+
el.appendChild(value[j]);
|
|
218
|
+
} else if (el.childNodes[j] !== value[j]) {
|
|
219
|
+
el.childNodes[j].replaceWith(value[j]);
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
: // set attributes
|
|
223
|
+
el.setAttribute(s, value);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
el.dataset["oState"] = state.state;
|
|
228
|
+
// autotag: set data-{o.autotag}="component-name" from states.name
|
|
229
|
+
if (o.autotag && state.name) {
|
|
230
|
+
el.dataset[o.autotag] = o.camelToKebab(state.name);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
if (__DEV__) {
|
|
235
|
+
/**
|
|
236
|
+
* Enable debug mode
|
|
237
|
+
*/
|
|
238
|
+
result.debug = returner(() => {
|
|
239
|
+
result.isDebug = true;
|
|
240
|
+
}, "debug ON");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Save object by key in o.getSaved{}
|
|
245
|
+
*/
|
|
246
|
+
result.saveAs = returner((key) => {
|
|
247
|
+
typeVerify([[key, [notEmptyStringType]]]);
|
|
248
|
+
if (!o.getSaved[key]) {
|
|
249
|
+
o.getSaved[key] = result;
|
|
250
|
+
} else if (o.debug || result.isDebug) {
|
|
251
|
+
console.warn("the key exists (not saved):" + key);
|
|
252
|
+
}
|
|
253
|
+
}, "saveAs");
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Unmount the component
|
|
257
|
+
* @returns {boolean} True if the component was unmounted
|
|
258
|
+
*/
|
|
259
|
+
result.unmount = () => {
|
|
260
|
+
if (o.debug || result.isDebug) {
|
|
261
|
+
console.log("unmount() for initID:" + result.initID);
|
|
262
|
+
}
|
|
263
|
+
if (type(result.remove) === functionType) {
|
|
264
|
+
result.remove();
|
|
265
|
+
} else if (result.els?.length) {
|
|
266
|
+
result.els.forEach((el) => {
|
|
267
|
+
if (el?.parentNode) el.parentNode.removeChild(el);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
o.inits[result.initID] = undefined;
|
|
271
|
+
result = {};
|
|
272
|
+
return true;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Initialize states and create state functions
|
|
277
|
+
* @param {Object|Function|Component} states - States object or Component function
|
|
278
|
+
*/
|
|
279
|
+
result.init = returner((states) => {
|
|
280
|
+
typeVerify([[states, [objectType, functionType]]]);
|
|
281
|
+
const initN = result.initID || o.inits.length || 0;
|
|
282
|
+
result.initID = initN;
|
|
283
|
+
setResultVals();
|
|
284
|
+
o.inits[result.initID] = result;
|
|
285
|
+
|
|
286
|
+
// React Component usage for rendering
|
|
287
|
+
if (type(states) === "function") {
|
|
288
|
+
result.render = returner((props) => {
|
|
289
|
+
const root = D.createElement("div");
|
|
290
|
+
setTimeout(() => {
|
|
291
|
+
o.reactRender(states, root, props);
|
|
292
|
+
});
|
|
293
|
+
result.add(root);
|
|
294
|
+
});
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// fast initialisation
|
|
299
|
+
if (type(states) !== objectType || states.render === u) {
|
|
300
|
+
states = {
|
|
301
|
+
render: states,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// cycle threw states
|
|
306
|
+
cycleObj(states, (state) => {
|
|
307
|
+
// prevent render override
|
|
308
|
+
if (result?.render && state === "render") {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
// save state name to clear object by reset();
|
|
312
|
+
result.states.push(state);
|
|
313
|
+
// add method named as state
|
|
314
|
+
result[state] = returner((props = [{}]) => {
|
|
315
|
+
result.currentState = state;
|
|
316
|
+
const data = states[state] || { tag: "div" };
|
|
317
|
+
const slice = Array.isArray(result.els)
|
|
318
|
+
? result.els.slice(finish, start + ONE)
|
|
319
|
+
: [];
|
|
320
|
+
const els = slice.length ? slice : (result.els || []);
|
|
321
|
+
|
|
322
|
+
if (type(data) === objectType) {
|
|
323
|
+
data.state = state;
|
|
324
|
+
data["data-o-init"] = initN;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// creation elements for prop in props
|
|
328
|
+
const newEl = (n, prop = {}) => {
|
|
329
|
+
if (type(data) === objectType) {
|
|
330
|
+
return D.createElement(data.tag || data.tagName || "div");
|
|
331
|
+
} else {
|
|
332
|
+
const newElem = D.createElement("div");
|
|
333
|
+
newElem.innerHTML = type(data) === functionType ? data(prop) : data;
|
|
334
|
+
if (newElem.children.length > ONE || !newElem.firstElementChild) {
|
|
335
|
+
newElem.dataset.oInit = n;
|
|
336
|
+
return newElem;
|
|
337
|
+
} else {
|
|
338
|
+
newElem.firstElementChild.dataset.oInit = n;
|
|
339
|
+
return newElem.firstElementChild;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// properties creation
|
|
345
|
+
const rawData = props; // raw argument before array-wrapping
|
|
346
|
+
!props.length ? (props = [props]) : props;
|
|
347
|
+
|
|
348
|
+
// creating elements if no one was selected
|
|
349
|
+
const creation = !els[0] && state === "render";
|
|
350
|
+
props = props.map((prop, i) => {
|
|
351
|
+
const newProp = Object.assign({}, type(prop) === objectType ? prop : {}, {
|
|
352
|
+
self: result,
|
|
353
|
+
o,
|
|
354
|
+
i: prop.i === u ? i : prop.i,
|
|
355
|
+
parent: result._parent,
|
|
356
|
+
data: Array.isArray(rawData) ? rawData[i] : rawData,
|
|
357
|
+
});
|
|
358
|
+
if (creation && (!data.ssr || ssr)) {
|
|
359
|
+
els.push(newEl(initN, newProp));
|
|
360
|
+
}
|
|
361
|
+
return newProp;
|
|
362
|
+
});
|
|
363
|
+
if (creation) {
|
|
364
|
+
result.els = els;
|
|
365
|
+
setResultVals(false);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// initing events
|
|
369
|
+
const initSSR = () => {
|
|
370
|
+
cycleObj(data.events, (event) => {
|
|
371
|
+
result.on(event, data.events[event]);
|
|
372
|
+
});
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// changing element if there is data object
|
|
376
|
+
if (els) {
|
|
377
|
+
j = els.length === props.length;
|
|
378
|
+
els.map((el, i) => {
|
|
379
|
+
props[j ? i : 0].i = i + finish;
|
|
380
|
+
const buff = type(data) === functionType ? data(props[j ? i : 0]) : data;
|
|
381
|
+
if (type(buff) === objectType) {
|
|
382
|
+
buff["state"] = state;
|
|
383
|
+
if (creation) {
|
|
384
|
+
buff["data-o-init"] = initN;
|
|
385
|
+
buff["data-o-init-i"] = i;
|
|
386
|
+
}
|
|
387
|
+
transform(el, buff, props[j ? i : 0]);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
if (creation) {
|
|
391
|
+
result.refs = {};
|
|
392
|
+
result.els.forEach((el) => {
|
|
393
|
+
if (!el.querySelectorAll) return;
|
|
394
|
+
el.querySelectorAll("[ref]").forEach((refEl) => {
|
|
395
|
+
result.refs[refEl.getAttribute("ref")] = o(refEl);
|
|
396
|
+
refEl.removeAttribute("ref");
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// init events if there is data object and events are defined
|
|
403
|
+
if (creation && type(data) === objectType && data.events) {
|
|
404
|
+
// check for SSR and SSR flag in data object to allow testing in browser
|
|
405
|
+
if (!ssr && !data.ssr) {
|
|
406
|
+
initSSR();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
const renderState = states.render || states;
|
|
412
|
+
if (
|
|
413
|
+
!ssr &&
|
|
414
|
+
type(renderState) === objectType &&
|
|
415
|
+
renderState.events &&
|
|
416
|
+
renderState.ssr
|
|
417
|
+
) {
|
|
418
|
+
result.initSSRAfterGettingSSR = () => {
|
|
419
|
+
result.refs = {};
|
|
420
|
+
result.els.forEach((el) => {
|
|
421
|
+
if (!el.querySelectorAll) return;
|
|
422
|
+
el.querySelectorAll("[ref]").forEach((refEl) => {
|
|
423
|
+
result.refs[refEl.getAttribute("ref")] = o(refEl);
|
|
424
|
+
refEl.removeAttribute("ref");
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
cycleObj(renderState.events, (event) => {
|
|
428
|
+
result.on(event, renderState.events[event]);
|
|
429
|
+
});
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}, "init");
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Connect loader to result
|
|
436
|
+
* @param {Object} loader - Loader object
|
|
437
|
+
* @param {string} state - State name
|
|
438
|
+
* @param {Function} fail - Fail state name
|
|
439
|
+
*/
|
|
440
|
+
result.connect = returner((loader, state = "render", fail) => {
|
|
441
|
+
typeVerify([
|
|
442
|
+
[loader, [objectType]],
|
|
443
|
+
[state, [notEmptyStringType]],
|
|
444
|
+
[fail, [stringType, undefinedType]],
|
|
445
|
+
]);
|
|
446
|
+
loader.connect(self, state, fail);
|
|
447
|
+
}, "connect");
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Get SSR elements
|
|
451
|
+
* @param {number} initId - Initialization ID
|
|
452
|
+
*/
|
|
453
|
+
result.getSSR = returner((initId) => {
|
|
454
|
+
typeVerify([[initId, [numberType, undefinedType]]]);
|
|
455
|
+
const effectiveId = initId !== undefined ? initId : result.initID;
|
|
456
|
+
if (
|
|
457
|
+
ssr ||
|
|
458
|
+
(type(initId) === undefinedType && type(result.initID) === undefinedType)
|
|
459
|
+
) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const ssrEls = o.D.querySelectorAll(`[data-o-init="${effectiveId}"]`);
|
|
463
|
+
|
|
464
|
+
if (ssrEls.length && !result.els.length) {
|
|
465
|
+
result.els = Array.from(ssrEls);
|
|
466
|
+
result.initID = initId;
|
|
467
|
+
o.inits[initId] = result;
|
|
468
|
+
setResultVals(false);
|
|
469
|
+
|
|
470
|
+
if (type(result.initSSRAfterGettingSSR) === functionType) {
|
|
471
|
+
result.initSSRAfterGettingSSR();
|
|
472
|
+
delete result.initSSRAfterGettingSSR;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}, "getSSR");
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Initialize state with props
|
|
479
|
+
* @param {Object} state - State object
|
|
480
|
+
* @param {Object} props - Props to initialize with
|
|
481
|
+
*/
|
|
482
|
+
result.initState = returner((state, props) => {
|
|
483
|
+
typeVerify([
|
|
484
|
+
[state, [objectType]],
|
|
485
|
+
[props, [objectType, undefinedType]],
|
|
486
|
+
]);
|
|
487
|
+
result.init(state).render(props);
|
|
488
|
+
}, "initState");
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Parse state of element
|
|
492
|
+
* @param {Element} el - Element to parse
|
|
493
|
+
* @param {String} stateId - Title for state saving
|
|
494
|
+
* @param {boolean} root - Parse child elements if true
|
|
495
|
+
* @returns {Object} State object
|
|
496
|
+
*/
|
|
497
|
+
const parseState = (el, stateId, root) => {
|
|
498
|
+
const attrs = el.attributes;
|
|
499
|
+
const stateData = {
|
|
500
|
+
tagName: el.tagName.toLowerCase(),
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
for (const attr of attrs) {
|
|
504
|
+
stateData[attr.nodeName] = attr.value;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (root) {
|
|
508
|
+
stateData.innerHTML = el.innerHTML;
|
|
509
|
+
stateData.revertChildren = [];
|
|
510
|
+
const initedChildren = el.querySelectorAll("[data-o-init]");
|
|
511
|
+
for (const child of initedChildren) {
|
|
512
|
+
const initId = child.getAttribute("data-o-init");
|
|
513
|
+
stateData.revertChildren.push(initId);
|
|
514
|
+
// save state of children for revert
|
|
515
|
+
o.inits[initId]?.saveState(stateId, false);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return stateData;
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Save state of elements
|
|
524
|
+
* @param {string|undefined} stateId - State string ID or will be 'fastSavedState'
|
|
525
|
+
* @param {boolean} root - Root element flag
|
|
526
|
+
* @returns {Object} State object
|
|
527
|
+
*/
|
|
528
|
+
result.saveState = returner((stateId, root = true) => {
|
|
529
|
+
typeVerify([
|
|
530
|
+
[stateId, [notEmptyStringType, undefinedType]],
|
|
531
|
+
[root, [booleanType]],
|
|
532
|
+
]);
|
|
533
|
+
|
|
534
|
+
if (!result.el) {
|
|
535
|
+
throw Error("saveState(): There are no elements to save");
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const targetState = stateId ? stateId : "fastSavedState";
|
|
539
|
+
const stateRevert = { els: [], parentNode: result.el.parentNode, root: root };
|
|
540
|
+
|
|
541
|
+
iterator(() => {
|
|
542
|
+
stateRevert.els.push(parseState(result.els[i], targetState, root));
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
stateRevert.ie = Object.assign({}, result.ie);
|
|
546
|
+
stateRevert.delegated = Object.assign({}, result.delegated);
|
|
547
|
+
stateRevert.store = Object.assign({}, result.store);
|
|
548
|
+
|
|
549
|
+
// save the save flag
|
|
550
|
+
result.isRoot = result.isRoot || root;
|
|
551
|
+
result.savedStates[targetState] = stateRevert;
|
|
552
|
+
}, "saveState");
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Revert state of elements
|
|
556
|
+
* @param {string|undefined} state - State name or will be 'fastSavedState'
|
|
557
|
+
*/
|
|
558
|
+
result.revertState = returner((state) => {
|
|
559
|
+
typeVerify([[state, [notEmptyStringType, undefinedType]]]);
|
|
560
|
+
|
|
561
|
+
const targetState = state ? state : "fastSavedState";
|
|
562
|
+
|
|
563
|
+
if (!result.savedStates[targetState]) {
|
|
564
|
+
throw Error(
|
|
565
|
+
`revertState(): The state "${targetState}" should have been saved by saveState()`,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const stateRevert = result.savedStates[targetState];
|
|
570
|
+
|
|
571
|
+
// turn off all event listeners connected to the elements
|
|
572
|
+
result.offAll();
|
|
573
|
+
result.offDelegate();
|
|
574
|
+
|
|
575
|
+
// revert elements
|
|
576
|
+
result.store = Object.assign({}, stateRevert.store);
|
|
577
|
+
stateRevert.els.forEach((elData, index) => {
|
|
578
|
+
// create element if not exist
|
|
579
|
+
if (!result.els[index]) {
|
|
580
|
+
const newEl = o.D.createElement(elData.tagName);
|
|
581
|
+
// if element was in DOM
|
|
582
|
+
if (stateRevert.parentNode) {
|
|
583
|
+
if (index) {
|
|
584
|
+
result.els[index - 1].after(newEl);
|
|
585
|
+
} else {
|
|
586
|
+
stateRevert.parentNode.append(newEl);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
result.add(newEl);
|
|
590
|
+
}
|
|
591
|
+
transform(result.els[index], elData);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// revert event listeners
|
|
595
|
+
result.delegated = Object.assign({}, stateRevert.delegated);
|
|
596
|
+
result.ie = Object.assign({}, stateRevert.ie);
|
|
597
|
+
result.onAll();
|
|
598
|
+
cycleObj(stateRevert.delegated, (ev) => {
|
|
599
|
+
stateRevert.delegated[ev].forEach((f) => {
|
|
600
|
+
iterator(() => {
|
|
601
|
+
result.els[i].addEventListener(ev, f);
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
result.currentState = targetState;
|
|
607
|
+
|
|
608
|
+
// revert children from HTML
|
|
609
|
+
if (stateRevert.root) {
|
|
610
|
+
stateRevert.els.forEach(({ rootElement }) => {
|
|
611
|
+
rootElement.revertChildren.forEach((initId) => {
|
|
612
|
+
o.inits[initId]?.revertState(targetState);
|
|
613
|
+
o('[data-o-init="' + initId + '"]').els.forEach((el, index) => {
|
|
614
|
+
el.replaceWith(o.inits[initId]?.els[index]);
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}, "revertState");
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Lose state of elements
|
|
623
|
+
* @param {string} stateId - State ID
|
|
624
|
+
*/
|
|
625
|
+
result.loseState = returner((stateId) => {
|
|
626
|
+
typeVerify([[stateId, [notEmptyStringType]]]);
|
|
627
|
+
|
|
628
|
+
if (result.savedStates[stateId]) {
|
|
629
|
+
delete result.savedStates[stateId];
|
|
630
|
+
iterator(() => {
|
|
631
|
+
const initedChildren = result.els[i].querySelectorAll("[data-o-init]");
|
|
632
|
+
for (const child of initedChildren) {
|
|
633
|
+
const initId = child.getAttribute("data-o-init");
|
|
634
|
+
o.inits[initId]?.loseState(stateId);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}, "sample");
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Get state object from existing DOM element
|
|
642
|
+
* @param {string} state - State title (optional)
|
|
643
|
+
* @returns {Object} State object
|
|
644
|
+
*/
|
|
645
|
+
result.sample = returner((state = "render") => {
|
|
646
|
+
typeVerify([[state, [notEmptyStringType]]]);
|
|
647
|
+
return { [state]: parseState(result.els[finish]) };
|
|
648
|
+
}, "sample");
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Select element to control
|
|
652
|
+
* @param {number} i - Index of element to select
|
|
653
|
+
*/
|
|
654
|
+
result.select = returner((i) => {
|
|
655
|
+
typeVerify([[i, [numberType, undefinedType]]]);
|
|
656
|
+
if (i === u) {
|
|
657
|
+
i = result.length - ONE;
|
|
658
|
+
}
|
|
659
|
+
start = i;
|
|
660
|
+
finish = i;
|
|
661
|
+
result.el = result.els[i];
|
|
662
|
+
select = ONE;
|
|
663
|
+
}, "select");
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Select all elements to control
|
|
667
|
+
*/
|
|
668
|
+
result.all = returner(() => {
|
|
669
|
+
start = result.length - ONE;
|
|
670
|
+
finish = 0;
|
|
671
|
+
result.el = result.els[0];
|
|
672
|
+
select = 0;
|
|
673
|
+
}, "all");
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Remove selected element or all elements from DOM
|
|
677
|
+
* @param {number} j - Index of element to remove
|
|
678
|
+
*/
|
|
679
|
+
result.remove = returner((j) => {
|
|
680
|
+
typeVerify([[j, [numberType, undefinedType]]]);
|
|
681
|
+
if (j === u && select) {
|
|
682
|
+
j = finish;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (j !== u) {
|
|
686
|
+
const el = result.els[j];
|
|
687
|
+
if (el?.parentNode) {
|
|
688
|
+
el.parentNode.removeChild(el);
|
|
689
|
+
} else if (el === undefined && j >= result.els.length) {
|
|
690
|
+
if (o.onError) o.onError("remove(" + j + "): index out of bounds", "remove");
|
|
691
|
+
}
|
|
692
|
+
} else {
|
|
693
|
+
iterator(() => {
|
|
694
|
+
const el = result.els[i];
|
|
695
|
+
if (el?.parentNode) el.parentNode.removeChild(el);
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
setResultVals(false);
|
|
699
|
+
}, "remove");
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Skip element from control list
|
|
703
|
+
* @param {number} j - Index of element to skip
|
|
704
|
+
*/
|
|
705
|
+
result.skip = returner((j) => {
|
|
706
|
+
typeVerify([[j, [numberType, undefinedType]]]);
|
|
707
|
+
if (j === u) {
|
|
708
|
+
j = finish;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
result.els.splice(i, ONE);
|
|
712
|
+
setResultVals();
|
|
713
|
+
}, "skip");
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Add element to control list
|
|
717
|
+
* @param {any} el - Element to add
|
|
718
|
+
*/
|
|
719
|
+
result.add = returner((el) => {
|
|
720
|
+
typeVerify([[el, [stringType, objectType, numberType]]]);
|
|
721
|
+
if (result.initID !== u) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (type(el) === "string" && el !== "") {
|
|
726
|
+
result.els.push(...Array.from(D.querySelectorAll(el)));
|
|
727
|
+
} else if (type(el) === objectType) {
|
|
728
|
+
if (el.tagName) {
|
|
729
|
+
result.els.push(el);
|
|
730
|
+
} else if (el.els) {
|
|
731
|
+
result.els.push(...el.els);
|
|
732
|
+
} else if (el.length && el[0].tagName) {
|
|
733
|
+
result.els.push(...el);
|
|
734
|
+
}
|
|
735
|
+
} else if (type(el) === "number" && o.inits[el]) {
|
|
736
|
+
result = o.inits[el];
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
setResultVals(false);
|
|
740
|
+
|
|
741
|
+
if (result.initID !== u) {
|
|
742
|
+
result.dataset({ oInit: result.initID });
|
|
743
|
+
}
|
|
744
|
+
}, "add");
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Append elements inside another element
|
|
748
|
+
* @param {Element|String} el - Parent element
|
|
749
|
+
*/
|
|
750
|
+
result.appendInside = returner((el) => {
|
|
751
|
+
typeVerify([[el, [objectType, notEmptyStringType]]]);
|
|
752
|
+
if (el?.els) result._parent = el; // store ObjsInstance parent reference
|
|
753
|
+
iterator(() => {
|
|
754
|
+
toEl(el).appendChild(result.els[i]);
|
|
755
|
+
});
|
|
756
|
+
}, "appendInside");
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Append elements before another element
|
|
760
|
+
* @param {Element} el - Reference element
|
|
761
|
+
*/
|
|
762
|
+
result.appendBefore = returner((el) => {
|
|
763
|
+
typeVerify([[el, [objectType, notEmptyStringType]]]);
|
|
764
|
+
iterator(() => {
|
|
765
|
+
toEl(el).parentNode.insertBefore(result.els[i], toEl(el));
|
|
766
|
+
});
|
|
767
|
+
}, "appendBefore");
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Append elements after another element
|
|
771
|
+
* @param {Element} el - Reference element
|
|
772
|
+
*/
|
|
773
|
+
result.appendAfter = returner((el) => {
|
|
774
|
+
typeVerify([[el, [objectType, notEmptyStringType]]]);
|
|
775
|
+
iterator(() => {
|
|
776
|
+
toEl(el).after(...result.els);
|
|
777
|
+
});
|
|
778
|
+
}, "appendAfter");
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Find child elements
|
|
782
|
+
* @param {string} innerQuery - Query selector
|
|
783
|
+
* @returns {Object} Objs instance with found elements
|
|
784
|
+
*/
|
|
785
|
+
result.find = returner((innerQuery = "") => {
|
|
786
|
+
typeVerify([[innerQuery, stringType]]);
|
|
787
|
+
const newEls = [];
|
|
788
|
+
|
|
789
|
+
iterator(() => {
|
|
790
|
+
newEls.push(...Array.from(result.els[i].querySelectorAll(":scope " + innerQuery)));
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
return o(newEls);
|
|
794
|
+
}, "find");
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Find first child element by query
|
|
798
|
+
* @param {string} innerQuery - Query selector
|
|
799
|
+
* @returns {Object} Objs instance with found element
|
|
800
|
+
*/
|
|
801
|
+
result.first = returner((innerQuery = "") => {
|
|
802
|
+
typeVerify([[innerQuery, stringType]]);
|
|
803
|
+
let buff = u;
|
|
804
|
+
const newEls = [];
|
|
805
|
+
|
|
806
|
+
iterator(() => {
|
|
807
|
+
buff = result.els[i].querySelector(innerQuery);
|
|
808
|
+
if (buff) {
|
|
809
|
+
newEls.push(buff);
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
return o(newEls);
|
|
814
|
+
}, "first");
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Set, delete or get attribute
|
|
818
|
+
* @param {string} attr - Attribute name
|
|
819
|
+
* @param {any} val - Attribute value
|
|
820
|
+
*/
|
|
821
|
+
result.attr = returner((attr, val) => {
|
|
822
|
+
if (val !== null) {
|
|
823
|
+
typeVerify([
|
|
824
|
+
[attr, stringType],
|
|
825
|
+
[val, [stringType, undefinedType]],
|
|
826
|
+
]);
|
|
827
|
+
}
|
|
828
|
+
if (val === u) {
|
|
829
|
+
const attrs = [];
|
|
830
|
+
iterator(() => {
|
|
831
|
+
attrs[i] = result.els[i].getAttribute(attr);
|
|
832
|
+
});
|
|
833
|
+
return select ? attrs[0] : attrs;
|
|
834
|
+
} else if (val !== null) {
|
|
835
|
+
iterator(() => {
|
|
836
|
+
result.els[i].setAttribute(attr, val);
|
|
837
|
+
});
|
|
838
|
+
} else {
|
|
839
|
+
iterator(() => {
|
|
840
|
+
result.els[i].removeAttribute(attr);
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
}, "attr");
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Get all attributes
|
|
847
|
+
* @returns {Array|Object} Array of attribute objects or single attribute object
|
|
848
|
+
*/
|
|
849
|
+
result.attrs = returner(() => {
|
|
850
|
+
const res = [];
|
|
851
|
+
iterator(() => {
|
|
852
|
+
const obj = {};
|
|
853
|
+
[...result.els[i].attributes].forEach((attr) => {
|
|
854
|
+
obj[attr.nodeName] = attr.nodeValue;
|
|
855
|
+
});
|
|
856
|
+
res.push(obj);
|
|
857
|
+
});
|
|
858
|
+
return select ? res[0] : res;
|
|
859
|
+
}, "attrs");
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Control dataset
|
|
863
|
+
* @param {Object} values - Dataset values
|
|
864
|
+
*/
|
|
865
|
+
result.dataset = returner((values) => {
|
|
866
|
+
typeVerify([[values, [objectType, undefinedType]]]);
|
|
867
|
+
if (typeof values === objectType) {
|
|
868
|
+
iterator(() => {
|
|
869
|
+
cycleObj(values, (data) => {
|
|
870
|
+
result.els[i].dataset[data] = values[data];
|
|
871
|
+
});
|
|
872
|
+
});
|
|
873
|
+
} else {
|
|
874
|
+
const res = [];
|
|
875
|
+
iterator(() => {
|
|
876
|
+
res.push({ ...result.els[i].dataset });
|
|
877
|
+
});
|
|
878
|
+
return select ? res[0] : res;
|
|
879
|
+
}
|
|
880
|
+
}, "dataset");
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Set style attribute
|
|
884
|
+
* @param {string} val - Style value
|
|
885
|
+
*/
|
|
886
|
+
result.style = returner((val) => {
|
|
887
|
+
if (val !== null) typeVerify([[val, [stringType, undefinedType]]]);
|
|
888
|
+
result.attr("style", val);
|
|
889
|
+
}, "style");
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Set CSS styles from object. Pass null to remove the style attribute entirely.
|
|
893
|
+
* @param {Object|null} styles - CSS styles object, or null to remove
|
|
894
|
+
*/
|
|
895
|
+
result.css = returner((styles = {}) => {
|
|
896
|
+
if (styles === null) {
|
|
897
|
+
result.style(null);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
typeVerify([[styles, objectType]]);
|
|
901
|
+
let val = "";
|
|
902
|
+
cycleObj(styles, (style) => {
|
|
903
|
+
val += style + ":" + styles[style].replace('"', "'") + ";";
|
|
904
|
+
});
|
|
905
|
+
result.style(val || null);
|
|
906
|
+
}, "css");
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Set class attribute
|
|
910
|
+
* @param {string} cl - Class name
|
|
911
|
+
*/
|
|
912
|
+
result.setClass = returner((cl) => {
|
|
913
|
+
typeVerify([[cl, stringType]]);
|
|
914
|
+
iterator(() => {
|
|
915
|
+
result.els[i].setAttribute("class", cl);
|
|
916
|
+
});
|
|
917
|
+
}, "setClass");
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Add one or more classes to elements
|
|
921
|
+
* @param {...string} cls - Class names
|
|
922
|
+
*/
|
|
923
|
+
result.addClass = returner((...cls) => {
|
|
924
|
+
iterator(() => {
|
|
925
|
+
result.els[i].classList.add(...cls);
|
|
926
|
+
});
|
|
927
|
+
}, "addClass");
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Remove one or more classes from elements
|
|
931
|
+
* @param {...string} cls - Class names
|
|
932
|
+
*/
|
|
933
|
+
result.removeClass = returner((...cls) => {
|
|
934
|
+
iterator(() => {
|
|
935
|
+
result.els[i].classList.remove(...cls);
|
|
936
|
+
});
|
|
937
|
+
}, "removeClass");
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Toggle class on elements
|
|
941
|
+
* @param {string} cl - Class name
|
|
942
|
+
* @param {boolean} check - Whether to add or remove class
|
|
943
|
+
*/
|
|
944
|
+
result.toggleClass = returner((cl, check) => {
|
|
945
|
+
typeVerify([
|
|
946
|
+
[cl, notEmptyStringType],
|
|
947
|
+
[check, [booleanType, undefinedType]],
|
|
948
|
+
]);
|
|
949
|
+
iterator(() => {
|
|
950
|
+
result.els[i].classList.toggle(cl, check);
|
|
951
|
+
});
|
|
952
|
+
}, "toggleClass");
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Check if elements have class
|
|
956
|
+
* @param {string} cl - Class name
|
|
957
|
+
* @returns {boolean} Whether elements have the class
|
|
958
|
+
*/
|
|
959
|
+
result.haveClass = (cl) => {
|
|
960
|
+
typeVerify([[cl, notEmptyStringType]]);
|
|
961
|
+
let res = true;
|
|
962
|
+
iterator(() => {
|
|
963
|
+
if (!result.els[i].classList.contains(cl)) {
|
|
964
|
+
res = false;
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
if (result.isDebug || o.debug) {
|
|
968
|
+
console.log("haveClass() with", cl);
|
|
969
|
+
}
|
|
970
|
+
return res;
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Set or get innerHTML
|
|
975
|
+
* @param {string} html - HTML content
|
|
976
|
+
*/
|
|
977
|
+
result.innerHTML = returner((html) => {
|
|
978
|
+
typeVerify([[html, [stringType, undefinedType]]]);
|
|
979
|
+
if (html !== u) {
|
|
980
|
+
iterator(() => {
|
|
981
|
+
result.els[i].innerHTML = html;
|
|
982
|
+
});
|
|
983
|
+
} else {
|
|
984
|
+
let res = "";
|
|
985
|
+
iterator(() => {
|
|
986
|
+
res +=
|
|
987
|
+
ssr && result.els[i].innerHTML.length === 0
|
|
988
|
+
? o.D.parseElement(result.els[i], false)
|
|
989
|
+
: result.els[i].innerHTML;
|
|
990
|
+
});
|
|
991
|
+
return res;
|
|
992
|
+
}
|
|
993
|
+
}, "innerHTML");
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Set innerText
|
|
997
|
+
* @param {string} text - Text content
|
|
998
|
+
*/
|
|
999
|
+
result.innerText = returner((text) => {
|
|
1000
|
+
typeVerify([[text, [stringType]]]);
|
|
1001
|
+
iterator(() => {
|
|
1002
|
+
result.els[i].innerText = text;
|
|
1003
|
+
});
|
|
1004
|
+
}, "innerText");
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Set textContent
|
|
1008
|
+
* @param {string} text - Text content
|
|
1009
|
+
*/
|
|
1010
|
+
result.textContent = returner((text) => {
|
|
1011
|
+
typeVerify([[text, [stringType]]]);
|
|
1012
|
+
iterator(() => {
|
|
1013
|
+
result.els[i].textContent = text;
|
|
1014
|
+
});
|
|
1015
|
+
}, "textContent");
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* Get or set HTML of DOM elements
|
|
1019
|
+
* @param {string} value - HTML value to set
|
|
1020
|
+
* @returns {string} HTML content
|
|
1021
|
+
*/
|
|
1022
|
+
result.html = returner((value) => {
|
|
1023
|
+
typeVerify([[value, [stringType, undefinedType]]]);
|
|
1024
|
+
if (value !== undefined) {
|
|
1025
|
+
result.innerHTML(value);
|
|
1026
|
+
} else {
|
|
1027
|
+
let html = "";
|
|
1028
|
+
iterator(() => {
|
|
1029
|
+
html += ssr ? result.els[i].outerHTML() : result.els[i].outerHTML;
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
return html;
|
|
1033
|
+
}
|
|
1034
|
+
}, "html");
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Get or set the value property of form elements (input, textarea, select).
|
|
1038
|
+
* @param {string} [value] - Value to set. Omit to get.
|
|
1039
|
+
* @returns {string|ObjsInstance} Current value (getter) or instance (setter)
|
|
1040
|
+
*/
|
|
1041
|
+
result.val = returner((value) => {
|
|
1042
|
+
if (value === undefined) return result.el?.value;
|
|
1043
|
+
iterator(() => {
|
|
1044
|
+
result.els[i].value = value;
|
|
1045
|
+
});
|
|
1046
|
+
}, "val");
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Iterate through elements
|
|
1050
|
+
* @param {Function} f - Function to execute for each element
|
|
1051
|
+
*/
|
|
1052
|
+
result.forEach = returner((f) => {
|
|
1053
|
+
typeVerify([[f, [functionType]]]);
|
|
1054
|
+
iterator(() => {
|
|
1055
|
+
f({ self: result, i, o, el: result.els[i] });
|
|
1056
|
+
});
|
|
1057
|
+
}, "forEach");
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* Transform to React Element or Component
|
|
1061
|
+
* @param {Object} reactObj - React
|
|
1062
|
+
* @returns {Object} React Element or Component
|
|
1063
|
+
*/
|
|
1064
|
+
result.prepareFor = returner((reactArg, ReactComponent) => {
|
|
1065
|
+
typeVerify([
|
|
1066
|
+
[reactArg, [objectType, functionType, undefinedType]],
|
|
1067
|
+
[ReactComponent, [functionType, undefinedType]],
|
|
1068
|
+
]);
|
|
1069
|
+
|
|
1070
|
+
// Accept either the full React object or React.createElement directly
|
|
1071
|
+
const isFullReact =
|
|
1072
|
+
reactArg && type(reactArg) === objectType && reactArg.createElement;
|
|
1073
|
+
if (!isFullReact && type(reactArg) !== functionType) {
|
|
1074
|
+
throw Error(
|
|
1075
|
+
"prepareFor(): pass React (full object) or React.createElement as first argument",
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const createElement = isFullReact ? reactArg.createElement : reactArg;
|
|
1080
|
+
const useEffect = isFullReact ? reactArg.useEffect : undefined;
|
|
1081
|
+
|
|
1082
|
+
// Component function
|
|
1083
|
+
return (p) => {
|
|
1084
|
+
if (p.ref === u) {
|
|
1085
|
+
throw Error("No ref property to convert Objs to React");
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const props = Object.assign({}, p);
|
|
1089
|
+
const reactElement = createElement("div", { ref: p.ref });
|
|
1090
|
+
delete props.ref;
|
|
1091
|
+
|
|
1092
|
+
// render once
|
|
1093
|
+
useEffect(() => {
|
|
1094
|
+
cycleObj(props, (key) => {
|
|
1095
|
+
if (key.substring(0, 1) === "on") {
|
|
1096
|
+
const e = o.camelToKebab(key).split("-")[1];
|
|
1097
|
+
result.on(e, props[key]);
|
|
1098
|
+
delete props[key];
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
// create elements
|
|
1103
|
+
result.render(props);
|
|
1104
|
+
result.appendInside(reactElement.ref.current);
|
|
1105
|
+
}, []);
|
|
1106
|
+
|
|
1107
|
+
return reactElement;
|
|
1108
|
+
};
|
|
1109
|
+
}, "prepareFor");
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* Add event listener
|
|
1113
|
+
* @param {string} a - Event types
|
|
1114
|
+
* @param {Function} b - Event handler
|
|
1115
|
+
* @param {Object} c - Options
|
|
1116
|
+
* @param {boolean} d - Use capture
|
|
1117
|
+
*/
|
|
1118
|
+
result.on = returner((a, b, c, d) => {
|
|
1119
|
+
typeVerify([
|
|
1120
|
+
[a, [notEmptyStringType]],
|
|
1121
|
+
[b, [functionType]],
|
|
1122
|
+
[c, [objectType, undefinedType]],
|
|
1123
|
+
[d, [booleanType, undefinedType]],
|
|
1124
|
+
]);
|
|
1125
|
+
a.split(", ").forEach((ev) => {
|
|
1126
|
+
iterator(() => {
|
|
1127
|
+
result.els[i].addEventListener(ev, b, c, d);
|
|
1128
|
+
});
|
|
1129
|
+
if (!result.ie[ev]) {
|
|
1130
|
+
result.ie[ev] = [];
|
|
1131
|
+
}
|
|
1132
|
+
result.ie[ev].push([b, c, d]);
|
|
1133
|
+
});
|
|
1134
|
+
}, "on");
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* Remove event listener
|
|
1138
|
+
* @param {string} a - Event types
|
|
1139
|
+
* @param {Function} b - Event handler
|
|
1140
|
+
* @param {Object} c - Options
|
|
1141
|
+
*/
|
|
1142
|
+
result.off = returner((a, b, c) => {
|
|
1143
|
+
typeVerify([
|
|
1144
|
+
[a, [notEmptyStringType]],
|
|
1145
|
+
[b, [functionType]],
|
|
1146
|
+
[c, [objectType, undefinedType]],
|
|
1147
|
+
]);
|
|
1148
|
+
a.split(", ").forEach((ev) => {
|
|
1149
|
+
iterator(() => {
|
|
1150
|
+
result.els[i].removeEventListener(ev, b, c);
|
|
1151
|
+
});
|
|
1152
|
+
if (result.ie[ev]) {
|
|
1153
|
+
result.ie[ev] = result.ie[ev].filter((f) => f[0] !== b);
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
}, "off");
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* Delegate event by selector
|
|
1160
|
+
* @param {string} e - Event types
|
|
1161
|
+
* @param {string} selector - CSS selector
|
|
1162
|
+
* @param {Function} f - Event handler
|
|
1163
|
+
*/
|
|
1164
|
+
result.onDelegate = returner((e, selector, f) => {
|
|
1165
|
+
typeVerify([
|
|
1166
|
+
[e, [notEmptyStringType]],
|
|
1167
|
+
[selector, [notEmptyStringType]],
|
|
1168
|
+
[f, [functionType]],
|
|
1169
|
+
]);
|
|
1170
|
+
e.split(", ").forEach((ev) => {
|
|
1171
|
+
const delegateCheck = (event) => {
|
|
1172
|
+
const delegate = event.target.closest(selector);
|
|
1173
|
+
if (delegate) {
|
|
1174
|
+
event.delegate = delegate;
|
|
1175
|
+
event.objs = result;
|
|
1176
|
+
f(event);
|
|
1177
|
+
}
|
|
1178
|
+
};
|
|
1179
|
+
iterator(() => {
|
|
1180
|
+
result.els[i].addEventListener(ev, delegateCheck);
|
|
1181
|
+
});
|
|
1182
|
+
if (!result.delegated[ev]) {
|
|
1183
|
+
result.delegated[ev] = [];
|
|
1184
|
+
}
|
|
1185
|
+
result.delegated[ev].push(delegateCheck);
|
|
1186
|
+
});
|
|
1187
|
+
}, "onDelegate");
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* Remove delegated events
|
|
1191
|
+
* @param {string} e - Event type
|
|
1192
|
+
*/
|
|
1193
|
+
result.offDelegate = returner((e) => {
|
|
1194
|
+
typeVerify([[e, [notEmptyStringType]]]);
|
|
1195
|
+
cycleObj(result.delegated, (ev) => {
|
|
1196
|
+
if (!e || e === ev) {
|
|
1197
|
+
while (result.delegated[ev].length) {
|
|
1198
|
+
const f = result.delegated[ev].pop();
|
|
1199
|
+
iterator(() => {
|
|
1200
|
+
result.els[i].removeEventListener(ev, f);
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
result.delegated = {};
|
|
1206
|
+
}, "offDelegate");
|
|
1207
|
+
|
|
1208
|
+
/**
|
|
1209
|
+
* Add parent event listener
|
|
1210
|
+
* @param {string} e - Event types
|
|
1211
|
+
* @param {string|Object} selector - CSS selector or parent element
|
|
1212
|
+
* @param {Function} f - Event handler
|
|
1213
|
+
*/
|
|
1214
|
+
result.onParent = returner((e, selector, f) => {
|
|
1215
|
+
typeVerify([
|
|
1216
|
+
[e, [notEmptyStringType]],
|
|
1217
|
+
[selector, [notEmptyStringType, objectType]],
|
|
1218
|
+
[f, [functionType]],
|
|
1219
|
+
]);
|
|
1220
|
+
const parent = type(selector) === objectType ? selector : o.D.querySelector(selector);
|
|
1221
|
+
e.split(", ").forEach((ev) => {
|
|
1222
|
+
const parentCheck = (event) => {
|
|
1223
|
+
event.objs = result;
|
|
1224
|
+
f(event);
|
|
1225
|
+
};
|
|
1226
|
+
parent.addEventListener(ev, parentCheck);
|
|
1227
|
+
if (!result.parented[ev]) {
|
|
1228
|
+
result.parented[ev] = [];
|
|
1229
|
+
}
|
|
1230
|
+
result.parented[ev].push(parentCheck);
|
|
1231
|
+
});
|
|
1232
|
+
}, "onParent");
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* Remove parent event listener
|
|
1236
|
+
* @param {string} e - Event type
|
|
1237
|
+
* @param {string|Object} query - CSS selector or parent element
|
|
1238
|
+
*/
|
|
1239
|
+
result.offParent = returner((e, query) => {
|
|
1240
|
+
typeVerify([
|
|
1241
|
+
[e, [notEmptyStringType]],
|
|
1242
|
+
[query, [notEmptyStringType, objectType]],
|
|
1243
|
+
]);
|
|
1244
|
+
const parent = type(query) === objectType ? query : o.D.querySelector(query);
|
|
1245
|
+
cycleObj(result.parented, (ev) => {
|
|
1246
|
+
if (!e || e === ev) {
|
|
1247
|
+
result.parented[ev].forEach((f) => {
|
|
1248
|
+
parent.removeEventListener(ev, f);
|
|
1249
|
+
});
|
|
1250
|
+
delete result.parented[ev];
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
1253
|
+
}, "offParent");
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* Turn on or off all event listeners
|
|
1257
|
+
* @param {string} type - Event type
|
|
1258
|
+
* @param {boolean} off - Whether to remove listeners
|
|
1259
|
+
*/
|
|
1260
|
+
result.onAll = returner((type, off) => {
|
|
1261
|
+
typeVerify([
|
|
1262
|
+
[type, [notEmptyStringType, undefinedType]],
|
|
1263
|
+
[off, [booleanType, undefinedType]],
|
|
1264
|
+
]);
|
|
1265
|
+
cycleObj(result.ie, (ev, events) => {
|
|
1266
|
+
if (!type || type === ev) {
|
|
1267
|
+
events[ev].forEach((data) => {
|
|
1268
|
+
iterator(() => {
|
|
1269
|
+
if (off) {
|
|
1270
|
+
result.els[i].removeEventListener(ev, data[0]);
|
|
1271
|
+
} else {
|
|
1272
|
+
result.els[i].addEventListener(ev, data[0], data[ONE], data[TWO]);
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
}, "onAll");
|
|
1279
|
+
|
|
1280
|
+
/**
|
|
1281
|
+
* Turn off all event listeners
|
|
1282
|
+
* @param {string} type - Event type
|
|
1283
|
+
*/
|
|
1284
|
+
result.offAll = returner((type) => {
|
|
1285
|
+
typeVerify([[type, [notEmptyStringType]]]);
|
|
1286
|
+
result.onAll(type, ONE);
|
|
1287
|
+
}, "offAll");
|
|
1288
|
+
|
|
1289
|
+
/**
|
|
1290
|
+
* Making result object
|
|
1291
|
+
*/
|
|
1292
|
+
if (query) {
|
|
1293
|
+
result.add(query);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
result.take = (innerQuery) => {
|
|
1297
|
+
typeVerify([[innerQuery, [stringType, objectType, numberType]]]);
|
|
1298
|
+
result.add(innerQuery);
|
|
1299
|
+
|
|
1300
|
+
if (result.el) {
|
|
1301
|
+
const initID = result.el.dataset["oInit"];
|
|
1302
|
+
|
|
1303
|
+
if (initID !== u && o.inits[initID]) {
|
|
1304
|
+
if (result.length === ONE) {
|
|
1305
|
+
j = result.els[0];
|
|
1306
|
+
Object.assign(result, o.inits[initID]);
|
|
1307
|
+
result.els = [j];
|
|
1308
|
+
} else {
|
|
1309
|
+
result = o.inits[initID];
|
|
1310
|
+
}
|
|
1311
|
+
setResultVals(false, result.els);
|
|
1312
|
+
return result;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
1316
|
+
|
|
1317
|
+
return result;
|
|
1318
|
+
};
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Get first element matching query
|
|
1322
|
+
* @param {string} query - CSS selector
|
|
1323
|
+
* @returns {Object} Objs instance with found element
|
|
1324
|
+
*/
|
|
1325
|
+
o.first = (query) => {
|
|
1326
|
+
o.verify([[query, ["notEmptyString"]]]);
|
|
1327
|
+
if (__DEV__ && o.debug) {
|
|
1328
|
+
console.log(query, " -> ", "o.first()");
|
|
1329
|
+
}
|
|
1330
|
+
return o(o.D.querySelector(query)).select(0);
|
|
1331
|
+
};
|
|
1332
|
+
|
|
1333
|
+
o.inits = []; // all initialised objects
|
|
1334
|
+
o.getSaved = {}; // all saved elements for metrics
|
|
1335
|
+
o.errors = []; // all errors
|
|
1336
|
+
o.showErrors = false; // on and off errors
|
|
1337
|
+
o.logErrors = () => {
|
|
1338
|
+
o.errors.length ? o.errors.forEach((e) => console.error(e)) : console.log("No errors");
|
|
1339
|
+
}; // log errors
|
|
1340
|
+
o.onError = (e, name) => {
|
|
1341
|
+
// function for errors
|
|
1342
|
+
if (o.showErrors) {
|
|
1343
|
+
console.error(e, name);
|
|
1344
|
+
} else {
|
|
1345
|
+
o.errors.push(e);
|
|
1346
|
+
if (name) {
|
|
1347
|
+
o.errors.push(name);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
o.reactRender = () => new Error("React render function is not defined");
|
|
1352
|
+
/**
|
|
1353
|
+
* When set to a string (e.g. "qa"), auto-sets data-{autotag}="component-name" on
|
|
1354
|
+
* rendered elements using states.name (camelCase → kebab-case).
|
|
1355
|
+
* @type {string|undefined}
|
|
1356
|
+
* @example o.autotag = "qa"; // adds data-qa="submit-button" if states.name = "SubmitButton"
|
|
1357
|
+
*/
|
|
1358
|
+
o.autotag = undefined;
|
|
1359
|
+
|
|
1360
|
+
/**
|
|
1361
|
+
* Returns a { 'data-{autotag}': 'kebab-name' } object for spreading onto React JSX elements.
|
|
1362
|
+
* @param {string} componentName - e.g. 'CheckoutButton'
|
|
1363
|
+
* @returns {Record<string, string>} e.g. { 'data-qa': 'checkout-button' }
|
|
1364
|
+
* @example <button {...o.reactQA('CheckoutButton')} onClick={fn}>Checkout</button>
|
|
1365
|
+
*/
|
|
1366
|
+
o.reactQA = (name) => ({
|
|
1367
|
+
["data-" + (o.autotag || "qa")]: name
|
|
1368
|
+
.replace(/([A-Z])/g, (_, l) => "-" + l.toLowerCase())
|
|
1369
|
+
.replace(/^-/, ""),
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
o.specialTypes = {
|
|
1373
|
+
notEmptyString: (val, type) => {
|
|
1374
|
+
return type === "string" && val.length;
|
|
1375
|
+
},
|
|
1376
|
+
array: (val) => {
|
|
1377
|
+
return Array.isArray(val);
|
|
1378
|
+
},
|
|
1379
|
+
promise: (val) => {
|
|
1380
|
+
return val instanceof Promise || Boolean(val && typeof val.then === "function");
|
|
1381
|
+
},
|
|
1382
|
+
};
|
|
1383
|
+
|
|
1384
|
+
/**
|
|
1385
|
+
* Verify types of pairs
|
|
1386
|
+
* @param {Array} pairs - Array of pairs
|
|
1387
|
+
* @param {boolean} safe - Whether to return false if type verification fails
|
|
1388
|
+
* @returns {boolean} True if type verification passes, false if safe is true, otherwise Error
|
|
1389
|
+
*/
|
|
1390
|
+
o.verify = (pairs, safe = false) => {
|
|
1391
|
+
for (const pair of pairs) {
|
|
1392
|
+
const type = typeof pair[0];
|
|
1393
|
+
let expectedTypes = Array.isArray(pair[1]) ? pair[1] : [pair[1]];
|
|
1394
|
+
let isValid = false;
|
|
1395
|
+
|
|
1396
|
+
// Check if type matches any of the remaining expected types
|
|
1397
|
+
if (expectedTypes.includes(type)) {
|
|
1398
|
+
return true;
|
|
1399
|
+
} else {
|
|
1400
|
+
expectedTypes = expectedTypes.filter((t) => !!o.specialTypes[t]);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// Check for special types
|
|
1404
|
+
for (const expectedType of expectedTypes) {
|
|
1405
|
+
isValid = o.specialTypes[expectedType](pair[0], type);
|
|
1406
|
+
if (isValid) {
|
|
1407
|
+
return true;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
if (safe) {
|
|
1413
|
+
return false;
|
|
1414
|
+
}
|
|
1415
|
+
return new Error("Type verification failed");
|
|
1416
|
+
};
|
|
1417
|
+
o.safeVerify = (pairs) => {
|
|
1418
|
+
return o.verify(pairs, true);
|
|
1419
|
+
};
|
|
1420
|
+
|
|
1421
|
+
/**
|
|
1422
|
+
* Creating elements from state
|
|
1423
|
+
*
|
|
1424
|
+
* @param {object} states State, states or function/string
|
|
1425
|
+
*/
|
|
1426
|
+
o.init = (states, reactRender) => o().init(states, reactRender);
|
|
1427
|
+
|
|
1428
|
+
/**
|
|
1429
|
+
* Initialize state with props
|
|
1430
|
+
* @param {Object} state - State object
|
|
1431
|
+
* @param {Object} props - Props to initialize with
|
|
1432
|
+
*/
|
|
1433
|
+
o.initState = (state, props) => o().init(state).render(props);
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* Take query
|
|
1437
|
+
* @param {string} query - Query
|
|
1438
|
+
* @returns {Object} Objs instance with found elements
|
|
1439
|
+
*/
|
|
1440
|
+
o.take = (query) => o().take(query);
|
|
1441
|
+
|
|
1442
|
+
/**
|
|
1443
|
+
* Get states
|
|
1444
|
+
* @returns {Array} States
|
|
1445
|
+
*/
|
|
1446
|
+
o.getStates = () =>
|
|
1447
|
+
o.inits.reduce((acc, result) => {
|
|
1448
|
+
acc.push(result?.states);
|
|
1449
|
+
return acc;
|
|
1450
|
+
}, []);
|
|
1451
|
+
|
|
1452
|
+
/**
|
|
1453
|
+
* Get stores
|
|
1454
|
+
* @returns {Array} Stores
|
|
1455
|
+
*/
|
|
1456
|
+
o.getStores = () =>
|
|
1457
|
+
o.inits.reduce((acc, result) => {
|
|
1458
|
+
acc.push(result?.store);
|
|
1459
|
+
return acc;
|
|
1460
|
+
}, []);
|
|
1461
|
+
|
|
1462
|
+
/**
|
|
1463
|
+
* Get listeners
|
|
1464
|
+
* @returns {Array} Listeners
|
|
1465
|
+
*/
|
|
1466
|
+
o.getListeners = () =>
|
|
1467
|
+
o.inits.reduce((acc, result) => {
|
|
1468
|
+
acc.push(result?.ie);
|
|
1469
|
+
return acc;
|
|
1470
|
+
}, []);
|
|
1471
|
+
|
|
1472
|
+
/**
|
|
1473
|
+
* Create a reactive store with built-in subscribe / notify.
|
|
1474
|
+
* @param {Object} defaults - Initial state and methods
|
|
1475
|
+
* @returns {Object} Store with .subscribe(component, stateName), .notify(), and .reset()
|
|
1476
|
+
*/
|
|
1477
|
+
o.createStore = (defaults) => {
|
|
1478
|
+
const store = Object.assign({}, defaults);
|
|
1479
|
+
store._defaults = Object.assign({}, defaults);
|
|
1480
|
+
store._listeners = [];
|
|
1481
|
+
store.subscribe = function (component, stateName) {
|
|
1482
|
+
this._listeners.push((data) => component[stateName]?.(data));
|
|
1483
|
+
return this;
|
|
1484
|
+
};
|
|
1485
|
+
store.notify = function () {
|
|
1486
|
+
this._listeners.forEach((fn) => fn(this));
|
|
1487
|
+
};
|
|
1488
|
+
store.reset = function () {
|
|
1489
|
+
const skip = { _listeners: 1, subscribe: 1, notify: 1, _defaults: 1, reset: 1 };
|
|
1490
|
+
for (const key of Object.keys(this._defaults)) {
|
|
1491
|
+
if (!skip[key]) this[key] = this._defaults[key];
|
|
1492
|
+
}
|
|
1493
|
+
};
|
|
1494
|
+
return store;
|
|
1495
|
+
};
|
|
1496
|
+
|
|
1497
|
+
// Short values (o.U = async "waiting" sentinel for tests)
|
|
1498
|
+
o.U = undefined;
|
|
1499
|
+
o.W = 2;
|
|
1500
|
+
o.H = 100;
|
|
1501
|
+
o.F = false;
|
|
1502
|
+
|
|
1503
|
+
/**
|
|
1504
|
+
* Check if object has property
|
|
1505
|
+
* @param {Object} a - Object
|
|
1506
|
+
* @param {string} b - Property
|
|
1507
|
+
* @returns {boolean} Whether object has property
|
|
1508
|
+
*/
|
|
1509
|
+
o.C = (a, b) => {
|
|
1510
|
+
return Object.hasOwn(a, b);
|
|
1511
|
+
};
|
|
1512
|
+
|
|
1513
|
+
/**
|
|
1514
|
+
* Convert kebab case to camel case
|
|
1515
|
+
* @param {string} str - String
|
|
1516
|
+
* @returns {string} Converted string
|
|
1517
|
+
*/
|
|
1518
|
+
o.kebabToCamel = (str) => str.replace(/-./g, (m) => m.toUpperCase()[1]);
|
|
1519
|
+
|
|
1520
|
+
/**
|
|
1521
|
+
* Convert camel case to kebab case
|
|
1522
|
+
* @param {string} str - String
|
|
1523
|
+
* @returns {string} Converted string
|
|
1524
|
+
*/
|
|
1525
|
+
o.camelToKebab = (str) => str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
1526
|
+
|
|
1527
|
+
/**
|
|
1528
|
+
* Route
|
|
1529
|
+
* @param {string|function|boolean} path - Path or function returning path
|
|
1530
|
+
* @param {function|object} task - Callback function or state object (optional)
|
|
1531
|
+
* @returns {boolean|Object} True/false for path match current path and callback is provided, otherwise object with objs instance or empty object for inline logic
|
|
1532
|
+
*/
|
|
1533
|
+
o.route = (path, task) => {
|
|
1534
|
+
o.verify([
|
|
1535
|
+
[path, ["notEmptyString", "function", "boolean"]],
|
|
1536
|
+
[task, ["function", "object"]],
|
|
1537
|
+
]);
|
|
1538
|
+
const result = typeof path === "function" ? path(window.location.pathname) : path;
|
|
1539
|
+
|
|
1540
|
+
if (result === true || window.location.pathname === result) {
|
|
1541
|
+
if (task) {
|
|
1542
|
+
if (typeof task === "function") {
|
|
1543
|
+
task();
|
|
1544
|
+
return true;
|
|
1545
|
+
} else {
|
|
1546
|
+
return task;
|
|
1547
|
+
}
|
|
1548
|
+
} else {
|
|
1549
|
+
return o;
|
|
1550
|
+
}
|
|
1551
|
+
} else {
|
|
1552
|
+
return typeof task === "function" ? false : {};
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1555
|
+
|
|
1556
|
+
/**
|
|
1557
|
+
* Router
|
|
1558
|
+
* @param {Object} routes - Routes
|
|
1559
|
+
* @returns {boolean} True if path matches any path
|
|
1560
|
+
*/
|
|
1561
|
+
o.router = (routes = {}) => {
|
|
1562
|
+
o.verify([[routes, ["object"]]]);
|
|
1563
|
+
for (const route in routes) {
|
|
1564
|
+
if (o.C(routes, route)) {
|
|
1565
|
+
if (window.location.pathname === route) {
|
|
1566
|
+
if (typeof routes[route] === "function") {
|
|
1567
|
+
routes[route]();
|
|
1568
|
+
return true;
|
|
1569
|
+
} else {
|
|
1570
|
+
return routes[route];
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
return false;
|
|
1576
|
+
};
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* DocumentMVP parser for Objs SSR core function
|
|
1580
|
+
*/
|
|
1581
|
+
o.DocumentMVP = {
|
|
1582
|
+
addEventListener: () => {},
|
|
1583
|
+
parseElement: (elem, outer = true) => {
|
|
1584
|
+
o.verify([
|
|
1585
|
+
[elem, ["object", "string"]],
|
|
1586
|
+
[outer, ["boolean"]],
|
|
1587
|
+
]);
|
|
1588
|
+
const attrToStr = (attrs, prefix = "") => {
|
|
1589
|
+
let attrStr = "";
|
|
1590
|
+
for (const attr in attrs) {
|
|
1591
|
+
if (o.C(attrs, attr)) {
|
|
1592
|
+
attrStr += ` ${prefix}${o.camelToKebab(attr)}="${
|
|
1593
|
+
typeof attrs[attr] !== "object"
|
|
1594
|
+
? attrs[attr]
|
|
1595
|
+
: Object.entries(attrs[attr])
|
|
1596
|
+
.map((e) => `${e[0]}: ${e[1]};`)
|
|
1597
|
+
.join(" ")
|
|
1598
|
+
}"`;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
return attrStr;
|
|
1602
|
+
};
|
|
1603
|
+
|
|
1604
|
+
if (typeof elem === "string") {
|
|
1605
|
+
return elem;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
if (outer) {
|
|
1609
|
+
const selfClosing = [
|
|
1610
|
+
"area",
|
|
1611
|
+
"base",
|
|
1612
|
+
"br",
|
|
1613
|
+
"col",
|
|
1614
|
+
"embed",
|
|
1615
|
+
"hr",
|
|
1616
|
+
"img",
|
|
1617
|
+
"input",
|
|
1618
|
+
"link",
|
|
1619
|
+
"meta",
|
|
1620
|
+
"param",
|
|
1621
|
+
"source",
|
|
1622
|
+
"track",
|
|
1623
|
+
"wbr",
|
|
1624
|
+
];
|
|
1625
|
+
const tagName = elem.tagName.toLowerCase();
|
|
1626
|
+
// Ensure data-o-init is always serialized (SSR hydration)
|
|
1627
|
+
const dataOInit = elem.attributes["data-o-init"];
|
|
1628
|
+
const dataOInitAttr = dataOInit !== undefined ? ` data-o-init="${dataOInit}"` : "";
|
|
1629
|
+
return `<${tagName}${elem.className ? ` class="${elem.className}"` : ""}${attrToStr(elem.attributes)}${dataOInitAttr}${attrToStr(elem.dataset, "data-")}${selfClosing.includes(tagName) ? "/" : ""}>${selfClosing.includes(tagName) ? "" : elem.innerHTML.length ? elem.innerHTML : elem.children.map((el) => o.D.parseElement(el)).join("")}${!selfClosing.includes(tagName) ? `</${tagName}>` : ""}`;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
return elem.innerHTML.length
|
|
1633
|
+
? elem.innerHTML
|
|
1634
|
+
: elem.children.map((el) => o.D.parseElement(el)).join("");
|
|
1635
|
+
},
|
|
1636
|
+
createElement: (tag) => {
|
|
1637
|
+
o.verify([[tag, ["notEmptyString"]]]);
|
|
1638
|
+
const elem = {
|
|
1639
|
+
tagName: tag.toUpperCase(),
|
|
1640
|
+
attributes: {},
|
|
1641
|
+
innerHTML: "",
|
|
1642
|
+
children: [],
|
|
1643
|
+
dataset: {},
|
|
1644
|
+
className: "",
|
|
1645
|
+
classArray: [],
|
|
1646
|
+
style: {},
|
|
1647
|
+
addEventListener: () => {},
|
|
1648
|
+
removeEventListener: () => {},
|
|
1649
|
+
};
|
|
1650
|
+
elem.classList = {
|
|
1651
|
+
add: (...cl) => {
|
|
1652
|
+
o.verify([[cl, ["array"]]]);
|
|
1653
|
+
elem.classArray.push(cl);
|
|
1654
|
+
elem.className = elem.classArray.join(" ");
|
|
1655
|
+
},
|
|
1656
|
+
has: (cl) => {
|
|
1657
|
+
o.verify([[cl, ["notEmptyString"]]]);
|
|
1658
|
+
return elem.classArray.includes(cl);
|
|
1659
|
+
},
|
|
1660
|
+
remove: (cl) => {
|
|
1661
|
+
o.verify([[cl, ["notEmptyString"]]]);
|
|
1662
|
+
elem.classArray = elem.classArray.filter((listed) => cl !== listed);
|
|
1663
|
+
elem.className = elem.classArray.join(" ");
|
|
1664
|
+
},
|
|
1665
|
+
};
|
|
1666
|
+
elem.classList.toggle = (cl) => {
|
|
1667
|
+
o.verify([[cl, ["notEmptyString"]]]);
|
|
1668
|
+
if (elem.classList.has(cl)) {
|
|
1669
|
+
elem.classList.remove(cl);
|
|
1670
|
+
} else {
|
|
1671
|
+
elem.classList.add(cl);
|
|
1672
|
+
}
|
|
1673
|
+
};
|
|
1674
|
+
elem.setAttribute = (attr, val) => {
|
|
1675
|
+
o.verify([
|
|
1676
|
+
[attr, ["notEmptyString"]],
|
|
1677
|
+
[val, ["string", "number", "boolean", "undefined"]],
|
|
1678
|
+
]);
|
|
1679
|
+
elem.attributes[attr] = val;
|
|
1680
|
+
};
|
|
1681
|
+
elem.getAttribute = (attr) => {
|
|
1682
|
+
o.verify([[attr, ["notEmptyString"]]]);
|
|
1683
|
+
return elem.attributes[attr];
|
|
1684
|
+
};
|
|
1685
|
+
elem.removeAttribute = (attr) => {
|
|
1686
|
+
o.verify([[attr, ["notEmptyString"]]]);
|
|
1687
|
+
delete elem.attributes[attr];
|
|
1688
|
+
};
|
|
1689
|
+
elem.appendChild = (el) => {
|
|
1690
|
+
o.verify([[el, ["object"]]]);
|
|
1691
|
+
elem.children.push(el);
|
|
1692
|
+
elem.firstElementChild = elem.children[0];
|
|
1693
|
+
};
|
|
1694
|
+
elem.outerHTML = () => o.D.parseElement(elem);
|
|
1695
|
+
return elem;
|
|
1696
|
+
},
|
|
1697
|
+
};
|
|
1698
|
+
|
|
1699
|
+
/**
|
|
1700
|
+
* Document object
|
|
1701
|
+
* @type {Object}
|
|
1702
|
+
*/
|
|
1703
|
+
o.D =
|
|
1704
|
+
typeof document !== "undefined" && typeof process === "undefined"
|
|
1705
|
+
? document
|
|
1706
|
+
: o.DocumentMVP;
|
|
1707
|
+
|
|
1708
|
+
/**
|
|
1709
|
+
* Cookies
|
|
1710
|
+
* Minimal version. You can set your own functions for more comfortable usage.
|
|
1711
|
+
*
|
|
1712
|
+
* @argument {string} title Cookie name
|
|
1713
|
+
* @argument {string} value Cookie name
|
|
1714
|
+
* @argument {object} params parameters
|
|
1715
|
+
*
|
|
1716
|
+
* @returns {void}
|
|
1717
|
+
*/
|
|
1718
|
+
o.setCookie = (title, value = "", params = {}) => {
|
|
1719
|
+
o.verify([
|
|
1720
|
+
[title, ["notEmptyString"]],
|
|
1721
|
+
[value, ["string", "number", "boolean"]],
|
|
1722
|
+
[params, ["object"]],
|
|
1723
|
+
]);
|
|
1724
|
+
if (o.D.cookie === undefined) {
|
|
1725
|
+
console.log("Cookies are not supported on server side");
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
let str = encodeURIComponent(title) + "=" + encodeURIComponent(value);
|
|
1729
|
+
params = {
|
|
1730
|
+
path: "/",
|
|
1731
|
+
...params,
|
|
1732
|
+
};
|
|
1733
|
+
if (params.expires instanceof Date) {
|
|
1734
|
+
params.expires = params.expires.toUTCString();
|
|
1735
|
+
} else if (typeof params.expires === "number") {
|
|
1736
|
+
params.expires = new Date(params.expires).toUTCString();
|
|
1737
|
+
}
|
|
1738
|
+
for (const key in params) {
|
|
1739
|
+
str += "; " + key;
|
|
1740
|
+
const r = params[key];
|
|
1741
|
+
if (r !== true) {
|
|
1742
|
+
str += "=" + r;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
o.D.cookie = str;
|
|
1746
|
+
};
|
|
1747
|
+
|
|
1748
|
+
/**
|
|
1749
|
+
* Get cookie value
|
|
1750
|
+
* @param {string} title - Cookie name
|
|
1751
|
+
* @returns {string|undefined} Cookie value
|
|
1752
|
+
*/
|
|
1753
|
+
o.getCookie = (title) => {
|
|
1754
|
+
o.verify([[title, ["notEmptyString"]]]);
|
|
1755
|
+
if (o.D.cookie === undefined) {
|
|
1756
|
+
console.log("Cookies are not supported on server side");
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
const val = o.D.cookie.match(
|
|
1760
|
+
RegExp("(?:^|; )" + title.replace(/([.$?*|{}()[\]\\/+^])/g, "\\$1") + "=([^;]*)"),
|
|
1761
|
+
);
|
|
1762
|
+
return val ? decodeURIComponent(val[1]) : void 0;
|
|
1763
|
+
};
|
|
1764
|
+
|
|
1765
|
+
/**
|
|
1766
|
+
* Delete cookie
|
|
1767
|
+
* @param {string} title - Cookie name
|
|
1768
|
+
*/
|
|
1769
|
+
o.deleteCookie = (title) => {
|
|
1770
|
+
o.verify([[title, ["notEmptyString"]]]);
|
|
1771
|
+
o.setCookie(title, "", { "max-age": 0 });
|
|
1772
|
+
};
|
|
1773
|
+
|
|
1774
|
+
/**
|
|
1775
|
+
* Clear all cookies
|
|
1776
|
+
*/
|
|
1777
|
+
o.clearCookies = () => {
|
|
1778
|
+
if (o.D.cookie === undefined) {
|
|
1779
|
+
console.log("Cookies are not supported on server side");
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
const ca = o.D.cookie.split(";");
|
|
1783
|
+
while (ca.length) {
|
|
1784
|
+
let c = ca.pop();
|
|
1785
|
+
while (c.charAt(0) === " ") {
|
|
1786
|
+
c = c.substring(1);
|
|
1787
|
+
}
|
|
1788
|
+
const key = c.split("=")[0];
|
|
1789
|
+
o.deleteCookie(key);
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* Clear localStorage
|
|
1795
|
+
* @param {boolean} all - Whether to clear all items
|
|
1796
|
+
*/
|
|
1797
|
+
o.clearLocalStorage = (all) => {
|
|
1798
|
+
o.verify([[all, ["boolean", "undefined"]]]);
|
|
1799
|
+
if (typeof localStorage === "undefined") {
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
if (all) {
|
|
1803
|
+
localStorage.clear();
|
|
1804
|
+
} else {
|
|
1805
|
+
for (let i = localStorage.length - 1; i >= 0; i--) {
|
|
1806
|
+
const key = localStorage.key(i);
|
|
1807
|
+
if (key.indexOf("oInc-") === -1 && key.indexOf("oTest-") === -1) {
|
|
1808
|
+
localStorage.removeItem(key);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
};
|
|
1813
|
+
|
|
1814
|
+
/**
|
|
1815
|
+
* Clear sessionStorage
|
|
1816
|
+
* @param {boolean} onlyTests - Whether to clear only test items
|
|
1817
|
+
*/
|
|
1818
|
+
o.clearSessionStorage = (onlyTests) => {
|
|
1819
|
+
o.verify([[onlyTests, ["boolean", "undefined"]]]);
|
|
1820
|
+
if (typeof sessionStorage === "undefined") {
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
if (!onlyTests) {
|
|
1824
|
+
sessionStorage.clear();
|
|
1825
|
+
} else {
|
|
1826
|
+
for (let i = sessionStorage.length - 1; i >= 0; i--) {
|
|
1827
|
+
const key = sessionStorage.key(i);
|
|
1828
|
+
if (key && key.indexOf("oTest-") === 0) {
|
|
1829
|
+
sessionStorage.removeItem(key);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
};
|
|
1834
|
+
|
|
1835
|
+
/**
|
|
1836
|
+
* Clear test storage
|
|
1837
|
+
*/
|
|
1838
|
+
o.clearTestsStorage = () => {
|
|
1839
|
+
o.clearSessionStorage(1);
|
|
1840
|
+
};
|
|
1841
|
+
|
|
1842
|
+
/**
|
|
1843
|
+
* Clear cookies, localStorage (non-inc/test keys), and test-related sessionStorage.
|
|
1844
|
+
* Call after Cookies/LS/SS tests for a clean slate.
|
|
1845
|
+
*/
|
|
1846
|
+
o.clearAfterTests = () => {
|
|
1847
|
+
o.clearCookies();
|
|
1848
|
+
o.clearLocalStorage(false);
|
|
1849
|
+
o.clearTestsStorage();
|
|
1850
|
+
};
|
|
1851
|
+
|
|
1852
|
+
/**
|
|
1853
|
+
* Make AJAX request
|
|
1854
|
+
* @param {string} url - Request URL
|
|
1855
|
+
* @param {Object} props - Request properties
|
|
1856
|
+
* @returns {Promise} Fetch promise
|
|
1857
|
+
*/
|
|
1858
|
+
o.ajax = (url, props = {}) => {
|
|
1859
|
+
o.verify([
|
|
1860
|
+
[url, ["notEmptyString"]],
|
|
1861
|
+
[props, ["object"]],
|
|
1862
|
+
]);
|
|
1863
|
+
const row = new URLSearchParams();
|
|
1864
|
+
if (props.data && typeof props.data === "object") {
|
|
1865
|
+
for (const param in props.data) {
|
|
1866
|
+
if (o.C(props.data, param)) {
|
|
1867
|
+
if (typeof props.data[param] === "object") {
|
|
1868
|
+
row.set(param, encodeURIComponent(JSON.stringify(props.data[param])));
|
|
1869
|
+
} else {
|
|
1870
|
+
row.set(param, props.data[param]);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
if (props.method.toLowerCase() === "get") {
|
|
1875
|
+
url += "?" + row.toString();
|
|
1876
|
+
} else if (!props.body) {
|
|
1877
|
+
props.body = row;
|
|
1878
|
+
}
|
|
1879
|
+
delete props.data;
|
|
1880
|
+
}
|
|
1881
|
+
if (!props.headers) {
|
|
1882
|
+
props.headers = { "X-Requested-With": "XMLHttpRequest" };
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
return fetch(url, props);
|
|
1886
|
+
};
|
|
1887
|
+
|
|
1888
|
+
/**
|
|
1889
|
+
* Make GET request
|
|
1890
|
+
* @param {string} url - Request URL
|
|
1891
|
+
* @param {Object} props - Request properties
|
|
1892
|
+
* @returns {Promise} Fetch promise
|
|
1893
|
+
*/
|
|
1894
|
+
o.get = (url, props = {}) => {
|
|
1895
|
+
o.verify([
|
|
1896
|
+
[url, ["notEmptyString"]],
|
|
1897
|
+
[props, ["object"]],
|
|
1898
|
+
]);
|
|
1899
|
+
return o.ajax(url, { ...props, method: "GET" });
|
|
1900
|
+
};
|
|
1901
|
+
|
|
1902
|
+
/**
|
|
1903
|
+
* Make POST request
|
|
1904
|
+
* @param {string} url - Request URL
|
|
1905
|
+
* @param {Object} props - Request properties
|
|
1906
|
+
* @returns {Promise} Fetch promise
|
|
1907
|
+
*/
|
|
1908
|
+
o.post = (url, props = {}) => {
|
|
1909
|
+
o.verify([
|
|
1910
|
+
[url, ["notEmptyString"]],
|
|
1911
|
+
[props, ["object"]],
|
|
1912
|
+
]);
|
|
1913
|
+
return o.ajax(url, { ...props, method: "POST" });
|
|
1914
|
+
};
|
|
1915
|
+
|
|
1916
|
+
/**
|
|
1917
|
+
* New loader
|
|
1918
|
+
* @param {Promise|undefined} promise - Promise
|
|
1919
|
+
* @returns {Object} Loader object
|
|
1920
|
+
*/
|
|
1921
|
+
o.newLoader = (promise) => {
|
|
1922
|
+
o.verify([[promise, ["promise", "undefined"]]]);
|
|
1923
|
+
let listeners = [];
|
|
1924
|
+
let data = null;
|
|
1925
|
+
let finished = false;
|
|
1926
|
+
let error = false;
|
|
1927
|
+
|
|
1928
|
+
const reload = (p) => {
|
|
1929
|
+
finished = false;
|
|
1930
|
+
error = false;
|
|
1931
|
+
data = null;
|
|
1932
|
+
setTimeout(() => {
|
|
1933
|
+
p.then((response) => {
|
|
1934
|
+
finished = true;
|
|
1935
|
+
if (!response.ok && typeof response.ok !== "undefined") {
|
|
1936
|
+
error = true;
|
|
1937
|
+
listeners.forEach(([listener, _state, fail]) => {
|
|
1938
|
+
fail ? listener[fail](response) : "";
|
|
1939
|
+
});
|
|
1940
|
+
} else if (typeof response.json === "function") {
|
|
1941
|
+
response.json().then((jsonData) => {
|
|
1942
|
+
data = jsonData;
|
|
1943
|
+
listeners.forEach(([listener, state]) => {
|
|
1944
|
+
listener[state](data);
|
|
1945
|
+
});
|
|
1946
|
+
});
|
|
1947
|
+
} else {
|
|
1948
|
+
data = response;
|
|
1949
|
+
listeners.forEach(([listener, state]) => {
|
|
1950
|
+
listener[state](data);
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
}).catch((err) => {
|
|
1954
|
+
error = true;
|
|
1955
|
+
listeners.forEach(([listener, _state, fail]) => {
|
|
1956
|
+
fail ? listener[fail](err) : "";
|
|
1957
|
+
});
|
|
1958
|
+
});
|
|
1959
|
+
}, 33);
|
|
1960
|
+
};
|
|
1961
|
+
|
|
1962
|
+
if (promise) {
|
|
1963
|
+
reload(promise);
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
return {
|
|
1967
|
+
reload,
|
|
1968
|
+
isObjsLoader: true,
|
|
1969
|
+
listeners,
|
|
1970
|
+
isFinished: () => finished,
|
|
1971
|
+
getStore: () => data,
|
|
1972
|
+
connect: (listener, state = "render", fail) => {
|
|
1973
|
+
o.verify([
|
|
1974
|
+
[listener, ["object"]],
|
|
1975
|
+
[state, ["notEmptyString"]],
|
|
1976
|
+
[fail, ["string", "undefined"]],
|
|
1977
|
+
]);
|
|
1978
|
+
if (finished) {
|
|
1979
|
+
if (error) {
|
|
1980
|
+
fail ? listener[fail]() : "";
|
|
1981
|
+
} else if (typeof listener[state] === "function") {
|
|
1982
|
+
listener[state](data);
|
|
1983
|
+
}
|
|
1984
|
+
} else {
|
|
1985
|
+
listeners.push([listener, state, fail]);
|
|
1986
|
+
}
|
|
1987
|
+
},
|
|
1988
|
+
disconnect: (listener) => {
|
|
1989
|
+
o.verify([[listener, ["object"]]]);
|
|
1990
|
+
listeners = listeners.filter(([l]) => l !== listener);
|
|
1991
|
+
},
|
|
1992
|
+
};
|
|
1993
|
+
};
|
|
1994
|
+
|
|
1995
|
+
/**
|
|
1996
|
+
* Get URL parameters
|
|
1997
|
+
* @param {string} key - Parameter key
|
|
1998
|
+
* @returns {Object|string} Parameters object or specific parameter value
|
|
1999
|
+
*/
|
|
2000
|
+
o.getParams = (key) => {
|
|
2001
|
+
o.verify([[key, ["string", "undefined"]]]);
|
|
2002
|
+
const params = {};
|
|
2003
|
+
const paramsRaw = new URLSearchParams(window.location.search).entries();
|
|
2004
|
+
|
|
2005
|
+
for (const entry of paramsRaw) {
|
|
2006
|
+
params[entry[0]] = entry[1];
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
return key ? params[key] : params;
|
|
2010
|
+
};
|
|
2011
|
+
|
|
2012
|
+
/* include function parameters */
|
|
2013
|
+
o.incCache = true; // cache in localStorage
|
|
2014
|
+
o.incCacheExp = 1000 * 60 * 60 * 24; // cache time
|
|
2015
|
+
o.incTimeout = 6000; // ms for timing to load function
|
|
2016
|
+
o.incSource = ""; // link to source folder
|
|
2017
|
+
o.incForce = o.F; // reload loaded files or not
|
|
2018
|
+
o.incAsync = true; // async or in order loading
|
|
2019
|
+
o.incCors = o.F; // allow loading from other domains
|
|
2020
|
+
o.incSeparator = "?"; // separator for file hash cache
|
|
2021
|
+
o.incFns = {}; // array of name:status for all functions
|
|
2022
|
+
o.incSet = [0]; // saving callbacks and change them for 1 value if executed
|
|
2023
|
+
o.incReady = [0]; // array of all included sets and statuses
|
|
2024
|
+
o.incN = 0; // index of the next set
|
|
2025
|
+
o.incGetHash = (path) => path.split(o.incSeparator)[1] || "";
|
|
2026
|
+
|
|
2027
|
+
/**
|
|
2028
|
+
* Check the state status or a function in it, also checks its status to true
|
|
2029
|
+
*
|
|
2030
|
+
* @param {number} set index
|
|
2031
|
+
* @param {number} fnId index
|
|
2032
|
+
* @returns {boolean}
|
|
2033
|
+
*/
|
|
2034
|
+
o.incCheck = (set = 0, fnId, loaded = 0) => {
|
|
2035
|
+
o.verify([
|
|
2036
|
+
[set, ["number"]],
|
|
2037
|
+
[fnId, ["number", "undefined"]],
|
|
2038
|
+
[loaded, ["number"]],
|
|
2039
|
+
]);
|
|
2040
|
+
if (!loaded && set && fnId === o.U && o.incReady[set]) {
|
|
2041
|
+
return o.incSet[set] === 1;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
if (o.incReady[set] === o.U || o.incReady[set][fnId] === o.U) {
|
|
2045
|
+
return o.F;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
o.incReady[set][fnId].loaded = loaded;
|
|
2049
|
+
o.incFns[o.incReady[set][fnId].name] = loaded;
|
|
2050
|
+
o.incReady[set][0] += loaded;
|
|
2051
|
+
|
|
2052
|
+
if (set && o.incReady[set].length === o.incReady[set][0]) {
|
|
2053
|
+
if (typeof o.incSet[set] === "function") {
|
|
2054
|
+
o.incSet[set](set);
|
|
2055
|
+
}
|
|
2056
|
+
o.incSet[set] = 1;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
return o.incSet[set] === 1;
|
|
2060
|
+
};
|
|
2061
|
+
|
|
2062
|
+
/**
|
|
2063
|
+
* Clear all cache and all loaded files info
|
|
2064
|
+
* @param {boolean} all - Whether to clear cache or cache and DOM elements
|
|
2065
|
+
*/
|
|
2066
|
+
o.incCacheClear = (all = o.F) => {
|
|
2067
|
+
o.verify([[all, ["boolean"]]]);
|
|
2068
|
+
for (const name in o.incFns) {
|
|
2069
|
+
if (o.C(o.incFns, name)) {
|
|
2070
|
+
localStorage.removeItem("oInc-" + name);
|
|
2071
|
+
localStorage.removeItem("oInc-" + name + "-expires");
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
if (all) {
|
|
2075
|
+
o.incReady.forEach((val, i) => {
|
|
2076
|
+
if (i) {
|
|
2077
|
+
val.forEach((_a, j) => {
|
|
2078
|
+
if (j) {
|
|
2079
|
+
o("#oInc-" + i + "-" + j).remove();
|
|
2080
|
+
}
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
o.incN = 0;
|
|
2086
|
+
o.incFns = {};
|
|
2087
|
+
o.incSet = [0];
|
|
2088
|
+
o.incReady = [0];
|
|
2089
|
+
}
|
|
2090
|
+
return true;
|
|
2091
|
+
};
|
|
2092
|
+
|
|
2093
|
+
/**
|
|
2094
|
+
* Include external resources
|
|
2095
|
+
* @param {Object} sources - Resources to include
|
|
2096
|
+
* @param {Function} callBack - Success callback
|
|
2097
|
+
* @param {Function} callBad - Error callback
|
|
2098
|
+
* @returns {boolean} Whether inclusion was successful
|
|
2099
|
+
*/
|
|
2100
|
+
o.inc = (sources, callBack, callBad) => {
|
|
2101
|
+
o.verify([
|
|
2102
|
+
[sources, ["object", "undefined"]],
|
|
2103
|
+
[callBack, ["function", "undefined"]],
|
|
2104
|
+
[callBad, ["function", "undefined"]],
|
|
2105
|
+
]);
|
|
2106
|
+
if (typeof localStorage === "undefined") {
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
let sourcesN = 0,
|
|
2110
|
+
sourcesReady = 0,
|
|
2111
|
+
hash = "",
|
|
2112
|
+
preload = false;
|
|
2113
|
+
const f = "function",
|
|
2114
|
+
no = -1;
|
|
2115
|
+
|
|
2116
|
+
if (typeof sources !== "object" || !sources) {
|
|
2117
|
+
return o.incSet[0];
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
o.incSet[0]++;
|
|
2121
|
+
const setN = o.incSet[0];
|
|
2122
|
+
o.incSet[setN] = callBack || 0;
|
|
2123
|
+
o.incReady[setN] = [];
|
|
2124
|
+
const fnsStatus = o.incReady[setN];
|
|
2125
|
+
fnsStatus[0] = 1;
|
|
2126
|
+
const fnId = {};
|
|
2127
|
+
|
|
2128
|
+
for (const name in sources) {
|
|
2129
|
+
if (o.C(sources, name)) {
|
|
2130
|
+
// preload and cache functionality
|
|
2131
|
+
if (name === "preload") {
|
|
2132
|
+
preload = true;
|
|
2133
|
+
continue;
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
hash = o.incGetHash(sources[name]);
|
|
2137
|
+
sourcesN++;
|
|
2138
|
+
o.incN++;
|
|
2139
|
+
|
|
2140
|
+
const tag = sources[name].indexOf(".css") > no ? "style" : "script";
|
|
2141
|
+
sources[name] = (o.incSource ? o.incSource + "/" : "") + sources[name];
|
|
2142
|
+
|
|
2143
|
+
// skip loading if already loaded with page and not forced
|
|
2144
|
+
if (
|
|
2145
|
+
Number.isNaN(Number(name)) &&
|
|
2146
|
+
o.C(o.incFns, name) &&
|
|
2147
|
+
o.incFns[name] &&
|
|
2148
|
+
!o.incForce
|
|
2149
|
+
) {
|
|
2150
|
+
fnsStatus[sourcesN] = {
|
|
2151
|
+
name,
|
|
2152
|
+
loaded: 1,
|
|
2153
|
+
};
|
|
2154
|
+
sourcesReady++;
|
|
2155
|
+
continue;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
// fixing if loaded needed
|
|
2159
|
+
fnsStatus[sourcesN] = {
|
|
2160
|
+
name,
|
|
2161
|
+
loaded: 0,
|
|
2162
|
+
};
|
|
2163
|
+
|
|
2164
|
+
if (Number.isNaN(Number(name))) {
|
|
2165
|
+
o.incFns[name] = 0;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
if (
|
|
2169
|
+
Number.isNaN(Number(name)) &&
|
|
2170
|
+
o.incCache &&
|
|
2171
|
+
(sources[name].substring(0, 4) !== "http" || !o.incCors) &&
|
|
2172
|
+
window.location.protocol !== "file:" &&
|
|
2173
|
+
(sources[name].indexOf(".css") > no || sources[name].indexOf(".js") > no)
|
|
2174
|
+
) {
|
|
2175
|
+
// if cache is enabled
|
|
2176
|
+
const ls = localStorage,
|
|
2177
|
+
script = ls.getItem("oInc-" + name),
|
|
2178
|
+
cacheSavedTill = ls.getItem("oInc-" + name + "-expires"),
|
|
2179
|
+
cacheHash = ls.getItem("oInc-" + name + "-hash");
|
|
2180
|
+
|
|
2181
|
+
if (
|
|
2182
|
+
script &&
|
|
2183
|
+
cacheSavedTill &&
|
|
2184
|
+
Date.now() < cacheSavedTill &&
|
|
2185
|
+
cacheHash === hash
|
|
2186
|
+
) {
|
|
2187
|
+
// load from cache if not preload
|
|
2188
|
+
if (!preload) {
|
|
2189
|
+
o.initState({
|
|
2190
|
+
tag,
|
|
2191
|
+
id: "oInc-" + setN + "-" + sourcesN,
|
|
2192
|
+
innerHTML: script,
|
|
2193
|
+
"data-o-inc": setN,
|
|
2194
|
+
}).appendInside("head");
|
|
2195
|
+
}
|
|
2196
|
+
fnsStatus[sourcesN].loaded = 1;
|
|
2197
|
+
o.incFns[name] = 1;
|
|
2198
|
+
sourcesReady++;
|
|
2199
|
+
} else {
|
|
2200
|
+
// loading and caching new files
|
|
2201
|
+
fnId[name] = sourcesN;
|
|
2202
|
+
o.get(sources[name], { mode: o.incCors ? "cors" : "same-origin" }).then(
|
|
2203
|
+
(response) => {
|
|
2204
|
+
if (response.status !== 200) {
|
|
2205
|
+
o.onError
|
|
2206
|
+
? o.onError({
|
|
2207
|
+
message: o.incSource + sources[name] + " was not loaded",
|
|
2208
|
+
})
|
|
2209
|
+
: "";
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
response.text().then((script) => {
|
|
2213
|
+
ls.setItem("oInc-" + name, script);
|
|
2214
|
+
ls.setItem("oInc-" + name + "-expires", Date.now() + o.incCacheExp);
|
|
2215
|
+
ls.setItem("oInc-" + name + "-hash", hash);
|
|
2216
|
+
// execute if not preload
|
|
2217
|
+
if (!preload) {
|
|
2218
|
+
o.initState({
|
|
2219
|
+
tag,
|
|
2220
|
+
id: "oInc-" + setN + "-" + fnId[name],
|
|
2221
|
+
innerHTML: script,
|
|
2222
|
+
"data-o-inc": setN,
|
|
2223
|
+
}).appendInside("head");
|
|
2224
|
+
}
|
|
2225
|
+
o.incCheck(setN, fnId[name], 1);
|
|
2226
|
+
});
|
|
2227
|
+
},
|
|
2228
|
+
);
|
|
2229
|
+
}
|
|
2230
|
+
} else {
|
|
2231
|
+
// standart loading without caching
|
|
2232
|
+
const state = {
|
|
2233
|
+
tag,
|
|
2234
|
+
id: "oInc-" + setN + "-" + sourcesN,
|
|
2235
|
+
"data-o-inc": setN,
|
|
2236
|
+
async: o.incAsync,
|
|
2237
|
+
onload: "o.incCheck(" + setN + "," + sourcesN + ",1)",
|
|
2238
|
+
};
|
|
2239
|
+
if (sources[name].indexOf(".css") > no) {
|
|
2240
|
+
state.tag = "link";
|
|
2241
|
+
state.rel = "stylesheet";
|
|
2242
|
+
state.href = sources[name];
|
|
2243
|
+
} else if (sources[name].indexOf(".js") > no) {
|
|
2244
|
+
state.src = sources[name];
|
|
2245
|
+
} else {
|
|
2246
|
+
state.tag = "img";
|
|
2247
|
+
state.style = "display:none;";
|
|
2248
|
+
state.src = sources[name];
|
|
2249
|
+
}
|
|
2250
|
+
o.initState(state).appendInside(state.style ? "body" : "head");
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
fnsStatus[0] += sourcesReady;
|
|
2256
|
+
|
|
2257
|
+
if (sourcesN !== 0) {
|
|
2258
|
+
if (sourcesReady === sourcesN) {
|
|
2259
|
+
// if everything included
|
|
2260
|
+
if (typeof callBack === f) {
|
|
2261
|
+
callBack(setN);
|
|
2262
|
+
}
|
|
2263
|
+
} else {
|
|
2264
|
+
// starting timeout for loading
|
|
2265
|
+
setTimeout(
|
|
2266
|
+
(set) => {
|
|
2267
|
+
if (o.incReady[set] && o.incReady[set].length < o.incReady[set][0]) {
|
|
2268
|
+
o.incSet[set] = 0;
|
|
2269
|
+
if (typeof callBad === f) {
|
|
2270
|
+
callBad(setN);
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
},
|
|
2274
|
+
o.incTimeout,
|
|
2275
|
+
setN,
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
return o.incSet[0];
|
|
2281
|
+
};
|
|
2282
|
+
|
|
2283
|
+
// ─── Store adapters (always present, prod + dev) ──────────────────────────
|
|
2284
|
+
|
|
2285
|
+
/**
|
|
2286
|
+
* Connect a Redux store to an Objs component state.
|
|
2287
|
+
* Calls component[state](selector(store.getState())) immediately and on every store change.
|
|
2288
|
+
* Multiple connections per component are allowed — each covers a different state/slice.
|
|
2289
|
+
*
|
|
2290
|
+
* @param {Object} store - Redux store (must have .getState() and .subscribe())
|
|
2291
|
+
* @param {Function} selector - (storeState) => sliceData
|
|
2292
|
+
* @param {Object} component - Objs instance (result of o.init(...).render())
|
|
2293
|
+
* @param {string} [state='render'] - Name of the component state method to call with the data
|
|
2294
|
+
* @returns {Function} Unsubscribe function
|
|
2295
|
+
* @example
|
|
2296
|
+
* const card = o.init(cardStates).render();
|
|
2297
|
+
* const unsub = o.connectRedux(store, s => s.userName, card, 'updateName');
|
|
2298
|
+
* // later: unsub() to disconnect
|
|
2299
|
+
*/
|
|
2300
|
+
o.connectRedux = (store, selector, component, state = "render") => {
|
|
2301
|
+
o.verify([
|
|
2302
|
+
[store, ["object"]],
|
|
2303
|
+
[selector, ["function"]],
|
|
2304
|
+
[component, ["object"]],
|
|
2305
|
+
[state, ["notEmptyString"]],
|
|
2306
|
+
]);
|
|
2307
|
+
const update = () => {
|
|
2308
|
+
if (typeof component[state] === "function") {
|
|
2309
|
+
const val = selector(store.getState());
|
|
2310
|
+
component[state](val !== null && typeof val === "object" ? val : { value: val });
|
|
2311
|
+
}
|
|
2312
|
+
};
|
|
2313
|
+
update(); // fire immediately with current state
|
|
2314
|
+
return store.subscribe(update);
|
|
2315
|
+
};
|
|
2316
|
+
|
|
2317
|
+
/**
|
|
2318
|
+
* Connect a MobX observable to an Objs component state via autorun.
|
|
2319
|
+
* Accepts mobx as first param to avoid a hard dependency.
|
|
2320
|
+
*
|
|
2321
|
+
* @param {Object} mobx - MobX instance (must have .autorun())
|
|
2322
|
+
* @param {Object} observable - MobX observable object
|
|
2323
|
+
* @param {Function} accessor - (observable) => sliceData
|
|
2324
|
+
* @param {Object} component - Objs instance
|
|
2325
|
+
* @param {string} [state='render'] - Component state method name
|
|
2326
|
+
* @returns {Function} Disposer function (call to stop observing)
|
|
2327
|
+
* @example
|
|
2328
|
+
* const unsub = o.connectMobX(mobx, appStore, s => s.count, counter, 'updateCount');
|
|
2329
|
+
*/
|
|
2330
|
+
o.connectMobX = (mobx, observable, accessor, component, state = "render") => {
|
|
2331
|
+
o.verify([
|
|
2332
|
+
[mobx, ["object"]],
|
|
2333
|
+
[observable, ["object"]],
|
|
2334
|
+
[accessor, ["function"]],
|
|
2335
|
+
[component, ["object"]],
|
|
2336
|
+
[state, ["notEmptyString"]],
|
|
2337
|
+
]);
|
|
2338
|
+
return mobx.autorun(() => {
|
|
2339
|
+
if (typeof component[state] === "function") {
|
|
2340
|
+
const val = accessor(observable);
|
|
2341
|
+
component[state](val !== null && typeof val === "object" ? val : { value: val });
|
|
2342
|
+
}
|
|
2343
|
+
});
|
|
2344
|
+
};
|
|
2345
|
+
|
|
2346
|
+
/**
|
|
2347
|
+
* Default React context object for bridging Objs with React trees.
|
|
2348
|
+
* Use as the value of React.createContext() in your app.
|
|
2349
|
+
* @type {Object}
|
|
2350
|
+
*/
|
|
2351
|
+
o.ObjsContext = null;
|
|
2352
|
+
|
|
2353
|
+
/**
|
|
2354
|
+
* Create a React HOC that connects a React Context value to an Objs component.
|
|
2355
|
+
* The returned component renders nothing — it is a pure side-effect bridge.
|
|
2356
|
+
*
|
|
2357
|
+
* @param {Object} React - React instance
|
|
2358
|
+
* @param {Object} Context - React context created with React.createContext()
|
|
2359
|
+
* @param {Function} selector - (contextValue) => data to pass to component state
|
|
2360
|
+
* @param {Object} component - Objs instance
|
|
2361
|
+
* @param {string} [state='render'] - Component state method name
|
|
2362
|
+
* @returns {Function} React functional component (bridge)
|
|
2363
|
+
* @example
|
|
2364
|
+
* const CartBridge = o.withReactContext(React, CartContext, ctx => ctx.count, menuCart, 'updateCount');
|
|
2365
|
+
* // Mount <CartBridge /> anywhere inside <CartContext.Provider>
|
|
2366
|
+
*/
|
|
2367
|
+
o.withReactContext = (React, Context, selector, component, state = "render") => {
|
|
2368
|
+
return function ObjsContextBridge() {
|
|
2369
|
+
const value = React.useContext(Context);
|
|
2370
|
+
React.useEffect(() => {
|
|
2371
|
+
if (typeof component[state] === "function") {
|
|
2372
|
+
const val = selector(value);
|
|
2373
|
+
component[state](val !== null && typeof val === "object" ? val : { value: val });
|
|
2374
|
+
}
|
|
2375
|
+
}, [value]);
|
|
2376
|
+
return null;
|
|
2377
|
+
};
|
|
2378
|
+
};
|
|
2379
|
+
|
|
2380
|
+
if (__DEV__) {
|
|
2381
|
+
o.debug = false; // on and off debug output
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
/* tests function parameters */
|
|
2385
|
+
o.tLog = []; // test sessions and results
|
|
2386
|
+
o.tRes = []; // test results
|
|
2387
|
+
o.tStatus = []; // test statuses
|
|
2388
|
+
o.tFns = []; // callbacks
|
|
2389
|
+
o.tShowOk = o.F; // show success tests or only errors
|
|
2390
|
+
o.tStyled = o.F; // styled HTML results or plain style
|
|
2391
|
+
o.tTime = 2000; // timeout for async tests
|
|
2392
|
+
o.tests = []; // tests with storage
|
|
2393
|
+
o.tAutolog = o.F; // auto log to console
|
|
2394
|
+
o.tBeforeEach = undefined; // called before each test case
|
|
2395
|
+
o.tAfterEach = undefined; // called after each test case
|
|
2396
|
+
|
|
2397
|
+
/**
|
|
2398
|
+
* Add test
|
|
2399
|
+
* @param {string} title - Test title
|
|
2400
|
+
* @param {...Array} tests - Test cases
|
|
2401
|
+
* @returns {Object} Test object with run method and testId
|
|
2402
|
+
*/
|
|
2403
|
+
o.addTest = (title, ...tests) => {
|
|
2404
|
+
o.verify([
|
|
2405
|
+
[title, ["notEmptyString"]],
|
|
2406
|
+
[tests, ["array"]],
|
|
2407
|
+
]);
|
|
2408
|
+
// Support {before, after} hooks object as last argument
|
|
2409
|
+
let hooks = {};
|
|
2410
|
+
if (
|
|
2411
|
+
tests.length &&
|
|
2412
|
+
typeof tests[tests.length - 1] === "object" &&
|
|
2413
|
+
!Array.isArray(tests[tests.length - 1])
|
|
2414
|
+
) {
|
|
2415
|
+
hooks = tests.pop();
|
|
2416
|
+
}
|
|
2417
|
+
const testId = o.tests.length;
|
|
2418
|
+
o.tests[testId] = { title, tests, hooks };
|
|
2419
|
+
|
|
2420
|
+
return {
|
|
2421
|
+
run: () => {
|
|
2422
|
+
if (typeof hooks.before === "function") {
|
|
2423
|
+
o.tBeforeEach = hooks.before;
|
|
2424
|
+
}
|
|
2425
|
+
if (typeof hooks.after === "function") {
|
|
2426
|
+
o.tAfterEach = hooks.after;
|
|
2427
|
+
}
|
|
2428
|
+
o.runTest(testId);
|
|
2429
|
+
},
|
|
2430
|
+
autorun: () => {
|
|
2431
|
+
if (typeof hooks.before === "function") {
|
|
2432
|
+
o.tBeforeEach = hooks.before;
|
|
2433
|
+
}
|
|
2434
|
+
if (typeof hooks.after === "function") {
|
|
2435
|
+
o.tAfterEach = hooks.after;
|
|
2436
|
+
}
|
|
2437
|
+
o.runTest(testId, true);
|
|
2438
|
+
},
|
|
2439
|
+
testId,
|
|
2440
|
+
};
|
|
2441
|
+
};
|
|
2442
|
+
|
|
2443
|
+
/**
|
|
2444
|
+
* Load Logs from session storage
|
|
2445
|
+
* @returns {Array} Logs from sessionStorage
|
|
2446
|
+
*/
|
|
2447
|
+
o.updateLogs = () => {
|
|
2448
|
+
for (let i = 0; i < o.tests.length; i++) {
|
|
2449
|
+
o.tLog[i] = o.tLog[testN] = sessionStorage.getItem(`oTest-Log-${testN}`) || "";
|
|
2450
|
+
}
|
|
2451
|
+
return o.tLog;
|
|
2452
|
+
};
|
|
2453
|
+
|
|
2454
|
+
/**
|
|
2455
|
+
* Run test by ID and autorun
|
|
2456
|
+
* @param {number} testId - Test ID
|
|
2457
|
+
* @param {boolean} autoRun - Whether to autorun for reload page tests
|
|
2458
|
+
*/
|
|
2459
|
+
o.runTest = (testId = 0, autoRun, savePrev) => {
|
|
2460
|
+
o.verify([
|
|
2461
|
+
[testId, ["number"]],
|
|
2462
|
+
[autoRun, ["boolean", "undefined"]],
|
|
2463
|
+
[savePrev, ["boolean", "undefined"]],
|
|
2464
|
+
]);
|
|
2465
|
+
if (!o.tests[testId]) {
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
if (!savePrev) {
|
|
2470
|
+
sessionStorage?.removeItem(`oTest-Log-${testId}`);
|
|
2471
|
+
sessionStorage?.removeItem(`oTest-Res-${testId}`);
|
|
2472
|
+
sessionStorage?.removeItem(`oTest-Status-${testId}`);
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
// run with sessionStorage cache
|
|
2476
|
+
sessionStorage?.setItem(`oTest-Run`, testId);
|
|
2477
|
+
|
|
2478
|
+
if (autoRun) {
|
|
2479
|
+
sessionStorage?.setItem(`oTest-Autorun`, autoRun);
|
|
2480
|
+
} else {
|
|
2481
|
+
sessionStorage?.removeItem(`oTest-Autorun`);
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
const testSession = o.tests[testId];
|
|
2485
|
+
let lastTest = testSession.tests.pop();
|
|
2486
|
+
|
|
2487
|
+
// creates callback function with autostop/autorun
|
|
2488
|
+
if (typeof lastTest !== "function") {
|
|
2489
|
+
testSession.tests.push(lastTest);
|
|
2490
|
+
lastTest = () => {};
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
testSession.tests.push((testN) => {
|
|
2494
|
+
lastTest(testN);
|
|
2495
|
+
sessionStorage.setItem("dddd", 1);
|
|
2496
|
+
sessionStorage?.removeItem(`oTest-Run`);
|
|
2497
|
+
if (autoRun) {
|
|
2498
|
+
o.runTest(testId + 1, autoRun);
|
|
2499
|
+
}
|
|
2500
|
+
});
|
|
2501
|
+
|
|
2502
|
+
o.test(testSession.title, ...testSession.tests);
|
|
2503
|
+
};
|
|
2504
|
+
|
|
2505
|
+
// service
|
|
2506
|
+
o.tPre = '<div style="font-family:monospace;text-align:left;">';
|
|
2507
|
+
o.tOk = '<span style="background:#cfc;padding: 0 15px;">OK</span> ';
|
|
2508
|
+
o.tXx = '<div style="background:#fcc;padding:3px;">';
|
|
2509
|
+
o.tDc = "</div>";
|
|
2510
|
+
|
|
2511
|
+
/**
|
|
2512
|
+
* Run test with title and test cases
|
|
2513
|
+
* @param {string} title - Test title
|
|
2514
|
+
* @param {...Array} tests - Array of test cases, each containing test title and test function/result
|
|
2515
|
+
* @returns {Object} Test control object with run method and testId
|
|
2516
|
+
*/
|
|
2517
|
+
o.test = (title = "", ...tests) => {
|
|
2518
|
+
o.verify([
|
|
2519
|
+
[title, ["notEmptyString"]],
|
|
2520
|
+
[tests, ["array"]],
|
|
2521
|
+
]);
|
|
2522
|
+
// get test ID from sessionStorage
|
|
2523
|
+
const testSession = sessionStorage?.getItem(`oTest-Run`);
|
|
2524
|
+
const testN = testSession || o.tLog.length;
|
|
2525
|
+
let waits = 0,
|
|
2526
|
+
preOk = "├ OK: ",
|
|
2527
|
+
preXx = "├ ✘ ",
|
|
2528
|
+
posOk = "\n",
|
|
2529
|
+
posXx = "\n",
|
|
2530
|
+
row = "",
|
|
2531
|
+
num = tests.length,
|
|
2532
|
+
done = 0;
|
|
2533
|
+
|
|
2534
|
+
const log = (line = "", error = false, log = false) => {
|
|
2535
|
+
if (o.tAutolog) {
|
|
2536
|
+
if (error) {
|
|
2537
|
+
console.error(line);
|
|
2538
|
+
} else if (o.tShowOk || log) {
|
|
2539
|
+
console.log(line);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
};
|
|
2543
|
+
|
|
2544
|
+
if (typeof tests[num - 1] === "function") {
|
|
2545
|
+
o.tFns[testN] = tests[num - 1];
|
|
2546
|
+
num--;
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
// get tLog from sessionStorage
|
|
2550
|
+
if (testSession) {
|
|
2551
|
+
o.tLog[testN] = sessionStorage.getItem(`oTest-Log-${testN}`) || "";
|
|
2552
|
+
o.tRes[testN] = sessionStorage.getItem(`oTest-Res-${testN}`) || false;
|
|
2553
|
+
o.tStatus[testN] = JSON.parse(
|
|
2554
|
+
sessionStorage.getItem(`oTest-Status-${testN}`) || "[]",
|
|
2555
|
+
);
|
|
2556
|
+
for (let i = 0; i < o.tStatus[testN].length; i++) {
|
|
2557
|
+
const s = o.tStatus[testN][i];
|
|
2558
|
+
if (s === true || s === false) done++;
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
if (o.tStyled) {
|
|
2563
|
+
preOk = o.tPre + o.tOk;
|
|
2564
|
+
preXx = o.tPre + o.tXx;
|
|
2565
|
+
posOk = o.tDc;
|
|
2566
|
+
posXx = posOk + posOk;
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
if (!testSession || o.tLog[testN].length === 0) {
|
|
2570
|
+
log("╒ " + title + " #" + testN, false, true);
|
|
2571
|
+
|
|
2572
|
+
if (o.tStyled) {
|
|
2573
|
+
o.tLog[testN] = "<div><b>" + title + " #" + testN + "</b></div>";
|
|
2574
|
+
} else {
|
|
2575
|
+
o.tLog[testN] = "╒ " + title + " #" + testN + "\n";
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
o.tRes[testN] = o.F;
|
|
2579
|
+
o.tStatus[testN] = [];
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
for (let i = o.tStatus[testN].length; i < num; i++) {
|
|
2583
|
+
const testInfo = {
|
|
2584
|
+
n: testN,
|
|
2585
|
+
i,
|
|
2586
|
+
title: tests[i][0],
|
|
2587
|
+
tShowOk: o.tShowOk,
|
|
2588
|
+
tStyled: o.tStyled,
|
|
2589
|
+
};
|
|
2590
|
+
|
|
2591
|
+
// test or title only
|
|
2592
|
+
let res = tests[i][1];
|
|
2593
|
+
if (typeof res === "undefined") {
|
|
2594
|
+
if (o.tStyled) {
|
|
2595
|
+
o.tLog[testN] += "<div>" + testInfo.title + "</div>";
|
|
2596
|
+
} else {
|
|
2597
|
+
o.tLog[testN] += testInfo.title + "\n";
|
|
2598
|
+
}
|
|
2599
|
+
log("├ " + testInfo.title, false, true);
|
|
2600
|
+
o.tStatus[testN][i] = true;
|
|
2601
|
+
done++;
|
|
2602
|
+
continue;
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
if (typeof o.tBeforeEach === "function") {
|
|
2606
|
+
o.tBeforeEach(testInfo);
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
if (typeof res === "function") {
|
|
2610
|
+
try {
|
|
2611
|
+
res = res(testInfo);
|
|
2612
|
+
} catch (error) {
|
|
2613
|
+
res = error.message;
|
|
2614
|
+
if (o.onError) {
|
|
2615
|
+
o.onError(error);
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
if (typeof o.tAfterEach === "function") {
|
|
2621
|
+
o.tAfterEach(testInfo, res);
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
// async step: wait for Promise then call testUpdate
|
|
2625
|
+
if (res && typeof res.then === "function") {
|
|
2626
|
+
waits++;
|
|
2627
|
+
const timeoutId = setTimeout(() => {
|
|
2628
|
+
testInfo.title += " (timeout)";
|
|
2629
|
+
o.testUpdate(testInfo);
|
|
2630
|
+
}, o.tTime);
|
|
2631
|
+
res
|
|
2632
|
+
.then((value) => {
|
|
2633
|
+
clearTimeout(timeoutId);
|
|
2634
|
+
const ok = value === true || (value && value.ok === true);
|
|
2635
|
+
const msg =
|
|
2636
|
+
value && value.errors && value.errors.length
|
|
2637
|
+
? value.errors.join("; ")
|
|
2638
|
+
: typeof value === "string"
|
|
2639
|
+
? value
|
|
2640
|
+
: "";
|
|
2641
|
+
o.testUpdate(testInfo, ok, ok ? "" : msg ? ": " + msg : "");
|
|
2642
|
+
})
|
|
2643
|
+
.catch((err) => {
|
|
2644
|
+
clearTimeout(timeoutId);
|
|
2645
|
+
o.testUpdate(testInfo, false, err.message || "Promise rejected");
|
|
2646
|
+
});
|
|
2647
|
+
continue;
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
// test processing
|
|
2651
|
+
if (typeof o.tStatus[testN][i] === "undefined") {
|
|
2652
|
+
o.tStatus[testN][i] = typeof res === "string" ? o.F : res;
|
|
2653
|
+
} else {
|
|
2654
|
+
// stop for reload test
|
|
2655
|
+
sessionStorage.setItem(`oTest-Status-${testN}`, JSON.stringify(o.tStatus[testN]));
|
|
2656
|
+
return; // reloading...
|
|
2657
|
+
}
|
|
2658
|
+
if (res === true) {
|
|
2659
|
+
done++;
|
|
2660
|
+
if (o.tShowOk) {
|
|
2661
|
+
o.tLog[testN] += preOk + tests[i][0] + posOk;
|
|
2662
|
+
log("├ OK: " + tests[i][0]);
|
|
2663
|
+
}
|
|
2664
|
+
} else if (res !== o.U) {
|
|
2665
|
+
o.tLog[testN] += preXx + tests[i][0] + (res !== o.F ? ": " + res : "") + posXx;
|
|
2666
|
+
log("├ ✘ " + tests[i][0] + (res !== o.F ? ": " + res : ""), true);
|
|
2667
|
+
} else {
|
|
2668
|
+
waits++;
|
|
2669
|
+
setTimeout(
|
|
2670
|
+
(info) => {
|
|
2671
|
+
info.title += " (timeout)";
|
|
2672
|
+
o.testUpdate(info);
|
|
2673
|
+
},
|
|
2674
|
+
o.tTime,
|
|
2675
|
+
testInfo,
|
|
2676
|
+
);
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
o.tRes[testN] = done === num;
|
|
2681
|
+
row = waits ? "├ " : "╘ ";
|
|
2682
|
+
row += "DONE " + done + "/" + num + (waits ? ", waiting: " + waits : "");
|
|
2683
|
+
log(row, done + waits !== num);
|
|
2684
|
+
if (!waits) {
|
|
2685
|
+
log();
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
if (o.tStyled) {
|
|
2689
|
+
o.tLog[testN] +=
|
|
2690
|
+
o.tPre +
|
|
2691
|
+
'<div style="color:' +
|
|
2692
|
+
(done + waits !== num ? "red" : "green") +
|
|
2693
|
+
';"><b>DONE ' +
|
|
2694
|
+
done +
|
|
2695
|
+
"/" +
|
|
2696
|
+
num +
|
|
2697
|
+
(waits ? ", waiting: " + waits : "") +
|
|
2698
|
+
"</b>" +
|
|
2699
|
+
o.tDc +
|
|
2700
|
+
o.tDc;
|
|
2701
|
+
} else {
|
|
2702
|
+
o.tLog[testN] += row + "\n";
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
// Save test results to sessionStorage
|
|
2706
|
+
if (testSession) {
|
|
2707
|
+
sessionStorage.setItem(`oTest-Log-${testN}`, o.tLog[testN]);
|
|
2708
|
+
sessionStorage.setItem(`oTest-Res-${testN}`, o.tRes[testN]);
|
|
2709
|
+
sessionStorage.setItem(`oTest-Status-${testN}`, JSON.stringify(o.tStatus[testN]));
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
if (!waits && typeof o.tFns[testN] === "function") {
|
|
2713
|
+
o.tFns[testN](testN);
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
return testN;
|
|
2717
|
+
};
|
|
2718
|
+
|
|
2719
|
+
/**
|
|
2720
|
+
* Update test state
|
|
2721
|
+
* @param {Object} info - Test info
|
|
2722
|
+
* @param {boolean} res - Test result
|
|
2723
|
+
* @param {string} suff - Suffix
|
|
2724
|
+
*/
|
|
2725
|
+
o.testUpdate = (info, res = o.F, suff = "") => {
|
|
2726
|
+
o.verify([
|
|
2727
|
+
[info, ["object"]],
|
|
2728
|
+
[res, ["boolean", "string"]],
|
|
2729
|
+
[suff, ["string"]],
|
|
2730
|
+
]);
|
|
2731
|
+
let row = "";
|
|
2732
|
+
const testN = info.n;
|
|
2733
|
+
const log = (line = "", error = false) => {
|
|
2734
|
+
if (o.tAutolog) {
|
|
2735
|
+
if (error) {
|
|
2736
|
+
console.error(line);
|
|
2737
|
+
} else {
|
|
2738
|
+
console.log(line);
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
};
|
|
2742
|
+
|
|
2743
|
+
if (o.tStatus[testN][info.i] === o.U || o.tStatus[testN][info.i] === null) {
|
|
2744
|
+
o.tStatus[testN][info.i] = res === true;
|
|
2745
|
+
if (res === true) {
|
|
2746
|
+
if (info.tShowOk) {
|
|
2747
|
+
row = "├ OK: " + info.title + suff;
|
|
2748
|
+
log(row);
|
|
2749
|
+
if (info.tStyled) {
|
|
2750
|
+
o.tLog[testN] += o.tPre + o.tOk + info.title + suff + o.tDc;
|
|
2751
|
+
} else {
|
|
2752
|
+
o.tLog[testN] += row + "\n";
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
} else {
|
|
2756
|
+
o.tRes[testN] = o.F;
|
|
2757
|
+
row = "├ ✘ " + info.title + (res ? ": " + res : "") + suff;
|
|
2758
|
+
log(row, true);
|
|
2759
|
+
if (info.tStyled) {
|
|
2760
|
+
o.tLog[testN] +=
|
|
2761
|
+
o.tPre + o.tXx + info.title + suff + (res ? ": " + res : "") + o.tDc + o.tDc;
|
|
2762
|
+
} else {
|
|
2763
|
+
o.tLog[testN] += row + "\n";
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
let fails = 0,
|
|
2768
|
+
n = 0;
|
|
2769
|
+
|
|
2770
|
+
for (const s of o.tStatus[testN]) {
|
|
2771
|
+
if (s === o.U || s === null) {
|
|
2772
|
+
// if waiting tests there (null = restored from JSON)
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
2775
|
+
if (!s) {
|
|
2776
|
+
fails++;
|
|
2777
|
+
}
|
|
2778
|
+
n++;
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
// if test is in progress and not completed
|
|
2782
|
+
if (sessionStorage?.getItem("oTest-Run") === testN) {
|
|
2783
|
+
// save test results to sessionStorage
|
|
2784
|
+
sessionStorage.setItem(`oTest-Log-${testN}`, o.tLog[testN]);
|
|
2785
|
+
sessionStorage.setItem(`oTest-Res-${testN}`, o.tRes[testN]);
|
|
2786
|
+
sessionStorage.setItem(`oTest-Status-${testN}`, JSON.stringify(o.tStatus[testN]));
|
|
2787
|
+
|
|
2788
|
+
if (n < o.tests[testN].tests.length) {
|
|
2789
|
+
return;
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
o.tRes[testN] = !fails;
|
|
2794
|
+
row = fails ? "FAILED " + fails + "/" + n : "DONE " + n + "/" + n;
|
|
2795
|
+
log("╘ " + row, Boolean(fails));
|
|
2796
|
+
log();
|
|
2797
|
+
|
|
2798
|
+
if (info.tStyled) {
|
|
2799
|
+
o.tLog[testN] +=
|
|
2800
|
+
o.tPre +
|
|
2801
|
+
'<b style="color:' +
|
|
2802
|
+
(!fails ? "green" : "red") +
|
|
2803
|
+
';">' +
|
|
2804
|
+
row +
|
|
2805
|
+
"</b>" +
|
|
2806
|
+
o.tDc;
|
|
2807
|
+
} else {
|
|
2808
|
+
o.tLog[testN] += "╘ " + row + "\n";
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
if (typeof o.tFns[testN] === "function") {
|
|
2812
|
+
o.tFns[testN](testN);
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
};
|
|
2816
|
+
|
|
2817
|
+
if (sessionStorage?.getItem("oTest-Run")) {
|
|
2818
|
+
window?.addEventListener(
|
|
2819
|
+
"load",
|
|
2820
|
+
() => {
|
|
2821
|
+
o.runTest(
|
|
2822
|
+
sessionStorage?.getItem("oTest-Run"),
|
|
2823
|
+
sessionStorage?.getItem("oTest-Autorun") || o.F,
|
|
2824
|
+
true,
|
|
2825
|
+
);
|
|
2826
|
+
},
|
|
2827
|
+
false,
|
|
2828
|
+
);
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
/**
|
|
2832
|
+
* Measure element dimensions and visibility
|
|
2833
|
+
* @param {Element} el - DOM element
|
|
2834
|
+
* @returns {{width:number, height:number, top:number, left:number, visible:boolean, opacity:string, zIndex:string}}
|
|
2835
|
+
*/
|
|
2836
|
+
o.measure = (el) => {
|
|
2837
|
+
if (!el) {
|
|
2838
|
+
return {};
|
|
2839
|
+
}
|
|
2840
|
+
const rect = el.getBoundingClientRect();
|
|
2841
|
+
const style = window.getComputedStyle(el);
|
|
2842
|
+
return {
|
|
2843
|
+
width: rect.width,
|
|
2844
|
+
height: rect.height,
|
|
2845
|
+
top: rect.top,
|
|
2846
|
+
left: rect.left,
|
|
2847
|
+
visible: style.display !== "none" && style.visibility !== "hidden" && rect.width > 0,
|
|
2848
|
+
opacity: style.opacity,
|
|
2849
|
+
zIndex: style.zIndex,
|
|
2850
|
+
};
|
|
2851
|
+
};
|
|
2852
|
+
|
|
2853
|
+
/**
|
|
2854
|
+
* Assert element is visible — returns true/false for use in o.test()
|
|
2855
|
+
* @param {Element} el
|
|
2856
|
+
* @returns {boolean}
|
|
2857
|
+
*/
|
|
2858
|
+
o.assertVisible = (el) => {
|
|
2859
|
+
const m = o.measure(el);
|
|
2860
|
+
return Boolean(m.visible);
|
|
2861
|
+
};
|
|
2862
|
+
|
|
2863
|
+
/**
|
|
2864
|
+
* Assert element matches expected size, padding, or margin (design system / UI verification).
|
|
2865
|
+
* @param {Element} el
|
|
2866
|
+
* @param {{w?: number, h?: number, padding?: number, paddingTop?: number, paddingRight?: number, paddingBottom?: number, paddingLeft?: number, margin?: number, marginTop?: number, marginRight?: number, marginBottom?: number, marginLeft?: number}} expected
|
|
2867
|
+
* @returns {boolean|string}
|
|
2868
|
+
*/
|
|
2869
|
+
o.assertSize = (el, expected = {}) => {
|
|
2870
|
+
if (!el) return "element is null";
|
|
2871
|
+
const m = o.measure(el);
|
|
2872
|
+
if (expected.w !== undefined && Math.round(m.width) !== expected.w) {
|
|
2873
|
+
return `width: expected ${expected.w}, got ${Math.round(m.width)}`;
|
|
2874
|
+
}
|
|
2875
|
+
if (expected.h !== undefined && Math.round(m.height) !== expected.h) {
|
|
2876
|
+
return `height: expected ${expected.h}, got ${Math.round(m.height)}`;
|
|
2877
|
+
}
|
|
2878
|
+
const getPx = (prop) => {
|
|
2879
|
+
const v = window.getComputedStyle(el).getPropertyValue(prop);
|
|
2880
|
+
return v ? parseFloat(v) : 0;
|
|
2881
|
+
};
|
|
2882
|
+
if (expected.padding !== undefined && getPx("padding-top") !== expected.padding) {
|
|
2883
|
+
return `padding: expected ${expected.padding}, got ${getPx("padding-top")}`;
|
|
2884
|
+
}
|
|
2885
|
+
if (expected.paddingTop !== undefined && getPx("padding-top") !== expected.paddingTop) {
|
|
2886
|
+
return `paddingTop: expected ${expected.paddingTop}, got ${getPx("padding-top")}`;
|
|
2887
|
+
}
|
|
2888
|
+
if (
|
|
2889
|
+
expected.paddingRight !== undefined &&
|
|
2890
|
+
getPx("padding-right") !== expected.paddingRight
|
|
2891
|
+
) {
|
|
2892
|
+
return `paddingRight: expected ${expected.paddingRight}, got ${getPx("padding-right")}`;
|
|
2893
|
+
}
|
|
2894
|
+
if (
|
|
2895
|
+
expected.paddingBottom !== undefined &&
|
|
2896
|
+
getPx("padding-bottom") !== expected.paddingBottom
|
|
2897
|
+
) {
|
|
2898
|
+
return `paddingBottom: expected ${expected.paddingBottom}, got ${getPx("padding-bottom")}`;
|
|
2899
|
+
}
|
|
2900
|
+
if (
|
|
2901
|
+
expected.paddingLeft !== undefined &&
|
|
2902
|
+
getPx("padding-left") !== expected.paddingLeft
|
|
2903
|
+
) {
|
|
2904
|
+
return `paddingLeft: expected ${expected.paddingLeft}, got ${getPx("padding-left")}`;
|
|
2905
|
+
}
|
|
2906
|
+
if (expected.margin !== undefined && getPx("margin-top") !== expected.margin) {
|
|
2907
|
+
return `margin: expected ${expected.margin}, got ${getPx("margin-top")}`;
|
|
2908
|
+
}
|
|
2909
|
+
if (expected.marginTop !== undefined && getPx("margin-top") !== expected.marginTop) {
|
|
2910
|
+
return `marginTop: expected ${expected.marginTop}, got ${getPx("margin-top")}`;
|
|
2911
|
+
}
|
|
2912
|
+
if (
|
|
2913
|
+
expected.marginRight !== undefined &&
|
|
2914
|
+
getPx("margin-right") !== expected.marginRight
|
|
2915
|
+
) {
|
|
2916
|
+
return `marginRight: expected ${expected.marginRight}, got ${getPx("margin-right")}`;
|
|
2917
|
+
}
|
|
2918
|
+
if (
|
|
2919
|
+
expected.marginBottom !== undefined &&
|
|
2920
|
+
getPx("margin-bottom") !== expected.marginBottom
|
|
2921
|
+
) {
|
|
2922
|
+
return `marginBottom: expected ${expected.marginBottom}, got ${getPx("margin-bottom")}`;
|
|
2923
|
+
}
|
|
2924
|
+
if (expected.marginLeft !== undefined && getPx("margin-left") !== expected.marginLeft) {
|
|
2925
|
+
return `marginLeft: expected ${expected.marginLeft}, got ${getPx("margin-left")}`;
|
|
2926
|
+
}
|
|
2927
|
+
return true;
|
|
2928
|
+
};
|
|
2929
|
+
|
|
2930
|
+
/**
|
|
2931
|
+
* Recorder state for user action capture.
|
|
2932
|
+
* Available in all builds so QA testers can record on staging/production.
|
|
2933
|
+
* Note: intercepts window.fetch and captures request/response data — review before using on production.
|
|
2934
|
+
*/
|
|
2935
|
+
o.recorder = {
|
|
2936
|
+
active: false,
|
|
2937
|
+
actions: [],
|
|
2938
|
+
mocks: {},
|
|
2939
|
+
initialData: {},
|
|
2940
|
+
assertions: [],
|
|
2941
|
+
observeRoot: null,
|
|
2942
|
+
_originalFetch: null,
|
|
2943
|
+
_listeners: [],
|
|
2944
|
+
_observer: null,
|
|
2945
|
+
};
|
|
2946
|
+
|
|
2947
|
+
/**
|
|
2948
|
+
* Start recording user interactions
|
|
2949
|
+
* @param {string} [observe] - CSS selector to scope the MutationObserver (reduces assertion noise)
|
|
2950
|
+
* @param {string[]} [events] - Events to record (default: click, mouseover, scroll, input, change)
|
|
2951
|
+
* @param {{[event: string]: number}} [timeouts] - Debounce delays per event type in ms
|
|
2952
|
+
*/
|
|
2953
|
+
o.startRecording = (observe, events, timeouts) => {
|
|
2954
|
+
if (o.recorder.active) {
|
|
2955
|
+
return;
|
|
2956
|
+
}
|
|
2957
|
+
const defaultEvents = ["click", "mouseover", "scroll", "input", "change"];
|
|
2958
|
+
const defaultStepDelays = {
|
|
2959
|
+
click: 100,
|
|
2960
|
+
mouseover: 50,
|
|
2961
|
+
scroll: 30,
|
|
2962
|
+
input: 50,
|
|
2963
|
+
change: 50,
|
|
2964
|
+
};
|
|
2965
|
+
const listenEvents = events || defaultEvents;
|
|
2966
|
+
const stepDelays = Object.assign({}, defaultStepDelays, timeouts || {});
|
|
2967
|
+
const captureDebounce = { scroll: 30, mouseover: 50 };
|
|
2968
|
+
const rec = o.recorder;
|
|
2969
|
+
rec.active = true;
|
|
2970
|
+
rec.actions = [];
|
|
2971
|
+
rec.mocks = {};
|
|
2972
|
+
rec.stepDelays = stepDelays;
|
|
2973
|
+
rec.initialData = { url: window.location.href, timestamp: Date.now() };
|
|
2974
|
+
|
|
2975
|
+
rec.observeRoot = observe || null;
|
|
2976
|
+
rec.assertions = [];
|
|
2977
|
+
|
|
2978
|
+
// snapshot current o.inits data
|
|
2979
|
+
o.inits.forEach((inst, idx) => {
|
|
2980
|
+
if (inst?.store) {
|
|
2981
|
+
rec.initialData["init_" + idx] = JSON.parse(JSON.stringify(inst.store));
|
|
2982
|
+
}
|
|
2983
|
+
});
|
|
2984
|
+
|
|
2985
|
+
// intercept fetch
|
|
2986
|
+
rec._originalFetch = window.fetch;
|
|
2987
|
+
window.fetch = async (url, opts = {}) => {
|
|
2988
|
+
const method = (opts.method || "GET").toUpperCase();
|
|
2989
|
+
let reqBody;
|
|
2990
|
+
try {
|
|
2991
|
+
reqBody = opts.body ? JSON.parse(opts.body) : undefined;
|
|
2992
|
+
} catch (_e) {
|
|
2993
|
+
reqBody = opts.body;
|
|
2994
|
+
}
|
|
2995
|
+
const response = await rec._originalFetch(url, opts);
|
|
2996
|
+
const clone = response.clone();
|
|
2997
|
+
let respBody;
|
|
2998
|
+
try {
|
|
2999
|
+
respBody = await clone.json();
|
|
3000
|
+
} catch (_e) {
|
|
3001
|
+
respBody = await clone.text().catch(() => null);
|
|
3002
|
+
}
|
|
3003
|
+
const key = method + ":" + url;
|
|
3004
|
+
rec.mocks[key] = {
|
|
3005
|
+
url,
|
|
3006
|
+
method,
|
|
3007
|
+
request: reqBody,
|
|
3008
|
+
response: respBody,
|
|
3009
|
+
status: response.status,
|
|
3010
|
+
};
|
|
3011
|
+
return response;
|
|
3012
|
+
};
|
|
3013
|
+
|
|
3014
|
+
// Internal Objs attributes must not be used for selectors (they change across restores).
|
|
3015
|
+
const unstableDataAttrs = { "data-o-init": 1, "data-o-init-i": 1, "data-o-state": 1 };
|
|
3016
|
+
const qualify = (sel, fromNode) => {
|
|
3017
|
+
if (o.D.querySelectorAll(sel).length <= 1) return sel;
|
|
3018
|
+
let node = fromNode;
|
|
3019
|
+
while (node && node !== o.D.body) {
|
|
3020
|
+
let ancestorSel = node.id ? "#" + node.id : null;
|
|
3021
|
+
if (!ancestorSel && node.attributes) {
|
|
3022
|
+
const autotagAttr = o.autotag ? "data-" + o.autotag : null;
|
|
3023
|
+
if (autotagAttr) {
|
|
3024
|
+
const val = node.getAttribute(autotagAttr);
|
|
3025
|
+
if (val != null) ancestorSel = `[${autotagAttr}="${val}"]`;
|
|
3026
|
+
}
|
|
3027
|
+
if (!ancestorSel) {
|
|
3028
|
+
for (const attr of node.attributes) {
|
|
3029
|
+
if (attr.name.startsWith("data-") && !unstableDataAttrs[attr.name]) {
|
|
3030
|
+
ancestorSel = `[${attr.name}="${attr.value}"]`;
|
|
3031
|
+
break;
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
if (ancestorSel) {
|
|
3037
|
+
const q = ancestorSel + " " + sel;
|
|
3038
|
+
if (o.D.querySelectorAll(q).length === 1) return q;
|
|
3039
|
+
}
|
|
3040
|
+
node = node.parentElement;
|
|
3041
|
+
}
|
|
3042
|
+
return sel;
|
|
3043
|
+
};
|
|
3044
|
+
|
|
3045
|
+
// Build a selector string for a DOM node (reuses qualify logic)
|
|
3046
|
+
const buildSelector = (node) => {
|
|
3047
|
+
if (!node || node.nodeType !== 1) return "";
|
|
3048
|
+
let sel = "";
|
|
3049
|
+
if (node.dataset) {
|
|
3050
|
+
const qaKey = o.autotag && node.dataset[o.autotag];
|
|
3051
|
+
if (qaKey) {
|
|
3052
|
+
sel = qualify(`[data-${o.autotag}="${qaKey}"]`, node.parentElement);
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
if (!sel && node.tagName) {
|
|
3056
|
+
const base = node.id
|
|
3057
|
+
? "#" + node.id
|
|
3058
|
+
: node.className
|
|
3059
|
+
? node.tagName.toLowerCase() + "." + [...node.classList].join(".")
|
|
3060
|
+
: node.tagName.toLowerCase();
|
|
3061
|
+
sel = node.id ? base : qualify(base, node.parentElement);
|
|
3062
|
+
}
|
|
3063
|
+
return sel;
|
|
3064
|
+
};
|
|
3065
|
+
|
|
3066
|
+
// Scoped MutationObserver: captures DOM mutations tied to the last recorded action
|
|
3067
|
+
const observeTarget = (observe && o.D.querySelector(observe)) || o.D.body;
|
|
3068
|
+
rec._observer = new MutationObserver((mutations) => {
|
|
3069
|
+
const actionIdx = rec.actions.length - 1;
|
|
3070
|
+
if (actionIdx < 0) return;
|
|
3071
|
+
mutations.forEach((m) => {
|
|
3072
|
+
const addAssertionIndex = (sel, node) => {
|
|
3073
|
+
let listSelector;
|
|
3074
|
+
let index;
|
|
3075
|
+
if (sel && observeTarget) {
|
|
3076
|
+
const matches = observeTarget.querySelectorAll(sel);
|
|
3077
|
+
if (matches.length > 1) {
|
|
3078
|
+
let n = node;
|
|
3079
|
+
while (n && n !== observeTarget && n.nodeType === 1) {
|
|
3080
|
+
const qaAttr = o.autotag && n.dataset && n.dataset[o.autotag];
|
|
3081
|
+
if (qaAttr) {
|
|
3082
|
+
const itemSel = `[data-${o.autotag}="${qaAttr}"]`;
|
|
3083
|
+
const itemMatches = observeTarget.querySelectorAll(itemSel);
|
|
3084
|
+
if (itemMatches.length > 1) {
|
|
3085
|
+
const idx = [...itemMatches].indexOf(n);
|
|
3086
|
+
if (idx !== -1) {
|
|
3087
|
+
listSelector = itemSel;
|
|
3088
|
+
index = idx;
|
|
3089
|
+
break;
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
n = n.parentElement;
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
return { listSelector, index };
|
|
3098
|
+
};
|
|
3099
|
+
if (m.type === "childList") {
|
|
3100
|
+
m.addedNodes.forEach((node) => {
|
|
3101
|
+
if (node.nodeType !== 1) return;
|
|
3102
|
+
if (!node.offsetParent) return;
|
|
3103
|
+
const sel = buildSelector(node);
|
|
3104
|
+
if (!sel) return;
|
|
3105
|
+
if (
|
|
3106
|
+
rec.assertions.some(
|
|
3107
|
+
(a) =>
|
|
3108
|
+
a.actionIdx === actionIdx && a.selector === sel && a.type === "visible",
|
|
3109
|
+
)
|
|
3110
|
+
)
|
|
3111
|
+
return;
|
|
3112
|
+
// Prefer stable content (e.g. .task-text) so assertions survive reorder/restore
|
|
3113
|
+
const textEl = node.querySelector?.(".task-text") || node;
|
|
3114
|
+
const text =
|
|
3115
|
+
(textEl.textContent?.trim() || node.textContent?.trim() || "").slice(0, 80) ||
|
|
3116
|
+
undefined;
|
|
3117
|
+
const { listSelector: aListSel, index: aIdx } = addAssertionIndex(sel, node);
|
|
3118
|
+
const a = { actionIdx, type: "visible", selector: sel, text };
|
|
3119
|
+
if (aListSel != null) a.listSelector = aListSel;
|
|
3120
|
+
if (aIdx != null) a.index = aIdx;
|
|
3121
|
+
rec.assertions.push(a);
|
|
3122
|
+
});
|
|
3123
|
+
}
|
|
3124
|
+
if (m.type === "attributes") {
|
|
3125
|
+
const sel = buildSelector(m.target);
|
|
3126
|
+
if (!sel) return;
|
|
3127
|
+
if (
|
|
3128
|
+
rec.assertions.some(
|
|
3129
|
+
(a) => a.actionIdx === actionIdx && a.selector === sel && a.type === "class",
|
|
3130
|
+
)
|
|
3131
|
+
)
|
|
3132
|
+
return;
|
|
3133
|
+
const { listSelector: aListSel, index: aIdx } = addAssertionIndex(sel, m.target);
|
|
3134
|
+
const a = {
|
|
3135
|
+
actionIdx,
|
|
3136
|
+
type: "class",
|
|
3137
|
+
selector: sel,
|
|
3138
|
+
className: m.target.className,
|
|
3139
|
+
};
|
|
3140
|
+
if (aListSel != null) a.listSelector = aListSel;
|
|
3141
|
+
if (aIdx != null) a.index = aIdx;
|
|
3142
|
+
rec.assertions.push(a);
|
|
3143
|
+
}
|
|
3144
|
+
});
|
|
3145
|
+
});
|
|
3146
|
+
rec._observer.observe(observeTarget, {
|
|
3147
|
+
childList: true,
|
|
3148
|
+
subtree: true,
|
|
3149
|
+
attributes: true,
|
|
3150
|
+
attributeFilter: [
|
|
3151
|
+
"class",
|
|
3152
|
+
"style",
|
|
3153
|
+
"hidden",
|
|
3154
|
+
"disabled",
|
|
3155
|
+
"aria-expanded",
|
|
3156
|
+
"aria-checked",
|
|
3157
|
+
],
|
|
3158
|
+
});
|
|
3159
|
+
|
|
3160
|
+
// attach DOM listeners
|
|
3161
|
+
const timers = {};
|
|
3162
|
+
listenEvents.forEach((ev) => {
|
|
3163
|
+
const handler = (e) => {
|
|
3164
|
+
const target = e.target;
|
|
3165
|
+
if (
|
|
3166
|
+
observe &&
|
|
3167
|
+
observeTarget &&
|
|
3168
|
+
target?.nodeType === 1 &&
|
|
3169
|
+
!observeTarget.contains(target)
|
|
3170
|
+
) {
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
// Capture selector and values EAGERLY in the capture phase, before any
|
|
3174
|
+
// bubble listener can mutate the DOM (e.g. a delete button removing its own li).
|
|
3175
|
+
let selector = "";
|
|
3176
|
+
if (target?.dataset) {
|
|
3177
|
+
const qaKey = o.autotag && target.dataset[o.autotag];
|
|
3178
|
+
if (qaKey) {
|
|
3179
|
+
selector = qualify(`[data-${o.autotag}="${qaKey}"]`, target.parentElement);
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
if (!selector && target?.tagName) {
|
|
3183
|
+
const base = target.id
|
|
3184
|
+
? "#" + target.id
|
|
3185
|
+
: target.className
|
|
3186
|
+
? target.tagName.toLowerCase() + "." + [...target.classList].join(".")
|
|
3187
|
+
: target.tagName.toLowerCase();
|
|
3188
|
+
selector = target.id ? base : qualify(base, target.parentElement);
|
|
3189
|
+
}
|
|
3190
|
+
// When selector matches multiple elements, store listSelector + targetIndex for replay by index
|
|
3191
|
+
let listSelector;
|
|
3192
|
+
let targetIndex;
|
|
3193
|
+
if (selector && observeTarget) {
|
|
3194
|
+
const matches = observeTarget.querySelectorAll(selector);
|
|
3195
|
+
if (matches.length > 1) {
|
|
3196
|
+
let node = target;
|
|
3197
|
+
while (node && node !== observeTarget && node.nodeType === 1) {
|
|
3198
|
+
const qaAttr = o.autotag && node.dataset && node.dataset[o.autotag];
|
|
3199
|
+
if (qaAttr) {
|
|
3200
|
+
const itemSel = `[data-${o.autotag}="${qaAttr}"]`;
|
|
3201
|
+
const itemMatches = observeTarget.querySelectorAll(itemSel);
|
|
3202
|
+
if (itemMatches.length > 1) {
|
|
3203
|
+
const idx = [...itemMatches].indexOf(node);
|
|
3204
|
+
if (idx !== -1) {
|
|
3205
|
+
listSelector = itemSel;
|
|
3206
|
+
targetIndex = idx;
|
|
3207
|
+
break;
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
node = node.parentElement;
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
const targetType = target?.tagName
|
|
3216
|
+
? target.tagName.toLowerCase() + (target.type ? ":" + target.type : "")
|
|
3217
|
+
: undefined;
|
|
3218
|
+
// For scroll, capture position at event time (before page may jump)
|
|
3219
|
+
const scrollY = ev === "scroll" ? window.scrollY : undefined;
|
|
3220
|
+
// For input/change, capture value; for checkboxes also capture checked state
|
|
3221
|
+
const value = ev === "input" || ev === "change" ? target?.value : undefined;
|
|
3222
|
+
const checked =
|
|
3223
|
+
ev === "change" && (target?.type === "checkbox" || target?.type === "radio")
|
|
3224
|
+
? target?.checked
|
|
3225
|
+
: undefined;
|
|
3226
|
+
|
|
3227
|
+
const delay =
|
|
3228
|
+
stepDelays[ev] !== undefined ? stepDelays[ev] : (captureDebounce[ev] ?? 0);
|
|
3229
|
+
const pushAction = () => {
|
|
3230
|
+
const action = { type: ev, target: selector, time: Date.now() };
|
|
3231
|
+
if (targetType) action.targetType = targetType;
|
|
3232
|
+
if (scrollY !== undefined) action.scrollY = scrollY;
|
|
3233
|
+
if (value !== undefined) action.value = value;
|
|
3234
|
+
if (checked !== undefined) action.checked = checked;
|
|
3235
|
+
if (listSelector != null) action.listSelector = listSelector;
|
|
3236
|
+
if (targetIndex != null) action.targetIndex = targetIndex;
|
|
3237
|
+
rec.actions.push(action);
|
|
3238
|
+
};
|
|
3239
|
+
if (delay === 0) {
|
|
3240
|
+
pushAction();
|
|
3241
|
+
} else {
|
|
3242
|
+
clearTimeout(timers[ev]);
|
|
3243
|
+
timers[ev] = setTimeout(pushAction, delay);
|
|
3244
|
+
}
|
|
3245
|
+
};
|
|
3246
|
+
o.D.addEventListener(ev, handler, true);
|
|
3247
|
+
rec._listeners.push({ ev, handler });
|
|
3248
|
+
});
|
|
3249
|
+
};
|
|
3250
|
+
|
|
3251
|
+
/**
|
|
3252
|
+
* Stop recording and return captured data
|
|
3253
|
+
* @returns {{actions: Array, mocks: Object, initialData: Object}}
|
|
3254
|
+
*/
|
|
3255
|
+
o.stopRecording = () => {
|
|
3256
|
+
const rec = o.recorder;
|
|
3257
|
+
rec.active = false;
|
|
3258
|
+
if (rec._originalFetch) {
|
|
3259
|
+
window.fetch = rec._originalFetch;
|
|
3260
|
+
rec._originalFetch = null;
|
|
3261
|
+
}
|
|
3262
|
+
rec._listeners.forEach(({ ev, handler }) => {
|
|
3263
|
+
o.D.removeEventListener(ev, handler, true);
|
|
3264
|
+
});
|
|
3265
|
+
rec._listeners = [];
|
|
3266
|
+
if (rec._observer) {
|
|
3267
|
+
rec._observer.disconnect();
|
|
3268
|
+
rec._observer = null;
|
|
3269
|
+
}
|
|
3270
|
+
return {
|
|
3271
|
+
actions: [...rec.actions],
|
|
3272
|
+
mocks: { ...rec.mocks },
|
|
3273
|
+
initialData: { ...rec.initialData },
|
|
3274
|
+
stepDelays: { ...rec.stepDelays },
|
|
3275
|
+
assertions: [...(rec.assertions || [])],
|
|
3276
|
+
observeRoot: rec.observeRoot || null,
|
|
3277
|
+
};
|
|
3278
|
+
};
|
|
3279
|
+
|
|
3280
|
+
/**
|
|
3281
|
+
* Clear recording from sessionStorage
|
|
3282
|
+
* @param {number} [id] - Recording ID, or clears all if omitted
|
|
3283
|
+
*/
|
|
3284
|
+
o.clearRecording = (id) => {
|
|
3285
|
+
if (id !== undefined) {
|
|
3286
|
+
sessionStorage?.removeItem("oTest-Recording-" + id);
|
|
3287
|
+
} else {
|
|
3288
|
+
for (let i = sessionStorage?.length - 1; i >= 0; i--) {
|
|
3289
|
+
const key = sessionStorage?.key(i);
|
|
3290
|
+
if (key && key.indexOf("oTest-Recording-") === 0) {
|
|
3291
|
+
sessionStorage?.removeItem(key);
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
};
|
|
3296
|
+
|
|
3297
|
+
/**
|
|
3298
|
+
* Export a recording as a ready-to-commit o.addTest() code string.
|
|
3299
|
+
* Available in all builds so QA testers can export tests from staging.
|
|
3300
|
+
* @param {{actions: Array, mocks: Object, initialData: Object}} recording
|
|
3301
|
+
* @returns {string}
|
|
3302
|
+
*/
|
|
3303
|
+
o.exportTest = (recording) => {
|
|
3304
|
+
const cases = recording.actions
|
|
3305
|
+
.map((a) => {
|
|
3306
|
+
let body;
|
|
3307
|
+
if (a.type === "scroll") {
|
|
3308
|
+
body = ` window.scrollTo(0, ${a.scrollY || 0}); return true;\n`;
|
|
3309
|
+
} else if (a.type === "input" || a.type === "change") {
|
|
3310
|
+
body =
|
|
3311
|
+
(a.value !== undefined ? ` el.value = ${JSON.stringify(a.value)};\n` : "") +
|
|
3312
|
+
(a.checked !== undefined ? ` el.checked = ${a.checked};\n` : "") +
|
|
3313
|
+
` el.dispatchEvent(new Event('${a.type}', {bubbles:true})); return true;\n`;
|
|
3314
|
+
} else {
|
|
3315
|
+
const useNativeClick = a.type === "click";
|
|
3316
|
+
body = useNativeClick
|
|
3317
|
+
? ` el.click(); return true;\n`
|
|
3318
|
+
: ` el.dispatchEvent(new MouseEvent('${a.type}', {bubbles:true,cancelable:true})); return true;\n`;
|
|
3319
|
+
}
|
|
3320
|
+
return (
|
|
3321
|
+
` ['${a.type} on ${a.target}', () => {\n` +
|
|
3322
|
+
` const el = document.querySelector('${a.target}');\n` +
|
|
3323
|
+
` if (!el) return 'element not found';\n` +
|
|
3324
|
+
body +
|
|
3325
|
+
` }],`
|
|
3326
|
+
);
|
|
3327
|
+
})
|
|
3328
|
+
.join("\n");
|
|
3329
|
+
|
|
3330
|
+
const mocksStr = Object.keys(recording.mocks).length
|
|
3331
|
+
? JSON.stringify(recording.mocks, null, 2)
|
|
3332
|
+
: "{}";
|
|
3333
|
+
|
|
3334
|
+
return (
|
|
3335
|
+
`// Auto-generated by o.exportTest() — review and anonymize mocks before committing\n` +
|
|
3336
|
+
`const recordingMocks = ${mocksStr};\n\n` +
|
|
3337
|
+
`o.addTest('Recorded test', [\n${cases}\n], () => {\n` +
|
|
3338
|
+
` // teardown\n});\n`
|
|
3339
|
+
);
|
|
3340
|
+
};
|
|
3341
|
+
|
|
3342
|
+
/**
|
|
3343
|
+
* Export a recording as a ready-to-run Playwright .spec.ts file string.
|
|
3344
|
+
* Available in all builds so QA testers can generate Playwright tests from staging.
|
|
3345
|
+
* @param {{actions: Array, mocks: Object, initialData: Object}} recording
|
|
3346
|
+
* @param {{testName?: string, baseUrl?: string}} [options]
|
|
3347
|
+
* @returns {string}
|
|
3348
|
+
*/
|
|
3349
|
+
o.exportPlaywrightTest = (recording, options = {}) => {
|
|
3350
|
+
const testName = options.testName || "Recorded session";
|
|
3351
|
+
const rawUrl = recording.initialData?.url ?? "/";
|
|
3352
|
+
let path = "/";
|
|
3353
|
+
try {
|
|
3354
|
+
path = new URL(rawUrl).pathname || "/";
|
|
3355
|
+
} catch (_e) {
|
|
3356
|
+
path = rawUrl;
|
|
3357
|
+
}
|
|
3358
|
+
const baseUrl = options.baseUrl || path;
|
|
3359
|
+
|
|
3360
|
+
const routes = Object.values(recording.mocks)
|
|
3361
|
+
.map((mock) => {
|
|
3362
|
+
const urlPath = mock.url.startsWith("/") ? mock.url : "/" + mock.url;
|
|
3363
|
+
const body = JSON.stringify(mock.response);
|
|
3364
|
+
return (
|
|
3365
|
+
` await page.route('**${urlPath}', async route => {\n` +
|
|
3366
|
+
` await route.fulfill({ status: ${mock.status || 200}, contentType: 'application/json',\n` +
|
|
3367
|
+
` body: JSON.stringify(${body}) });\n` +
|
|
3368
|
+
` });`
|
|
3369
|
+
);
|
|
3370
|
+
})
|
|
3371
|
+
.join("\n");
|
|
3372
|
+
|
|
3373
|
+
const sd = Object.assign(
|
|
3374
|
+
{ click: 100, mouseover: 50, scroll: 30, input: 50, change: 50 },
|
|
3375
|
+
recording.stepDelays || {},
|
|
3376
|
+
);
|
|
3377
|
+
const steps = recording.actions
|
|
3378
|
+
.map((action, i) => {
|
|
3379
|
+
const loc =
|
|
3380
|
+
action.listSelector != null && action.targetIndex != null
|
|
3381
|
+
? action.target !== action.listSelector
|
|
3382
|
+
? `page.locator(${JSON.stringify(action.listSelector)}).nth(${action.targetIndex}).locator(${JSON.stringify(action.target)})`
|
|
3383
|
+
: `page.locator(${JSON.stringify(action.listSelector)}).nth(${action.targetIndex})`
|
|
3384
|
+
: `page.locator(${JSON.stringify(action.target)})`;
|
|
3385
|
+
const wait = ` await page.waitForTimeout(${sd[action.type] || 50});`;
|
|
3386
|
+
let step;
|
|
3387
|
+
if (action.type === "scroll") {
|
|
3388
|
+
step = ` await page.evaluate(() => window.scrollTo(0, ${action.scrollY || 0}));`;
|
|
3389
|
+
} else if (action.type === "mouseover") {
|
|
3390
|
+
step = ` await ${loc}.hover();\n // Pure CSS :hover — no DOM mutation to assert.\n // Fix: toggle a class in a mouseover handler, or add a page.screenshot() visual check.`;
|
|
3391
|
+
} else if (action.type === "input") {
|
|
3392
|
+
step = ` await ${loc}.fill(${JSON.stringify(action.value || "")});`;
|
|
3393
|
+
} else if (action.type === "change") {
|
|
3394
|
+
const tt = action.targetType || "";
|
|
3395
|
+
if (tt.indexOf(":checkbox") !== -1 || tt.indexOf(":radio") !== -1) {
|
|
3396
|
+
const on =
|
|
3397
|
+
action.checked !== undefined
|
|
3398
|
+
? action.checked
|
|
3399
|
+
: action.value === "true" || action.value === "on";
|
|
3400
|
+
step = ` await ${loc}.${on ? "check" : "uncheck"}();`;
|
|
3401
|
+
} else if (tt.indexOf("select") !== -1) {
|
|
3402
|
+
step = ` await ${loc}.selectOption(${JSON.stringify(action.value || "")});`;
|
|
3403
|
+
} else {
|
|
3404
|
+
step = ` await ${loc}.fill(${JSON.stringify(action.value || "")});`;
|
|
3405
|
+
}
|
|
3406
|
+
} else {
|
|
3407
|
+
step = ` await ${loc}.click();`;
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
const asserts = (recording.assertions || [])
|
|
3411
|
+
.filter((a) => a.actionIdx === i)
|
|
3412
|
+
.filter(
|
|
3413
|
+
(a, j, arr) =>
|
|
3414
|
+
arr.findIndex(
|
|
3415
|
+
(x) =>
|
|
3416
|
+
x.selector === a.selector && x.type === a.type && x.index === a.index,
|
|
3417
|
+
) === j,
|
|
3418
|
+
)
|
|
3419
|
+
.map((a) => {
|
|
3420
|
+
const aLoc =
|
|
3421
|
+
a.listSelector != null && a.index != null
|
|
3422
|
+
? a.selector !== a.listSelector
|
|
3423
|
+
? `page.locator(${JSON.stringify(a.listSelector)}).nth(${a.index}).locator(${JSON.stringify(a.selector)})`
|
|
3424
|
+
: `page.locator(${JSON.stringify(a.listSelector)}).nth(${a.index})`
|
|
3425
|
+
: `page.locator(${JSON.stringify(a.selector)})`;
|
|
3426
|
+
if (a.type === "visible") {
|
|
3427
|
+
let s = ` await expect(${aLoc}).toBeVisible();`;
|
|
3428
|
+
if (a.text)
|
|
3429
|
+
s += `\n await expect(${aLoc}).toContainText(${JSON.stringify(a.text)});`;
|
|
3430
|
+
return s;
|
|
3431
|
+
}
|
|
3432
|
+
if (a.type === "class") {
|
|
3433
|
+
return ` // class on ${a.selector} changed to: "${a.className}"`;
|
|
3434
|
+
}
|
|
3435
|
+
return "";
|
|
3436
|
+
})
|
|
3437
|
+
.filter(Boolean)
|
|
3438
|
+
.join("\n");
|
|
3439
|
+
|
|
3440
|
+
return step + "\n" + wait + (asserts ? "\n" + asserts : "");
|
|
3441
|
+
})
|
|
3442
|
+
.join("\n");
|
|
3443
|
+
|
|
3444
|
+
const hasAutoAssertions = (recording.assertions || []).length > 0;
|
|
3445
|
+
return (
|
|
3446
|
+
`// Auto-generated by o.exportPlaywrightTest() — review and anonymize mocks before committing\n` +
|
|
3447
|
+
`// Prerequisites: npm install @playwright/test && npx playwright install chromium\n` +
|
|
3448
|
+
`// Run: npx playwright test recorded.spec.ts\n` +
|
|
3449
|
+
`import { test, expect } from '@playwright/test';\n\n` +
|
|
3450
|
+
`test(${JSON.stringify(testName)}, async ({ page }) => {\n` +
|
|
3451
|
+
(routes
|
|
3452
|
+
? ` // Network mocks — edit/anonymize before committing\n` + routes + "\n\n"
|
|
3453
|
+
: "") +
|
|
3454
|
+
` // Set baseURL in playwright.config.ts: { use: { baseURL: 'https://staging.example.com' } }\n` +
|
|
3455
|
+
` await page.goto(${JSON.stringify(baseUrl)});\n\n` +
|
|
3456
|
+
(steps ? steps + "\n\n" : "") +
|
|
3457
|
+
(!hasAutoAssertions
|
|
3458
|
+
? ` // TODO: Add assertions before committing, e.g.:\n` +
|
|
3459
|
+
` // await expect(page.locator('[data-qa="success-panel"]')).toBeVisible();\n` +
|
|
3460
|
+
` // await expect(page).toHaveURL(/\\/confirmation/);\n` +
|
|
3461
|
+
` // await expect(page.locator('[data-qa="error-banner"]')).not.toBeVisible();\n`
|
|
3462
|
+
: ` // Auto-generated assertions above — review for correctness before committing\n`) +
|
|
3463
|
+
`});\n`
|
|
3464
|
+
);
|
|
3465
|
+
};
|
|
3466
|
+
|
|
3467
|
+
// Available in all builds so assessors can replay and see results (testOverlay) on staging.
|
|
3468
|
+
/**
|
|
3469
|
+
* Play back a recording as an automated test sequence
|
|
3470
|
+
* @param {{actions: Array, mocks: Object}} recording
|
|
3471
|
+
* @param {Object} [mockOverrides] - Additional mock overrides (anonymized data)
|
|
3472
|
+
* @returns {number} testId
|
|
3473
|
+
*/
|
|
3474
|
+
o.playRecording = (recording, mockOverrides = {}) => {
|
|
3475
|
+
const allMocks = Object.assign({}, recording.mocks, mockOverrides);
|
|
3476
|
+
// install mock fetch
|
|
3477
|
+
const origFetch = window.fetch;
|
|
3478
|
+
window.fetch = (url, opts = {}) => {
|
|
3479
|
+
const method = (opts.method || "GET").toUpperCase();
|
|
3480
|
+
const key = method + ":" + url;
|
|
3481
|
+
if (allMocks[key]) {
|
|
3482
|
+
const mock = allMocks[key];
|
|
3483
|
+
const body =
|
|
3484
|
+
typeof mock.response === "string" ? mock.response : JSON.stringify(mock.response);
|
|
3485
|
+
return Promise.resolve(new Response(body, { status: mock.status || 200 }));
|
|
3486
|
+
}
|
|
3487
|
+
return origFetch(url, opts);
|
|
3488
|
+
};
|
|
3489
|
+
|
|
3490
|
+
const testCases = recording.actions.map((action) => [
|
|
3491
|
+
`${action.type} on ${action.target}`,
|
|
3492
|
+
() => {
|
|
3493
|
+
let el = null;
|
|
3494
|
+
if (action.target) {
|
|
3495
|
+
if (action.listSelector != null && action.targetIndex != null) {
|
|
3496
|
+
const items = o.D.querySelectorAll(action.listSelector);
|
|
3497
|
+
const item = items[action.targetIndex];
|
|
3498
|
+
if (item) {
|
|
3499
|
+
el =
|
|
3500
|
+
action.target !== action.listSelector
|
|
3501
|
+
? item.querySelector(action.target)
|
|
3502
|
+
: item;
|
|
3503
|
+
if (!el && action.target !== action.listSelector) el = item;
|
|
3504
|
+
}
|
|
3505
|
+
} else {
|
|
3506
|
+
el = o.D.querySelector(action.target);
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
if (!el && action.type !== "scroll") {
|
|
3510
|
+
return `element not found: ${action.target}`;
|
|
3511
|
+
}
|
|
3512
|
+
if (action.type === "scroll") {
|
|
3513
|
+
window.scrollTo(0, action.scrollY || 0);
|
|
3514
|
+
} else if (action.type === "input" || action.type === "change") {
|
|
3515
|
+
if (action.value !== undefined) el.value = action.value;
|
|
3516
|
+
if (action.checked !== undefined) el.checked = action.checked;
|
|
3517
|
+
el.dispatchEvent(new Event(action.type, { bubbles: true }));
|
|
3518
|
+
} else {
|
|
3519
|
+
if (action.type === "click") {
|
|
3520
|
+
el.click();
|
|
3521
|
+
} else {
|
|
3522
|
+
el.dispatchEvent(
|
|
3523
|
+
new MouseEvent(action.type, { bubbles: true, cancelable: true }),
|
|
3524
|
+
);
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
return true;
|
|
3528
|
+
},
|
|
3529
|
+
]);
|
|
3530
|
+
|
|
3531
|
+
const testId = o.test("Recorded playback", ...testCases, () => {
|
|
3532
|
+
window.fetch = origFetch;
|
|
3533
|
+
});
|
|
3534
|
+
return testId;
|
|
3535
|
+
};
|
|
3536
|
+
|
|
3537
|
+
// ─── Test results overlay (all builds — for assessors to see auto + manual results) ───
|
|
3538
|
+
|
|
3539
|
+
/**
|
|
3540
|
+
* Render a draggable overlay (same position and style as testConfirm) that shows test results (o.tLog / o.tRes).
|
|
3541
|
+
* Fully closable (Close removes it from DOM). Reopened when o.testOverlay() is called again (e.g. after a new test run).
|
|
3542
|
+
* Call o.testOverlay() then o.testOverlay.showPanel() after a test run to show results (creates overlay if closed).
|
|
3543
|
+
*/
|
|
3544
|
+
o.testOverlay = () => {
|
|
3545
|
+
const btnId = "o-test-overlay-btn";
|
|
3546
|
+
const panelId = "o-test-overlay-panel";
|
|
3547
|
+
if (o("#" + btnId).el) {
|
|
3548
|
+
return;
|
|
3549
|
+
}
|
|
3550
|
+
|
|
3551
|
+
const updatePanel = () => {
|
|
3552
|
+
const panel = o("#" + panelId);
|
|
3553
|
+
if (!panel.el) return;
|
|
3554
|
+
const total = o.tRes.length;
|
|
3555
|
+
const passed = o.tRes.filter(Boolean).length;
|
|
3556
|
+
let html = `<b>Tests: ${passed}/${total}</b><hr style="margin:4px 0">`;
|
|
3557
|
+
o.tLog.forEach((log, i) => {
|
|
3558
|
+
const ok = o.tRes[i];
|
|
3559
|
+
html += `<div style="margin:2px 0;padding:2px 4px;border-radius:3px;background:${ok ? "#d4edda" : "#f8d7da"};color:${ok ? "#155724" : "#721c24"};font-size:11px;white-space:pre-wrap">${log || "Test #" + i}</div>`;
|
|
3560
|
+
});
|
|
3561
|
+
html += `<button id="o-test-export" style="margin-top:6px;padding:4px 8px;font-size:11px;cursor:pointer">Export results</button>`;
|
|
3562
|
+
panel.html(html);
|
|
3563
|
+
o("#o-test-export").on("click", () => {
|
|
3564
|
+
const data = JSON.stringify({ results: o.tRes, logs: o.tLog }, null, 2);
|
|
3565
|
+
const blob = new Blob([data], { type: "application/json" });
|
|
3566
|
+
const a = o.D.createElement("a");
|
|
3567
|
+
a.href = URL.createObjectURL(blob);
|
|
3568
|
+
a.download = "objs-test-results.json";
|
|
3569
|
+
a.click();
|
|
3570
|
+
});
|
|
3571
|
+
};
|
|
3572
|
+
|
|
3573
|
+
const overlayStyle = {
|
|
3574
|
+
position: "fixed",
|
|
3575
|
+
left: "50%",
|
|
3576
|
+
bottom: "50px",
|
|
3577
|
+
transform: "translateX(-50%)",
|
|
3578
|
+
"z-index": "999999",
|
|
3579
|
+
width: "fit-content",
|
|
3580
|
+
"max-width": "min(90vw, 420px)",
|
|
3581
|
+
"font-family": "system-ui,sans-serif",
|
|
3582
|
+
cursor: "grab",
|
|
3583
|
+
"user-select": "text",
|
|
3584
|
+
};
|
|
3585
|
+
|
|
3586
|
+
const box = o
|
|
3587
|
+
.initState({
|
|
3588
|
+
tag: "div",
|
|
3589
|
+
id: btnId,
|
|
3590
|
+
className: "o-test-overlay",
|
|
3591
|
+
style:
|
|
3592
|
+
"position:fixed;left:50%;bottom:50px;transform:translateX(-50%);z-index:999999;width:fit-content;max-width:min(90vw,420px);font-family:system-ui,sans-serif;cursor:grab;user-select:text;",
|
|
3593
|
+
html:
|
|
3594
|
+
`<div class="o-test-overlay-bar" style="display:flex;flex-direction:column;align-items:stretch;padding:10px 14px;background:#1e293b;color:#e2e8f0;border:1px solid #334155;border-radius:8px;font-size:14px;cursor:grab;min-width:200px;">` +
|
|
3595
|
+
`<div style="display:flex;align-items:center;gap:12px;">` +
|
|
3596
|
+
`<span class="o-test-overlay-summary" style="flex:1;font-size:13px;">Tests: 0/0</span>` +
|
|
3597
|
+
`<button type="button" class="o-test-overlay-toggle" style="padding:6px 10px;background:#334155;color:#e2e8f0;border:none;border-radius:6px;cursor:pointer;font-size:12px;">List</button>` +
|
|
3598
|
+
`<button type="button" class="o-test-overlay-close" style="padding:4px 8px;background:transparent;color:#94a3b8;border:none;border-radius:4px;cursor:pointer;font-size:16px;line-height:1;" title="Close">×</button>` +
|
|
3599
|
+
`</div></div>` +
|
|
3600
|
+
`<div id="${panelId}" style="display:none;margin-top:4px;padding:8px;background:#fff;border:1px solid #334155;border-radius:6px;max-height:60vh;overflow-y:auto;box-shadow:0 2px 8px rgba(0,0,0,.15);font-size:11px;user-select:text;cursor:text;"></div>`,
|
|
3601
|
+
})
|
|
3602
|
+
.appendInside("body");
|
|
3603
|
+
|
|
3604
|
+
const applyOverlayStyle = () => {
|
|
3605
|
+
box.css(overlayStyle);
|
|
3606
|
+
};
|
|
3607
|
+
let drag = null;
|
|
3608
|
+
const onMove = (e) => {
|
|
3609
|
+
if (!drag) return;
|
|
3610
|
+
overlayStyle.left = drag.left + (e.clientX - drag.startX) + "px";
|
|
3611
|
+
overlayStyle.top = drag.top + (e.clientY - drag.startY) + "px";
|
|
3612
|
+
delete overlayStyle.bottom;
|
|
3613
|
+
overlayStyle.transform = "none";
|
|
3614
|
+
applyOverlayStyle();
|
|
3615
|
+
};
|
|
3616
|
+
const onUp = () => {
|
|
3617
|
+
if (drag) {
|
|
3618
|
+
overlayStyle.cursor = "grab";
|
|
3619
|
+
applyOverlayStyle();
|
|
3620
|
+
}
|
|
3621
|
+
drag = null;
|
|
3622
|
+
};
|
|
3623
|
+
box.on("mousedown", (e) => {
|
|
3624
|
+
if (
|
|
3625
|
+
e.target.closest(".o-test-overlay-close") ||
|
|
3626
|
+
e.target.closest(".o-test-overlay-toggle") ||
|
|
3627
|
+
e.target.closest("#" + panelId)
|
|
3628
|
+
)
|
|
3629
|
+
return;
|
|
3630
|
+
const r = box.el.getBoundingClientRect();
|
|
3631
|
+
drag = { startX: e.clientX, startY: e.clientY, left: r.left, top: r.top };
|
|
3632
|
+
overlayStyle.cursor = "grabbing";
|
|
3633
|
+
applyOverlayStyle();
|
|
3634
|
+
});
|
|
3635
|
+
o.D.addEventListener("mousemove", onMove);
|
|
3636
|
+
o.D.addEventListener("mouseup", onUp);
|
|
3637
|
+
|
|
3638
|
+
const refreshSummary = () => {
|
|
3639
|
+
const summary = o(".o-test-overlay-summary");
|
|
3640
|
+
if (summary.els.length)
|
|
3641
|
+
summary.innerText(`Tests: ${o.tRes.filter(Boolean).length}/${o.tRes.length}`);
|
|
3642
|
+
};
|
|
3643
|
+
|
|
3644
|
+
box.first(".o-test-overlay-toggle").on("click", () => {
|
|
3645
|
+
const panel = o("#" + panelId);
|
|
3646
|
+
if (!panel.el) return;
|
|
3647
|
+
const isOpen = panel.el.style.display !== "none";
|
|
3648
|
+
panel.css({ display: isOpen ? "none" : "block" });
|
|
3649
|
+
if (!isOpen) updatePanel();
|
|
3650
|
+
});
|
|
3651
|
+
|
|
3652
|
+
box.first(".o-test-overlay-close").on("click", () => {
|
|
3653
|
+
o.D.removeEventListener("mousemove", onMove);
|
|
3654
|
+
o.D.removeEventListener("mouseup", onUp);
|
|
3655
|
+
box.remove();
|
|
3656
|
+
});
|
|
3657
|
+
|
|
3658
|
+
o.testOverlay.showPanel = () => {
|
|
3659
|
+
const panel = o("#" + panelId);
|
|
3660
|
+
if (!panel.el) return;
|
|
3661
|
+
panel.css({ display: "block" });
|
|
3662
|
+
updatePanel();
|
|
3663
|
+
refreshSummary();
|
|
3664
|
+
};
|
|
3665
|
+
|
|
3666
|
+
// Single patch of o.test to refresh panel when tests complete (use base so we don't stack)
|
|
3667
|
+
if (!o._testOverlayBase) o._testOverlayBase = o.test;
|
|
3668
|
+
o.test = (...args) => {
|
|
3669
|
+
const id = o._testOverlayBase(...args);
|
|
3670
|
+
const origFn = o.tFns[id];
|
|
3671
|
+
o.tFns[id] = (n) => {
|
|
3672
|
+
if (typeof origFn === "function") origFn(n);
|
|
3673
|
+
const panel = o("#" + panelId);
|
|
3674
|
+
if (panel.el && panel.el.style.display !== "none") updatePanel();
|
|
3675
|
+
refreshSummary();
|
|
3676
|
+
};
|
|
3677
|
+
return id;
|
|
3678
|
+
};
|
|
3679
|
+
};
|
|
3680
|
+
|
|
3681
|
+
/**
|
|
3682
|
+
* Pause an Objs browser test; minimal draggable bar so operator can see the page.
|
|
3683
|
+
* Only available in dev builds. NOT referenced in exportPlaywrightTest.
|
|
3684
|
+
* @param {string} label - Test title (shown as "Test title: Paused")
|
|
3685
|
+
* @param {string[]} [items] - Optional checklist for the operator (e.g. hover effects to verify); use labels so clicking text toggles checkbox
|
|
3686
|
+
* @param {{ confirm?: string }} [opts] - Continue button label (default "Continue")
|
|
3687
|
+
* @returns {Promise<{ ok: boolean, errors?: string[] }>} ok true if all items checked; errors = list of unchecked item texts when ok false
|
|
3688
|
+
*/
|
|
3689
|
+
o.testConfirm = (label, items = [], opts = {}) =>
|
|
3690
|
+
new Promise((resolve) => {
|
|
3691
|
+
o(".o-tc-overlay").remove();
|
|
3692
|
+
const btnLabel = opts.confirm || "Continue";
|
|
3693
|
+
const hasCheckboxes = items.length > 0;
|
|
3694
|
+
const btnBg = hasCheckboxes ? "#dc2626" : "#2563eb";
|
|
3695
|
+
const itemIds = items.map((_, idx) => "o-tc-cb-" + idx);
|
|
3696
|
+
const checkboxStyle =
|
|
3697
|
+
".o-tc-item-cb{appearance:none;-webkit-appearance:none;width:16px;height:16px;border:2px solid #ef4444;border-radius:3px;background:#fef2f2;flex-shrink:0;cursor:pointer;}" +
|
|
3698
|
+
".o-tc-item-cb:checked{border-color:#22c55e;background:#22c55e;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E\");background-size:12px 12px;background-position:center;}";
|
|
3699
|
+
const itemsHtml = hasCheckboxes
|
|
3700
|
+
? `<style>${checkboxStyle}</style><ul class="o-tc-list" style="margin:8px 0 0;padding:0;list-style:none;font-size:13px;color:#cbd5e1;">` +
|
|
3701
|
+
items
|
|
3702
|
+
.map(
|
|
3703
|
+
(i, idx) =>
|
|
3704
|
+
`<li style="margin-bottom:4px;"><label for="${itemIds[idx]}" style="display:flex;align-items:center;gap:8px;cursor:pointer;user-select:none;"><input type="checkbox" id="${itemIds[idx]}" class="o-tc-item-cb"> <span>${i}</span></label></li>`,
|
|
3705
|
+
)
|
|
3706
|
+
.join("") +
|
|
3707
|
+
"</ul>"
|
|
3708
|
+
: "";
|
|
3709
|
+
const box = o
|
|
3710
|
+
.initState({
|
|
3711
|
+
tag: "div",
|
|
3712
|
+
className: "o-tc-overlay",
|
|
3713
|
+
style:
|
|
3714
|
+
"position:fixed;left:50%;bottom:50px;transform:translateX(-50%);z-index:999999;width:fit-content;max-width:min(90vw,400px);font-family:system-ui,sans-serif;cursor:grab;user-select:text;",
|
|
3715
|
+
html:
|
|
3716
|
+
`<div class="o-tc-bar" style="display:flex;flex-direction:column;align-items:stretch;padding:10px 14px;background:#1e293b;color:#e2e8f0;border:1px solid #334155;border-radius:8px;font-size:14px;cursor:grab;min-width:280px;">` +
|
|
3717
|
+
`<div style="display:flex;align-items:center;gap:12px;">` +
|
|
3718
|
+
`<span class="o-tc-label" style="flex:1;">${label}: Paused</span>` +
|
|
3719
|
+
`<button type="button" class="o-tc-ok" style="padding:6px 14px;background:${btnBg};color:#fff;border:none;border-radius:6px;font-weight:600;cursor:pointer;font-size:13px;flex-shrink:0;">${btnLabel}</button>` +
|
|
3720
|
+
`</div>` +
|
|
3721
|
+
itemsHtml +
|
|
3722
|
+
`</div>`,
|
|
3723
|
+
})
|
|
3724
|
+
.appendInside("body");
|
|
3725
|
+
|
|
3726
|
+
const okBtnStyles = {
|
|
3727
|
+
padding: "6px 14px",
|
|
3728
|
+
background: hasCheckboxes ? "#dc2626" : "#2563eb",
|
|
3729
|
+
color: "#fff",
|
|
3730
|
+
border: "none",
|
|
3731
|
+
"border-radius": "6px",
|
|
3732
|
+
"font-weight": "600",
|
|
3733
|
+
cursor: "pointer",
|
|
3734
|
+
"font-size": "13px",
|
|
3735
|
+
"flex-shrink": "0",
|
|
3736
|
+
};
|
|
3737
|
+
if (hasCheckboxes) {
|
|
3738
|
+
const okBtn = box.first(".o-tc-ok");
|
|
3739
|
+
const cbs = o(".o-tc-overlay .o-tc-item-cb");
|
|
3740
|
+
const updateBtn = () => {
|
|
3741
|
+
const allChecked = cbs.length > 0 && cbs.els.every((el) => el.checked);
|
|
3742
|
+
okBtn.css({ ...okBtnStyles, background: allChecked ? "#16a34a" : "#dc2626" });
|
|
3743
|
+
};
|
|
3744
|
+
cbs.on("change", updateBtn);
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3747
|
+
let drag = null;
|
|
3748
|
+
const overlayStyle = {
|
|
3749
|
+
position: "fixed",
|
|
3750
|
+
left: "50%",
|
|
3751
|
+
bottom: "50px",
|
|
3752
|
+
transform: "translateX(-50%)",
|
|
3753
|
+
"z-index": "999999",
|
|
3754
|
+
width: "fit-content",
|
|
3755
|
+
"max-width": "min(90vw, 400px)",
|
|
3756
|
+
"font-family": "system-ui,sans-serif",
|
|
3757
|
+
cursor: "grab",
|
|
3758
|
+
"user-select": "text",
|
|
3759
|
+
};
|
|
3760
|
+
const applyOverlayStyle = () => {
|
|
3761
|
+
box.css(overlayStyle);
|
|
3762
|
+
};
|
|
3763
|
+
const onMove = (e) => {
|
|
3764
|
+
if (!drag) return;
|
|
3765
|
+
overlayStyle.left = drag.left + (e.clientX - drag.startX) + "px";
|
|
3766
|
+
overlayStyle.top = drag.top + (e.clientY - drag.startY) + "px";
|
|
3767
|
+
delete overlayStyle.bottom;
|
|
3768
|
+
overlayStyle.transform = "none";
|
|
3769
|
+
applyOverlayStyle();
|
|
3770
|
+
};
|
|
3771
|
+
const onUp = () => {
|
|
3772
|
+
if (drag) {
|
|
3773
|
+
overlayStyle.cursor = "grab";
|
|
3774
|
+
applyOverlayStyle();
|
|
3775
|
+
}
|
|
3776
|
+
drag = null;
|
|
3777
|
+
};
|
|
3778
|
+
box.on("mousedown", (e) => {
|
|
3779
|
+
if (e.target.closest(".o-tc-ok")) return;
|
|
3780
|
+
const r = box.el.getBoundingClientRect();
|
|
3781
|
+
drag = { startX: e.clientX, startY: e.clientY, left: r.left, top: r.top };
|
|
3782
|
+
overlayStyle.cursor = "grabbing";
|
|
3783
|
+
applyOverlayStyle();
|
|
3784
|
+
});
|
|
3785
|
+
o.D.addEventListener("mousemove", onMove);
|
|
3786
|
+
o.D.addEventListener("mouseup", onUp);
|
|
3787
|
+
|
|
3788
|
+
box.first(".o-tc-ok").on("click", () => {
|
|
3789
|
+
o.D.removeEventListener("mousemove", onMove);
|
|
3790
|
+
o.D.removeEventListener("mouseup", onUp);
|
|
3791
|
+
let unchecked = [];
|
|
3792
|
+
if (hasCheckboxes) {
|
|
3793
|
+
const cbsList = o(".o-tc-overlay .o-tc-item-cb");
|
|
3794
|
+
cbsList.els.forEach((el, idx) => {
|
|
3795
|
+
if (!el.checked && items[idx] !== undefined) unchecked.push(items[idx]);
|
|
3796
|
+
});
|
|
3797
|
+
}
|
|
3798
|
+
box.remove();
|
|
3799
|
+
if (unchecked.length === 0) {
|
|
3800
|
+
resolve({ ok: true });
|
|
3801
|
+
} else {
|
|
3802
|
+
resolve({ ok: false, errors: unchecked });
|
|
3803
|
+
}
|
|
3804
|
+
});
|
|
3805
|
+
});
|