@cocreate/element-prototype 1.35.0 → 1.38.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/setValue.js CHANGED
@@ -1,282 +1,313 @@
1
1
  import { storage } from "./getValue.js";
2
+ import { getValueAttribute, transformValue, convertValue } from "@cocreate/utils";
2
3
 
3
4
  HTMLElement.prototype.setValue = function (value, dispatch) {
4
- setValue(this, value, dispatch);
5
+ setValue(this, value, dispatch);
5
6
  };
6
7
  SVGElement.prototype.setValue = function (value, dispatch) {
7
- setValue(this, value, dispatch);
8
+ setValue(this, value, dispatch);
8
9
  };
9
10
 
10
11
  // TODO: check if using a a switch case will provide better performance
11
12
  const setValue = (el, value, dispatch) => {
12
- let bubbles = el.hasAttribute("value-bubbles");
13
- let valueDispatch = el.hasAttribute("value-dispatch");
14
- if (valueDispatch || valueDispatch === "") {
15
- if (value === "$false" || value === undefined || value === null)
16
- return dispatchEvents(el, bubbles, dispatch);
17
- }
18
-
19
- if (value === null || value === undefined) return;
20
- if (el.hasAttribute("component"))
21
- return storage.set(el, value);
22
- else if (typeof value === "object") value = JSON.stringify(value, null, 2);
23
-
24
- if (["date", "time", "datetime", "datetime-local"].includes(el.getAttribute("type") || el.type )) {
13
+ let bubbles = ![null, false, "false"].includes(getValueAttribute(el, "bubbles", "set-value"));
14
+ let valueDispatch = ![null, false, "false"].includes(getValueAttribute(el, "dispatch", "set-value"));
15
+ if (valueDispatch || valueDispatch === "") {
16
+ if (value === "$false" || value === undefined || value === null)
17
+ return dispatchEvents(el, bubbles, dispatch);
18
+ }
19
+
20
+ if (value === null || value === undefined) return;
21
+ value = transformValue(el, value, "set-value");
22
+ 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") {
27
+ value = JSON.stringify(value, null, 2);
28
+ }
29
+
30
+ if (["date", "time", "datetime", "datetime-local"].includes(el.getAttribute("type") || el.type)) {
25
31
  if (value) {
26
32
  const date = new Date(value);
27
- // If 'use-utc' is present, we shift the time forward by the timezone offset.
28
- if (el.hasAttribute("use-utc")) {
29
- date.setMinutes(date.getMinutes() + date.getTimezoneOffset());
30
- }
33
+ // If 'use-utc' is present, we shift the time forward by the timezone offset.
34
+ if (el.hasAttribute("use-utc")) {
35
+ date.setMinutes(date.getMinutes() + date.getTimezoneOffset());
36
+ }
31
37
 
32
38
  if (el.tagName === "INPUT") {
33
- if (!isNaN(date.getTime())) {
34
- // We no longer need ternary operators or if/else blocks here.
35
- const year = date.getFullYear();
36
- const month = String(date.getMonth() + 1).padStart(2, "0");
37
- const day = String(date.getDate()).padStart(2, "0");
38
- const hours = String(date.getHours()).padStart(2, "0");
39
- const minutes = String(date.getMinutes()).padStart(2, "0");
40
-
41
- // 5. Set Value
42
- if (el.type === "time") {
43
- el.value = `${hours}:${minutes}`;
44
- } else if (el.type === "date") {
45
- el.value = `${year}-${month}-${day}`;
46
- } else {
47
- el.value = `${year}-${month}-${day}T${hours}:${minutes}`;
48
- }
49
- } else {
50
- console.warn(`Invalid date for input ${el.name}:`, value);
51
- el.value = "";
52
- }
53
- return dispatchEvents(el, bubbles, dispatch);
39
+ if (!isNaN(date.getTime())) {
40
+ // We no longer need ternary operators or if/else blocks here.
41
+ const year = date.getFullYear();
42
+ const month = String(date.getMonth() + 1).padStart(2, "0");
43
+ const day = String(date.getDate()).padStart(2, "0");
44
+ const hours = String(date.getHours()).padStart(2, "0");
45
+ const minutes = String(date.getMinutes()).padStart(2, "0");
46
+
47
+ // 5. Set Value
48
+ if (el.type === "time") {
49
+ el.value = `${hours}:${minutes}`;
50
+ } else if (el.type === "date") {
51
+ el.value = `${year}-${month}-${day}`;
52
+ } else {
53
+ el.value = `${year}-${month}-${day}T${hours}:${minutes}`;
54
+ }
55
+ } else {
56
+ console.warn(`Invalid date for input ${el.name}:`, value);
57
+ el.value = "";
58
+ }
59
+ return dispatchEvents(el, bubbles, dispatch);
54
60
  } else if (!isNaN(date.getTime())) {
55
- value = date.toLocaleString()
56
- }
61
+ value = date.toLocaleString()
62
+ }
57
63
  } else {
58
64
  el.value = "";
59
65
  }
60
66
  }
61
-
62
- let valueType = el.getAttribute("value-type");
63
- let prefix = el.getAttribute("value-prefix") || "";
64
- if (prefix) value = value.toString().replace(prefix, "");
65
-
66
- let suffix = el.getAttribute("value-suffix") || "";
67
- if (suffix) value = value.toString().replace(suffix, "");
68
-
69
- // TODO: el.options vs rendenring options from src
70
- if (
71
- el.tagName == "INPUT" ||
72
- el.tagName == "TEXTAREA" ||
73
- el.tagName == "SELECT"
74
- ) {
75
- // TODO: attribute config undefined when used with onload-value
76
- let isCrdt = el.getAttribute("crdt");
77
- if (isCrdt == "true" || el.type === "file") return;
78
-
79
- if (el.type == "checkbox") {
80
- let inputs = [el];
81
- let key = el.getAttribute("key");
82
- if (key)
83
- inputs = document.querySelectorAll(
84
- `input[type="${el.type}"][key="${key}"]`
85
- );
86
-
87
- for (let i = 0; i < inputs.length; i++) {
88
- let inputValue = inputs[i].getAttribute("value");
89
- if (inputs[i].value) {
90
- if (value === true || value === false)
91
- inputs[i].checked = value;
92
- else if (value.includes(inputValue) || value === "on") {
93
- inputs[i].checked = true;
94
- } else {
95
- inputs[i].checked = false;
96
- }
97
- } else {
98
- if (
99
- value === "true" ||
100
- value === true ||
101
- value === "checked"
102
- )
103
- inputs[i].checked = true;
104
- else inputs[i].checked = false;
105
- }
106
- }
107
- } else if (el.type === "radio") {
108
- const wasChecked = el.checked;
109
- if (el.value == value) {
110
- el.checked = true;
111
- } else {
112
- el.checked = false;
113
- }
114
- // Only continue if checked state changed
115
- if (el.checked === wasChecked) return;
116
- } else if (el.type === "password") {
117
- el.value = __decryptPassword(value);
118
- } else if (el.tagName == "SELECT") {
119
- let options = el.options;
120
- for (let i = 0; i < options.length; i++) {
121
- if (value.includes && value.includes(options[i].value)) {
122
- options[i].selected = "selected";
123
- } else {
124
- options[i].selected = "";
125
- }
126
- }
127
- } else {
128
- if (el.value === value && !valueDispatch) {
129
- return;
130
- }
131
-
132
- el.value = value;
133
- }
134
- // dispatchEvents(el, bubbles, dispatch);
135
- } else if (el.tagName === "IMG" || el.tagName === "SOURCE") {
136
- el.src = value;
137
- } else if (el.tagName === "IFRAME") {
138
- el.srcdoc = value;
139
- } else if (el.tagName === "SCRIPT") {
140
- setScript(el, value);
141
- } else if (el.hasAttribute("value")) {
142
- el.setAttribute("value", value);
143
- } else {
144
- if (el.hasAttribute("contenteditable") && el == document.activeElement)
145
- return;
146
-
147
- if (valueType == "string" || valueType == "text")
148
- el.textContent = value;
149
- else {
150
- let newElement = document.createElement("div");
151
- newElement.innerHTML = value;
152
- setState(newElement);
153
-
154
- let CoCreateJS = newElement.querySelector(
155
- 'script[src*="CoCreate.js"], script[src*="CoCreate.min.js"]'
156
- );
157
- if (CoCreateJS) CoCreateJS.remove();
158
-
159
- let CoCreateCSS = newElement.querySelector(
160
- 'link[href*="CoCreate.css"], link[href*="CoCreate.min.css"]'
161
- );
162
- if (CoCreateCSS) CoCreateCSS.remove();
163
-
164
- let css = newElement.querySelector("link[array], link[object]");
165
- if (css) css.remove();
166
-
167
- if (valueType == "outerHTML") {
168
- let parentNode = el.parentNode;
169
- if (parentNode) {
170
- if (newElement.children.length > 0) {
171
- let fragment = document.createDocumentFragment();
172
- while (newElement.firstChild) {
173
- fragment.appendChild(newElement.firstChild);
174
- }
175
- parentNode.replaceChild(fragment, el);
176
- } else {
177
- parentNode.replaceChild(newElement, el);
178
- }
179
- }
180
- } else el.innerHTML = newElement.innerHTML;
181
-
182
- let scripts = el.querySelectorAll("script");
183
- for (let script of scripts) {
184
- setScript(script);
185
- }
186
- }
187
-
188
- if (el.hasAttribute("value")) {
189
- el.setAttribute("value", value);
190
- }
191
- }
192
-
193
- // if (el.getAttribute("contenteditable")) dispatchEvents(el, bubbles, dispatch);
194
-
195
- if (el.tagName == "HEAD" || el.tagName == "BODY") {
196
- el.removeAttribute("array");
197
- el.removeAttribute("object");
198
- el.removeAttribute("state_id");
199
-
200
- let scripts = el.querySelectorAll("script");
201
- for (let script of scripts) {
202
- setScript(script);
203
- }
204
- }
205
-
206
- dispatchEvents(el, bubbles, dispatch);
207
- };
208
67
 
209
- function setState(el) {
210
- if (CoCreate.state) {
211
- let stateElements = el.querySelectorAll("[state_id]");
212
- if (stateElements) CoCreate.state.initElements(stateElements);
213
- }
214
- }
68
+ let valueType = getValueAttribute(el, "type", "set-value");
69
+ let prefix = getValueAttribute(el, "prefix", "set-value") || "";
70
+ if (prefix) value = el.hasAttribute("set-value-prefix")
71
+ ? prefix + value
72
+ : value.toString().replace(prefix, "");
73
+
74
+ let suffix = getValueAttribute(el, "suffix", "set-value") || "";
75
+ if (suffix) value = el.hasAttribute("set-value-suffix")
76
+ ? value + suffix
77
+ : value.toString().replace(suffix, "");
78
+
79
+ // TODO: el.options vs rendenring options from src
80
+ if (
81
+ el.tagName == "INPUT" ||
82
+ el.tagName == "TEXTAREA" ||
83
+ el.tagName == "SELECT"
84
+ ) {
85
+ // TODO: attribute config undefined when used with onload-value
86
+ let isCrdt = el.getAttribute("crdt");
87
+ if (isCrdt == "true" || el.type === "file") return;
88
+
89
+ if (el.type == "checkbox") {
90
+ let inputs = [el];
91
+ let key = el.getAttribute("key");
92
+ if (key)
93
+ inputs = document.querySelectorAll(
94
+ `input[type="${el.type}"][key="${key}"]`
95
+ );
96
+
97
+ for (let i = 0; i < inputs.length; i++) {
98
+ let inputValue = inputs[i].getAttribute("value");
99
+ if (inputs[i].value) {
100
+ if (value === true || value === false)
101
+ inputs[i].checked = value;
102
+ else if (value.includes(inputValue) || value === "on") {
103
+ inputs[i].checked = true;
104
+ } else {
105
+ inputs[i].checked = false;
106
+ }
107
+ } else {
108
+ if (
109
+ value === "true" ||
110
+ value === true ||
111
+ value === "checked"
112
+ )
113
+ inputs[i].checked = true;
114
+ else inputs[i].checked = false;
115
+ }
116
+ }
117
+ } else if (el.type === "radio") {
118
+ const wasChecked = el.checked;
119
+ if (el.value == value) {
120
+ el.checked = true;
121
+ } else {
122
+ el.checked = false;
123
+ }
124
+ // Only continue if checked state changed
125
+ if (el.checked === wasChecked) return;
126
+ } else if (el.type === "password") {
127
+ el.value = __decryptPassword(value);
128
+ } else if (el.tagName == "SELECT") {
129
+ let options = el.options;
130
+ for (let i = 0; i < options.length; i++) {
131
+ if (value.includes && value.includes(options[i].value)) {
132
+ options[i].selected = "selected";
133
+ } else {
134
+ options[i].selected = "";
135
+ }
136
+ }
137
+ } else {
138
+ if (el.value === value && !valueDispatch) {
139
+ return;
140
+ }
141
+
142
+ el.value = value;
143
+ }
144
+ // 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 === "SCRIPT") {
151
+ setScript(el, value);
152
+ } else if (el.hasAttribute("value")) {
153
+ el.setAttribute("value", value);
154
+ } else {
155
+ if (el.hasAttribute("contenteditable") && el == document.activeElement)
156
+ return;
157
+
158
+ if (valueType == "string" || valueType == "text" || valueType == "textContent")
159
+ el.textContent = value;
160
+ else if (valueType == "innerText")
161
+ el.innerText = value;
162
+ else {
163
+ let newElement = document.createElement("div");
164
+ newElement.innerHTML = value;
165
+
166
+ let CoCreateJS = newElement.querySelector(
167
+ 'script[src*="CoCreate.js"], script[src*="CoCreate.min.js"]'
168
+ );
169
+ if (CoCreateJS) CoCreateJS.remove();
170
+
171
+ let CoCreateCSS = newElement.querySelector(
172
+ 'link[href*="CoCreate.css"], link[href*="CoCreate.min.css"]'
173
+ );
174
+ if (CoCreateCSS) CoCreateCSS.remove();
175
+
176
+ let css = newElement.querySelector("link[array], link[object]");
177
+ if (css) css.remove();
178
+
179
+ if (document.head) {
180
+ const stylesheets = newElement.querySelectorAll(
181
+ 'link[rel~="stylesheet"][href]'
182
+ );
183
+ for (const stylesheet of stylesheets) {
184
+ const exists = Array.from(
185
+ document.head.querySelectorAll(
186
+ 'link[rel~="stylesheet"][href]'
187
+ )
188
+ ).some(
189
+ (link) =>
190
+ link.href === stylesheet.href &&
191
+ link.media === stylesheet.media
192
+ );
193
+ if (exists) stylesheet.remove();
194
+ else document.head.appendChild(stylesheet);
195
+ }
196
+ }
197
+
198
+ if (valueType?.toLowerCase() === "outerhtml") {
199
+ let parentNode = el.parentNode;
200
+ if (parentNode) {
201
+ if (newElement.children.length > 0) {
202
+ let fragment = document.createDocumentFragment();
203
+ while (newElement.firstChild) {
204
+ fragment.appendChild(newElement.firstChild);
205
+ }
206
+ parentNode.replaceChild(fragment, el);
207
+ } else {
208
+ parentNode.replaceChild(newElement, el);
209
+ }
210
+ }
211
+ } else el.innerHTML = newElement.innerHTML;
212
+
213
+ let scripts = el.querySelectorAll("script");
214
+ for (let script of scripts) {
215
+ setScript(script);
216
+ }
217
+ }
218
+
219
+ if (el.hasAttribute("value")) {
220
+ el.setAttribute("value", value);
221
+ }
222
+ }
223
+
224
+ // if (el.getAttribute("contenteditable")) dispatchEvents(el, bubbles, dispatch);
225
+
226
+ if (el.tagName == "HEAD" || el.tagName == "BODY") {
227
+ el.removeAttribute("array");
228
+ el.removeAttribute("object");
229
+ el.removeAttribute("state_id");
230
+
231
+ let scripts = el.querySelectorAll("script");
232
+ for (let script of scripts) {
233
+ setScript(script);
234
+ }
235
+ }
236
+
237
+ dispatchEvents(el, bubbles, dispatch);
238
+ };
215
239
 
216
240
  function setScript(script, value) {
217
- let srcAttribute = script.src;
218
- if (srcAttribute) {
219
- let pageOrigin = window.location.origin;
220
- let srcOrigin;
221
-
222
- try {
223
- srcOrigin = new URL(srcAttribute, document.baseURI).origin;
224
- } catch (e) {
225
- // Handle invalid URLs
226
- console.error("Invalid URL in src attribute:", srcAttribute);
227
- return;
228
- }
229
- if (pageOrigin !== srcOrigin) return;
230
- }
231
-
232
- let newScript = document.createElement("script");
233
- for (let attr of script.attributes) {
234
- newScript.setAttribute(attr.name, attr.value);
235
- }
236
- newScript.innerHTML = script.innerHTML;
237
- if (value) {
238
- if (script.hasAttribute("src")) newScript.src = value;
239
- else newScript.innerHTML = value;
240
- }
241
- try {
242
- script.replaceWith(newScript);
243
- } catch (error) {
244
- console.log(error);
245
- }
241
+ let srcAttribute = script.src;
242
+ if (srcAttribute) {
243
+ let pageOrigin = window.location.origin;
244
+ let srcOrigin;
245
+
246
+ try {
247
+ srcOrigin = new URL(srcAttribute, document.baseURI).origin;
248
+ } catch (e) {
249
+ // Handle invalid URLs
250
+ console.error("Invalid URL in src attribute:", srcAttribute);
251
+ return;
252
+ }
253
+ if (pageOrigin !== srcOrigin) return;
254
+ }
255
+
256
+ let newScript = document.createElement("script");
257
+ for (let attr of script.attributes) {
258
+ newScript.setAttribute(attr.name, attr.value);
259
+ }
260
+ newScript.innerHTML = script.innerHTML;
261
+ if (value) {
262
+ if (script.hasAttribute("src")) newScript.src = value;
263
+ else newScript.innerHTML = value;
264
+ }
265
+ try {
266
+ script.replaceWith(newScript);
267
+ } catch (error) {
268
+ console.log(error);
269
+ }
246
270
  }
247
271
 
248
272
  function __decryptPassword(str) {
249
- if (!str) return "";
250
- let decode_str = atob(str);
251
- return decode_str;
273
+ if (!str) return "";
274
+ const value = String(str);
275
+ try {
276
+ return atob(value);
277
+ } catch (error) {
278
+ // Password fields may also receive plain-text credentials from CRUD.
279
+ // Preserve those values instead of aborting the response render.
280
+ if (error.name !== "InvalidCharacterError") throw error;
281
+ return value;
282
+ }
252
283
  }
253
284
 
254
285
  function dispatchEvents(el, bubbles = true, skip = true) {
255
- let inputEvent = new CustomEvent("input", {
256
- bubbles,
257
- detail: {
258
- skip
259
- }
260
- });
261
-
262
- Object.defineProperty(inputEvent, "target", {
263
- writable: false,
264
- value: el
265
- });
266
- el.dispatchEvent(inputEvent);
267
-
268
- let changeEvent = new CustomEvent("change", {
269
- bubbles,
270
- detail: {
271
- skip
272
- }
273
- });
274
-
275
- Object.defineProperty(changeEvent, "target", {
276
- writable: false,
277
- value: el
278
- });
279
- el.dispatchEvent(changeEvent);
286
+ let inputEvent = new CustomEvent("input", {
287
+ bubbles,
288
+ detail: {
289
+ skip
290
+ }
291
+ });
292
+
293
+ Object.defineProperty(inputEvent, "target", {
294
+ writable: false,
295
+ value: el
296
+ });
297
+ el.dispatchEvent(inputEvent);
298
+
299
+ let changeEvent = new CustomEvent("change", {
300
+ bubbles,
301
+ detail: {
302
+ skip
303
+ }
304
+ });
305
+
306
+ Object.defineProperty(changeEvent, "target", {
307
+ writable: false,
308
+ value: el
309
+ });
310
+ el.dispatchEvent(changeEvent);
280
311
  }
281
312
 
282
313
  export { setValue };
@@ -0,0 +1,48 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const vm = require('node:vm');
5
+ const test = require('node:test');
6
+
7
+ async function setup() {
8
+ class Element {
9
+ constructor() { this.attributes = []; this.tagName = 'INPUT'; this.type = 'password'; this.value = ''; this.events = []; }
10
+ hasAttribute() { return false; }
11
+ getAttribute(name) { return name === 'type' ? this.type : null; }
12
+ dispatchEvent(event) { this.events.push(event.type); }
13
+ }
14
+ const context = vm.createContext({
15
+ HTMLElement: Element, SVGElement: class {}, atob,
16
+ CustomEvent: class { constructor(type) { this.type = type; } }
17
+ });
18
+ const module = new vm.SourceTextModule(fs.readFileSync(path.join(__dirname, '../src/setValue.js'), 'utf8'), { context });
19
+ const handlers = await import('../../CoCreate-utils/src/modules/valueHandler.js');
20
+ await module.link((specifier) => {
21
+ if (specifier === '@cocreate/utils') {
22
+ return new vm.SyntheticModule(Object.keys(handlers), function () {
23
+ for (const [name, value] of Object.entries(handlers)) this.setExport(name, value);
24
+ }, { context });
25
+ }
26
+ return new vm.SyntheticModule(['storage'], function () {
27
+ this.setExport('storage', new WeakMap());
28
+ }, { context });
29
+ });
30
+ await module.evaluate();
31
+ return new Element();
32
+ }
33
+
34
+ for (const [name, incoming, expected] of [
35
+ ['legacy Base64', btoa('sample-password!'), 'sample-password!'],
36
+ ['plain-text credential', 'sample-password!', 'sample-password!'],
37
+ ['malformed padding', 'YWJj===', 'YWJj==='],
38
+ ['plain Unicode', 'sample-密碼', 'sample-密碼'],
39
+ ['empty value', '', '']
40
+ ]) {
41
+ test(`password setValue handles ${name} and completes event dispatch`, async () => {
42
+ const element = await setup();
43
+ element.setValue(incoming);
44
+ assert.equal(element.value, expected);
45
+ assert.ok(element.events.includes('input'));
46
+ assert.ok(element.events.includes('change'));
47
+ });
48
+ }