@htmlplus/element 3.2.4 → 3.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -23
- package/dist/client.d.ts +328 -1
- package/dist/client.js +1919 -3
- package/dist/constants.d.ts +1 -2
- package/dist/constants.js +1 -2
- package/dist/internal.d.ts +1 -1
- package/dist/internal.js +1 -1
- package/dist/transformer.d.ts +2 -1
- package/dist/transformer.js +3 -3
- package/package.json +2 -2
- package/dist/client-B2DGXsxR.d.ts +0 -327
- package/dist/client-aQIGQIWV.js +0 -1929
package/dist/client.js
CHANGED
|
@@ -1,3 +1,1919 @@
|
|
|
1
|
-
|
|
2
|
-
import '
|
|
3
|
-
|
|
1
|
+
import { kebabCase, pascalCase } from 'change-case';
|
|
2
|
+
import { API_HOST, STATIC_TAG, API_STACKS, API_REQUEST, API_CONNECTED, LIFECYCLE_UPDATE, STATIC_STYLE, API_STYLE, LIFECYCLE_UPDATED, API_RENDER_COMPLETED, METHOD_RENDER, TYPE_BOOLEAN, TYPE_NUMBER, TYPE_NULL, TYPE_DATE, TYPE_ARRAY, TYPE_OBJECT, TYPE_UNDEFINED, KEY, LIFECYCLE_CONNECTED, LIFECYCLE_DISCONNECTED, LIFECYCLE_CONSTRUCTED, LIFECYCLE_ADOPTED, LIFECYCLE_READY } from './constants.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Indicates the host of the element.
|
|
6
|
+
*/
|
|
7
|
+
const host = (target) => {
|
|
8
|
+
try {
|
|
9
|
+
return target[API_HOST]();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return target;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const outsides = [];
|
|
17
|
+
/**
|
|
18
|
+
* TODO
|
|
19
|
+
*/
|
|
20
|
+
const dispatch = (target, type, eventInitDict) => {
|
|
21
|
+
const event = new CustomEvent(type, eventInitDict);
|
|
22
|
+
host(target).dispatchEvent(event);
|
|
23
|
+
return event;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* TODO
|
|
27
|
+
*/
|
|
28
|
+
const on = (target, type, handler, options) => {
|
|
29
|
+
const element = host(target);
|
|
30
|
+
if (type != 'outside') {
|
|
31
|
+
return element.addEventListener(type, handler, options);
|
|
32
|
+
}
|
|
33
|
+
const callback = (event) => {
|
|
34
|
+
!event.composedPath().some((item) => item == element) && handler(event);
|
|
35
|
+
};
|
|
36
|
+
type = 'ontouchstart' in window.document.documentElement ? 'touchstart' : 'click';
|
|
37
|
+
on(document, type, callback, options);
|
|
38
|
+
outsides.push({
|
|
39
|
+
callback,
|
|
40
|
+
element,
|
|
41
|
+
handler,
|
|
42
|
+
options,
|
|
43
|
+
type
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* TODO
|
|
48
|
+
*/
|
|
49
|
+
const off = (target, type, handler, options) => {
|
|
50
|
+
const element = host(target);
|
|
51
|
+
if (type != 'outside') {
|
|
52
|
+
return element.removeEventListener(type, handler, options);
|
|
53
|
+
}
|
|
54
|
+
const index = outsides.findIndex((outside) => {
|
|
55
|
+
return outside.element == element && outside.handler == handler && outside.options == options;
|
|
56
|
+
});
|
|
57
|
+
const outside = outsides[index];
|
|
58
|
+
if (!outside)
|
|
59
|
+
return;
|
|
60
|
+
off(document, outside.type, outside.callback, outside.options);
|
|
61
|
+
outsides.splice(index, 1);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const isEvent = (input) => {
|
|
65
|
+
return !!input?.match(/on[A-Z]\w+/g);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const toEvent = (input) => {
|
|
69
|
+
return input?.slice(2).toLowerCase();
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const updateAttribute = (target, key, value) => {
|
|
73
|
+
const element = host(target);
|
|
74
|
+
if ([undefined, null, false].includes(value)) {
|
|
75
|
+
element.removeAttribute(key);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
element.setAttribute(key, value === true ? '' : value);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const symbol = Symbol();
|
|
83
|
+
const attributes$2 = (target, attributes) => {
|
|
84
|
+
const element = host(target);
|
|
85
|
+
const prev = element[symbol] || {};
|
|
86
|
+
const next = Object.assign({}, ...attributes);
|
|
87
|
+
const prevClass = (prev.class || '').split(' ');
|
|
88
|
+
const nextClass = (next.class || '').split(' ');
|
|
89
|
+
const newClass = element.className
|
|
90
|
+
.split(' ')
|
|
91
|
+
.filter((key) => !prevClass.includes(key) && !nextClass.includes(key))
|
|
92
|
+
.concat(nextClass)
|
|
93
|
+
.filter((key) => key)
|
|
94
|
+
.join(' ');
|
|
95
|
+
updateAttribute(element, 'class', newClass || undefined);
|
|
96
|
+
if (prev.style || next.style)
|
|
97
|
+
element.setAttribute('style', next.style || '');
|
|
98
|
+
for (const key in prev)
|
|
99
|
+
isEvent(key) && off(element, toEvent(key), prev[key]);
|
|
100
|
+
for (const key in next) {
|
|
101
|
+
if (['class', 'style'].includes(key))
|
|
102
|
+
continue;
|
|
103
|
+
if (isEvent(key))
|
|
104
|
+
on(element, toEvent(key), next[key]);
|
|
105
|
+
else
|
|
106
|
+
updateAttribute(element, kebabCase(key), next[key]);
|
|
107
|
+
}
|
|
108
|
+
element[symbol] = { ...next };
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const call = (target, key, ...args) => {
|
|
112
|
+
return target[key]?.apply(target, args);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const typeOf = (input) => {
|
|
116
|
+
return Object.prototype.toString
|
|
117
|
+
.call(input)
|
|
118
|
+
.replace(/\[|\]|object| /g, '')
|
|
119
|
+
.toLowerCase();
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* TODO
|
|
124
|
+
*/
|
|
125
|
+
const classes = (input, smart) => {
|
|
126
|
+
const result = [];
|
|
127
|
+
switch (typeOf(input)) {
|
|
128
|
+
case 'array': {
|
|
129
|
+
for (const item of input) {
|
|
130
|
+
result.push(classes(item, smart));
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case 'object': {
|
|
135
|
+
const keys = Object.keys(input);
|
|
136
|
+
for (const key of keys) {
|
|
137
|
+
const value = input[key];
|
|
138
|
+
const name = kebabCase(key);
|
|
139
|
+
const type = typeOf(value);
|
|
140
|
+
if (!smart) {
|
|
141
|
+
value && result.push(name);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
switch (type) {
|
|
145
|
+
case 'boolean': {
|
|
146
|
+
value && result.push(`${name}`);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case 'number':
|
|
150
|
+
case 'string': {
|
|
151
|
+
result.push(`${name}-${value}`);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case 'string': {
|
|
159
|
+
result.push(input);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return result.filter((item) => item).join(' ');
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Indicates whether the current code is running on a server.
|
|
168
|
+
*/
|
|
169
|
+
const isServer = () => {
|
|
170
|
+
return !(typeof window != 'undefined' && window.document);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const merge = (target, ...sources) => {
|
|
174
|
+
for (const source of sources) {
|
|
175
|
+
if (!source)
|
|
176
|
+
continue;
|
|
177
|
+
if (typeOf(source) != 'object') {
|
|
178
|
+
target = source;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
for (const key of Object.keys(source)) {
|
|
182
|
+
if (target[key] instanceof Object &&
|
|
183
|
+
source[key] instanceof Object &&
|
|
184
|
+
target[key] !== source[key]) {
|
|
185
|
+
target[key] = merge(target[key], source[key]);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
target[key] = source[key];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return target;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const DEFAULTS = {
|
|
196
|
+
element: {}
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* TODO
|
|
200
|
+
*/
|
|
201
|
+
const getConfig = (...keys) => {
|
|
202
|
+
if (isServer())
|
|
203
|
+
return;
|
|
204
|
+
let config = window[`$htmlplus$`];
|
|
205
|
+
for (const key of keys) {
|
|
206
|
+
if (!config)
|
|
207
|
+
break;
|
|
208
|
+
config = config[key];
|
|
209
|
+
}
|
|
210
|
+
return config;
|
|
211
|
+
};
|
|
212
|
+
/**
|
|
213
|
+
* TODO
|
|
214
|
+
*/
|
|
215
|
+
const setConfig = (config, options) => {
|
|
216
|
+
if (isServer())
|
|
217
|
+
return;
|
|
218
|
+
const previous = options?.override ? {} : window[`$htmlplus$`];
|
|
219
|
+
window[`$htmlplus$`] = merge({}, DEFAULTS, previous, config);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const defineProperty = Object.defineProperty;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Indicates whether the [Direction](https://mdn.io/css-direction)
|
|
226
|
+
* of the element is `Right-To-Left` or `Left-To-Right`.
|
|
227
|
+
*/
|
|
228
|
+
const direction = (target) => {
|
|
229
|
+
return getComputedStyle(host(target)).getPropertyValue('direction');
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const getFramework = (target) => {
|
|
233
|
+
const element = host(target);
|
|
234
|
+
if ('_qc_' in element)
|
|
235
|
+
return 'qwik';
|
|
236
|
+
if ('_$owner' in element)
|
|
237
|
+
return 'solid';
|
|
238
|
+
if ('__svelte_meta' in element)
|
|
239
|
+
return 'svelte';
|
|
240
|
+
if ('__vnode' in element)
|
|
241
|
+
return 'vue';
|
|
242
|
+
const keys = Object.keys(element);
|
|
243
|
+
const has = (input) => keys.some((key) => key.startsWith(input));
|
|
244
|
+
if (has('_blazor'))
|
|
245
|
+
return 'blazor';
|
|
246
|
+
if (has('__react'))
|
|
247
|
+
return 'react';
|
|
248
|
+
if (has('__zone_symbol__'))
|
|
249
|
+
return 'angular';
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const getTag = (target) => {
|
|
253
|
+
return target.constructor[STATIC_TAG] ?? target[STATIC_TAG];
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Determines whether the given input string is a valid
|
|
258
|
+
* [CSS Color](https://mdn.io/color-value) or not.
|
|
259
|
+
*/
|
|
260
|
+
const isCSSColor = (input) => {
|
|
261
|
+
const option = new Option();
|
|
262
|
+
option.style.color = input;
|
|
263
|
+
return option.style.color !== '';
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Indicates whether the direction of the element is `Right-To-Left` or not.
|
|
268
|
+
*/
|
|
269
|
+
const isRTL = (target) => direction(target) == 'rtl';
|
|
270
|
+
|
|
271
|
+
const shadowRoot = (target) => {
|
|
272
|
+
return host(target)?.shadowRoot;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Selects the first element in the shadow dom that matches a specified CSS selector.
|
|
277
|
+
*/
|
|
278
|
+
function query(target, selectors) {
|
|
279
|
+
return shadowRoot(target)?.querySelector(selectors);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Selects all elements in the shadow dom that match a specified CSS selector.
|
|
284
|
+
*/
|
|
285
|
+
function queryAll(target, selectors) {
|
|
286
|
+
return shadowRoot(target)?.querySelectorAll(selectors);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const task = (options) => {
|
|
290
|
+
let running, promise;
|
|
291
|
+
const run = () => {
|
|
292
|
+
if (options.canStart && !options.canStart())
|
|
293
|
+
return Promise.resolve(false);
|
|
294
|
+
if (!running)
|
|
295
|
+
promise = enqueue();
|
|
296
|
+
return promise;
|
|
297
|
+
};
|
|
298
|
+
const enqueue = async () => {
|
|
299
|
+
running = true;
|
|
300
|
+
try {
|
|
301
|
+
await promise;
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
Promise.reject(error);
|
|
305
|
+
}
|
|
306
|
+
// TODO: maybe is optional
|
|
307
|
+
if (!running)
|
|
308
|
+
return promise;
|
|
309
|
+
try {
|
|
310
|
+
if (options.canRun && !options.canRun())
|
|
311
|
+
return (running = false);
|
|
312
|
+
options.handler();
|
|
313
|
+
running = false;
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
running = false;
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
return run;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
class MapSet extends Map {
|
|
325
|
+
set(key, value) {
|
|
326
|
+
super.set(key, value);
|
|
327
|
+
return value;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
class WeakMapSet extends WeakMap {
|
|
332
|
+
set(key, value) {
|
|
333
|
+
super.set(key, value);
|
|
334
|
+
return value;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/*! (c) Andrea Giammarchi - ISC */
|
|
339
|
+
const empty =
|
|
340
|
+
/^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i;
|
|
341
|
+
const elements = /<([a-z]+[a-z0-9:._-]*)([^>]*?)(\/?)>/g;
|
|
342
|
+
const attributes$1 = /([^\s\\>"'=]+)\s*=\s*(['"]?)\x01/g;
|
|
343
|
+
const holes = /[\x01\x02]/g;
|
|
344
|
+
|
|
345
|
+
// \x01 Node.ELEMENT_NODE
|
|
346
|
+
// \x02 Node.ATTRIBUTE_NODE
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Given a template, find holes as both nodes and attributes and
|
|
350
|
+
* return a string with holes as either comment nodes or named attributes.
|
|
351
|
+
* @param {string[]} template a template literal tag array
|
|
352
|
+
* @param {string} prefix prefix to use per each comment/attribute
|
|
353
|
+
* @param {boolean} svg enforces self-closing tags
|
|
354
|
+
* @returns {string} X/HTML with prefixed comments or attributes
|
|
355
|
+
*/
|
|
356
|
+
var instrument = (template, prefix, svg) => {
|
|
357
|
+
let i = 0;
|
|
358
|
+
return template
|
|
359
|
+
.join('\x01')
|
|
360
|
+
.trim()
|
|
361
|
+
.replace(elements, (_, name, attrs, selfClosing) => {
|
|
362
|
+
let ml = name + attrs.replace(attributes$1, '\x02=$2$1').trimEnd();
|
|
363
|
+
if (selfClosing.length) ml += svg || empty.test(name) ? ' /' : '></' + name;
|
|
364
|
+
return '<' + ml + '>';
|
|
365
|
+
})
|
|
366
|
+
.replace(holes, (hole) => (hole === '\x01' ? '<!--' + prefix + i++ + '-->' : prefix + i++));
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const ELEMENT_NODE = 1;
|
|
370
|
+
const nodeType = 111;
|
|
371
|
+
|
|
372
|
+
const remove = ({ firstChild, lastChild }) => {
|
|
373
|
+
const range = document.createRange();
|
|
374
|
+
range.setStartAfter(firstChild);
|
|
375
|
+
range.setEndAfter(lastChild);
|
|
376
|
+
range.deleteContents();
|
|
377
|
+
return firstChild;
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const diffable = (node, operation) =>
|
|
381
|
+
node.nodeType === nodeType
|
|
382
|
+
? 1 / operation < 0
|
|
383
|
+
? operation
|
|
384
|
+
? remove(node)
|
|
385
|
+
: node.lastChild
|
|
386
|
+
: operation
|
|
387
|
+
? node.valueOf()
|
|
388
|
+
: node.firstChild
|
|
389
|
+
: node;
|
|
390
|
+
const persistent = (fragment) => {
|
|
391
|
+
const { firstChild, lastChild } = fragment;
|
|
392
|
+
if (firstChild === lastChild) return lastChild || fragment;
|
|
393
|
+
const { childNodes } = fragment;
|
|
394
|
+
const nodes = [...childNodes];
|
|
395
|
+
return {
|
|
396
|
+
ELEMENT_NODE,
|
|
397
|
+
nodeType,
|
|
398
|
+
firstChild,
|
|
399
|
+
lastChild,
|
|
400
|
+
valueOf() {
|
|
401
|
+
if (childNodes.length !== nodes.length) fragment.append(...nodes);
|
|
402
|
+
return fragment;
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const { isArray: isArray$1 } = Array;
|
|
408
|
+
|
|
409
|
+
const aria = (node) => (values) => {
|
|
410
|
+
for (const key in values) {
|
|
411
|
+
const name = key === 'role' ? key : `aria-${key}`;
|
|
412
|
+
const value = values[key];
|
|
413
|
+
if (value == null) node.removeAttribute(name);
|
|
414
|
+
else node.setAttribute(name, value);
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const attribute = (node, name) => {
|
|
419
|
+
let oldValue,
|
|
420
|
+
orphan = true;
|
|
421
|
+
const attributeNode = document.createAttributeNS(null, name);
|
|
422
|
+
return (newValue) => {
|
|
423
|
+
if (oldValue !== newValue) {
|
|
424
|
+
oldValue = newValue;
|
|
425
|
+
if (oldValue == null) {
|
|
426
|
+
if (!orphan) {
|
|
427
|
+
node.removeAttributeNode(attributeNode);
|
|
428
|
+
orphan = true;
|
|
429
|
+
}
|
|
430
|
+
} else {
|
|
431
|
+
const value = newValue;
|
|
432
|
+
if (value == null) {
|
|
433
|
+
if (!orphan) node.removeAttributeNode(attributeNode);
|
|
434
|
+
orphan = true;
|
|
435
|
+
} else {
|
|
436
|
+
attributeNode.value = value;
|
|
437
|
+
if (orphan) {
|
|
438
|
+
node.setAttributeNodeNS(attributeNode);
|
|
439
|
+
orphan = false;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const boolean = (node, key, oldValue) => (newValue) => {
|
|
448
|
+
if (oldValue !== !!newValue) {
|
|
449
|
+
// when IE won't be around anymore ...
|
|
450
|
+
// node.toggleAttribute(key, oldValue = !!newValue);
|
|
451
|
+
if ((oldValue = !!newValue)) node.setAttribute(key, '');
|
|
452
|
+
else node.removeAttribute(key);
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const data =
|
|
457
|
+
({ dataset }) =>
|
|
458
|
+
(values) => {
|
|
459
|
+
for (const key in values) {
|
|
460
|
+
const value = values[key];
|
|
461
|
+
if (value == null) delete dataset[key];
|
|
462
|
+
else dataset[key] = value;
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const event = (node, name) => {
|
|
467
|
+
let oldValue,
|
|
468
|
+
lower,
|
|
469
|
+
type = name.slice(2);
|
|
470
|
+
if (!(name in node) && (lower = name.toLowerCase()) in node) type = lower.slice(2);
|
|
471
|
+
return (newValue) => {
|
|
472
|
+
const info = isArray$1(newValue) ? newValue : [newValue, false];
|
|
473
|
+
if (oldValue !== info[0]) {
|
|
474
|
+
if (oldValue) node.removeEventListener(type, oldValue, info[1]);
|
|
475
|
+
if ((oldValue = info[0])) node.addEventListener(type, oldValue, info[1]);
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const ref = (node) => {
|
|
481
|
+
let oldValue;
|
|
482
|
+
return (value) => {
|
|
483
|
+
if (oldValue !== value) {
|
|
484
|
+
oldValue = value;
|
|
485
|
+
if (typeof value === 'function') value(node);
|
|
486
|
+
else value.current = node;
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const setter = (node, key) =>
|
|
492
|
+
key === 'dataset'
|
|
493
|
+
? data(node)
|
|
494
|
+
: (value) => {
|
|
495
|
+
node[key] = value;
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
const text = (node) => {
|
|
499
|
+
let oldValue;
|
|
500
|
+
return (newValue) => {
|
|
501
|
+
if (oldValue != newValue) {
|
|
502
|
+
oldValue = newValue;
|
|
503
|
+
node.textContent = newValue == null ? '' : newValue;
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* ISC License
|
|
510
|
+
*
|
|
511
|
+
* Copyright (c) 2020, Andrea Giammarchi, @WebReflection
|
|
512
|
+
*
|
|
513
|
+
* Permission to use, copy, modify, and/or distribute this software for any
|
|
514
|
+
* purpose with or without fee is hereby granted, provided that the above
|
|
515
|
+
* copyright notice and this permission notice appear in all copies.
|
|
516
|
+
*
|
|
517
|
+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
518
|
+
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
519
|
+
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
520
|
+
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
521
|
+
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
|
522
|
+
* OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
523
|
+
* PERFORMANCE OF THIS SOFTWARE.
|
|
524
|
+
*/
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* @param {Node} parentNode The container where children live
|
|
528
|
+
* @param {Node[]} a The list of current/live children
|
|
529
|
+
* @param {Node[]} b The list of future children
|
|
530
|
+
* @param {(entry: Node, action: number) => Node} get
|
|
531
|
+
* The callback invoked per each entry related DOM operation.
|
|
532
|
+
* @param {Node} [before] The optional node used as anchor to insert before.
|
|
533
|
+
* @returns {Node[]} The same list of future children.
|
|
534
|
+
*/
|
|
535
|
+
var udomdiff = (parentNode, a, b, get, before) => {
|
|
536
|
+
const bLength = b.length;
|
|
537
|
+
let aEnd = a.length;
|
|
538
|
+
let bEnd = bLength;
|
|
539
|
+
let aStart = 0;
|
|
540
|
+
let bStart = 0;
|
|
541
|
+
let map = null;
|
|
542
|
+
while (aStart < aEnd || bStart < bEnd) {
|
|
543
|
+
// append head, tail, or nodes in between: fast path
|
|
544
|
+
if (aEnd === aStart) {
|
|
545
|
+
// we could be in a situation where the rest of nodes that
|
|
546
|
+
// need to be added are not at the end, and in such case
|
|
547
|
+
// the node to `insertBefore`, if the index is more than 0
|
|
548
|
+
// must be retrieved, otherwise it's gonna be the first item.
|
|
549
|
+
const node =
|
|
550
|
+
bEnd < bLength
|
|
551
|
+
? bStart
|
|
552
|
+
? get(b[bStart - 1], -0).nextSibling
|
|
553
|
+
: get(b[bEnd - bStart], 0)
|
|
554
|
+
: before;
|
|
555
|
+
while (bStart < bEnd) parentNode.insertBefore(get(b[bStart++], 1), node);
|
|
556
|
+
}
|
|
557
|
+
// remove head or tail: fast path
|
|
558
|
+
else if (bEnd === bStart) {
|
|
559
|
+
while (aStart < aEnd) {
|
|
560
|
+
// remove the node only if it's unknown or not live
|
|
561
|
+
if (!map || !map.has(a[aStart])) parentNode.removeChild(get(a[aStart], -1));
|
|
562
|
+
aStart++;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
// same node: fast path
|
|
566
|
+
else if (a[aStart] === b[bStart]) {
|
|
567
|
+
aStart++;
|
|
568
|
+
bStart++;
|
|
569
|
+
}
|
|
570
|
+
// same tail: fast path
|
|
571
|
+
else if (a[aEnd - 1] === b[bEnd - 1]) {
|
|
572
|
+
aEnd--;
|
|
573
|
+
bEnd--;
|
|
574
|
+
}
|
|
575
|
+
// The once here single last swap "fast path" has been removed in v1.1.0
|
|
576
|
+
// https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85
|
|
577
|
+
// reverse swap: also fast path
|
|
578
|
+
else if (a[aStart] === b[bEnd - 1] && b[bStart] === a[aEnd - 1]) {
|
|
579
|
+
// this is a "shrink" operation that could happen in these cases:
|
|
580
|
+
// [1, 2, 3, 4, 5]
|
|
581
|
+
// [1, 4, 3, 2, 5]
|
|
582
|
+
// or asymmetric too
|
|
583
|
+
// [1, 2, 3, 4, 5]
|
|
584
|
+
// [1, 2, 3, 5, 6, 4]
|
|
585
|
+
const node = get(a[--aEnd], -1).nextSibling;
|
|
586
|
+
parentNode.insertBefore(get(b[bStart++], 1), get(a[aStart++], -1).nextSibling);
|
|
587
|
+
parentNode.insertBefore(get(b[--bEnd], 1), node);
|
|
588
|
+
// mark the future index as identical (yeah, it's dirty, but cheap 👍)
|
|
589
|
+
// The main reason to do this, is that when a[aEnd] will be reached,
|
|
590
|
+
// the loop will likely be on the fast path, as identical to b[bEnd].
|
|
591
|
+
// In the best case scenario, the next loop will skip the tail,
|
|
592
|
+
// but in the worst one, this node will be considered as already
|
|
593
|
+
// processed, bailing out pretty quickly from the map index check
|
|
594
|
+
a[aEnd] = b[bEnd];
|
|
595
|
+
}
|
|
596
|
+
// map based fallback, "slow" path
|
|
597
|
+
else {
|
|
598
|
+
// the map requires an O(bEnd - bStart) operation once
|
|
599
|
+
// to store all future nodes indexes for later purposes.
|
|
600
|
+
// In the worst case scenario, this is a full O(N) cost,
|
|
601
|
+
// and such scenario happens at least when all nodes are different,
|
|
602
|
+
// but also if both first and last items of the lists are different
|
|
603
|
+
if (!map) {
|
|
604
|
+
map = new Map();
|
|
605
|
+
let i = bStart;
|
|
606
|
+
while (i < bEnd) map.set(b[i], i++);
|
|
607
|
+
}
|
|
608
|
+
// if it's a future node, hence it needs some handling
|
|
609
|
+
if (map.has(a[aStart])) {
|
|
610
|
+
// grab the index of such node, 'cause it might have been processed
|
|
611
|
+
const index = map.get(a[aStart]);
|
|
612
|
+
// if it's not already processed, look on demand for the next LCS
|
|
613
|
+
if (bStart < index && index < bEnd) {
|
|
614
|
+
let i = aStart;
|
|
615
|
+
// counts the amount of nodes that are the same in the future
|
|
616
|
+
let sequence = 1;
|
|
617
|
+
while (++i < aEnd && i < bEnd && map.get(a[i]) === index + sequence) sequence++;
|
|
618
|
+
// effort decision here: if the sequence is longer than replaces
|
|
619
|
+
// needed to reach such sequence, which would brings again this loop
|
|
620
|
+
// to the fast path, prepend the difference before a sequence,
|
|
621
|
+
// and move only the future list index forward, so that aStart
|
|
622
|
+
// and bStart will be aligned again, hence on the fast path.
|
|
623
|
+
// An example considering aStart and bStart are both 0:
|
|
624
|
+
// a: [1, 2, 3, 4]
|
|
625
|
+
// b: [7, 1, 2, 3, 6]
|
|
626
|
+
// this would place 7 before 1 and, from that time on, 1, 2, and 3
|
|
627
|
+
// will be processed at zero cost
|
|
628
|
+
if (sequence > index - bStart) {
|
|
629
|
+
const node = get(a[aStart], 0);
|
|
630
|
+
while (bStart < index) parentNode.insertBefore(get(b[bStart++], 1), node);
|
|
631
|
+
}
|
|
632
|
+
// if the effort wasn't good enough, fallback to a replace,
|
|
633
|
+
// moving both source and target indexes forward, hoping that some
|
|
634
|
+
// similar node will be found later on, to go back to the fast path
|
|
635
|
+
else {
|
|
636
|
+
parentNode.replaceChild(get(b[bStart++], 1), get(a[aStart++], -1));
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// otherwise move the source forward, 'cause there's nothing to do
|
|
640
|
+
else aStart++;
|
|
641
|
+
}
|
|
642
|
+
// this node has no meaning in the future list, so it's more than safe
|
|
643
|
+
// to remove it, and check the next live node out instead, meaning
|
|
644
|
+
// that only the live list index should be forwarded
|
|
645
|
+
else parentNode.removeChild(get(a[aStart++], -1));
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return b;
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
const { isArray, prototype } = Array;
|
|
652
|
+
const { indexOf } = prototype;
|
|
653
|
+
|
|
654
|
+
const {
|
|
655
|
+
createDocumentFragment,
|
|
656
|
+
createElement,
|
|
657
|
+
createElementNS,
|
|
658
|
+
createTextNode,
|
|
659
|
+
createTreeWalker,
|
|
660
|
+
importNode
|
|
661
|
+
} = new Proxy(typeof window == 'undefined' ? {} : window.document, {
|
|
662
|
+
get: (target, method) => (target[method] || function () {}).bind(target)
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
const createHTML = (html) => {
|
|
666
|
+
const template = createElement('template');
|
|
667
|
+
template.innerHTML = html;
|
|
668
|
+
return template.content;
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
let xml;
|
|
672
|
+
const createSVG = (svg) => {
|
|
673
|
+
if (!xml) xml = createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
674
|
+
xml.innerHTML = svg;
|
|
675
|
+
const content = createDocumentFragment();
|
|
676
|
+
content.append(...xml.childNodes);
|
|
677
|
+
return content;
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
const createContent = (text, svg) => (svg ? createSVG(text) : createHTML(text));
|
|
681
|
+
|
|
682
|
+
// from a generic path, retrieves the exact targeted node
|
|
683
|
+
const reducePath = ({ childNodes }, i) => childNodes[i];
|
|
684
|
+
|
|
685
|
+
// this helper avoid code bloat around handleAnything() callback
|
|
686
|
+
const diff = (comment, oldNodes, newNodes) =>
|
|
687
|
+
udomdiff(
|
|
688
|
+
comment.parentNode,
|
|
689
|
+
// TODO: there is a possible edge case where a node has been
|
|
690
|
+
// removed manually, or it was a keyed one, attached
|
|
691
|
+
// to a shared reference between renders.
|
|
692
|
+
// In this case udomdiff might fail at removing such node
|
|
693
|
+
// as its parent won't be the expected one.
|
|
694
|
+
// The best way to avoid this issue is to filter oldNodes
|
|
695
|
+
// in search of those not live, or not in the current parent
|
|
696
|
+
// anymore, but this would require both a change to uwire,
|
|
697
|
+
// exposing a parentNode from the firstChild, as example,
|
|
698
|
+
// but also a filter per each diff that should exclude nodes
|
|
699
|
+
// that are not in there, penalizing performance quite a lot.
|
|
700
|
+
// As this has been also a potential issue with domdiff,
|
|
701
|
+
// and both lighterhtml and hyperHTML might fail with this
|
|
702
|
+
// very specific edge case, I might as well document this possible
|
|
703
|
+
// "diffing shenanigan" and call it a day.
|
|
704
|
+
oldNodes,
|
|
705
|
+
newNodes,
|
|
706
|
+
diffable,
|
|
707
|
+
comment
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
// if an interpolation represents a comment, the whole
|
|
711
|
+
// diffing will be related to such comment.
|
|
712
|
+
// This helper is in charge of understanding how the new
|
|
713
|
+
// content for such interpolation/hole should be updated
|
|
714
|
+
const handleAnything = (comment) => {
|
|
715
|
+
let oldValue,
|
|
716
|
+
text,
|
|
717
|
+
nodes = [];
|
|
718
|
+
const anyContent = (newValue) => {
|
|
719
|
+
switch (typeof newValue) {
|
|
720
|
+
// primitives are handled as text content
|
|
721
|
+
case 'string':
|
|
722
|
+
case 'number':
|
|
723
|
+
case 'boolean':
|
|
724
|
+
if (oldValue !== newValue) {
|
|
725
|
+
oldValue = newValue;
|
|
726
|
+
if (!text) text = createTextNode('');
|
|
727
|
+
text.data = newValue;
|
|
728
|
+
nodes = diff(comment, nodes, [text]);
|
|
729
|
+
}
|
|
730
|
+
break;
|
|
731
|
+
// null, and undefined are used to cleanup previous content
|
|
732
|
+
case 'object':
|
|
733
|
+
case 'undefined':
|
|
734
|
+
if (newValue == null) {
|
|
735
|
+
if (oldValue != newValue) {
|
|
736
|
+
oldValue = newValue;
|
|
737
|
+
nodes = diff(comment, nodes, []);
|
|
738
|
+
}
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
// arrays and nodes have a special treatment
|
|
742
|
+
if (isArray(newValue)) {
|
|
743
|
+
oldValue = newValue;
|
|
744
|
+
// arrays can be used to cleanup, if empty
|
|
745
|
+
if (newValue.length === 0) nodes = diff(comment, nodes, []);
|
|
746
|
+
// or diffed, if these contains nodes or "wires"
|
|
747
|
+
else if (typeof newValue[0] === 'object') nodes = diff(comment, nodes, newValue);
|
|
748
|
+
// in all other cases the content is stringified as is
|
|
749
|
+
else anyContent(String(newValue));
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
// if the new value is a DOM node, or a wire, and it's
|
|
753
|
+
// different from the one already live, then it's diffed.
|
|
754
|
+
// if the node is a fragment, it's appended once via its childNodes
|
|
755
|
+
// There is no `else` here, meaning if the content
|
|
756
|
+
// is not expected one, nothing happens, as easy as that.
|
|
757
|
+
if (oldValue !== newValue && 'ELEMENT_NODE' in newValue) {
|
|
758
|
+
oldValue = newValue;
|
|
759
|
+
nodes = diff(
|
|
760
|
+
comment,
|
|
761
|
+
nodes,
|
|
762
|
+
newValue.nodeType === 11 ? [...newValue.childNodes] : [newValue]
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
break;
|
|
766
|
+
case 'function':
|
|
767
|
+
anyContent(newValue(comment));
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
return anyContent;
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
// attributes can be:
|
|
775
|
+
// * ref=${...} for hooks and other purposes
|
|
776
|
+
// * aria=${...} for aria attributes
|
|
777
|
+
// * ?boolean=${...} for boolean attributes
|
|
778
|
+
// * .dataset=${...} for dataset related attributes
|
|
779
|
+
// * .setter=${...} for Custom Elements setters or nodes with setters
|
|
780
|
+
// such as buttons, details, options, select, etc
|
|
781
|
+
// * @event=${...} to explicitly handle event listeners
|
|
782
|
+
// * onevent=${...} to automatically handle event listeners
|
|
783
|
+
// * generic=${...} to handle an attribute just like an attribute
|
|
784
|
+
const handleAttribute = (node, name /*, svg*/) => {
|
|
785
|
+
switch (name[0]) {
|
|
786
|
+
case '?':
|
|
787
|
+
return boolean(node, name.slice(1), false);
|
|
788
|
+
case '.':
|
|
789
|
+
return setter(node, name.slice(1));
|
|
790
|
+
case '@':
|
|
791
|
+
return event(node, 'on' + name.slice(1));
|
|
792
|
+
case 'o':
|
|
793
|
+
if (name[1] === 'n') return event(node, name);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
switch (name) {
|
|
797
|
+
case 'ref':
|
|
798
|
+
return ref(node);
|
|
799
|
+
case 'aria':
|
|
800
|
+
return aria(node);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return attribute(node, name /*, svg*/);
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
// each mapped update carries the update type and its path
|
|
807
|
+
// the type is either node, attribute, or text, while
|
|
808
|
+
// the path is how to retrieve the related node to update.
|
|
809
|
+
// In the attribute case, the attribute name is also carried along.
|
|
810
|
+
function handlers(options) {
|
|
811
|
+
const { type, path } = options;
|
|
812
|
+
const node = path.reduceRight(reducePath, this);
|
|
813
|
+
return type === 'node'
|
|
814
|
+
? handleAnything(node)
|
|
815
|
+
: type === 'attr'
|
|
816
|
+
? handleAttribute(node, options.name /*, options.svg*/)
|
|
817
|
+
: text(node);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// from a fragment container, create an array of indexes
|
|
821
|
+
// related to its child nodes, so that it's possible
|
|
822
|
+
// to retrieve later on exact node via reducePath
|
|
823
|
+
const createPath = (node) => {
|
|
824
|
+
const path = [];
|
|
825
|
+
let { parentNode } = node;
|
|
826
|
+
while (parentNode) {
|
|
827
|
+
path.push(indexOf.call(parentNode.childNodes, node));
|
|
828
|
+
node = parentNode;
|
|
829
|
+
({ parentNode } = node);
|
|
830
|
+
}
|
|
831
|
+
return path;
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
// the prefix is used to identify either comments, attributes, or nodes
|
|
835
|
+
// that contain the related unique id. In the attribute cases
|
|
836
|
+
// isµX="attribute-name" will be used to map current X update to that
|
|
837
|
+
// attribute name, while comments will be like <!--isµX-->, to map
|
|
838
|
+
// the update to that specific comment node, hence its parent.
|
|
839
|
+
// style and textarea will have <!--isµX--> text content, and are handled
|
|
840
|
+
// directly through text-only updates.
|
|
841
|
+
const prefix = 'isµ';
|
|
842
|
+
|
|
843
|
+
// Template Literals are unique per scope and static, meaning a template
|
|
844
|
+
// should be parsed once, and once only, as it will always represent the same
|
|
845
|
+
// content, within the exact same amount of updates each time.
|
|
846
|
+
// This cache relates each template to its unique content and updates.
|
|
847
|
+
const cache$1 = new WeakMapSet();
|
|
848
|
+
|
|
849
|
+
// a RegExp that helps checking nodes that cannot contain comments
|
|
850
|
+
const textOnly = /^(?:textarea|script|style|title|plaintext|xmp)$/;
|
|
851
|
+
|
|
852
|
+
const createCache = () => ({
|
|
853
|
+
stack: [], // each template gets a stack for each interpolation "hole"
|
|
854
|
+
|
|
855
|
+
entry: null, // each entry contains details, such as:
|
|
856
|
+
// * the template that is representing
|
|
857
|
+
// * the type of node it represents (html or svg)
|
|
858
|
+
// * the content fragment with all nodes
|
|
859
|
+
// * the list of updates per each node (template holes)
|
|
860
|
+
// * the "wired" node or fragment that will get updates
|
|
861
|
+
// if the template or type are different from the previous one
|
|
862
|
+
// the entry gets re-created each time
|
|
863
|
+
|
|
864
|
+
wire: null // each rendered node represent some wired content and
|
|
865
|
+
// this reference to the latest one. If different, the node
|
|
866
|
+
// will be cleaned up and the new "wire" will be appended
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
// the entry stored in the rendered node cache, and per each "hole"
|
|
870
|
+
const createEntry = (type, template) => {
|
|
871
|
+
const { content, updates } = mapUpdates(type, template);
|
|
872
|
+
return { type, template, content, updates, wire: null };
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
// a template is instrumented to be able to retrieve where updates are needed.
|
|
876
|
+
// Each unique template becomes a fragment, cloned once per each other
|
|
877
|
+
// operation based on the same template, i.e. data => html`<p>${data}</p>`
|
|
878
|
+
const mapTemplate = (type, template) => {
|
|
879
|
+
const svg = type === 'svg';
|
|
880
|
+
const text = instrument(template, prefix, svg);
|
|
881
|
+
const content = createContent(text, svg);
|
|
882
|
+
// once instrumented and reproduced as fragment, it's crawled
|
|
883
|
+
// to find out where each update is in the fragment tree
|
|
884
|
+
const tw = createTreeWalker(content, 1 | 128);
|
|
885
|
+
const nodes = [];
|
|
886
|
+
const length = template.length - 1;
|
|
887
|
+
let i = 0;
|
|
888
|
+
// updates are searched via unique names, linearly increased across the tree
|
|
889
|
+
// <div isµ0="attr" isµ1="other"><!--isµ2--><style><!--isµ3--</style></div>
|
|
890
|
+
let search = `${prefix}${i}`;
|
|
891
|
+
while (i < length) {
|
|
892
|
+
const node = tw.nextNode();
|
|
893
|
+
// if not all updates are bound but there's nothing else to crawl
|
|
894
|
+
// it means that there is something wrong with the template.
|
|
895
|
+
if (!node) throw `bad template: ${text}`;
|
|
896
|
+
// if the current node is a comment, and it contains isµX
|
|
897
|
+
// it means the update should take care of any content
|
|
898
|
+
if (node.nodeType === 8) {
|
|
899
|
+
// The only comments to be considered are those
|
|
900
|
+
// which content is exactly the same as the searched one.
|
|
901
|
+
if (node.data === search) {
|
|
902
|
+
nodes.push({ type: 'node', path: createPath(node) });
|
|
903
|
+
search = `${prefix}${++i}`;
|
|
904
|
+
}
|
|
905
|
+
} else {
|
|
906
|
+
// if the node is not a comment, loop through all its attributes
|
|
907
|
+
// named isµX and relate attribute updates to this node and the
|
|
908
|
+
// attribute name, retrieved through node.getAttribute("isµX")
|
|
909
|
+
// the isµX attribute will be removed as irrelevant for the layout
|
|
910
|
+
// let svg = -1;
|
|
911
|
+
while (node.hasAttribute(search)) {
|
|
912
|
+
nodes.push({
|
|
913
|
+
type: 'attr',
|
|
914
|
+
path: createPath(node),
|
|
915
|
+
name: node.getAttribute(search)
|
|
916
|
+
});
|
|
917
|
+
node.removeAttribute(search);
|
|
918
|
+
search = `${prefix}${++i}`;
|
|
919
|
+
}
|
|
920
|
+
// if the node was a style, textarea, or others, check its content
|
|
921
|
+
// and if it is <!--isµX--> then update tex-only this node
|
|
922
|
+
if (textOnly.test(node.localName) && node.textContent.trim() === `<!--${search}-->`) {
|
|
923
|
+
node.textContent = '';
|
|
924
|
+
nodes.push({ type: 'text', path: createPath(node) });
|
|
925
|
+
search = `${prefix}${++i}`;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
// once all nodes to update, or their attributes, are known, the content
|
|
930
|
+
// will be cloned in the future to represent the template, and all updates
|
|
931
|
+
// related to such content retrieved right away without needing to re-crawl
|
|
932
|
+
// the exact same template, and its content, more than once.
|
|
933
|
+
return { content, nodes };
|
|
934
|
+
};
|
|
935
|
+
|
|
936
|
+
// if a template is unknown, perform the previous mapping, otherwise grab
|
|
937
|
+
// its details such as the fragment with all nodes, and updates info.
|
|
938
|
+
const mapUpdates = (type, template) => {
|
|
939
|
+
const { content, nodes } =
|
|
940
|
+
cache$1.get(template) || cache$1.set(template, mapTemplate(type, template));
|
|
941
|
+
// clone deeply the fragment
|
|
942
|
+
const fragment = importNode(content, true);
|
|
943
|
+
// and relate an update handler per each node that needs one
|
|
944
|
+
const updates = nodes.map(handlers, fragment);
|
|
945
|
+
// return the fragment and all updates to use within its nodes
|
|
946
|
+
return { content: fragment, updates };
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
// as html and svg can be nested calls, but no parent node is known
|
|
950
|
+
// until rendered somewhere, the unroll operation is needed to
|
|
951
|
+
// discover what to do with each interpolation, which will result
|
|
952
|
+
// into an update operation.
|
|
953
|
+
const unroll = (info, { type, template, values }) => {
|
|
954
|
+
// interpolations can contain holes and arrays, so these need
|
|
955
|
+
// to be recursively discovered
|
|
956
|
+
const length = unrollValues(info, values);
|
|
957
|
+
let { entry } = info;
|
|
958
|
+
// if the cache entry is either null or different from the template
|
|
959
|
+
// and the type this unroll should resolve, create a new entry
|
|
960
|
+
// assigning a new content fragment and the list of updates.
|
|
961
|
+
if (!entry || entry.template !== template || entry.type !== type)
|
|
962
|
+
info.entry = entry = createEntry(type, template);
|
|
963
|
+
const { content, updates, wire } = entry;
|
|
964
|
+
// even if the fragment and its nodes is not live yet,
|
|
965
|
+
// it is already possible to update via interpolations values.
|
|
966
|
+
for (let i = 0; i < length; i++) updates[i](values[i]);
|
|
967
|
+
// if the entry was new, or representing a different template or type,
|
|
968
|
+
// create a new persistent entity to use during diffing.
|
|
969
|
+
// This is simply a DOM node, when the template has a single container,
|
|
970
|
+
// as in `<p></p>`, or a "wire" in `<p></p><p></p>` and similar cases.
|
|
971
|
+
return wire || (entry.wire = persistent(content));
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
// the stack retains, per each interpolation value, the cache
|
|
975
|
+
// related to each interpolation value, or null, if the render
|
|
976
|
+
// was conditional and the value is not special (Array or Hole)
|
|
977
|
+
const unrollValues = ({ stack }, values) => {
|
|
978
|
+
const { length } = values;
|
|
979
|
+
for (let i = 0; i < length; i++) {
|
|
980
|
+
const hole = values[i];
|
|
981
|
+
// each Hole gets unrolled and re-assigned as value
|
|
982
|
+
// so that domdiff will deal with a node/wire, not with a hole
|
|
983
|
+
if (hole instanceof Hole) values[i] = unroll(stack[i] || (stack[i] = createCache()), hole);
|
|
984
|
+
// arrays are recursively resolved so that each entry will contain
|
|
985
|
+
// also a DOM node or a wire, hence it can be diffed if/when needed
|
|
986
|
+
else if (isArray(hole)) unrollValues(stack[i] || (stack[i] = createCache()), hole);
|
|
987
|
+
// if the value is nothing special, the stack doesn't need to retain data
|
|
988
|
+
// this is useful also to cleanup previously retained data, if the value
|
|
989
|
+
// was a Hole, or an Array, but not anymore, i.e.:
|
|
990
|
+
// const update = content => html`<div>${content}</div>`;
|
|
991
|
+
// update(listOfItems); update(null); update(html`hole`)
|
|
992
|
+
else stack[i] = null;
|
|
993
|
+
}
|
|
994
|
+
if (length < stack.length) stack.splice(length);
|
|
995
|
+
return length;
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Holds all details wrappers needed to render the content further on.
|
|
1000
|
+
* @constructor
|
|
1001
|
+
* @param {string} type The hole type, either `html` or `svg`.
|
|
1002
|
+
* @param {string[]} template The template literals used to the define the content.
|
|
1003
|
+
* @param {Array} values Zero, one, or more interpolated values to render.
|
|
1004
|
+
*/
|
|
1005
|
+
class Hole {
|
|
1006
|
+
constructor(type, template, values) {
|
|
1007
|
+
this.type = type;
|
|
1008
|
+
this.template = template;
|
|
1009
|
+
this.values = values;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// both `html` and `svg` template literal tags are polluted
|
|
1014
|
+
// with a `for(ref[, id])` and a `node` tag too
|
|
1015
|
+
const tag = (type) => {
|
|
1016
|
+
// both `html` and `svg` tags have their own cache
|
|
1017
|
+
const keyed = new WeakMapSet();
|
|
1018
|
+
// keyed operations always re-use the same cache and unroll
|
|
1019
|
+
// the template and its interpolations right away
|
|
1020
|
+
const fixed =
|
|
1021
|
+
(cache) =>
|
|
1022
|
+
(template, ...values) =>
|
|
1023
|
+
unroll(cache, { type, template, values });
|
|
1024
|
+
return Object.assign(
|
|
1025
|
+
// non keyed operations are recognized as instance of Hole
|
|
1026
|
+
// during the "unroll", recursively resolved and updated
|
|
1027
|
+
(template, ...values) => new Hole(type, template, values),
|
|
1028
|
+
{
|
|
1029
|
+
// keyed operations need a reference object, usually the parent node
|
|
1030
|
+
// which is showing keyed results, and optionally a unique id per each
|
|
1031
|
+
// related node, handy with JSON results and mutable list of objects
|
|
1032
|
+
// that usually carry a unique identifier
|
|
1033
|
+
for(ref, id) {
|
|
1034
|
+
const memo = keyed.get(ref) || keyed.set(ref, new MapSet());
|
|
1035
|
+
return memo.get(id) || memo.set(id, fixed(createCache()));
|
|
1036
|
+
},
|
|
1037
|
+
// it is possible to create one-off content out of the box via node tag
|
|
1038
|
+
// this might return the single created node, or a fragment with all
|
|
1039
|
+
// nodes present at the root level and, of course, their child nodes
|
|
1040
|
+
node: (template, ...values) =>
|
|
1041
|
+
unroll(createCache(), new Hole(type, template, values)).valueOf()
|
|
1042
|
+
}
|
|
1043
|
+
);
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
// each rendered node gets its own cache
|
|
1047
|
+
const cache = new WeakMapSet();
|
|
1048
|
+
|
|
1049
|
+
// rendering means understanding what `html` or `svg` tags returned
|
|
1050
|
+
// and it relates a specific node to its own unique cache.
|
|
1051
|
+
// Each time the content to render changes, the node is cleaned up
|
|
1052
|
+
// and the new new content is appended, and if such content is a Hole
|
|
1053
|
+
// then it's "unrolled" to resolve all its inner nodes.
|
|
1054
|
+
const render = (where, what) => {
|
|
1055
|
+
const hole = typeof what === 'function' ? what() : what;
|
|
1056
|
+
const info = cache.get(where) || cache.set(where, createCache());
|
|
1057
|
+
const wire = hole instanceof Hole ? unroll(info, hole) : hole;
|
|
1058
|
+
if (wire !== info.wire) {
|
|
1059
|
+
info.wire = wire;
|
|
1060
|
+
// valueOf() simply returns the node itself, but in case it was a "wire"
|
|
1061
|
+
// it will eventually re-append all nodes to its fragment so that such
|
|
1062
|
+
// fragment can be re-appended many times in a meaningful way
|
|
1063
|
+
// (wires are basically persistent fragments facades with special behavior)
|
|
1064
|
+
where.replaceChildren(wire.valueOf());
|
|
1065
|
+
}
|
|
1066
|
+
return where;
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
const html$1 = tag('html');
|
|
1070
|
+
tag('svg');
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Updates the DOM with a scheduled task.
|
|
1074
|
+
* @param target The element instance.
|
|
1075
|
+
* @param name Property/State name.
|
|
1076
|
+
* @param previous The previous value of Property/State.
|
|
1077
|
+
* @param callback Invoked when the rendering phase is completed.
|
|
1078
|
+
*/
|
|
1079
|
+
const requestUpdate = (target, name, previous, callback) => {
|
|
1080
|
+
// Creates/Gets a stacks.
|
|
1081
|
+
const stacks = (target[API_STACKS] ||= new Map());
|
|
1082
|
+
// Creates/Updates a stack.
|
|
1083
|
+
const stack = stacks.get(name) || { callbacks: [], previous };
|
|
1084
|
+
// Adds the callback to the stack, if exists.
|
|
1085
|
+
callback && stack.callbacks.push(callback);
|
|
1086
|
+
// Stores the stack.
|
|
1087
|
+
stacks.set(name, stack);
|
|
1088
|
+
// Defines a handler.
|
|
1089
|
+
const handler = () => {
|
|
1090
|
+
// Skips the rendering phase if DOM isn't ready.
|
|
1091
|
+
if (!target[API_CONNECTED])
|
|
1092
|
+
return;
|
|
1093
|
+
// Calculates the states to pass into lifecycles' callbacks.
|
|
1094
|
+
const states = new Map(Array.from(stacks)
|
|
1095
|
+
.filter((stack) => stack[0])
|
|
1096
|
+
.map((stack) => [stack[0], stack[1].previous]));
|
|
1097
|
+
// Calls the lifecycle's callback before the rendering phase.
|
|
1098
|
+
call(target, LIFECYCLE_UPDATE, states);
|
|
1099
|
+
// Renders template to the DOM.
|
|
1100
|
+
render(shadowRoot(target), () => call(target, METHOD_RENDER) ?? null);
|
|
1101
|
+
// Invokes requests' callback.
|
|
1102
|
+
stacks.forEach((state) => {
|
|
1103
|
+
state.callbacks.forEach((callback, index, callbacks) => {
|
|
1104
|
+
callback(callbacks.length - 1 != index);
|
|
1105
|
+
});
|
|
1106
|
+
});
|
|
1107
|
+
// TODO
|
|
1108
|
+
(() => {
|
|
1109
|
+
const raw = target.constructor[STATIC_STYLE];
|
|
1110
|
+
if (!raw)
|
|
1111
|
+
return;
|
|
1112
|
+
const regex1 = /this-([\w-]+)(?:-([\w-]+))?/g;
|
|
1113
|
+
const regex2 = /(\s*\w+\s*:\s*(undefined|null)\s*;?)/g;
|
|
1114
|
+
const regex3 = /global\s+[^{]+\{[^{}]*\{[^{}]*\}[^{}]*\}|global\s+[^{]+\{[^{}]*\}/g;
|
|
1115
|
+
const hasGlobal = raw.includes('global');
|
|
1116
|
+
const hasVariable = raw.includes('this-');
|
|
1117
|
+
let localSheet = target[API_STYLE];
|
|
1118
|
+
let globalSheet = target.constructor[API_STYLE];
|
|
1119
|
+
if (!hasVariable && localSheet)
|
|
1120
|
+
return;
|
|
1121
|
+
const parsed = raw
|
|
1122
|
+
.replace(regex1, (match, key) => {
|
|
1123
|
+
let value = target;
|
|
1124
|
+
for (const section of key.split('-')) {
|
|
1125
|
+
value = value?.[section];
|
|
1126
|
+
}
|
|
1127
|
+
return value;
|
|
1128
|
+
})
|
|
1129
|
+
.replace(regex2, '');
|
|
1130
|
+
if (!localSheet) {
|
|
1131
|
+
localSheet = new CSSStyleSheet();
|
|
1132
|
+
target[API_STYLE] = localSheet;
|
|
1133
|
+
shadowRoot(target).adoptedStyleSheets.push(localSheet);
|
|
1134
|
+
}
|
|
1135
|
+
const localStyle = parsed.replace(regex3, '');
|
|
1136
|
+
localSheet.replaceSync(localStyle);
|
|
1137
|
+
if (!hasGlobal || globalSheet)
|
|
1138
|
+
return;
|
|
1139
|
+
if (!globalSheet) {
|
|
1140
|
+
globalSheet = new CSSStyleSheet();
|
|
1141
|
+
target.constructor[API_STYLE] = globalSheet;
|
|
1142
|
+
document.adoptedStyleSheets.push(globalSheet);
|
|
1143
|
+
}
|
|
1144
|
+
const globalStyle = parsed
|
|
1145
|
+
?.match(regex3)
|
|
1146
|
+
?.join('')
|
|
1147
|
+
?.replaceAll('global', '')
|
|
1148
|
+
?.replaceAll(':host', getTag(target));
|
|
1149
|
+
globalSheet.replaceSync(globalStyle);
|
|
1150
|
+
})();
|
|
1151
|
+
// Calls the lifecycle's callback after the rendering phase.
|
|
1152
|
+
call(target, LIFECYCLE_UPDATED, states);
|
|
1153
|
+
// Clears stacks.
|
|
1154
|
+
stacks.clear();
|
|
1155
|
+
// TODO: related to the @Watch decorator.
|
|
1156
|
+
target[API_RENDER_COMPLETED] = true;
|
|
1157
|
+
};
|
|
1158
|
+
// Creates/Gets a micro task function.
|
|
1159
|
+
target[API_REQUEST] ||= task({ handler });
|
|
1160
|
+
// Calls the micro task.
|
|
1161
|
+
call(target, API_REQUEST);
|
|
1162
|
+
};
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* Returns the slots name.
|
|
1166
|
+
*/
|
|
1167
|
+
const slots = (target) => {
|
|
1168
|
+
const element = host(target);
|
|
1169
|
+
const slots = {};
|
|
1170
|
+
const children = Array.from(element.childNodes);
|
|
1171
|
+
for (const child of children) {
|
|
1172
|
+
if (child.nodeName == '#comment')
|
|
1173
|
+
continue;
|
|
1174
|
+
const name = child['slot'] || (child.nodeValue?.trim() && 'default') || ('slot' in child && 'default');
|
|
1175
|
+
if (!name)
|
|
1176
|
+
continue;
|
|
1177
|
+
slots[name] = true;
|
|
1178
|
+
}
|
|
1179
|
+
return slots;
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
/**
|
|
1183
|
+
* Converts a JavaScript object containing CSS styles to a CSS string.
|
|
1184
|
+
*/
|
|
1185
|
+
const styles$1 = (input) => {
|
|
1186
|
+
return Object.keys(input)
|
|
1187
|
+
.filter((key) => input[key] !== undefined && input[key] !== null)
|
|
1188
|
+
.map((key) => `${key.startsWith('--') ? '--' : ''}${kebabCase(key)}: ${input[key]}`)
|
|
1189
|
+
.join('; ');
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
const toCSSColor = (input) => {
|
|
1193
|
+
return isCSSColor(input) ? input : undefined;
|
|
1194
|
+
};
|
|
1195
|
+
|
|
1196
|
+
const toCSSUnit = (input) => {
|
|
1197
|
+
if (input == null || input === '') {
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
if (typeof input === 'number' || !isNaN(Number(input))) {
|
|
1201
|
+
return `${input}px`;
|
|
1202
|
+
}
|
|
1203
|
+
if (/^\d+(\.\d+)?(px|pt|cm|mm|in|em|rem|%|vw|vh)$/.test(input)) {
|
|
1204
|
+
return input;
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1208
|
+
function toDecorator(util, ...args) {
|
|
1209
|
+
return function (target, key) {
|
|
1210
|
+
defineProperty(target, key, {
|
|
1211
|
+
get() {
|
|
1212
|
+
return util(this, ...args);
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
const toProperty = (input, type) => {
|
|
1219
|
+
if (type === undefined)
|
|
1220
|
+
return input;
|
|
1221
|
+
const string = `${input}`;
|
|
1222
|
+
if (TYPE_BOOLEAN & type || type === Boolean) {
|
|
1223
|
+
if (string === '')
|
|
1224
|
+
return true;
|
|
1225
|
+
if (string === 'true')
|
|
1226
|
+
return true;
|
|
1227
|
+
if (string === 'false')
|
|
1228
|
+
return false;
|
|
1229
|
+
}
|
|
1230
|
+
if (TYPE_NUMBER & type || type === Number) {
|
|
1231
|
+
if (string != '' && !isNaN(input)) {
|
|
1232
|
+
return parseFloat(input);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
if (TYPE_NULL & type || type === null) {
|
|
1236
|
+
if (string === 'null') {
|
|
1237
|
+
return null;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
if (TYPE_DATE & type || type === Date) {
|
|
1241
|
+
const value = new Date(input);
|
|
1242
|
+
if (value.toString() != 'Invalid Date') {
|
|
1243
|
+
return value;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
if (TYPE_ARRAY & type || type === Array) {
|
|
1247
|
+
try {
|
|
1248
|
+
const value = JSON.parse(input);
|
|
1249
|
+
if (typeOf(value) == 'array') {
|
|
1250
|
+
return value;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
catch { }
|
|
1254
|
+
}
|
|
1255
|
+
if (TYPE_OBJECT & type || type === Object) {
|
|
1256
|
+
try {
|
|
1257
|
+
const value = JSON.parse(input);
|
|
1258
|
+
if (typeOf(value) == 'object') {
|
|
1259
|
+
return value;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
catch { }
|
|
1263
|
+
}
|
|
1264
|
+
if (TYPE_UNDEFINED & type || type === undefined) {
|
|
1265
|
+
if (string === 'undefined') {
|
|
1266
|
+
return undefined;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
return input;
|
|
1270
|
+
};
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Converts a value to a unit.
|
|
1274
|
+
*/
|
|
1275
|
+
const toUnit = (input, unit = 'px') => {
|
|
1276
|
+
if (input === null || input === undefined || input === '')
|
|
1277
|
+
return input;
|
|
1278
|
+
if (isNaN(+input))
|
|
1279
|
+
return String(input);
|
|
1280
|
+
return Number(input) + unit;
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
const wrapMethod = (mode, target, key, handler) => {
|
|
1284
|
+
// Gets the original function
|
|
1285
|
+
const original = target[key];
|
|
1286
|
+
// Validate target property
|
|
1287
|
+
if (original && typeof original !== 'function') {
|
|
1288
|
+
throw new TypeError(`Property ${String(key)} is not a function`);
|
|
1289
|
+
}
|
|
1290
|
+
// Creates new function
|
|
1291
|
+
function wrapped(...args) {
|
|
1292
|
+
// Calls the handler before the original
|
|
1293
|
+
if (mode == 'before') {
|
|
1294
|
+
handler.apply(this, args);
|
|
1295
|
+
}
|
|
1296
|
+
// Calls the original
|
|
1297
|
+
const result = original?.apply(this, args);
|
|
1298
|
+
// Calls the handler after the original
|
|
1299
|
+
if (mode == 'after') {
|
|
1300
|
+
handler.apply(this, args);
|
|
1301
|
+
}
|
|
1302
|
+
// Returns the result
|
|
1303
|
+
return result;
|
|
1304
|
+
}
|
|
1305
|
+
// Replaces the wrapped with the original one
|
|
1306
|
+
target[key] = wrapped;
|
|
1307
|
+
};
|
|
1308
|
+
|
|
1309
|
+
/**
|
|
1310
|
+
* Used to bind a method of a class to the current context,
|
|
1311
|
+
* making it easier to reference `this` within the method.
|
|
1312
|
+
*/
|
|
1313
|
+
function Bind() {
|
|
1314
|
+
return function (target, key, descriptor) {
|
|
1315
|
+
const original = descriptor.value;
|
|
1316
|
+
return {
|
|
1317
|
+
configurable: true,
|
|
1318
|
+
get() {
|
|
1319
|
+
const next = original.bind(this);
|
|
1320
|
+
defineProperty(this, key, {
|
|
1321
|
+
value: next,
|
|
1322
|
+
configurable: true,
|
|
1323
|
+
writable: true
|
|
1324
|
+
});
|
|
1325
|
+
return next;
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
function Provider(namespace) {
|
|
1332
|
+
return function (target, key) {
|
|
1333
|
+
const symbol = Symbol();
|
|
1334
|
+
const [MAIN, SUB] = namespace.split('.');
|
|
1335
|
+
const prefix = `${KEY}:${MAIN}`;
|
|
1336
|
+
const cleanups = (instance) => {
|
|
1337
|
+
return (instance[symbol] ||= new Map());
|
|
1338
|
+
};
|
|
1339
|
+
const update = (instance) => {
|
|
1340
|
+
const options = {};
|
|
1341
|
+
options.detail = instance[key];
|
|
1342
|
+
dispatch(instance, `${prefix}:update`, options);
|
|
1343
|
+
if (!SUB)
|
|
1344
|
+
return;
|
|
1345
|
+
options.bubbles = true;
|
|
1346
|
+
dispatch(instance, `${prefix}:${instance[SUB]}:update`, options);
|
|
1347
|
+
};
|
|
1348
|
+
// TODO
|
|
1349
|
+
wrapMethod('after', target, LIFECYCLE_CONNECTED, function () {
|
|
1350
|
+
const cleanup = () => {
|
|
1351
|
+
off(this, `${prefix}:presence`, onPresence);
|
|
1352
|
+
cleanups(this).delete(prefix);
|
|
1353
|
+
};
|
|
1354
|
+
const onPresence = (event) => {
|
|
1355
|
+
event.stopPropagation();
|
|
1356
|
+
event.detail(this, this[key]);
|
|
1357
|
+
};
|
|
1358
|
+
on(this, `${prefix}:presence`, onPresence);
|
|
1359
|
+
cleanups(this).set(prefix, cleanup);
|
|
1360
|
+
});
|
|
1361
|
+
wrapMethod('after', target, LIFECYCLE_UPDATE, function (states) {
|
|
1362
|
+
update(this);
|
|
1363
|
+
if (cleanups(this).size && !states.has(SUB))
|
|
1364
|
+
return;
|
|
1365
|
+
cleanups(this).get(`${prefix}:${states.get(SUB)}`)?.();
|
|
1366
|
+
const type = `${prefix}:${this[SUB]}`;
|
|
1367
|
+
const cleanup = () => {
|
|
1368
|
+
off(window, `${type}:presence`, onPresence);
|
|
1369
|
+
cleanups(this).delete(type);
|
|
1370
|
+
dispatch(window, `${type}:disconnect`);
|
|
1371
|
+
};
|
|
1372
|
+
const onPresence = () => {
|
|
1373
|
+
update(this);
|
|
1374
|
+
};
|
|
1375
|
+
on(window, `${type}:presence`, onPresence);
|
|
1376
|
+
cleanups(this).set(type, cleanup);
|
|
1377
|
+
});
|
|
1378
|
+
wrapMethod('after', target, LIFECYCLE_DISCONNECTED, function () {
|
|
1379
|
+
cleanups(this).forEach((cleanup) => cleanup());
|
|
1380
|
+
});
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
function Consumer(namespace) {
|
|
1384
|
+
return function (target, key) {
|
|
1385
|
+
const symbol = Symbol();
|
|
1386
|
+
const [MAIN, SUB] = namespace.split('.');
|
|
1387
|
+
const prefix = `${KEY}:${MAIN}`;
|
|
1388
|
+
const cleanups = (instance) => {
|
|
1389
|
+
return (instance[symbol] ||= new Map());
|
|
1390
|
+
};
|
|
1391
|
+
const update = (instance, state) => {
|
|
1392
|
+
instance[key] = state;
|
|
1393
|
+
};
|
|
1394
|
+
// TODO
|
|
1395
|
+
wrapMethod('after', target, LIFECYCLE_CONNECTED, function () {
|
|
1396
|
+
// TODO
|
|
1397
|
+
if (SUB && this[SUB])
|
|
1398
|
+
return;
|
|
1399
|
+
// TODO
|
|
1400
|
+
let connected;
|
|
1401
|
+
const options = {
|
|
1402
|
+
bubbles: true
|
|
1403
|
+
};
|
|
1404
|
+
options.detail = (parent, state) => {
|
|
1405
|
+
// TODO
|
|
1406
|
+
connected = true;
|
|
1407
|
+
update(this, state);
|
|
1408
|
+
const cleanup = () => {
|
|
1409
|
+
off(parent, `${prefix}:update`, onUpdate);
|
|
1410
|
+
cleanups(this).delete(prefix);
|
|
1411
|
+
update(this, undefined);
|
|
1412
|
+
};
|
|
1413
|
+
const onUpdate = (event) => {
|
|
1414
|
+
update(this, event.detail);
|
|
1415
|
+
};
|
|
1416
|
+
on(parent, `${prefix}:update`, onUpdate);
|
|
1417
|
+
cleanups(this).set(prefix, cleanup);
|
|
1418
|
+
};
|
|
1419
|
+
dispatch(this, `${prefix}:presence`, options);
|
|
1420
|
+
// TODO: When the `Provider` element is activated after the `Consumer` element.
|
|
1421
|
+
!connected && setTimeout(() => dispatch(this, `${prefix}:presence`, options));
|
|
1422
|
+
});
|
|
1423
|
+
wrapMethod('after', target, LIFECYCLE_UPDATE, function (states) {
|
|
1424
|
+
if (cleanups(this).size && !states.has(SUB))
|
|
1425
|
+
return;
|
|
1426
|
+
cleanups(this).get(`${prefix}:${states.get(SUB)}`)?.();
|
|
1427
|
+
const type = `${prefix}:${this[SUB]}`;
|
|
1428
|
+
const cleanup = () => {
|
|
1429
|
+
off(window, `${type}:disconnect`, onDisconnect);
|
|
1430
|
+
off(window, `${type}:update`, onUpdate);
|
|
1431
|
+
cleanups(this).delete(type);
|
|
1432
|
+
update(this, undefined);
|
|
1433
|
+
};
|
|
1434
|
+
const onDisconnect = () => {
|
|
1435
|
+
update(this, undefined);
|
|
1436
|
+
};
|
|
1437
|
+
const onUpdate = (event) => {
|
|
1438
|
+
update(this, event.detail);
|
|
1439
|
+
};
|
|
1440
|
+
on(window, `${type}:disconnect`, onDisconnect);
|
|
1441
|
+
on(window, `${type}:update`, onUpdate);
|
|
1442
|
+
cleanups(this).set(type, cleanup);
|
|
1443
|
+
dispatch(window, `${type}:presence`);
|
|
1444
|
+
});
|
|
1445
|
+
wrapMethod('after', target, LIFECYCLE_DISCONNECTED, function () {
|
|
1446
|
+
cleanups(this).forEach((cleanup) => cleanup());
|
|
1447
|
+
});
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
/**
|
|
1452
|
+
* A method decorator that applies debounce behavior to a class method.
|
|
1453
|
+
* Ensures that the method executes only after the specified delay,
|
|
1454
|
+
* resetting the timer if called again within the delay period.
|
|
1455
|
+
*
|
|
1456
|
+
* @param {number} delay - The debounce delay in milliseconds.
|
|
1457
|
+
*/
|
|
1458
|
+
function Debounce(delay = 0) {
|
|
1459
|
+
return function (target, key, descriptor) {
|
|
1460
|
+
const KEY = Symbol();
|
|
1461
|
+
const original = descriptor.value;
|
|
1462
|
+
function clear() {
|
|
1463
|
+
if (!Object.hasOwn(this, KEY))
|
|
1464
|
+
return;
|
|
1465
|
+
clearTimeout(this[KEY]);
|
|
1466
|
+
delete this[KEY];
|
|
1467
|
+
}
|
|
1468
|
+
function debounced(...args) {
|
|
1469
|
+
clear.call(this);
|
|
1470
|
+
this[KEY] = window.setTimeout(() => {
|
|
1471
|
+
clear.call(this);
|
|
1472
|
+
original.apply(this, args);
|
|
1473
|
+
}, delay);
|
|
1474
|
+
}
|
|
1475
|
+
descriptor.value = debounced;
|
|
1476
|
+
return Bind()(target, key, descriptor);
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
/**
|
|
1481
|
+
* Indicates whether the [Direction](https://mdn.io/css-direction)
|
|
1482
|
+
* of the element is `Right-To-Left` or `Left-To-Right`.
|
|
1483
|
+
*/
|
|
1484
|
+
function Direction() {
|
|
1485
|
+
return toDecorator(direction);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/**
|
|
1489
|
+
* The class marked with this decorator is considered a
|
|
1490
|
+
* [Custom Element](https://mdn.io/using-custom-elements),
|
|
1491
|
+
* and its name, in kebab-case, serves as the element name.
|
|
1492
|
+
*/
|
|
1493
|
+
function Element() {
|
|
1494
|
+
return function (constructor) {
|
|
1495
|
+
if (isServer())
|
|
1496
|
+
return;
|
|
1497
|
+
const tag = getTag(constructor);
|
|
1498
|
+
if (customElements.get(tag))
|
|
1499
|
+
return;
|
|
1500
|
+
customElements.define(tag, proxy(constructor));
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
const proxy = (constructor) => {
|
|
1504
|
+
return class Plus extends HTMLElement {
|
|
1505
|
+
#instance;
|
|
1506
|
+
static formAssociated = constructor['formAssociated'];
|
|
1507
|
+
static observedAttributes = constructor['observedAttributes'];
|
|
1508
|
+
constructor() {
|
|
1509
|
+
super();
|
|
1510
|
+
this.attachShadow({
|
|
1511
|
+
mode: 'open',
|
|
1512
|
+
delegatesFocus: constructor['delegatesFocus'],
|
|
1513
|
+
slotAssignment: constructor['slotAssignment']
|
|
1514
|
+
});
|
|
1515
|
+
this.#instance = new constructor();
|
|
1516
|
+
this.#instance[API_HOST] = () => this;
|
|
1517
|
+
call(this.#instance, LIFECYCLE_CONSTRUCTED);
|
|
1518
|
+
}
|
|
1519
|
+
adoptedCallback() {
|
|
1520
|
+
call(this.#instance, LIFECYCLE_ADOPTED);
|
|
1521
|
+
}
|
|
1522
|
+
attributeChangedCallback(key, prev, next) {
|
|
1523
|
+
if (prev != next) {
|
|
1524
|
+
this.#instance['RAW:' + key] = next;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
connectedCallback() {
|
|
1528
|
+
// TODO: experimental for global config
|
|
1529
|
+
Object.assign(this.#instance, getConfig('element', getTag(this.#instance), 'property'));
|
|
1530
|
+
this.#instance[API_CONNECTED] = true;
|
|
1531
|
+
call(this.#instance, LIFECYCLE_CONNECTED);
|
|
1532
|
+
requestUpdate(this.#instance, undefined, undefined, () => {
|
|
1533
|
+
call(this.#instance, LIFECYCLE_READY);
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
disconnectedCallback() {
|
|
1537
|
+
call(this.#instance, LIFECYCLE_DISCONNECTED);
|
|
1538
|
+
}
|
|
1539
|
+
};
|
|
1540
|
+
};
|
|
1541
|
+
|
|
1542
|
+
/**
|
|
1543
|
+
* Provides the capability to dispatch a [CustomEvent](https://mdn.io/custom-event)
|
|
1544
|
+
* from an element.
|
|
1545
|
+
*
|
|
1546
|
+
* @param options An object that configures options for the event dispatcher.
|
|
1547
|
+
*/
|
|
1548
|
+
function Event(options = {}) {
|
|
1549
|
+
return function (target, key) {
|
|
1550
|
+
target[key] = function (detail) {
|
|
1551
|
+
const element = host(this);
|
|
1552
|
+
const framework = getFramework(this);
|
|
1553
|
+
options.bubbles ??= false;
|
|
1554
|
+
let type = String(key);
|
|
1555
|
+
switch (framework) {
|
|
1556
|
+
// TODO: Experimental
|
|
1557
|
+
case 'blazor':
|
|
1558
|
+
options.bubbles = true;
|
|
1559
|
+
type = pascalCase(type);
|
|
1560
|
+
try {
|
|
1561
|
+
window['Blazor'].registerCustomEventType(type, {
|
|
1562
|
+
createEventArgs: (event) => ({
|
|
1563
|
+
detail: event.detail
|
|
1564
|
+
})
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
catch { }
|
|
1568
|
+
break;
|
|
1569
|
+
case 'qwik':
|
|
1570
|
+
case 'solid':
|
|
1571
|
+
type = pascalCase(type).toLowerCase();
|
|
1572
|
+
break;
|
|
1573
|
+
case 'react':
|
|
1574
|
+
case 'preact':
|
|
1575
|
+
type = pascalCase(type);
|
|
1576
|
+
break;
|
|
1577
|
+
default:
|
|
1578
|
+
type = kebabCase(type);
|
|
1579
|
+
break;
|
|
1580
|
+
}
|
|
1581
|
+
let event;
|
|
1582
|
+
event ||= getConfig('event', 'resolver')?.({ detail, element, framework, options, type });
|
|
1583
|
+
event && element.dispatchEvent(event);
|
|
1584
|
+
event ||= dispatch(this, type, { ...options, detail });
|
|
1585
|
+
return event;
|
|
1586
|
+
};
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
/**
|
|
1591
|
+
* Indicates the host of the element.
|
|
1592
|
+
*/
|
|
1593
|
+
function Host() {
|
|
1594
|
+
return toDecorator(host);
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/**
|
|
1598
|
+
* Indicates whether the direction of the element is `Right-To-Left` or not.
|
|
1599
|
+
*/
|
|
1600
|
+
function IsRTL() {
|
|
1601
|
+
return toDecorator(isRTL);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
/**
|
|
1605
|
+
* Will be called whenever the specified event is delivered to the target
|
|
1606
|
+
* [More](https://mdn.io/add-event-listener).
|
|
1607
|
+
*
|
|
1608
|
+
* @param type A case-sensitive string representing the [Event Type](https://mdn.io/events) to listen for.
|
|
1609
|
+
* @param options An object that configures
|
|
1610
|
+
* [options](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener#options)
|
|
1611
|
+
* for the event listener.
|
|
1612
|
+
*/
|
|
1613
|
+
function Listen(type, options) {
|
|
1614
|
+
return function (target, key, descriptor) {
|
|
1615
|
+
const element = (instance) => {
|
|
1616
|
+
switch (options?.target) {
|
|
1617
|
+
case 'body':
|
|
1618
|
+
return window.document.body;
|
|
1619
|
+
case 'document':
|
|
1620
|
+
return window.document;
|
|
1621
|
+
case 'window':
|
|
1622
|
+
return window;
|
|
1623
|
+
case 'host':
|
|
1624
|
+
return instance;
|
|
1625
|
+
default:
|
|
1626
|
+
return instance;
|
|
1627
|
+
}
|
|
1628
|
+
};
|
|
1629
|
+
wrapMethod('before', target, LIFECYCLE_CONNECTED, function () {
|
|
1630
|
+
on(element(this), type, this[key], options);
|
|
1631
|
+
});
|
|
1632
|
+
wrapMethod('before', target, LIFECYCLE_DISCONNECTED, function () {
|
|
1633
|
+
off(element(this), type, this[key], options);
|
|
1634
|
+
});
|
|
1635
|
+
return Bind()(target, key, descriptor);
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
/**
|
|
1640
|
+
* Provides a way to encapsulate functionality within an element
|
|
1641
|
+
* and invoke it as needed, both internally and externally.
|
|
1642
|
+
*/
|
|
1643
|
+
function Method() {
|
|
1644
|
+
return function (target, key, descriptor) {
|
|
1645
|
+
wrapMethod('before', target, LIFECYCLE_CONSTRUCTED, function () {
|
|
1646
|
+
host(this)[key] = this[key].bind(this);
|
|
1647
|
+
});
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
/**
|
|
1652
|
+
* Creates a reactive property, reflecting a corresponding attribute value,
|
|
1653
|
+
* and updates the element when the property is set.
|
|
1654
|
+
*/
|
|
1655
|
+
function Property(options) {
|
|
1656
|
+
return function (target, key, descriptor) {
|
|
1657
|
+
// Unique symbol for property storage to avoid naming conflicts
|
|
1658
|
+
const KEY = Symbol();
|
|
1659
|
+
// Unique symbol for the lock flag to prevent infinite loops during updates
|
|
1660
|
+
const LOCKED = Symbol();
|
|
1661
|
+
// Calculate attribute name from the property key if not explicitly provided
|
|
1662
|
+
const attribute = options?.attribute || kebabCase(key);
|
|
1663
|
+
// Store the original setter (if it exists) to preserve its behavior
|
|
1664
|
+
const originalSetter = descriptor?.set;
|
|
1665
|
+
// Register the attribute in the observedAttributes array for the element
|
|
1666
|
+
(target.constructor['observedAttributes'] ||= []).push(attribute);
|
|
1667
|
+
// Getter function to retrieve the property value
|
|
1668
|
+
function get() {
|
|
1669
|
+
return this[KEY];
|
|
1670
|
+
}
|
|
1671
|
+
// Setter function to update the property value and trigger updates
|
|
1672
|
+
function set(value) {
|
|
1673
|
+
// Store the previous value
|
|
1674
|
+
const previous = this[KEY];
|
|
1675
|
+
// Store the new value
|
|
1676
|
+
const next = value;
|
|
1677
|
+
// Skip updates if the value hasn't changed and no custom setter is defined
|
|
1678
|
+
if (!originalSetter && next === previous)
|
|
1679
|
+
return;
|
|
1680
|
+
// If a custom setter exists, call it with the new value
|
|
1681
|
+
if (originalSetter) {
|
|
1682
|
+
originalSetter.call(this, next);
|
|
1683
|
+
}
|
|
1684
|
+
// Otherwise, update the property directly
|
|
1685
|
+
else {
|
|
1686
|
+
this[KEY] = next;
|
|
1687
|
+
}
|
|
1688
|
+
// Request an update
|
|
1689
|
+
requestUpdate(this, key, previous, (skipped) => {
|
|
1690
|
+
// Skip if the update was aborted
|
|
1691
|
+
if (skipped)
|
|
1692
|
+
return;
|
|
1693
|
+
// If reflection is enabled, update the corresponding attribute
|
|
1694
|
+
if (!options?.reflect)
|
|
1695
|
+
return;
|
|
1696
|
+
// Lock to prevent infinite loops
|
|
1697
|
+
this[LOCKED] = true;
|
|
1698
|
+
// Update the attribute
|
|
1699
|
+
updateAttribute(this, attribute, next);
|
|
1700
|
+
// Unlock
|
|
1701
|
+
this[LOCKED] = false;
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
// Override the property descriptor if a custom setter exists
|
|
1705
|
+
if (originalSetter) {
|
|
1706
|
+
descriptor.set = set;
|
|
1707
|
+
}
|
|
1708
|
+
// Attach the getter and setter to the target class property if no descriptor exists
|
|
1709
|
+
if (!descriptor) {
|
|
1710
|
+
defineProperty(target, key, { configurable: true, get, set });
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Define a raw property setter to handle updates that trigger from the `attributeChangedCallback`,
|
|
1714
|
+
* To intercept and process raw attribute values before they are assigned to the property
|
|
1715
|
+
*/
|
|
1716
|
+
defineProperty(target, 'RAW:' + attribute, {
|
|
1717
|
+
set(value) {
|
|
1718
|
+
if (!this[LOCKED]) {
|
|
1719
|
+
// Convert the raw value and set it to the corresponding property
|
|
1720
|
+
this[key] = toProperty(value, options?.type);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1724
|
+
// Attach getter and setter to the host element on construction
|
|
1725
|
+
wrapMethod('before', target, LIFECYCLE_CONSTRUCTED, function () {
|
|
1726
|
+
const get = () => {
|
|
1727
|
+
if (descriptor && !descriptor.get) {
|
|
1728
|
+
throw new Error(`Property '${key}' does not have a getter. Unable to retrieve value.`);
|
|
1729
|
+
}
|
|
1730
|
+
return this[key];
|
|
1731
|
+
};
|
|
1732
|
+
const set = (value) => {
|
|
1733
|
+
if (descriptor && !descriptor.set) {
|
|
1734
|
+
throw new Error(`Property '${key}' does not have a setter. Unable to assign value.`);
|
|
1735
|
+
}
|
|
1736
|
+
this[key] = value;
|
|
1737
|
+
};
|
|
1738
|
+
defineProperty(host(this), key, { configurable: true, get, set });
|
|
1739
|
+
});
|
|
1740
|
+
/**
|
|
1741
|
+
* TODO: Review these behaviors again.
|
|
1742
|
+
*
|
|
1743
|
+
* When a property has a reflect and either a getter, a setter, or both are available,
|
|
1744
|
+
* three approaches are possible:
|
|
1745
|
+
*
|
|
1746
|
+
* 1. Only a getter is present: The attribute updates after each render is completed.
|
|
1747
|
+
* 2. Only a setter is present: The attribute updates after each setter call.
|
|
1748
|
+
* 3. Both getter and setter are present: The attribute is updated via the setter call
|
|
1749
|
+
* and also after each render is completed, resulting in two attribute update processes.
|
|
1750
|
+
*/
|
|
1751
|
+
if (options?.reflect && descriptor?.get) {
|
|
1752
|
+
wrapMethod('before', target, LIFECYCLE_UPDATED, function () {
|
|
1753
|
+
// Lock to prevent infinite loops
|
|
1754
|
+
this[LOCKED] = true;
|
|
1755
|
+
// Update the attribute
|
|
1756
|
+
updateAttribute(this, attribute, this[key]);
|
|
1757
|
+
// Unlock
|
|
1758
|
+
this[LOCKED] = false;
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
/**
|
|
1765
|
+
* Selects the first element in the shadow dom that matches a specified CSS selector.
|
|
1766
|
+
*
|
|
1767
|
+
* @param selectors A string containing one or more selectors to match.
|
|
1768
|
+
* This string must be a valid CSS selector string; if it isn't, a `SyntaxError` exception is thrown. See
|
|
1769
|
+
* [Locating DOM elements using selectors](https://developer.mozilla.org/en-US/docs/Web/API/Document_object_model/Locating_DOM_elements_using_selectors)
|
|
1770
|
+
* for more about selectors and how to manage them.
|
|
1771
|
+
*/
|
|
1772
|
+
function Query(selectors) {
|
|
1773
|
+
return toDecorator(query, selectors);
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
/**
|
|
1777
|
+
* Selects all elements in the shadow dom that match a specified CSS selector.
|
|
1778
|
+
*
|
|
1779
|
+
* @param selectors A string containing one or more selectors to match against.
|
|
1780
|
+
* This string must be a valid
|
|
1781
|
+
* [CSS selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_selectors)
|
|
1782
|
+
* string; if it's not, a `SyntaxError` exception is thrown. See
|
|
1783
|
+
* [Locating DOM elements using selectors](https://developer.mozilla.org/en-US/docs/Web/API/Document_object_model/Locating_DOM_elements_using_selectors)
|
|
1784
|
+
* for more information about using selectors to identify elements.
|
|
1785
|
+
* Multiple selectors may be specified by separating them using commas.
|
|
1786
|
+
*/
|
|
1787
|
+
function QueryAll(selectors) {
|
|
1788
|
+
return toDecorator(queryAll, selectors);
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
/**
|
|
1792
|
+
* Returns the slots name.
|
|
1793
|
+
*/
|
|
1794
|
+
function Slots() {
|
|
1795
|
+
return toDecorator(slots);
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
/**
|
|
1799
|
+
* Applying this decorator to any `class property` will trigger the
|
|
1800
|
+
* element to re-render upon the desired property changes.
|
|
1801
|
+
*/
|
|
1802
|
+
function State() {
|
|
1803
|
+
return function (target, key) {
|
|
1804
|
+
const KEY = Symbol();
|
|
1805
|
+
const name = String(key);
|
|
1806
|
+
defineProperty(target, key, {
|
|
1807
|
+
enumerable: true,
|
|
1808
|
+
configurable: true,
|
|
1809
|
+
get() {
|
|
1810
|
+
return this[KEY];
|
|
1811
|
+
},
|
|
1812
|
+
set(next) {
|
|
1813
|
+
const previous = this[KEY];
|
|
1814
|
+
if (next === previous)
|
|
1815
|
+
return;
|
|
1816
|
+
this[KEY] = next;
|
|
1817
|
+
requestUpdate(this, name, previous);
|
|
1818
|
+
}
|
|
1819
|
+
});
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// TODO: check the logic
|
|
1824
|
+
function Style() {
|
|
1825
|
+
return function (target, key) {
|
|
1826
|
+
const LAST = Symbol();
|
|
1827
|
+
const SHEET = Symbol();
|
|
1828
|
+
wrapMethod('before', target, LIFECYCLE_UPDATED, function () {
|
|
1829
|
+
let sheet = this[SHEET];
|
|
1830
|
+
let value = this[key];
|
|
1831
|
+
const update = (value) => (result) => {
|
|
1832
|
+
if (value && value !== this[LAST])
|
|
1833
|
+
return;
|
|
1834
|
+
sheet.replaceSync(toCssString(result));
|
|
1835
|
+
this[LAST] = undefined;
|
|
1836
|
+
};
|
|
1837
|
+
if (!sheet) {
|
|
1838
|
+
sheet = new CSSStyleSheet();
|
|
1839
|
+
this[SHEET] = sheet;
|
|
1840
|
+
shadowRoot(this)?.adoptedStyleSheets.push(sheet);
|
|
1841
|
+
}
|
|
1842
|
+
if (typeof value === 'function') {
|
|
1843
|
+
value = value.call(this);
|
|
1844
|
+
}
|
|
1845
|
+
if (value instanceof Promise) {
|
|
1846
|
+
value.then(update((this[LAST] = value))).catch((error) => {
|
|
1847
|
+
throw new Error('TODO', { cause: error });
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
else {
|
|
1851
|
+
update()(value);
|
|
1852
|
+
}
|
|
1853
|
+
});
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
const toCssString = (input, parent) => {
|
|
1857
|
+
if (typeof input == 'string') {
|
|
1858
|
+
return input.trim();
|
|
1859
|
+
}
|
|
1860
|
+
if (Array.isArray(input)) {
|
|
1861
|
+
return input
|
|
1862
|
+
.map((item) => toCssString(item, parent))
|
|
1863
|
+
.filter(Boolean)
|
|
1864
|
+
.join('\n');
|
|
1865
|
+
}
|
|
1866
|
+
if (typeof input != 'object')
|
|
1867
|
+
return '';
|
|
1868
|
+
let result = '';
|
|
1869
|
+
for (const key of Object.keys(input)) {
|
|
1870
|
+
const value = input[key];
|
|
1871
|
+
const ignore = [null, undefined, false].includes(value);
|
|
1872
|
+
if (ignore)
|
|
1873
|
+
continue;
|
|
1874
|
+
const cssKey = key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
|
|
1875
|
+
if (typeof value === 'object') {
|
|
1876
|
+
result += `${cssKey} {${toCssString(value, cssKey)}}`;
|
|
1877
|
+
}
|
|
1878
|
+
else {
|
|
1879
|
+
result += `${cssKey}: ${value};`;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
return parent ? result : `:host {${result}}`;
|
|
1883
|
+
};
|
|
1884
|
+
|
|
1885
|
+
/**
|
|
1886
|
+
* Monitors `@Property` and `@State` to detect changes.
|
|
1887
|
+
* The decorated method will be called after any changes,
|
|
1888
|
+
* with the `key`, `newValue`, and `oldValue` as parameters.
|
|
1889
|
+
* If the `key` is not defined, all `@Property` and `@State` are considered.
|
|
1890
|
+
*
|
|
1891
|
+
* @param keys Collection of `@Property` and `@State` names.
|
|
1892
|
+
* @param immediate Triggers the callback immediately after initialization.
|
|
1893
|
+
*/
|
|
1894
|
+
function Watch(keys, immediate) {
|
|
1895
|
+
return function (target, key) {
|
|
1896
|
+
// Gets all keys
|
|
1897
|
+
const all = [keys].flat().filter((item) => item);
|
|
1898
|
+
// Registers a lifecycle to detect changes.
|
|
1899
|
+
wrapMethod('after', target, LIFECYCLE_UPDATED, function (states) {
|
|
1900
|
+
// Skips the logic if 'immediate' wasn't passed.
|
|
1901
|
+
if (!immediate && !this[API_RENDER_COMPLETED])
|
|
1902
|
+
return;
|
|
1903
|
+
// Loops the keys.
|
|
1904
|
+
states.forEach((previous, item) => {
|
|
1905
|
+
// Skips the current key.
|
|
1906
|
+
if (all.length && !all.includes(item))
|
|
1907
|
+
return;
|
|
1908
|
+
// Invokes the method with parameters.
|
|
1909
|
+
this[key](this[item], previous, item);
|
|
1910
|
+
});
|
|
1911
|
+
});
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
const attributes = attributes$2;
|
|
1916
|
+
const html = html$1;
|
|
1917
|
+
const styles = styles$1;
|
|
1918
|
+
|
|
1919
|
+
export { Bind, Consumer, Debounce, Direction, Element, Event, Host, IsRTL, Listen, Method, Property, Provider, Query, QueryAll, Slots, State, Style, Watch, attributes as a, classes, direction, dispatch, getConfig, html as h, host, isCSSColor, isRTL, off, on, query, queryAll, styles as s, setConfig, slots, toCSSColor, toCSSUnit, toUnit };
|