@cocreate/elements 1.48.0 → 2.0.0
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/.github/FUNDING.yml +1 -1
- package/.github/workflows/automated.yml +18 -7
- package/CHANGELOG.md +753 -700
- package/CONTRIBUTING.md +6 -6
- package/README.md +9 -9
- package/demo/index.html +1 -1
- package/docs/index.html +7 -7
- package/package.json +5 -5
- package/src/crudHandler.js +3 -1
- package/src/dataHandler.js +7 -0
- package/src/dataObject.js +54 -0
- package/src/formHandler.js +4 -3
- package/src/formSelectors.js +1 -0
- package/src/getDataHandler.js +2 -57
- package/src/hrefHandler.js +104 -0
- package/src/iframeHandler.js +90 -0
- package/src/index.js +446 -13
- package/src/setDataHandler.js +8 -3
- package/src/socketHandler.js +2 -2
- package/src/socketSelectors.js +3 -0
- package/src/srcHandler.js +1 -0
- package/src/valueQuery.js +3 -4
- package/src/valueQuerySelector.js +1 -0
- package/test/href-handler.test.cjs +132 -0
- package/test/lazy-loading.test.cjs +168 -0
- package/src/dndHandler.js +0 -158
package/src/index.js
CHANGED
|
@@ -19,36 +19,469 @@
|
|
|
19
19
|
* Commercial Licensing Information:
|
|
20
20
|
* For commercial use of this software without the copyleft provisions of the AGPLv3,
|
|
21
21
|
* you must obtain a commercial license from CoCreate LLC.
|
|
22
|
-
* For details, visit <https://
|
|
22
|
+
* For details, visit <https://CoCreateJS.com/licenses/> or contact us at sales@CoCreateJS.com.
|
|
23
23
|
*/
|
|
24
24
|
import {
|
|
25
25
|
getAttributes
|
|
26
26
|
} from "@cocreate/utils";
|
|
27
|
-
import
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
import "./
|
|
35
|
-
import "./
|
|
36
|
-
import "./socketHandler.js";
|
|
37
|
-
import "./valueQuery.js";
|
|
27
|
+
import Observer from "@cocreate/observer";
|
|
28
|
+
import { getDataObject } from "./dataObject.js";
|
|
29
|
+
import {
|
|
30
|
+
socketActionSelector,
|
|
31
|
+
socketOnSelector,
|
|
32
|
+
socketStatusSelector
|
|
33
|
+
} from "./socketSelectors.js";
|
|
34
|
+
import { formSelector } from "./formSelectors.js";
|
|
35
|
+
import { valueQuerySelector } from "./valueQuerySelector.js";
|
|
38
36
|
|
|
39
37
|
import { elements, forms, keys, debounce } from "./mapRegistry.js"
|
|
40
38
|
|
|
39
|
+
let hrefHandlerLoading;
|
|
40
|
+
let dataHandlerLoading;
|
|
41
|
+
let srcHandlerLoading;
|
|
42
|
+
let socketHandlerLoading;
|
|
43
|
+
let formHandlerLoading;
|
|
44
|
+
let iframeHandlerLoading;
|
|
45
|
+
let valueQueryHandlerLoading;
|
|
46
|
+
const socketSelectors = [socketOnSelector, socketStatusSelector, socketActionSelector].join(", ");
|
|
47
|
+
|
|
48
|
+
if (document.clickedElement === undefined) {
|
|
49
|
+
document.clickedElement = null;
|
|
50
|
+
document.addEventListener("click", event => {
|
|
51
|
+
document.clickedElement = event.target;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const hrefSelector = "[href], [target]";
|
|
56
|
+
const dataSelector = "[storage], [database], [array], [render-json]";
|
|
57
|
+
const srcSelector = "img, [src], [source], [stream]";
|
|
58
|
+
|
|
59
|
+
function loadHrefHandler() {
|
|
60
|
+
if (!hrefHandlerLoading) {
|
|
61
|
+
hrefHandlerLoading = import(/* webpackChunkName: "elements-href-handler" */ "./hrefHandler.js")
|
|
62
|
+
.catch(error => {
|
|
63
|
+
hrefHandlerLoading = undefined;
|
|
64
|
+
throw error;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
hrefHandlerLoading.catch(error => console.error("CoCreate: unable to load href handler.", error));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function loadDataHandler() {
|
|
71
|
+
if (!dataHandlerLoading) {
|
|
72
|
+
dataHandlerLoading = import(/* webpackChunkName: "elements-data-handler" */ "./dataHandler.js")
|
|
73
|
+
.catch(error => {
|
|
74
|
+
dataHandlerLoading = undefined;
|
|
75
|
+
throw error;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return dataHandlerLoading;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function initializeDataHandler() {
|
|
82
|
+
loadDataHandler().catch(error => console.error("CoCreate: unable to load data handler.", error));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function loadSrcHandler() {
|
|
86
|
+
if (!srcHandlerLoading) {
|
|
87
|
+
srcHandlerLoading = import(/* webpackChunkName: "elements-src-handler" */ "./srcHandler.js")
|
|
88
|
+
.catch(error => {
|
|
89
|
+
srcHandlerLoading = undefined;
|
|
90
|
+
throw error;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
srcHandlerLoading.catch(error => console.error("CoCreate: unable to load source handler.", error));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function loadIframeHandler() {
|
|
97
|
+
if (!iframeHandlerLoading) {
|
|
98
|
+
iframeHandlerLoading = import(/* webpackChunkName: "elements-iframe-handler" */ "./iframeHandler.js")
|
|
99
|
+
.catch(error => {
|
|
100
|
+
iframeHandlerLoading = undefined;
|
|
101
|
+
throw error;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
iframeHandlerLoading.catch(error => console.error("CoCreate: unable to load iframe handler.", error));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function loadValueQueryHandler() {
|
|
108
|
+
if (!valueQueryHandlerLoading) {
|
|
109
|
+
valueQueryHandlerLoading = import(/* webpackChunkName: "elements-value-query" */ "./valueQuery.js")
|
|
110
|
+
.catch(error => {
|
|
111
|
+
valueQueryHandlerLoading = undefined;
|
|
112
|
+
throw error;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
valueQueryHandlerLoading.catch(error => console.error("CoCreate: unable to load value query handler.", error));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function loadFormHandler() {
|
|
119
|
+
if (!formHandlerLoading) {
|
|
120
|
+
formHandlerLoading = import(/* webpackChunkName: "elements-form-handler" */ "./formHandler.js")
|
|
121
|
+
.catch(error => {
|
|
122
|
+
formHandlerLoading = undefined;
|
|
123
|
+
throw error;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return formHandlerLoading;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function initializeFormHandler() {
|
|
130
|
+
loadFormHandler().catch(error => console.error("CoCreate: unable to load form handler.", error));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function usesSocket(element) {
|
|
134
|
+
if (element.matches(socketOnSelector) || element.matches(socketStatusSelector)) return true;
|
|
135
|
+
return (element.getAttribute("actions") || "").split(",").some(action =>
|
|
136
|
+
action.trim().split("(", 1)[0].trim() === "send"
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function loadSocketHandler(element) {
|
|
141
|
+
if (!usesSocket(element)) return;
|
|
142
|
+
if (!socketHandlerLoading) {
|
|
143
|
+
socketHandlerLoading = import(/* webpackChunkName: "elements-socket-handler" */ "./socketHandler.js")
|
|
144
|
+
.catch(error => {
|
|
145
|
+
socketHandlerLoading = undefined;
|
|
146
|
+
throw error;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
socketHandlerLoading.catch(error => {
|
|
150
|
+
element.dispatchEvent(new CustomEvent("socketerror", {
|
|
151
|
+
bubbles: true, detail: { error, element }
|
|
152
|
+
}));
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
Observer.init([
|
|
157
|
+
{
|
|
158
|
+
name: "CoCreateElementsHrefLoaderAddedNodes",
|
|
159
|
+
types: ["addedNodes"],
|
|
160
|
+
selector: hrefSelector,
|
|
161
|
+
callback: loadHrefHandler
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: "CoCreateElementsHrefLoaderAttributes",
|
|
165
|
+
types: ["attributes"],
|
|
166
|
+
attributeFilter: ["href", "target"],
|
|
167
|
+
selector: hrefSelector,
|
|
168
|
+
callback: loadHrefHandler
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: "CoCreateElementsDataLoaderAddedNodes",
|
|
172
|
+
types: ["addedNodes"],
|
|
173
|
+
selector: dataSelector,
|
|
174
|
+
callback: initializeDataHandler
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: "CoCreateElementsDataLoaderAttributes",
|
|
178
|
+
types: ["attributes"],
|
|
179
|
+
attributeFilter: ["storage", "database", "array", "render-json"],
|
|
180
|
+
selector: dataSelector,
|
|
181
|
+
callback: initializeDataHandler
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: "CoCreateElementsSrcLoaderAddedNodes",
|
|
185
|
+
types: ["addedNodes"],
|
|
186
|
+
selector: srcSelector,
|
|
187
|
+
callback: loadSrcHandler
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: "CoCreateElementsSrcLoaderAttributes",
|
|
191
|
+
types: ["attributes"],
|
|
192
|
+
attributeFilter: ["src", "source", "stream"],
|
|
193
|
+
selector: srcSelector,
|
|
194
|
+
callback: loadSrcHandler
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: "CoCreateElementsActionLoaderAddedNodes",
|
|
198
|
+
types: ["addedNodes"],
|
|
199
|
+
selector: "[actions]",
|
|
200
|
+
callback: mutation => loadActionHandler(mutation.target)
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: "CoCreateElementsActionLoaderAttributes",
|
|
204
|
+
types: ["attributes"],
|
|
205
|
+
attributeFilter: ["actions"],
|
|
206
|
+
selector: "[actions]",
|
|
207
|
+
callback: mutation => loadActionHandler(mutation.target)
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: "CoCreateElementsFormLoader",
|
|
211
|
+
types: ["addedNodes"],
|
|
212
|
+
selector: formSelector,
|
|
213
|
+
callback: initializeFormHandler
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: "CoCreateElementsSocketLoaderAddedNodes",
|
|
217
|
+
types: ["addedNodes"],
|
|
218
|
+
selector: socketSelectors,
|
|
219
|
+
callback: mutation => loadSocketHandler(mutation.target)
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: "CoCreateElementsSocketLoaderAttributes",
|
|
223
|
+
types: ["attributes"],
|
|
224
|
+
attributeFilter: ["socket-on", "socket-status", "actions"],
|
|
225
|
+
selector: socketSelectors,
|
|
226
|
+
callback: mutation => loadSocketHandler(mutation.target)
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: "CoCreateElementsIframeLoader",
|
|
230
|
+
types: ["addedNodes"],
|
|
231
|
+
selector: "iframe",
|
|
232
|
+
callback: loadIframeHandler
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: "CoCreateElementsValueQueryLoaderAddedNodes",
|
|
236
|
+
types: ["addedNodes"],
|
|
237
|
+
selector: valueQuerySelector,
|
|
238
|
+
callback: loadValueQueryHandler
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: "CoCreateElementsValueQueryLoaderAttributes",
|
|
242
|
+
types: ["attributes"],
|
|
243
|
+
attributeFilter: ["value-query"],
|
|
244
|
+
selector: valueQuerySelector,
|
|
245
|
+
callback: loadValueQueryHandler
|
|
246
|
+
}
|
|
247
|
+
]);
|
|
248
|
+
|
|
249
|
+
function loadActionHandler(element) {
|
|
250
|
+
const actions = (element.getAttribute("actions") || "").split(",");
|
|
251
|
+
for (const action of actions) {
|
|
252
|
+
const name = action.trim().split("(", 1)[0].trim();
|
|
253
|
+
if (name === "save" || name === "delete") initializeDataHandler();
|
|
254
|
+
if (name === "reset") initializeFormHandler();
|
|
255
|
+
if (name === "disableHref" || name === "enableHref") loadHrefHandler();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
document.querySelectorAll("[actions]").forEach(loadActionHandler);
|
|
260
|
+
if (document.querySelector(hrefSelector)) loadHrefHandler();
|
|
261
|
+
if (document.querySelector(dataSelector)) initializeDataHandler();
|
|
262
|
+
if (document.querySelector(srcSelector)) loadSrcHandler();
|
|
263
|
+
document.querySelectorAll(socketSelectors).forEach(loadSocketHandler);
|
|
264
|
+
if (document.querySelector(formSelector)) initializeFormHandler();
|
|
265
|
+
if (document.querySelector("iframe")) loadIframeHandler();
|
|
266
|
+
if (document.querySelector(valueQuerySelector)) loadValueQueryHandler();
|
|
267
|
+
|
|
268
|
+
// Preserve the asynchronous public APIs while loading their implementation on demand.
|
|
269
|
+
const callDataHandler = (name, args) => loadDataHandler().then(module => module[name](...args));
|
|
270
|
+
/**
|
|
271
|
+
*
|
|
272
|
+
* @public
|
|
273
|
+
* @environment client
|
|
274
|
+
*/
|
|
275
|
+
const init = (...args) => callDataHandler("init", args);
|
|
276
|
+
/**
|
|
277
|
+
*
|
|
278
|
+
* @public
|
|
279
|
+
* @environment client
|
|
280
|
+
*/
|
|
281
|
+
const read = (...args) => callDataHandler("read", args);
|
|
282
|
+
/**
|
|
283
|
+
*
|
|
284
|
+
* @public
|
|
285
|
+
* @environment client
|
|
286
|
+
*/
|
|
287
|
+
const save = (...args) => callDataHandler("save", args);
|
|
288
|
+
/**
|
|
289
|
+
*
|
|
290
|
+
* @public
|
|
291
|
+
* @environment client
|
|
292
|
+
*/
|
|
293
|
+
const getData = (...args) => callDataHandler("getData", args);
|
|
294
|
+
/**
|
|
295
|
+
*
|
|
296
|
+
* @public
|
|
297
|
+
* @environment client
|
|
298
|
+
*/
|
|
299
|
+
const setDataType = (...args) => callDataHandler("setDataType", args);
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
*
|
|
303
|
+
* @public
|
|
304
|
+
* @environment client
|
|
305
|
+
*/
|
|
306
|
+
export function reset(action) {
|
|
307
|
+
return loadFormHandler().then(module => module.reset(action));
|
|
308
|
+
}
|
|
309
|
+
|
|
41
310
|
export default {
|
|
42
311
|
init,
|
|
43
312
|
read,
|
|
44
313
|
save,
|
|
45
314
|
getData,
|
|
315
|
+
/**
|
|
316
|
+
*
|
|
317
|
+
* @public
|
|
318
|
+
* @environment client
|
|
319
|
+
*/
|
|
46
320
|
getDataObject,
|
|
47
321
|
reset,
|
|
322
|
+
/**
|
|
323
|
+
*
|
|
324
|
+
* @public
|
|
325
|
+
* @environment client
|
|
326
|
+
*/
|
|
48
327
|
elements,
|
|
328
|
+
/**
|
|
329
|
+
*
|
|
330
|
+
* @public
|
|
331
|
+
* @environment client
|
|
332
|
+
*/
|
|
49
333
|
keys,
|
|
334
|
+
/**
|
|
335
|
+
*
|
|
336
|
+
* @public
|
|
337
|
+
* @environment client
|
|
338
|
+
*/
|
|
50
339
|
forms,
|
|
340
|
+
/**
|
|
341
|
+
*
|
|
342
|
+
* @public
|
|
343
|
+
* @environment client
|
|
344
|
+
*/
|
|
51
345
|
debounce,
|
|
346
|
+
/**
|
|
347
|
+
*
|
|
348
|
+
* @public
|
|
349
|
+
* @environment client
|
|
350
|
+
*/
|
|
52
351
|
getAttributes,
|
|
53
352
|
setDataType
|
|
54
|
-
};
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
*
|
|
357
|
+
* @name actions
|
|
358
|
+
* @kind member
|
|
359
|
+
* @attribute
|
|
360
|
+
* @environment client
|
|
361
|
+
*/
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
*
|
|
365
|
+
* @name array
|
|
366
|
+
* @kind member
|
|
367
|
+
* @attribute
|
|
368
|
+
* @environment client
|
|
369
|
+
*/
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
*
|
|
373
|
+
* @name database
|
|
374
|
+
* @kind member
|
|
375
|
+
* @attribute
|
|
376
|
+
* @environment client
|
|
377
|
+
*/
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
*
|
|
381
|
+
* @name href
|
|
382
|
+
* @kind member
|
|
383
|
+
* @attribute
|
|
384
|
+
* @environment client
|
|
385
|
+
*/
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
*
|
|
389
|
+
* @name render-json
|
|
390
|
+
* @kind member
|
|
391
|
+
* @attribute
|
|
392
|
+
* @environment client
|
|
393
|
+
*/
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
*
|
|
397
|
+
* @name socket-on
|
|
398
|
+
* @kind member
|
|
399
|
+
* @attribute
|
|
400
|
+
* @environment client
|
|
401
|
+
*/
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
*
|
|
405
|
+
* @name socket-status
|
|
406
|
+
* @kind member
|
|
407
|
+
* @attribute
|
|
408
|
+
* @environment client
|
|
409
|
+
*/
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
*
|
|
413
|
+
* @name source
|
|
414
|
+
* @kind member
|
|
415
|
+
* @attribute
|
|
416
|
+
* @environment client
|
|
417
|
+
*/
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
*
|
|
421
|
+
* @name src
|
|
422
|
+
* @kind member
|
|
423
|
+
* @attribute
|
|
424
|
+
* @environment client
|
|
425
|
+
*/
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
*
|
|
429
|
+
* @name storage
|
|
430
|
+
* @kind member
|
|
431
|
+
* @attribute
|
|
432
|
+
* @environment client
|
|
433
|
+
*/
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
*
|
|
437
|
+
* @name stream
|
|
438
|
+
* @kind member
|
|
439
|
+
* @attribute
|
|
440
|
+
* @environment client
|
|
441
|
+
*/
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
*
|
|
445
|
+
* @name target
|
|
446
|
+
* @kind member
|
|
447
|
+
* @attribute
|
|
448
|
+
* @environment client
|
|
449
|
+
*/
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
*
|
|
453
|
+
* @name value-query
|
|
454
|
+
* @kind member
|
|
455
|
+
* @attribute
|
|
456
|
+
* @environment client
|
|
457
|
+
*/
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Fetch content into any html element by using html5 attributes to query data stored in CoCreate headless CMS. If data is changed it will instantly reflect in html tags as changes are broadcasted in realtime
|
|
461
|
+
*
|
|
462
|
+
* @module elements
|
|
463
|
+
* @environment client
|
|
464
|
+
*/
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Declarative selectors recognized by this module.
|
|
468
|
+
*
|
|
469
|
+
* @name declarative-selectors
|
|
470
|
+
* @kind member
|
|
471
|
+
* @selectors
|
|
472
|
+
* @environment client
|
|
473
|
+
* @example
|
|
474
|
+
* ```json
|
|
475
|
+
* [
|
|
476
|
+
* "[actions]",
|
|
477
|
+
* "iframe",
|
|
478
|
+
* "[href], [target]",
|
|
479
|
+
* "[storage], [database], [array], [render-json]",
|
|
480
|
+
* "img, [src], [source], [stream]",
|
|
481
|
+
* "[value-query]",
|
|
482
|
+
* "form, fieldset",
|
|
483
|
+
* "[socket-on]",
|
|
484
|
+
* "[socket-status]"
|
|
485
|
+
* ]
|
|
486
|
+
* ```
|
|
487
|
+
*/
|
package/src/setDataHandler.js
CHANGED
|
@@ -157,11 +157,16 @@ export async function setData(element, data) {
|
|
|
157
157
|
if (action && ["database", "array", "object", "key"].includes(action))
|
|
158
158
|
continue;
|
|
159
159
|
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
const { key, isRead, isUpdate, isListen, isCrdt } = getAttributes(el);
|
|
161
|
+
if (key !== undefined && typeof key !== "string") {
|
|
162
|
+
console.error("[CoCreate elements] Ignoring element with a non-string key", {
|
|
163
|
+
key,
|
|
164
|
+
keyType: typeof key,
|
|
165
|
+
element: el
|
|
166
|
+
});
|
|
167
|
+
continue;
|
|
162
168
|
}
|
|
163
169
|
|
|
164
|
-
const { key, isRead, isUpdate, isListen, isCrdt } = getAttributes(el);
|
|
165
170
|
const hasRenderController =
|
|
166
171
|
el.hasAttribute("render-query") || el.hasAttribute("render-json");
|
|
167
172
|
|
package/src/socketHandler.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
+
import "@cocreate/element-prototype";
|
|
1
2
|
import Actions from "@cocreate/actions";
|
|
2
3
|
import Observer from "@cocreate/observer";
|
|
3
4
|
import Socket from "@cocreate/socket-client";
|
|
4
5
|
import { dotNotationToObject, getValueFromObject, queryData, uuid } from "@cocreate/utils";
|
|
6
|
+
import { socketOnSelector, socketStatusSelector } from "./socketSelectors.js";
|
|
5
7
|
|
|
6
|
-
const socketOnSelector = "[socket-on]";
|
|
7
8
|
const socketOnElements = new WeakMap();
|
|
8
9
|
const socketOnListeners = new Map();
|
|
9
|
-
const socketStatusSelector = "[socket-status]";
|
|
10
10
|
const socketStatusElements = new Set();
|
|
11
11
|
let socketStatusListenersInitialized = false;
|
|
12
12
|
|
package/src/srcHandler.js
CHANGED
package/src/valueQuery.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import Observer from "@cocreate/observer";
|
|
2
2
|
import "@cocreate/element-prototype";
|
|
3
3
|
import { queryElements } from "@cocreate/utils";
|
|
4
|
-
|
|
5
|
-
const selector = "[value-query]";
|
|
4
|
+
import { valueQuerySelector } from "./valueQuerySelector.js";
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Initializes elements with value-* attributes to enable dynamic value retrieval and setting.
|
|
@@ -18,7 +17,7 @@ async function init(element) {
|
|
|
18
17
|
) {
|
|
19
18
|
element = [element];
|
|
20
19
|
} else if (!element) {
|
|
21
|
-
element = document.querySelectorAll(
|
|
20
|
+
element = document.querySelectorAll(valueQuerySelector);
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
for (let i = 0; i < element.length; i++) {
|
|
@@ -87,7 +86,7 @@ init();
|
|
|
87
86
|
Observer.init({
|
|
88
87
|
name: "CoCreateElementsValueAdded",
|
|
89
88
|
types: ["addedNodes"],
|
|
90
|
-
selector:
|
|
89
|
+
selector: valueQuerySelector,
|
|
91
90
|
callback: function (mutation) {
|
|
92
91
|
init(mutation.target);
|
|
93
92
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const valueQuerySelector = "[value-query]";
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const test = require('node:test');
|
|
5
|
+
const vm = require('node:vm');
|
|
6
|
+
|
|
7
|
+
function documentStub() {
|
|
8
|
+
const listeners = new Set();
|
|
9
|
+
return {
|
|
10
|
+
nodeType: 9,
|
|
11
|
+
addEventListener: (type, callback) => listeners.add(callback),
|
|
12
|
+
removeEventListener: (type, callback) => listeners.delete(callback),
|
|
13
|
+
click(target) {
|
|
14
|
+
const event = { target, prevented: false, preventDefault() { this.prevented = true; } };
|
|
15
|
+
for (const listener of listeners) listener(event);
|
|
16
|
+
return event;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function setup() {
|
|
22
|
+
const document = documentStub(), opened = [], definitions = [];
|
|
23
|
+
const location = { href: 'https://example.test/' };
|
|
24
|
+
const actions = { init: entries => definitions.push(...entries), href() { throw new Error('Default href'); } };
|
|
25
|
+
let targets = [];
|
|
26
|
+
const stubs = {
|
|
27
|
+
'@cocreate/actions': { default: actions },
|
|
28
|
+
'@cocreate/utils': { queryElements: () => targets }
|
|
29
|
+
};
|
|
30
|
+
const context = vm.createContext({ document, location, URL, window: { open: (...args) => opened.push(args) } });
|
|
31
|
+
const module = new vm.SourceTextModule(fs.readFileSync(path.join(__dirname, '../src/hrefHandler.js'), 'utf8'), { context });
|
|
32
|
+
await module.link(name => {
|
|
33
|
+
const exports = stubs[name];
|
|
34
|
+
return new vm.SyntheticModule(Object.keys(exports), function () {
|
|
35
|
+
for (const [key, value] of Object.entries(exports)) this.setExport(key, value);
|
|
36
|
+
}, { context });
|
|
37
|
+
});
|
|
38
|
+
await module.evaluate();
|
|
39
|
+
function element(attributes = {}, parent = null) {
|
|
40
|
+
const node = {
|
|
41
|
+
ownerDocument: document, parent, children: [],
|
|
42
|
+
hasAttribute: name => Object.hasOwn(attributes, name),
|
|
43
|
+
getAttribute: name => attributes[name] ?? null,
|
|
44
|
+
matches: selector => selector.split(',').some(part => Object.hasOwn(attributes, part.trim().slice(1, -1))),
|
|
45
|
+
closest(selector) { return this.matches(selector) ? this : parent?.closest(selector) || null; },
|
|
46
|
+
querySelector(selector) { return this.children.find(child => child.matches(selector)) || null; }
|
|
47
|
+
};
|
|
48
|
+
parent?.children.push(node);
|
|
49
|
+
return node;
|
|
50
|
+
}
|
|
51
|
+
return { document, location, opened, actions, definitions, element, module: module.namespace,
|
|
52
|
+
select: value => { targets = value; } };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
test('automatic setup registers actions and exposes only navigation controls', async () => {
|
|
56
|
+
const f = await setup();
|
|
57
|
+
assert.deepEqual(Object.keys(f.module).sort(), ['default', 'disableHref', 'enableHref', 'open']);
|
|
58
|
+
assert.deepEqual(Object.keys(f.module.default).sort(), ['disableHref', 'enableHref', 'open']);
|
|
59
|
+
for (const name of ['open', 'disableHref', 'enableHref']) {
|
|
60
|
+
assert.equal(f.module.default[name], f.module[name]);
|
|
61
|
+
}
|
|
62
|
+
assert.equal(typeof f.actions.href, 'function');
|
|
63
|
+
assert.equal(f.document.link.islink, 'true');
|
|
64
|
+
assert.deepEqual(f.definitions.map(definition => definition.name), ['disableHref', 'enableHref']);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('delegation handles newly inserted links and preserves window targeting', async () => {
|
|
68
|
+
const f = await setup();
|
|
69
|
+
const link = f.element({ href: '/next', target: '_window' });
|
|
70
|
+
const icon = f.element({}, link);
|
|
71
|
+
assert.equal(f.document.click(icon).prevented, true);
|
|
72
|
+
assert.deepEqual(f.opened, [['/next', '_blank', 'location=yes,height=570,width=520,scrollbars=yes,status=yes']]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('direct clicks defer action links to the chain href hook', async () => {
|
|
76
|
+
const f = await setup();
|
|
77
|
+
const link = f.element({ actions: 'save', href: '/next' });
|
|
78
|
+
const event = f.document.click(link);
|
|
79
|
+
assert.equal(event.prevented, false);
|
|
80
|
+
assert.equal(f.location.href, 'https://example.test/');
|
|
81
|
+
f.actions.href(link, { event });
|
|
82
|
+
assert.equal(event.prevented, true);
|
|
83
|
+
assert.equal(f.location.href, '/next');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('the chain hook resolves child links and leaves downloads alone', async () => {
|
|
87
|
+
const f = await setup();
|
|
88
|
+
const button = f.element({ actions: 'save' });
|
|
89
|
+
f.element({ href: '/next', target: 'preview' }, button);
|
|
90
|
+
f.actions.href(button);
|
|
91
|
+
assert.deepEqual(f.opened, [['/next', 'preview']]);
|
|
92
|
+
const download = f.element({ href: '/file', download: '' });
|
|
93
|
+
assert.equal(f.document.click(download).prevented, false);
|
|
94
|
+
f.actions.href(download);
|
|
95
|
+
assert.equal(f.location.href, 'https://example.test/');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('enable and disable actions update document state and complete their chains', async () => {
|
|
99
|
+
const f = await setup();
|
|
100
|
+
const button = f.element(), link = f.element({ href: '/next' });
|
|
101
|
+
let completed = 0;
|
|
102
|
+
f.definitions[0].callback({ element: button, end() { completed++; } });
|
|
103
|
+
assert.equal(f.document.click(link).prevented, true);
|
|
104
|
+
f.actions.href(link);
|
|
105
|
+
assert.equal(f.location.href, 'https://example.test/');
|
|
106
|
+
f.definitions[1].callback({ element: button, end() { completed++; } });
|
|
107
|
+
assert.equal(completed, 2);
|
|
108
|
+
f.document.click(link);
|
|
109
|
+
assert.equal(f.location.href, '/next');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('link-query scopes enable and disable to the selected iframe document', async () => {
|
|
113
|
+
const f = await setup();
|
|
114
|
+
const frameDocument = documentStub();
|
|
115
|
+
f.select([{ contentDocument: frameDocument }]);
|
|
116
|
+
const button = f.element({ 'link-query': '#preview' });
|
|
117
|
+
f.module.disableHref(button);
|
|
118
|
+
assert.equal(frameDocument.link.islink, 'false');
|
|
119
|
+
assert.equal(f.document.link.islink, 'true');
|
|
120
|
+
f.module.enableHref(button);
|
|
121
|
+
assert.equal(frameDocument.link.islink, 'true');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('applications can replace the href hook after the feature loads', async () => {
|
|
125
|
+
const f = await setup();
|
|
126
|
+
const custom = () => {};
|
|
127
|
+
f.actions.href = custom;
|
|
128
|
+
const button = f.element();
|
|
129
|
+
f.module.disableHref(button);
|
|
130
|
+
f.module.enableHref(button);
|
|
131
|
+
assert.equal(f.actions.href, custom);
|
|
132
|
+
});
|