@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/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
- if (el.hasAttribute("component")) {
24
- storage.set(el, value);
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
- // TODO: attribute config undefined when used with onload-value
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 use src when the src attribute is present and srcdoc otherwise', async () => {
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
+ });