@cocreate/elements 1.48.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/FUNDING.yml +1 -1
- package/.github/workflows/automated.yml +18 -7
- package/CHANGELOG.md +760 -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 +14 -14
- 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 +433 -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
|
@@ -0,0 +1,168 @@
|
|
|
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 element(tagName, attributes = {}) {
|
|
8
|
+
return {
|
|
9
|
+
tagName: tagName.toUpperCase(), attributes,
|
|
10
|
+
getAttribute: name => attributes[name] ?? null,
|
|
11
|
+
matches(selector) {
|
|
12
|
+
return selector.split(',').some(part => {
|
|
13
|
+
const value = part.trim();
|
|
14
|
+
return value.startsWith('[')
|
|
15
|
+
? Object.hasOwn(attributes, value.slice(1, -1))
|
|
16
|
+
: value.toUpperCase() === this.tagName;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function setup(nodes = [], failures = new Set()) {
|
|
23
|
+
const imports = [], observers = [], calls = [], errors = [];
|
|
24
|
+
const context = vm.createContext({
|
|
25
|
+
console: { error: (...args) => errors.push(args) },
|
|
26
|
+
localStorage: { getItem: () => 'production' },
|
|
27
|
+
document: {
|
|
28
|
+
addEventListener() {},
|
|
29
|
+
querySelector: selector => nodes.find(node => node.matches(selector)),
|
|
30
|
+
querySelectorAll: selector => nodes.filter(node => node.matches(selector))
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
const stubs = {
|
|
34
|
+
'@cocreate/observer': { default: { init: configs => observers.push(...configs) } },
|
|
35
|
+
'@cocreate/utils': {
|
|
36
|
+
getAttributes: node => ({ ...node.attributes }),
|
|
37
|
+
checkValue: () => true
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const modules = new Map();
|
|
41
|
+
function synthetic(exports) {
|
|
42
|
+
return new vm.SyntheticModule(Object.keys(exports), function () {
|
|
43
|
+
for (const [key, value] of Object.entries(exports)) this.setExport(key, value);
|
|
44
|
+
}, { context });
|
|
45
|
+
}
|
|
46
|
+
async function dynamicImport(name) {
|
|
47
|
+
imports.push(name);
|
|
48
|
+
if (failures.delete(name)) throw new Error('Temporary chunk failure');
|
|
49
|
+
const methods = ['init', 'read', 'save', 'getData', 'setDataType', 'reset'];
|
|
50
|
+
const module = synthetic(Object.fromEntries(methods.map(method => [method, (...args) => {
|
|
51
|
+
calls.push({ name, method, args });
|
|
52
|
+
return args[0];
|
|
53
|
+
}])));
|
|
54
|
+
await module.link(() => {});
|
|
55
|
+
await module.evaluate();
|
|
56
|
+
return module;
|
|
57
|
+
}
|
|
58
|
+
function load(name) {
|
|
59
|
+
name = name.replace(/^\.\//, '');
|
|
60
|
+
if (modules.has(name)) return modules.get(name);
|
|
61
|
+
const module = stubs[name] ? synthetic(stubs[name]) : new vm.SourceTextModule(
|
|
62
|
+
fs.readFileSync(path.join(__dirname, '../src', name), 'utf8'),
|
|
63
|
+
{ context, identifier: name, importModuleDynamically: dynamicImport }
|
|
64
|
+
);
|
|
65
|
+
modules.set(name, module);
|
|
66
|
+
return module;
|
|
67
|
+
}
|
|
68
|
+
const entry = load('index.js');
|
|
69
|
+
await entry.link(load);
|
|
70
|
+
await entry.evaluate();
|
|
71
|
+
const flush = () => new Promise(resolve => setImmediate(resolve));
|
|
72
|
+
await flush();
|
|
73
|
+
return {
|
|
74
|
+
api: entry.namespace.default, imports, calls, errors, flush,
|
|
75
|
+
mutate(node, type, attributeName) {
|
|
76
|
+
for (const observer of observers) {
|
|
77
|
+
if (!observer.types.includes(type) || !node.matches(observer.selector)) continue;
|
|
78
|
+
if (type === 'attributes' && !observer.attributeFilter.includes(attributeName)) continue;
|
|
79
|
+
observer.callback({ target: node, attributeName });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
test('an empty document loads no feature runtime and retains synchronous metadata APIs', async () => {
|
|
86
|
+
const f = await setup();
|
|
87
|
+
assert.deepEqual(f.imports, []);
|
|
88
|
+
const data = f.api.getDataObject(element('input', { array: 'files', key: 'name' }));
|
|
89
|
+
assert.equal(data.type, 'object');
|
|
90
|
+
assert.equal(data.database, 'production');
|
|
91
|
+
assert.equal(data.then, undefined);
|
|
92
|
+
f.api.keys.set('example', 1);
|
|
93
|
+
assert.equal(f.api.keys.get('example'), 1);
|
|
94
|
+
assert.deepEqual(f.imports, []);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
for (const [tag, attrs, expected] of [
|
|
98
|
+
['div', { src: '/partial.html' }, './srcHandler.js'],
|
|
99
|
+
['div', { source: '/partial.html' }, './srcHandler.js'],
|
|
100
|
+
['div', { stream: '/stream' }, './srcHandler.js'],
|
|
101
|
+
['form', {}, './formHandler.js'],
|
|
102
|
+
['fieldset', {}, './formHandler.js'],
|
|
103
|
+
['a', { href: '/next' }, './hrefHandler.js'],
|
|
104
|
+
['a', { target: '_window' }, './hrefHandler.js'],
|
|
105
|
+
['div', { array: 'files' }, './dataHandler.js'],
|
|
106
|
+
['div', { 'render-json': 'true' }, './dataHandler.js'],
|
|
107
|
+
['iframe', {}, './iframeHandler.js'],
|
|
108
|
+
['input', { 'value-query': '.source' }, './valueQuery.js'],
|
|
109
|
+
['div', { 'socket-on': 'object.read' }, './socketHandler.js'],
|
|
110
|
+
['button', { actions: 'send' }, './socketHandler.js'],
|
|
111
|
+
['button', { actions: 'save' }, './dataHandler.js'],
|
|
112
|
+
['button', { actions: 'delete' }, './dataHandler.js'],
|
|
113
|
+
['button', { actions: 'reset' }, './formHandler.js'],
|
|
114
|
+
['button', { actions: 'disableHref' }, './hrefHandler.js'],
|
|
115
|
+
['button', { actions: 'enableHref' }, './hrefHandler.js']
|
|
116
|
+
]) {
|
|
117
|
+
test(`initial ${tag} ${JSON.stringify(attrs)} loads only ${expected}`, async () => {
|
|
118
|
+
const f = await setup([element(tag, attrs)]);
|
|
119
|
+
assert.deepEqual(f.imports, [expected]);
|
|
120
|
+
assert.deepEqual(f.errors, []);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
test('unrelated actions load no Elements runtime', async () => {
|
|
125
|
+
const f = await setup([element('button', { actions: 'validate, upload' })]);
|
|
126
|
+
assert.deepEqual(f.imports, []);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('added elements and attribute changes load each feature once', async () => {
|
|
130
|
+
const f = await setup();
|
|
131
|
+
const source = element('div', { src: '/partial.html' });
|
|
132
|
+
const data = element('div', { array: 'files' });
|
|
133
|
+
f.mutate(source, 'addedNodes');
|
|
134
|
+
f.mutate(source, 'attributes', 'src');
|
|
135
|
+
f.mutate(data, 'attributes', 'array');
|
|
136
|
+
f.mutate(data, 'addedNodes');
|
|
137
|
+
await f.flush();
|
|
138
|
+
assert.deepEqual(f.imports, ['./srcHandler.js', './dataHandler.js']);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('programmatic APIs await and share the data runtime', async () => {
|
|
142
|
+
const f = await setup();
|
|
143
|
+
const node = element('input');
|
|
144
|
+
for (const name of ['init', 'read', 'save', 'getData', 'setDataType']) {
|
|
145
|
+
assert.equal(await f.api[name](node), node);
|
|
146
|
+
}
|
|
147
|
+
assert.deepEqual(f.imports, ['./dataHandler.js']);
|
|
148
|
+
assert.deepEqual(f.calls.map(call => call.method), ['init', 'read', 'save', 'getData', 'setDataType']);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('adding href to an existing element loads its handler once', async () => {
|
|
152
|
+
const f = await setup();
|
|
153
|
+
const link = element('a', { href: '/next' });
|
|
154
|
+
f.mutate(link, 'attributes', 'href');
|
|
155
|
+
f.mutate(link, 'addedNodes');
|
|
156
|
+
await f.flush();
|
|
157
|
+
assert.deepEqual(f.imports, ['./hrefHandler.js']);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('failed feature imports can be retried by a subsequent mutation', async () => {
|
|
161
|
+
const node = element('div', { src: '/partial.html' });
|
|
162
|
+
const f = await setup([node], new Set(['./srcHandler.js']));
|
|
163
|
+
assert.equal(f.errors.length, 1);
|
|
164
|
+
f.mutate(node, 'attributes', 'src');
|
|
165
|
+
await f.flush();
|
|
166
|
+
assert.deepEqual(f.imports, ['./srcHandler.js', './srcHandler.js']);
|
|
167
|
+
assert.equal(f.errors.length, 1);
|
|
168
|
+
});
|
package/src/dndHandler.js
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { dotNotationToObject } from "@cocreate/utils";
|
|
2
|
-
import CRUD from "@cocreate/crud-client";
|
|
3
|
-
import Observer from "@cocreate/observer";
|
|
4
|
-
import { render, renderValue, renderedNodes } from "@cocreate/render";
|
|
5
|
-
import { elements, keys, debounce } from "./mapRegistry.js";
|
|
6
|
-
import { getDataElements, getDataObject } from "./getDataHandler.js";
|
|
7
|
-
|
|
8
|
-
// TODO: has the potential to work on most of the crud elements specifically if the value is an object or an array
|
|
9
|
-
async function dndCrud(draggedEl, draggedFrom, droppedEl, droppedIn) {
|
|
10
|
-
let from = await dndCrudData(draggedEl, draggedFrom, "remove");
|
|
11
|
-
let to = await dndCrudData(droppedEl, droppedIn, "add");
|
|
12
|
-
|
|
13
|
-
if (from && to && !draggedFrom.isSameNode(droppedIn)) {
|
|
14
|
-
let element = getDataElements(from.data);
|
|
15
|
-
if (!element.length) return;
|
|
16
|
-
|
|
17
|
-
if (!element.includes(droppedIn)) {
|
|
18
|
-
if (from.keyPath.includes(".")) {
|
|
19
|
-
let test = { [from.keyPath]: undefined };
|
|
20
|
-
|
|
21
|
-
from.clonedParentData = dotNotationToObject(test, from.clonedParentData);
|
|
22
|
-
dndCrudSend(from.clonedParentData, "update");
|
|
23
|
-
} else {
|
|
24
|
-
const index = from.keyPath.match(/\[(\d+)\]/);
|
|
25
|
-
let removeData = from.clonedParentData[from.clonedParentData.type].splice(
|
|
26
|
-
index,
|
|
27
|
-
1
|
|
28
|
-
)[0];
|
|
29
|
-
if (removeData) {
|
|
30
|
-
removeData.type = from.clonedParentData.type;
|
|
31
|
-
removeData[removeData.type] = removeData;
|
|
32
|
-
dndCrudSend(removeData, "delete");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (from.clonedParentData[from.clonedParentData.type].length)
|
|
36
|
-
dndCrudSend(from.clonedParentData, "update");
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (to) dndCrudSend(to.clonedParentData, "update");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function dndCrudData(element, parent, operator) {
|
|
45
|
-
if (!elements.has(parent)) return;
|
|
46
|
-
let data = await parent.getData();
|
|
47
|
-
let clonedParentData = getDataObject(parent);
|
|
48
|
-
|
|
49
|
-
let { dragData, sortName, sortDirection, keyPath, clones, index } = dndNewData(
|
|
50
|
-
element,
|
|
51
|
-
data
|
|
52
|
-
);
|
|
53
|
-
clonedParentData[clonedParentData.type] = [];
|
|
54
|
-
|
|
55
|
-
if (sortName) {
|
|
56
|
-
for (let i = 0; i < clones.length; i++) {
|
|
57
|
-
if (i > index) {
|
|
58
|
-
let previousData = data[data.type][index];
|
|
59
|
-
dragData[sortName] = operator === "add" ? i + 1 : i - 1;
|
|
60
|
-
|
|
61
|
-
clonedParentData[clonedParentData.type].push({ ...previousData, ...dragData });
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
} else {
|
|
65
|
-
clonedParentData[clonedParentData.type] = [{ ...data[data.type][index], ...dragData }];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return { data, clonedParentData, keyPath, clones, index };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function dndNewData(element, data) {
|
|
72
|
-
let dragData = {};
|
|
73
|
-
if (data.filter.query) {
|
|
74
|
-
dndNewDataUpdate(dragData, data.filter.query);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
let sortName, sortDirection;
|
|
78
|
-
let sort = data.filter.sort;
|
|
79
|
-
if (sort && sort.length) {
|
|
80
|
-
for (let i = sort.length - 1; i >= 0; i--) {
|
|
81
|
-
if (typeof data[sort.key] === "number") {
|
|
82
|
-
sortName = sort.key;
|
|
83
|
-
sortDirection = sort.direction;
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
let keyPath, clonesMap, clones, index;
|
|
90
|
-
|
|
91
|
-
let renderedNode = renderedNodes.get(element);
|
|
92
|
-
if (renderedNode) {
|
|
93
|
-
const renderKeys = renderedNode.keys || {};
|
|
94
|
-
const activeKey = Object.keys(renderKeys).at(-1);
|
|
95
|
-
keyPath = activeKey ? renderKeys[activeKey].path : undefined;
|
|
96
|
-
clonesMap = renderedNode.template.clones;
|
|
97
|
-
clones = Array.from(clonesMap.values());
|
|
98
|
-
index = clones.indexOf(element);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return { dragData, sortName, sortDirection, keyPath, clones, index };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function dndNewDataUpdate(dragData, query) {
|
|
105
|
-
for (let key of Object.keys(query)) {
|
|
106
|
-
if (key === "$and" || key === "$or" || key === "$nor") {
|
|
107
|
-
for (let i = 0; i < query[key].length; i++) {
|
|
108
|
-
dndNewDataUpdate(dragData, query[key][i]);
|
|
109
|
-
}
|
|
110
|
-
} else if (
|
|
111
|
-
typeof query[key] === "object" &&
|
|
112
|
-
!Array.isArray(query[key])
|
|
113
|
-
) {
|
|
114
|
-
if (query[key].$eq) dragData[key] = query[key].$eq;
|
|
115
|
-
if (query[key].$ne) dragData[key] = query[key].$ne;
|
|
116
|
-
} else {
|
|
117
|
-
dragData[key] = query[key];
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function dndCrudSend(data, crudType) {
|
|
123
|
-
if (CRUD) {
|
|
124
|
-
let action = data.type;
|
|
125
|
-
action = action.charAt(0).toUpperCase() + action.slice(1);
|
|
126
|
-
CRUD[crudType + action](data);
|
|
127
|
-
} else console.log("dnd reordered data set as crud is unavailable");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
Observer.init({
|
|
131
|
-
name: "render",
|
|
132
|
-
types: ["addedNodes"],
|
|
133
|
-
selector: "[render-clone]",
|
|
134
|
-
callback: function (mutation) {
|
|
135
|
-
if (
|
|
136
|
-
mutation.parentElement &&
|
|
137
|
-
!mutation.parentElement.hasAttribute("dnd")
|
|
138
|
-
)
|
|
139
|
-
return;
|
|
140
|
-
let delayTimer = debounce.get(mutation);
|
|
141
|
-
clearTimeout(delayTimer);
|
|
142
|
-
debounce.delete(mutation.target);
|
|
143
|
-
|
|
144
|
-
let renderedNode = renderedNodes.get(mutation.target);
|
|
145
|
-
if (!renderedNode) return;
|
|
146
|
-
|
|
147
|
-
if (!mutation.move || mutation.move.isDescendant) return;
|
|
148
|
-
let draggedEl = mutation.move.movedElement;
|
|
149
|
-
let draggedFrom = mutation.move.from.parentElement;
|
|
150
|
-
let droppedEl =
|
|
151
|
-
mutation.target.nextElementSibling ||
|
|
152
|
-
mutation.target.previousElementSibling;
|
|
153
|
-
let droppedIn = mutation.parentElement;
|
|
154
|
-
|
|
155
|
-
if (!draggedFrom || !droppedIn) return;
|
|
156
|
-
dndCrud(draggedEl, draggedFrom, droppedEl, droppedIn);
|
|
157
|
-
}
|
|
158
|
-
});
|