@cocreate/element-prototype 1.38.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 +453 -415
- package/CONTRIBUTING.md +6 -6
- package/CoCreate.config.js +2 -2
- package/README.md +69 -20
- package/demo/index.html +2 -2
- package/docs/index.html +4 -4
- package/package.json +7 -7
- package/src/getAttribute.js +44 -10
- package/src/getValue.js +48 -25
- package/src/index.js +168 -1
- package/src/setValue.js +44 -20
- package/test/getAttributeAst.test.mjs +104 -0
- package/test/value-handler.test.cjs +52 -7
package/src/setValue.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { storage } from "./getValue.js";
|
|
2
1
|
import { getValueAttribute, transformValue, convertValue } from "@cocreate/utils";
|
|
3
2
|
|
|
4
3
|
HTMLElement.prototype.setValue = function (value, dispatch) {
|
|
@@ -8,7 +7,6 @@ SVGElement.prototype.setValue = function (value, dispatch) {
|
|
|
8
7
|
setValue(this, value, dispatch);
|
|
9
8
|
};
|
|
10
9
|
|
|
11
|
-
// TODO: check if using a a switch case will provide better performance
|
|
12
10
|
const setValue = (el, value, dispatch) => {
|
|
13
11
|
let bubbles = ![null, false, "false"].includes(getValueAttribute(el, "bubbles", "set-value"));
|
|
14
12
|
let valueDispatch = ![null, false, "false"].includes(getValueAttribute(el, "dispatch", "set-value"));
|
|
@@ -20,13 +18,22 @@ const setValue = (el, value, dispatch) => {
|
|
|
20
18
|
if (value === null || value === undefined) return;
|
|
21
19
|
value = transformValue(el, value, "set-value");
|
|
22
20
|
value = convertValue(value, getValueAttribute(el, "type", "set-value") || "");
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return dispatchEvents(el, bubbles, dispatch);
|
|
26
|
-
} else if (typeof value === "object") {
|
|
21
|
+
|
|
22
|
+
if (typeof value === "object" && !el.hasAttribute("component")) {
|
|
27
23
|
value = JSON.stringify(value, null, 2);
|
|
28
24
|
}
|
|
29
25
|
|
|
26
|
+
const nativeValueElement = ["INPUT", "TEXTAREA", "SELECT"].includes(el.tagName);
|
|
27
|
+
if (!nativeValueElement && (
|
|
28
|
+
el.hasAttribute("component") ||
|
|
29
|
+
((el.__cocreateEditor || el.hasAttribute("editor") ||
|
|
30
|
+
(el.hasAttribute("contenteditable") && el.getAttribute("contenteditable") !== "false")) &&
|
|
31
|
+
el.tagName !== "IFRAME")
|
|
32
|
+
)) {
|
|
33
|
+
el.value = value;
|
|
34
|
+
return dispatchEvents(el, bubbles, dispatch, value);
|
|
35
|
+
}
|
|
36
|
+
|
|
30
37
|
if (["date", "time", "datetime", "datetime-local"].includes(el.getAttribute("type") || el.type)) {
|
|
31
38
|
if (value) {
|
|
32
39
|
const date = new Date(value);
|
|
@@ -82,9 +89,7 @@ const setValue = (el, value, dispatch) => {
|
|
|
82
89
|
el.tagName == "TEXTAREA" ||
|
|
83
90
|
el.tagName == "SELECT"
|
|
84
91
|
) {
|
|
85
|
-
|
|
86
|
-
let isCrdt = el.getAttribute("crdt");
|
|
87
|
-
if (isCrdt == "true" || el.type === "file") return;
|
|
92
|
+
if (el.type === "file") return;
|
|
88
93
|
|
|
89
94
|
if (el.type == "checkbox") {
|
|
90
95
|
let inputs = [el];
|
|
@@ -142,11 +147,28 @@ const setValue = (el, value, dispatch) => {
|
|
|
142
147
|
el.value = value;
|
|
143
148
|
}
|
|
144
149
|
// dispatchEvents(el, bubbles, dispatch);
|
|
145
|
-
} else if (el.tagName === "IMG" || el.tagName === "SOURCE") {
|
|
146
|
-
el.src = value;
|
|
147
|
-
} else if (el.tagName === "IFRAME") {
|
|
148
|
-
if (el.hasAttribute("src")) el.src = value;
|
|
149
|
-
else el.srcdoc = value;
|
|
150
|
+
} else if (el.tagName === "IMG" || el.tagName === "SOURCE") {
|
|
151
|
+
el.src = value;
|
|
152
|
+
} else if (el.tagName === "IFRAME") {
|
|
153
|
+
if (el.hasAttribute("src")) el.src = value;
|
|
154
|
+
else if (el.hasAttribute("srcdoc")) el.srcdoc = value;
|
|
155
|
+
else {
|
|
156
|
+
const doc = el.contentDocument;
|
|
157
|
+
const parsedDocument = new DOMParser().parseFromString(
|
|
158
|
+
String(value),
|
|
159
|
+
"text/html"
|
|
160
|
+
);
|
|
161
|
+
const sourceRoot = parsedDocument.documentElement;
|
|
162
|
+
const targetRoot = doc.documentElement;
|
|
163
|
+
|
|
164
|
+
for (const attribute of Array.from(sourceRoot.attributes))
|
|
165
|
+
targetRoot.setAttribute(attribute.name, attribute.value);
|
|
166
|
+
|
|
167
|
+
targetRoot.replaceChildren(...Array.from(sourceRoot.childNodes));
|
|
168
|
+
|
|
169
|
+
for (const script of targetRoot.querySelectorAll("script"))
|
|
170
|
+
setScript(script, undefined, true);
|
|
171
|
+
}
|
|
150
172
|
} else if (el.tagName === "SCRIPT") {
|
|
151
173
|
setScript(el, value);
|
|
152
174
|
} else if (el.hasAttribute("value")) {
|
|
@@ -234,12 +256,12 @@ const setValue = (el, value, dispatch) => {
|
|
|
234
256
|
}
|
|
235
257
|
}
|
|
236
258
|
|
|
237
|
-
dispatchEvents(el, bubbles, dispatch);
|
|
259
|
+
dispatchEvents(el, bubbles, dispatch, value);
|
|
238
260
|
};
|
|
239
261
|
|
|
240
|
-
function setScript(script, value) {
|
|
262
|
+
function setScript(script, value, allowCrossOrigin = false) {
|
|
241
263
|
let srcAttribute = script.src;
|
|
242
|
-
if (srcAttribute) {
|
|
264
|
+
if (srcAttribute && !allowCrossOrigin) {
|
|
243
265
|
let pageOrigin = window.location.origin;
|
|
244
266
|
let srcOrigin;
|
|
245
267
|
|
|
@@ -282,11 +304,12 @@ function __decryptPassword(str) {
|
|
|
282
304
|
}
|
|
283
305
|
}
|
|
284
306
|
|
|
285
|
-
function dispatchEvents(el, bubbles = true, skip = true) {
|
|
307
|
+
function dispatchEvents(el, bubbles = true, skip = true, value) {
|
|
286
308
|
let inputEvent = new CustomEvent("input", {
|
|
287
309
|
bubbles,
|
|
288
310
|
detail: {
|
|
289
|
-
skip
|
|
311
|
+
skip,
|
|
312
|
+
value
|
|
290
313
|
}
|
|
291
314
|
});
|
|
292
315
|
|
|
@@ -299,7 +322,8 @@ function dispatchEvents(el, bubbles = true, skip = true) {
|
|
|
299
322
|
let changeEvent = new CustomEvent("change", {
|
|
300
323
|
bubbles,
|
|
301
324
|
detail: {
|
|
302
|
-
skip
|
|
325
|
+
skip,
|
|
326
|
+
value
|
|
303
327
|
}
|
|
304
328
|
});
|
|
305
329
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
globalThis.window = {
|
|
5
|
+
innerWidth: 1024,
|
|
6
|
+
innerHeight: 768,
|
|
7
|
+
location: {
|
|
8
|
+
href: "https://example.test/",
|
|
9
|
+
origin: "https://example.test",
|
|
10
|
+
protocol: "https:",
|
|
11
|
+
hostname: "example.test",
|
|
12
|
+
host: "example.test",
|
|
13
|
+
port: "",
|
|
14
|
+
pathname: "/",
|
|
15
|
+
hash: ""
|
|
16
|
+
},
|
|
17
|
+
addEventListener() {}
|
|
18
|
+
};
|
|
19
|
+
globalThis.localStorage = { getItem: () => null };
|
|
20
|
+
|
|
21
|
+
class TestElement {
|
|
22
|
+
constructor(attributes, value) {
|
|
23
|
+
this.attributes = new Map(Object.entries(attributes));
|
|
24
|
+
this.value = value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getAttribute(name) {
|
|
28
|
+
return this.attributes.has(name) ? this.attributes.get(name) : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getValue() {
|
|
32
|
+
return this.value;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
globalThis.Element = TestElement;
|
|
37
|
+
|
|
38
|
+
const { getAttribute, getAttributeAsync } = await import("../src/getAttribute.js");
|
|
39
|
+
|
|
40
|
+
const labelExpression = JSON.stringify({
|
|
41
|
+
parsed: { $parse: "$value" },
|
|
42
|
+
$return: {
|
|
43
|
+
$if: {
|
|
44
|
+
$parsed: {
|
|
45
|
+
$eq: {
|
|
46
|
+
$true: "Enabled",
|
|
47
|
+
$false: "Disabled",
|
|
48
|
+
"true": "Enabled",
|
|
49
|
+
"false": "Disabled",
|
|
50
|
+
Enabled: "Enabled",
|
|
51
|
+
Disabled: "Disabled",
|
|
52
|
+
$else: ""
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const directStringLabelExpression = JSON.stringify({
|
|
60
|
+
$if: {
|
|
61
|
+
$value: {
|
|
62
|
+
$eq: {
|
|
63
|
+
"true": "Enabled",
|
|
64
|
+
"false": "Disabled",
|
|
65
|
+
$else: ""
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const quotedOperatorLabelExpression =
|
|
72
|
+
"$(`\u0024value` === 'true' ? 'Enabled' : 'Disabled')";
|
|
73
|
+
|
|
74
|
+
test("input.value evaluates direct string equality branches", async () => {
|
|
75
|
+
for (const [value, expected] of [["true", "Enabled"], ["false", "Disabled"]]) {
|
|
76
|
+
const element = new TestElement({ "input.value": directStringLabelExpression }, value);
|
|
77
|
+
|
|
78
|
+
assert.equal(getAttribute(element, "input.value"), expected);
|
|
79
|
+
assert.equal(await getAttributeAsync(element, "input.value"), expected);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("input.value evaluates backtick OperatorJS values as strings", () => {
|
|
84
|
+
for (const [value, expected] of [["true", "Enabled"], ["false", "Disabled"]]) {
|
|
85
|
+
const element = new TestElement({ "input.value": quotedOperatorLabelExpression }, value);
|
|
86
|
+
assert.equal(getAttribute(element, "input.value"), expected);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("client getAttribute resolves $value from the element and maps it through AST", async () => {
|
|
91
|
+
for (const [value, expected] of [
|
|
92
|
+
[true, "Enabled"],
|
|
93
|
+
[false, "Disabled"],
|
|
94
|
+
["true", "Enabled"],
|
|
95
|
+
["false", "Disabled"],
|
|
96
|
+
["Enabled", "Enabled"],
|
|
97
|
+
["Disabled", "Disabled"]
|
|
98
|
+
]) {
|
|
99
|
+
const element = new TestElement({ "input.value": labelExpression }, value);
|
|
100
|
+
|
|
101
|
+
assert.equal(getAttribute(element, "input.value"), expected);
|
|
102
|
+
assert.equal(await getAttributeAsync(element, "input.value"), expected);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
@@ -37,6 +37,9 @@ async function setup() {
|
|
|
37
37
|
this.setExport('astSync', ({ executeAST }) => executeAST);
|
|
38
38
|
this.setExport('astAsync', async ({ executeAST }) => executeAST);
|
|
39
39
|
}, { context });
|
|
40
|
+
const getter = load('getValue.js');
|
|
41
|
+
await getter.link(() => utils);
|
|
42
|
+
await getter.evaluate();
|
|
40
43
|
const setter = load('setValue.js');
|
|
41
44
|
await setter.link(specifier => specifier === '@cocreate/utils' ? utils : load(specifier));
|
|
42
45
|
await setter.evaluate();
|
|
@@ -77,7 +80,7 @@ test('component storage retains parsed objects and arrays', async () => {
|
|
|
77
80
|
['array.$keys', '{"a":1}', ['a']],
|
|
78
81
|
['boolean', 'false', false]
|
|
79
82
|
]) {
|
|
80
|
-
const el = new Element({ component: '', 'set-value-type': type });
|
|
83
|
+
const el = new Element({ component: '', 'set-value-type': type }, 'DIV');
|
|
81
84
|
el.setValue(input);
|
|
82
85
|
assert.equal(JSON.stringify(el.getValue()), JSON.stringify(expected));
|
|
83
86
|
assert.deepEqual(el.events, ['input', 'change']);
|
|
@@ -87,7 +90,7 @@ test('component storage retains parsed objects and arrays', async () => {
|
|
|
87
90
|
test('existing objects keep their identity in component storage', async () => {
|
|
88
91
|
const Element = await setup();
|
|
89
92
|
const value = { a: 1 };
|
|
90
|
-
const el = new Element({ component: '', 'set-value-type': 'object', 'set-value-uppercase': '' });
|
|
93
|
+
const el = new Element({ component: '', 'set-value-type': 'object', 'set-value-uppercase': '' }, 'DIV');
|
|
91
94
|
el.setValue(value);
|
|
92
95
|
assert.equal(el.getValue(), value);
|
|
93
96
|
});
|
|
@@ -101,7 +104,7 @@ test('textarea serializes objects after setter transformations', async () => {
|
|
|
101
104
|
|
|
102
105
|
test('regex may return a non-string without subsequent string operations failing', async () => {
|
|
103
106
|
const Element = await setup();
|
|
104
|
-
const el = new Element({ component: '', 'set-value-split': ',', 'set-value-replace': 'a' });
|
|
107
|
+
const el = new Element({ component: '', 'set-value-split': ',', 'set-value-replace': 'a' }, 'DIV');
|
|
105
108
|
el.setValue('a,b');
|
|
106
109
|
assert.equal(JSON.stringify(el.getValue()), '["a","b"]');
|
|
107
110
|
});
|
|
@@ -161,10 +164,10 @@ test('each option falls back independently to shared attributes', async () => {
|
|
|
161
164
|
|
|
162
165
|
test('shared and scoped type conversion applies before component storage', async () => {
|
|
163
166
|
const Element = await setup();
|
|
164
|
-
const shared = new Element({ component: '', 'value-type': 'object' });
|
|
167
|
+
const shared = new Element({ component: '', 'value-type': 'object' }, 'DIV');
|
|
165
168
|
shared.setValue('{"a":1}');
|
|
166
169
|
assert.equal(JSON.stringify(shared.getValue()), '{"a":1}');
|
|
167
|
-
const scoped = new Element({ component: '', 'value-type': 'object', 'set-value-type': 'array.$keys' });
|
|
170
|
+
const scoped = new Element({ component: '', 'value-type': 'object', 'set-value-type': 'array.$keys' }, 'DIV');
|
|
168
171
|
scoped.setValue('{"a":1}');
|
|
169
172
|
assert.equal(JSON.stringify(scoped.getValue()), '["a"]');
|
|
170
173
|
});
|
|
@@ -246,10 +249,10 @@ test('explicit setter prefix and suffix build the rendered value', async () => {
|
|
|
246
249
|
assert.equal(el.value, '/assets/images/providers/aws.svg');
|
|
247
250
|
});
|
|
248
251
|
|
|
249
|
-
test('iframes
|
|
252
|
+
test('iframes honor explicit src and srcdoc setter paths', async () => {
|
|
250
253
|
const Element = await setup();
|
|
251
254
|
const srcFrame = new Element({ src: '' }, 'IFRAME');
|
|
252
|
-
const srcdocFrame = new Element({}, 'IFRAME');
|
|
255
|
+
const srcdocFrame = new Element({ srcdoc: '' }, 'IFRAME');
|
|
253
256
|
|
|
254
257
|
srcFrame.setValue('/assets/files/preview.html');
|
|
255
258
|
srcdocFrame.setValue('<h1>Preview</h1>');
|
|
@@ -273,3 +276,45 @@ test('empty setter affixes override shared affixes', async () => {
|
|
|
273
276
|
el.setValue('[aws]');
|
|
274
277
|
assert.equal(el.value, '[aws]');
|
|
275
278
|
});
|
|
279
|
+
|
|
280
|
+
test('generic values prefer attributes then properties while explicit reads select the DOM', async () => {
|
|
281
|
+
const Element = await setup();
|
|
282
|
+
const el = new Element({ value: '' }, 'DIV');
|
|
283
|
+
el.value = 'property';
|
|
284
|
+
el.innerHTML = '<b>markup</b>';
|
|
285
|
+
el.textContent = 'markup';
|
|
286
|
+
assert.equal(el.getValue(), '');
|
|
287
|
+
assert.equal(await el.getValueAsync(), '');
|
|
288
|
+
assert.equal(el.getValue('textContent'), 'markup');
|
|
289
|
+
assert.equal(await el.getValueAsync('innerHTML'), '<b>markup</b>');
|
|
290
|
+
el.attributes.splice(0);
|
|
291
|
+
el.value = '';
|
|
292
|
+
assert.equal(el.getValue(), '');
|
|
293
|
+
delete el.value;
|
|
294
|
+
assert.equal(el.getValue(), '<b>markup</b>');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('component values stay on the element across repeated asynchronous reads', async () => {
|
|
298
|
+
const Element = await setup();
|
|
299
|
+
const el = new Element({ component: '' }, 'DIV'), value = { source: 'stored' };
|
|
300
|
+
el.setValue(value);
|
|
301
|
+
assert.equal(el.value, value);
|
|
302
|
+
assert.equal(await el.getValueAsync(), value);
|
|
303
|
+
assert.equal(await el.getValueAsync(), value);
|
|
304
|
+
assert.equal(el.getValue(), value);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('component and editor markers do not bypass native checkbox handling', async () => {
|
|
308
|
+
const Element = await setup();
|
|
309
|
+
for (const marker of ['component', 'editor', 'language']) {
|
|
310
|
+
const el = new Element({ [marker]: '', value: 'yes' }, 'INPUT');
|
|
311
|
+
el.type = 'checkbox';
|
|
312
|
+
el.value = 'yes';
|
|
313
|
+
el.setValue(true);
|
|
314
|
+
assert.equal(el.checked, true);
|
|
315
|
+
assert.equal(el.value, 'yes');
|
|
316
|
+
el.setValue(false);
|
|
317
|
+
assert.equal(el.checked, false);
|
|
318
|
+
assert.equal(el.getValue(), false);
|
|
319
|
+
}
|
|
320
|
+
});
|