@cocreate/element-prototype 1.38.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/src/index.js CHANGED
@@ -18,7 +18,7 @@
18
18
  // Commercial Licensing Information:
19
19
  // For commercial use of this software without the copyleft provisions of the AGPLv3,
20
20
  // you must obtain a commercial license from CoCreate LLC.
21
- // For details, visit <https://cocreate.app/licenses/> or contact us at sales@cocreate.app.
21
+ // For details, visit <https://CoCreateJS.com/licenses/> or contact us at sales@CoCreateJS.com.
22
22
 
23
23
  import { getAttribute } from "./getAttribute.js";
24
24
  import { setValue } from "./setValue.js";
@@ -26,8 +26,195 @@ import { getValue } from "./getValue.js";
26
26
  import { queryElements } from "./queryElements.js";
27
27
 
28
28
  export default {
29
+ /**
30
+ *
31
+ * @public
32
+ * @environment client
33
+ */
29
34
  getAttribute,
35
+ /**
36
+ *
37
+ * @public
38
+ * @environment client
39
+ */
30
40
  getValue,
41
+ /**
42
+ *
43
+ * @public
44
+ * @environment client
45
+ */
31
46
  setValue,
47
+ /**
48
+ *
49
+ * @public
50
+ * @environment client
51
+ */
32
52
  queryElements
33
53
  };
54
+
55
+ /**
56
+ *
57
+ * @name array
58
+ * @kind member
59
+ * @attribute
60
+ * @environment client
61
+ */
62
+
63
+ /**
64
+ *
65
+ * @name component
66
+ * @kind member
67
+ * @attribute
68
+ * @environment client
69
+ */
70
+
71
+ /**
72
+ *
73
+ * @name contenteditable
74
+ * @kind member
75
+ * @attribute
76
+ * @environment client
77
+ */
78
+
79
+ /**
80
+ *
81
+ * @name editor
82
+ * @kind member
83
+ * @attribute
84
+ * @environment client
85
+ */
86
+
87
+ /**
88
+ *
89
+ * @name key
90
+ * @kind member
91
+ * @attribute
92
+ * @environment client
93
+ */
94
+
95
+ /**
96
+ *
97
+ * @name locale
98
+ * @kind member
99
+ * @attribute
100
+ * @environment client
101
+ */
102
+
103
+ /**
104
+ *
105
+ * @name multiple
106
+ * @kind member
107
+ * @attribute
108
+ * @environment client
109
+ */
110
+
111
+ /**
112
+ *
113
+ * @name now
114
+ * @kind member
115
+ * @attribute
116
+ * @environment client
117
+ */
118
+
119
+ /**
120
+ *
121
+ * @name object
122
+ * @kind member
123
+ * @attribute
124
+ * @environment client
125
+ */
126
+
127
+ /**
128
+ *
129
+ * @name set-value-prefix
130
+ * @kind member
131
+ * @attribute
132
+ * @environment client
133
+ */
134
+
135
+ /**
136
+ *
137
+ * @name set-value-suffix
138
+ * @kind member
139
+ * @attribute
140
+ * @environment client
141
+ */
142
+
143
+ /**
144
+ *
145
+ * @name src
146
+ * @kind member
147
+ * @attribute
148
+ * @environment client
149
+ */
150
+
151
+ /**
152
+ *
153
+ * @name srcdoc
154
+ * @kind member
155
+ * @attribute
156
+ * @environment client
157
+ */
158
+
159
+ /**
160
+ *
161
+ * @name state_id
162
+ * @kind member
163
+ * @attribute
164
+ * @environment client
165
+ */
166
+
167
+ /**
168
+ *
169
+ * @name time-ago-limit
170
+ * @kind member
171
+ * @attribute
172
+ * @environment client
173
+ */
174
+
175
+ /**
176
+ *
177
+ * @name time-to-units
178
+ * @kind member
179
+ * @attribute
180
+ * @environment client
181
+ */
182
+
183
+ /**
184
+ *
185
+ * @name type
186
+ * @kind member
187
+ * @attribute
188
+ * @environment client
189
+ */
190
+
191
+ /**
192
+ *
193
+ * @name use-utc
194
+ * @kind member
195
+ * @attribute
196
+ * @environment client
197
+ */
198
+
199
+ /**
200
+ *
201
+ * @name value
202
+ * @kind member
203
+ * @attribute
204
+ * @environment client
205
+ */
206
+
207
+ /**
208
+ *
209
+ * @name week-start-day
210
+ * @kind member
211
+ * @attribute
212
+ * @environment client
213
+ */
214
+
215
+ /**
216
+ * A simple element-prototype component in vanilla javascript. Easily configured using HTML5 data-attributes and/or JavaScript API.
217
+ *
218
+ * @module element-prototype
219
+ * @environment client
220
+ */
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
+ });