@cocreate/events 1.29.0 → 1.30.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +1 -1
  3. package/src/index.js +605 -1121
package/src/index.js CHANGED
@@ -20,1134 +20,618 @@
20
20
  // you must obtain a commercial license from CoCreate LLC.
21
21
  // For details, visit <https://cocreate.app/licenses/> or contact us at sales@cocreate.app.
22
22
 
23
- import { queryElements, queryData } from "@cocreate/utils";
23
+ import { queryElements } from "@cocreate/utils";
24
24
  import action from "@cocreate/actions";
25
25
  import observer from "@cocreate/observer";
26
26
  import "@cocreate/element-prototype";
27
+ import { getAttribute } from "@cocreate/element-prototype/src/getAttribute";
27
28
 
28
29
  const CoCreateEvents = {
29
- elements2: new Map(),
30
- init: function (prefix, events) {
31
- if (prefix && events) {
32
- this.initPrefix(prefix, events);
33
- } else {
34
- this.initPrefix("toggle", ["click"]);
35
- this.initPrefix("click", ["click"]);
36
- this.initPrefix("hover", ["mouseover", "mouseout"]);
37
- this.initPrefix("mouseover", ["mouseover"]);
38
- this.initPrefix("mouseout", ["mouseout"]);
39
- this.initPrefix("input", ["input"]);
40
- this.initPrefix("change", ["change"]);
41
- this.initPrefix("selected", ["click"]);
42
- this.initPrefix("onload", ["onload"]);
43
- this.initPrefix("observe", ["observer"]);
44
- this.initPrefix("intersection", ["intersection"]);
45
- this.initPrefix("resize", ["onload", "resize"]);
46
- this.initPrefix("localstorage", ["onload"]);
47
- this.initPrefix("focus", ["focus"]);
48
- this.initPrefix("blur", ["blur"]);
49
- }
50
-
51
- let customEventEls = document.querySelectorAll("[event-name]");
52
- let names = {};
53
- for (let customEventEl of customEventEls) {
54
- let name = customEventEl.getAttribute("event-name");
55
- name = name.split(",");
56
- for (let i = 0; i < name.length; i++) {
57
- name[i].trim();
58
- if (!names[name]) {
59
- names[name] = name;
60
- this.initPrefix(name);
61
- }
62
- }
63
- }
64
-
65
- const self = this;
66
- observer.init({
67
- name: "CoCreateEventName",
68
- types: ["addedNodes"],
69
- selector: `[event-name]`,
70
- callback: function (mutation) {
71
- let name = mutation.target.getAttribute("event-name");
72
- self.initPrefix(name);
73
- }
74
- });
75
- },
76
-
77
- initPrefix: function (prefix, events) {
78
- const self = this;
79
-
80
- action.init({
81
- name: prefix,
82
- endEvent: `${prefix}End`,
83
- callback: (data) => {
84
- this.__updateElements(
85
- data.element,
86
- prefix,
87
- null,
88
- null,
89
- data.params
90
- );
91
- }
92
- });
93
-
94
- let selector = `[${prefix}], [${prefix}-key], [${prefix}-attribute], [${prefix}-value], [${prefix}-action], [${prefix}-query]`;
95
-
96
- observer.init({
97
- name: "CoCreateEventattributes",
98
- types: ["attributes", "addedNodes"],
99
- attributeFilter: [
100
- `${prefix}-key`,
101
- `${prefix}-value`,
102
- `${prefix}-query`,
103
- `${prefix}-events`,
104
- `${prefix}-bubbles`,
105
- `${prefix}-cancelable`,
106
- `${prefix}-composed`,
107
- `${prefix}-preventDefault`,
108
- `${prefix}-stopPropagation`,
109
- `${prefix}-stopImmediatePropagation`
110
- ],
111
- selector,
112
- callback: function (mutation) {
113
- self.initElements([mutation.target], prefix, events);
114
- }
115
- });
116
-
117
- let elements = document.querySelectorAll(selector);
118
- this.initElements(elements, prefix, events);
119
- },
120
-
121
- elements: new Map(),
122
- initElements: function (elements, prefix, events = []) {
123
- const self = this;
124
- for (const el of elements) {
125
- let isEventable = true;
126
- let prefixes = this.elements.get(el);
127
- if (!prefixes) {
128
- prefixes = { [prefix]: { events } };
129
- this.elements.set(el, prefixes);
130
- } else if (!prefixes[prefix]) {
131
- prefixes[prefix] = { events };
132
- } else {
133
- isEventable = false;
134
- }
135
-
136
- let customEvents = el.getAttribute(`${prefix}-events`);
137
- if (customEvents) {
138
- customEvents = customEvents.split(",");
139
- for (let i = 0; i < customEvents.length; i++)
140
- customEvents[i] = customEvents[i].trim();
141
-
142
- for (let i = 0; i < events.length; i++)
143
- el.removeEventListener(events[i], eventFunction);
144
-
145
- events = customEvents;
146
- prefixes[prefix].events = events;
147
- isEventable = true;
148
- }
149
-
150
- if (!events || !isEventable) continue;
151
-
152
- if (events.includes("onload")) {
153
- this.__updateElements(el, prefix);
154
- }
155
-
156
- if (events.includes("observer")) {
157
- let target;
158
- for (let attribute of el.attributes) {
159
- if (attribute.name === "observe-target") {
160
- target = attribute.value;
161
- break;
162
- } else if ([`${prefix}-query`].includes(attribute.name)) {
163
- target = attribute.value;
164
- break;
165
- }
166
- }
167
-
168
- let attributeFilter = (
169
- el.getAttribute(`${prefix}-attributes`) || ""
170
- )
171
- .split(/\s*,\s*/)
172
- .filter((item) => item);
173
-
174
- let observeAttribute = (
175
- el.getAttribute(`${prefix}-observe`) || ""
176
- )
177
- .split(/\s*,\s*/)
178
- .filter((item) => item);
179
-
180
- let observerConfig = {
181
- types: observeAttribute,
182
- callback: function (mutation) {
183
- self.__updateElements(el, prefix, mutation.target);
184
- }
185
- };
186
-
187
- if (target) {
188
- if (
189
- target &&
190
- !observeAttribute.length &&
191
- !attributeFilter.length
192
- ) {
193
- observerConfig.types.push("addedNodes");
194
- }
195
- observerConfig.target = target;
196
- }
197
-
198
- if (attributeFilter.length) {
199
- observerConfig.types.push("attributes");
200
- observerConfig.attributeFilter = attributeFilter;
201
- }
202
-
203
- if (observerConfig.types.length) observer.init(observerConfig);
204
- }
205
-
206
- if (events.includes("resize")) {
207
- const resizeObserver = new ResizeObserver((entries) => {
208
- const lastEntry = entries[entries.length - 1];
209
- const { width, height } = lastEntry.contentRect;
210
- el.setAttribute(`${prefix}-if-value`, width + "px");
211
- self.__updateElements(el, prefix);
212
- });
213
- resizeObserver.observe(el);
214
- }
215
-
216
- if (events.includes("intersection")) {
217
- const rootSelector = el.getAttribute(`${prefix}-root`);
218
- const rootMargin =
219
- el.getAttribute(`${prefix}-root-margin`) || "0px";
220
- const threshold = el.getAttribute(`${prefix}-threshold`)
221
- ? JSON.parse(el.getAttribute(`${prefix}-threshold`))
222
- : [0];
223
- const trigger = el.getAttribute(`${prefix}-trigger`) || "both"; // Default to "both"
224
-
225
- // Resolve root from the selector or use null for viewport
226
- const root = rootSelector
227
- ? document.querySelector(rootSelector)
228
- : null;
229
-
230
- // Validate and filter threshold values
231
- const validThreshold = Array.isArray(threshold)
232
- ? threshold.filter((value) => value >= 0 && value <= 1)
233
- : [0];
234
-
235
- // Returns true if attribute is present AND does not equal "false"
236
- const onceAttr = el.getAttribute(`${prefix}-once`);
237
- const once = onceAttr !== null && onceAttr !== "false";
238
-
239
- // Create IntersectionObserver
240
- const intersectionObserver = new IntersectionObserver(
241
- (entries) => {
242
- entries.forEach((entry) => {
243
- const isVisible = entry.isIntersecting;
244
-
245
- // Determine if we should trigger based on the "trigger" attribute
246
- if (
247
- (trigger === "visible" && isVisible) ||
248
- (trigger === "not-visible" && !isVisible) ||
249
- trigger === "both"
250
- ) {
251
- // Set an attribute to reflect visibility state
252
- el.setAttribute(
253
- `${prefix}-is-visible`,
254
- isVisible ? "true" : "false"
255
- );
256
-
257
- // Call __updateElements
258
- self.__updateElements(
259
- el,
260
- prefix
261
- );
262
- // Disconnect if once is true
263
- if (once) {
264
- intersectionObserver.unobserve(entry.target);
30
+ elements: new Map(),
31
+
32
+ init: function (prefix, events) {
33
+ if (prefix && events) {
34
+ this.initPrefix(prefix, events);
35
+ } else {
36
+ const defaults = {
37
+ "toggle": ["click"],
38
+ "click": ["click"],
39
+ "hover": ["mouseover", "mouseout"],
40
+ "mouseover": ["mouseover"],
41
+ "mouseout": ["mouseout"],
42
+ "input": ["input"],
43
+ "change": ["change"],
44
+ "selected": ["click"],
45
+ "onload": ["onload"],
46
+ "observe": ["observer"],
47
+ "intersection": ["intersection"],
48
+ "resize": ["onload", "resize"],
49
+ "localstorage": ["onload"],
50
+ "focus": ["focus"],
51
+ "blur": ["blur"]
52
+ };
53
+
54
+ for (const [p, e] of Object.entries(defaults)) {
55
+ this.initPrefix(p, e);
56
+ }
57
+ }
58
+
59
+ // Initialize from existing elements with [event-name]
60
+ const customEventEls = document.querySelectorAll("[event-name]");
61
+ const processedNames = new Set();
62
+
63
+ for (const customEventEl of customEventEls) {
64
+ let names = customEventEl.getAttribute("event-name");
65
+ if (!names) continue;
66
+
67
+ names.split(",").forEach(name => {
68
+ name = name.trim();
69
+ if (name && !processedNames.has(name)) {
70
+ processedNames.add(name);
71
+ this.initPrefix(name);
72
+ }
73
+ });
74
+ }
75
+
76
+ // Observe for new elements with [event-name]
77
+ observer.init({
78
+ name: "CoCreateEventName",
79
+ types: ["addedNodes"],
80
+ selector: `[event-name]`,
81
+ callback: (mutation) => {
82
+ const names = mutation.target.getAttribute("event-name");
83
+ if (names) {
84
+ names.split(",").forEach(name => {
85
+ this.initPrefix(name.trim());
86
+ });
87
+ }
88
+ }
89
+ });
90
+ },
91
+
92
+ initPrefix: function (prefix, events) {
93
+ action.init({
94
+ name: prefix,
95
+ endEvent: `${prefix}End`,
96
+ callback: (data) => {
97
+ this.__updateElements(
98
+ data.element,
99
+ prefix,
100
+ null,
101
+ null,
102
+ data.params
103
+ );
104
+ }
105
+ });
106
+
107
+ // Updated selector to use dash for options: prefix-group, prefix-query
108
+ const selector = `[${prefix}], [${prefix}-group], [${prefix}-query]`;
109
+
110
+ observer.init({
111
+ name: "CoCreateEventAttributes",
112
+ types: ["attributes", "addedNodes"],
113
+ selector,
114
+ callback: (mutation) => {
115
+ if (mutation.type === 'attributes') {
116
+ if (mutation.attributeName.startsWith(prefix)) {
117
+ this.initElements([mutation.target], prefix, events);
118
+ }
119
+ } else {
120
+ this.initElements([mutation.target], prefix, events);
121
+ }
122
+ }
123
+ });
124
+
125
+ const elements = document.querySelectorAll(selector);
126
+ this.initElements(elements, prefix, events);
127
+ },
128
+
129
+ initElements: function (elements, prefix, events = []) {
130
+ for (const el of elements) {
131
+ let isEventable = true;
132
+ let elData = this.elements.get(el);
133
+
134
+ if (!elData) {
135
+ elData = { [prefix]: { events } };
136
+ this.elements.set(el, elData);
137
+ } else if (!elData[prefix]) {
138
+ elData[prefix] = { events };
139
+ } else {
140
+ isEventable = false;
141
+ }
142
+
143
+ // Check for custom events defined on the element using dash
144
+ let customEventsAttr = el.getAttribute(`${prefix}-events`);
145
+ if (customEventsAttr) {
146
+ // Remove listeners for previously assumed events before overriding
147
+ events.forEach(event => el.removeEventListener(event, eventFunction));
148
+
149
+ events = customEventsAttr.split(",").map(e => e.trim());
150
+ elData[prefix].events = events;
151
+ isEventable = true;
152
+ }
153
+
154
+ if (!events || !isEventable) continue;
155
+
156
+ // --- Handle Special Events ---
157
+
158
+ if (events.includes("onload")) {
159
+ this.__updateElements(el, prefix);
160
+ }
161
+
162
+ if (events.includes("observer")) {
163
+ this._initObserver(el, prefix);
164
+ }
165
+
166
+ if (events.includes("resize")) {
167
+ this._initResizeObserver(el, prefix);
168
+ }
169
+
170
+ if (events.includes("intersection")) {
171
+ this._initIntersectionObserver(el, prefix);
172
+ }
173
+
174
+ // --- Handle Standard Events ---
175
+
176
+ for (const eventName of events) {
177
+ if (["onload", "observer", "intersection", "resize"].includes(eventName)) {
178
+ continue;
179
+ }
180
+
181
+ // Remove existing listener to prevent duplicates
182
+ el.removeEventListener(eventName, eventFunction);
183
+
184
+ const options = {};
185
+ // Updated to use dash for options
186
+ const attributes = ["bubbles", "cancelable", "composed"];
187
+
188
+ for (const attr of attributes) {
189
+ const attrValue = el.getAttribute(`${eventName}-${attr}`);
190
+ if (attrValue !== null) {
191
+ options[attr] = attrValue !== "false";
192
+ }
193
+ }
194
+
195
+ el.addEventListener(eventName, eventFunction, options);
196
+ }
197
+ }
198
+
199
+ const self = this;
200
+ function eventFunction(event) {
201
+ const target = event.currentTarget;
202
+ if (!target) return;
203
+
204
+ const elData = self.elements.get(target);
205
+ if (!elData || !elData[prefix]) return;
206
+
207
+ const prefixData = elData[prefix];
208
+
209
+ // Debounce/Prevent duplicate mouse events if needed
210
+ if (prefixData.prev === event.type && ["mouseover", "mouseout"].includes(event.type)) {
211
+ return;
212
+ }
213
+ prefixData.prev = event.type;
214
+
215
+ // Check for 'actions' blocking
216
+ const actionsAttr = target.getAttribute("actions") || "";
217
+ if (actionsAttr.includes(prefix)) return;
218
+
219
+ // Handle lifecycle controls (using dash)
220
+ const lifecycleAttributes = [
221
+ { attr: "prevent-default", method: "preventDefault" },
222
+ { attr: "stop-propagation", method: "stopPropagation" },
223
+ { attr: "stop-immediate-propagation", method: "stopImmediatePropagation" }
224
+ ];
225
+
226
+ for (const { attr, method } of lifecycleAttributes) {
227
+ const attrValue = target.getAttribute(`${prefix}-${attr}`);
228
+ if (attrValue !== null && attrValue !== "false") {
229
+ event[method]();
230
+ }
231
+ }
232
+
233
+ self.__updateElements(target, prefix, prefixData.events);
234
+ }
235
+ },
236
+
237
+ _initObserver: function (el, prefix) {
238
+ // Updated to dash: prefix-selector, prefix-query, prefix-attribute-filter, prefix-types
239
+ let selector = el.getAttribute(`${prefix}-selector`) || el.getAttribute(`${prefix}-query`) || "";
240
+ let attributeFilter = (el.getAttribute(`${prefix}-attribute-filter`) || "")
241
+ .split(/\s*,\s*/)
242
+ .filter(item => item);
243
+
244
+ let types = (el.getAttribute(`${prefix}-types`) || "")
245
+ .split(/\s*,\s*/)
246
+ .filter(item => item);
247
+
248
+ const config = {
249
+ types: types,
250
+ callback: () => {
251
+ this.__updateElements(el, prefix);
252
+ }
253
+ };
254
+
255
+ if (selector) {
256
+ if (!types.length && !attributeFilter.length) {
257
+ config.types.push("addedNodes");
258
+ }
259
+ config.selector = selector;
260
+ }
261
+
262
+ if (attributeFilter.length) {
263
+ config.types.push("attributes");
264
+ config.attributeFilter = attributeFilter;
265
+ }
266
+
267
+ if (config.types.length) {
268
+ observer.init(config);
269
+ }
270
+ },
271
+
272
+ _initResizeObserver: function (el, prefix) {
273
+ const resizeObserver = new ResizeObserver((entries) => {
274
+ const lastEntry = entries[entries.length - 1];
275
+ const { width } = lastEntry.contentRect;
276
+ // Keeps dash style for this status attribute
277
+ el.setAttribute(`${prefix}-if-value`, width + "px");
278
+ this.__updateElements(el, prefix);
279
+ });
280
+ resizeObserver.observe(el);
281
+ },
282
+
283
+ _initIntersectionObserver: function (el, prefix) {
284
+ // Updated to dash
285
+ const rootSelector = el.getAttribute(`${prefix}-root`);
286
+ const rootMargin = el.getAttribute(`${prefix}-root-margin`) || "0px";
287
+ const thresholdRaw = el.getAttribute(`${prefix}-threshold`);
288
+ const threshold = thresholdRaw ? JSON.parse(thresholdRaw) : [0];
289
+ const trigger = el.getAttribute(`${prefix}-trigger`) || "both";
290
+
291
+ const root = rootSelector ? document.querySelector(rootSelector) : null;
292
+ const validThreshold = Array.isArray(threshold)
293
+ ? threshold.filter(v => v >= 0 && v <= 1)
294
+ : [0];
295
+
296
+ const once = el.getAttribute(`${prefix}-once`);
297
+ const isOnce = once !== null && once !== "false";
298
+
299
+ const intersectionObserver = new IntersectionObserver(
300
+ (entries) => {
301
+ entries.forEach((entry) => {
302
+ const isVisible = entry.isIntersecting;
303
+
304
+ if (
305
+ (trigger === "visible" && isVisible) ||
306
+ (trigger === "not-visible" && !isVisible) ||
307
+ trigger === "both"
308
+ ) {
309
+ el.setAttribute(`${prefix}-is-visible`, isVisible ? "true" : "false");
310
+ this.__updateElements(el, prefix);
311
+
312
+ if (isOnce) {
313
+ intersectionObserver.unobserve(entry.target);
314
+ }
315
+ }
316
+ });
317
+ },
318
+ { root, rootMargin, threshold: validThreshold }
319
+ );
320
+
321
+ intersectionObserver.observe(el);
322
+ },
323
+
324
+ __updateElements: async function (rootElement, prefix, events, params) {
325
+ if (!rootElement.isConnected) return;
326
+
327
+ // 1. Determine targets based on prefix-group
328
+ let sourceElements = [rootElement];
329
+ const sourceGroup = rootElement.getAttribute(`${prefix}-group`);
330
+
331
+ if (sourceGroup) {
332
+ sourceElements = document.querySelectorAll(`[${prefix}-group="${sourceGroup}"]`);
333
+ }
334
+
335
+ // 2. Iterate through sourceElements
336
+ for (const sourceElement of sourceElements) {
337
+
338
+ // --- Global Options (Dash) ---
339
+
340
+ // onEvent
341
+ const onEvent = sourceElement.getAttribute(`${prefix}-on`);
342
+ if (onEvent) {
343
+ await new Promise((resolve) => {
344
+ const handleEvent = () => {
345
+ document.removeEventListener(onEvent, handleEvent);
346
+ resolve();
347
+ };
348
+ document.addEventListener(onEvent, handleEvent);
349
+ });
350
+ }
351
+
352
+ // setInterval
353
+ const intervalAttr = sourceElement.getAttribute(`${prefix}-set-interval`);
354
+ if (intervalAttr !== null) {
355
+ const intervalMs = parseInt(intervalAttr, 10) || 0;
356
+
357
+ if (!sourceElement.eventsInterval) {
358
+ sourceElement.eventsInterval = {};
359
+ }
360
+
361
+ const currentInterval = sourceElement.eventsInterval[prefix];
362
+
363
+ if (intervalMs > 0) {
364
+ // Start or update interval
365
+ if (!currentInterval || currentInterval.delay !== intervalMs) {
366
+ if (currentInterval) clearInterval(currentInterval.id);
367
+
368
+ sourceElement.eventsInterval[prefix] = {
369
+ delay: intervalMs,
370
+ id: setInterval(() => {
371
+ if (!sourceElement.isConnected) {
372
+ const entry = sourceElement.eventsInterval?.[prefix];
373
+ if (entry) {
374
+ clearInterval(entry.id);
375
+ delete sourceElement.eventsInterval[prefix];
376
+ }
377
+ return;
265
378
  }
266
- }
267
- });
268
- },
269
- { root, rootMargin, threshold: validThreshold }
270
- );
271
-
272
- intersectionObserver.observe(el);
273
- }
274
-
275
- for (let i = 0; i < events.length; i++) {
276
- // Skip specific lifecycle events
277
- if (
278
- events[i] === "onload" ||
279
- events[i] === "observer" ||
280
- events[i] === "intersection" ||
281
- events[i] === "resize"
282
- ) {
283
- continue;
284
- }
285
-
286
- // Remove any existing event listener
287
- el.removeEventListener(events[i], eventFunction);
288
-
289
- // Initialize an empty options object
290
- const options = {};
291
-
292
- // Attributes to check dynamically
293
- const attributes = ["bubbles", "cancelable", "composed"];
294
-
295
- for (let j = 0; j < attributes.length; j++) {
296
- const attrValue = el.getAttribute(
297
- `${events[i]}-${attributes[j]}`
298
- );
299
- if (attrValue !== null) {
300
- options[attributes[j]] = attrValue !== "false";
301
- }
302
- }
303
-
304
- // Add the new event listener with the dynamically constructed options
305
- el.addEventListener(events[i], eventFunction, options);
306
- }
307
-
308
- function eventFunction(event) {
309
- // TODO: apply debounce.
310
- // let debounce;
311
- // clearTimeout(debounce);
312
- // debounce = setTimeout(function() {
313
- const target = event.currentTarget;
314
- if (target) {
315
- let prefixes = self.elements.get(target);
316
- if (
317
- prefixes[prefix].prev === event.type &&
318
- ["mouseover", "mouseout"].includes(event.type)
319
- )
320
- return;
321
- else prefixes[prefix].prev = event.type;
322
-
323
- let attribute = target.getAttribute("actions") || "";
324
- if (attribute.includes(prefix)) return;
325
- // if (target.closest(`[actions*="${prefix}"]`))
326
- // return;
327
-
328
- // Define the lifecycle attributes (matching JavaScript event methods)
329
- const lifecycleAttributes = [
330
- "preventDefault",
331
- "stopPropagation",
332
- "stopImmediatePropagation"
333
- ];
334
-
335
- // Handle lifecycle attributes dynamically
336
- for (let i = 0; i < lifecycleAttributes.length; i++) {
337
- const attr = `${prefix}-${lifecycleAttributes[i]}`; // Add prefix dynamically
338
- const attrValue = target.getAttribute(attr); // Directly get the attribute value
339
-
340
- // If attribute exists and is not explicitly "false", apply the corresponding event method
341
- if (attrValue !== null && attrValue !== "false") {
342
- event[lifecycleAttributes[i]]();
343
- }
344
- }
345
-
346
- self.__updateElements(
347
- target,
348
- prefix,
349
- null,
350
- prefixes[prefix].events
351
- );
352
- }
353
- }
354
- }
355
- },
356
-
357
- __updateElements: async function (el, prefix, target, events, params) {
358
- if (!el.isConnected) return;
359
-
360
- let elements = [el];
361
- let targetGroup = el.getAttribute(`${prefix}-group`);
362
- if (targetGroup) {
363
- elements = document.querySelectorAll(
364
- `[${prefix}-group="${targetGroup}"]`
365
- );
366
- }
367
-
368
- for (let element of elements) {
369
- // const requestAnimationFrameAttr = element.getAttribute(
370
- // `${prefix}-requestAnimationFrame`
371
- // );
372
-
373
- // if (
374
- // requestAnimationFrameAttr !== null &&
375
- // requestAnimationFrameAttr !== "false"
376
- // ) {
377
- // // Double requestAnimationFrame
378
- // await new Promise((resolve) => {
379
- // requestAnimationFrame(() => {
380
- // requestAnimationFrame(() => {
381
- // resolve();
382
- // });
383
- // });
384
- // });
385
- // }
386
-
387
- let interval = element.getAttribute(`${prefix}-setInterval`);
388
- if (interval !== null) {
389
- let intervalMs = parseInt(interval, 10) || 0;
390
- if (!element.eventsInterval) {
391
- element.eventsInterval = {};
392
- }
393
- let intervalEntry = element.eventsInterval[prefix];
394
- if (intervalMs > 0) {
395
- if (!intervalEntry || intervalEntry.delay !== intervalMs) {
396
- if (intervalEntry) {
397
- clearInterval(intervalEntry.id);
398
- }
399
- element.eventsInterval[prefix] = {
400
- delay: intervalMs,
401
- id: setInterval(() => {
402
- if (!element.isConnected) {
403
- const entry =
404
- element.eventsInterval &&
405
- element.eventsInterval[prefix];
406
- if (entry) {
407
- clearInterval(entry.id);
408
- delete element.eventsInterval[prefix];
409
- }
410
- return;
411
- }
412
- self.__updateElements(
413
- element,
414
- prefix,
415
- target,
416
- events,
417
- params
418
- );
419
- }, intervalMs)
420
- };
421
- }
422
- } else if (intervalEntry) {
423
- clearInterval(intervalEntry.id);
424
- delete element.eventsInterval[prefix];
425
- }
426
- }
427
-
428
- let delay = element.getAttribute(`${prefix}-setTimeout`);
429
- if (delay) {
430
- delay = parseInt(delay, 10) || 0;
431
- if (delay > 0) {
432
- await new Promise((resolve) => setTimeout(resolve, delay));
433
- }
434
- }
435
-
436
- if (!element.isConnected) return;
437
-
438
- let once = element.getAttribute(`${prefix}-once`);
439
- if (once || once === "") {
440
- if (!element.eventsOnce) {
441
- element.eventsOnce = [prefix];
442
- } else if (element.eventsOnce.includes(prefix)) {
443
- continue;
444
- } else {
445
- element.eventsOnce.push(prefix);
446
- }
447
- }
448
-
449
- // TODO: support empty value when prefix-attribute defined, add and remove the attribute
450
- let targetAttribute = element.getAttribute(`${prefix}-attribute`);
451
- let targetPosition = element.getAttribute(
452
- `${prefix}-insert-adjacent`
453
- );
454
-
455
- let onEvent = element.getAttribute(`${prefix}-on`);
456
- if (onEvent) {
457
- await new Promise((resolve, reject) => {
458
- const handleEvent = () => {
459
- document.removeEventListener(onEvent, handleEvent);
460
- resolve();
461
- };
462
- document.addEventListener(onEvent, handleEvent);
463
- });
464
- }
465
-
466
- let values;
467
- if (prefix === "localstorage") {
468
- let key = element.getAttribute("localstorage-get");
469
- if (key) {
470
- values = localStorage.getItem(key);
471
- } else if ((key = element.getAttribute("localstorage-set"))) {
472
- values = await element.getValue();
473
- if (values) {
474
- localStorage.setItem(key, values);
475
- }
476
- }
477
-
478
- if (!key) return;
479
- } else {
480
- values = element.getAttribute(`${prefix}-value`);
481
-
482
- if (values === null) {
483
- let valueElements = queryElements({
484
- element,
485
- prefix: `${prefix}-value`
486
- });
487
- if (valueElements.length) {
488
- let elementValues = [];
489
- for (let i = 0; i < valueElements.length; i++) {
490
- elementValues.push(valueElements[i].getValue());
491
- }
492
- if (elementValues.length) {
493
- values = elementValues;
494
- }
495
- }
496
- }
497
-
498
- if (values === null) {
499
- values = element.getAttribute(`${prefix}-if-value`);
500
- }
501
- if (values === null) {
502
- values = element.getAttribute(prefix);
503
- }
504
- }
505
-
506
- if (values || values === "") {
507
- if (typeof values === "string") {
508
- values = values.split(",");
509
- } else if (!Array.isArray(values)) {
510
- values = [values];
511
- }
512
- } else {
513
- values = await element.getValue();
514
- if (!Array.isArray(values)) {
515
- values = [values];
516
- }
517
- }
518
-
519
- if ((targetAttribute && !values) || values.length === 0) {
520
- return;
521
- }
522
-
523
- let ifCondition = element.getAttribute(`${prefix}-if`);
524
- let elseIfCondition = element.getAttribute(`${prefix}-else-if`);
525
- let elseCondition = element.getAttribute(`${prefix}-else`);
526
-
527
- let ifValue = element.getAttribute(`${prefix}-if-value`);
528
- if (ifCondition) {
529
- if (!ifValue && ifValue !== "")
530
- ifValue = (await element.getValue()) || values;
531
- //values // await element.getValue()
532
- else if (ifValue || ifValue === "") {
533
- ifValue = [ifValue];
534
- } else {
535
- ifValue = values;
536
- }
537
-
538
- if (!Array.isArray(ifValue)) {
539
- ifValue = [ifValue];
540
- }
541
- }
542
-
543
- if (ifCondition) {
544
- // If the 'if' condition is not met, stop processing this element.
545
- if (!evaluateCondition(ifCondition, ifValue)) {
546
- return;
547
- }
548
- } else if (elseCondition) {
549
- // If there's no 'if' condition but there is an 'else' condition,
550
- // stop processing if the 'else' condition is met.
551
- // This creates an "execute unless" behavior for the 'else' attribute.
552
- if (evaluateCondition(elseIfCondition, ifValue)) {
553
- return;
554
- }
555
- }
556
-
557
- let targetText = element.getAttribute(`${prefix}-text`);
558
- let targetHtml = element.getAttribute(`${prefix}-html`);
559
- let targetKey = element.getAttribute(`${prefix}-key`);
560
-
561
- if (prefix === "selected") {
562
- if (el !== element) {
563
- element.removeAttribute("selected");
564
- } else {
565
- element.setAttribute("selected", "");
566
- }
567
- }
568
-
569
- let deactivate = false;
570
- if (el !== element) {
571
- deactivate = true;
572
- }
573
- if (events && events.includes("click")) {
574
- // TODO: handling of attributes that represent states aria-expanded, aria-selected etc.
575
- }
576
-
577
- // values = values.map(x => x.trim());
578
- values = values.map((x) => {
579
- if (typeof x === "string") x = x.trim(); // Update x with the trimmed value
580
- let prop = element.getAttribute(`${prefix}-property`);
581
- if (prop) {
582
- x = `${prop}:${x}`; // Update x with prop if it exists
583
- }
584
- return x; // Return the updated x
585
- });
586
- let targetElements;
587
- if (element.hasAttribute(`${prefix}-query`)) {
588
- targetElements = queryElements({
589
- element,
590
- prefix,
591
- selector: params
592
- });
593
- } else {
594
- targetElements = [element];
595
- }
596
-
597
- let action =
598
- element.getAttribute(`${prefix}-action`) ||
599
- element.getAttribute(`${prefix}-method`);
600
- for (let i = 0; i < targetElements.length; i++) {
601
- if (action) {
602
- if (targetElements[i][action]) {
603
- targetElements[i][action]();
604
- }
605
- } else if (
606
- !targetAttribute &&
607
- targetAttribute !== "" &&
608
- !targetPosition &&
609
- ["click", "focus", "blur"].includes(prefix)
610
- ) {
611
- // TODO: click causes an infinite loop if targetElement[i] is a child of element do to event propactaion
612
- if (
613
- (targetElements[i] === element &&
614
- element.hasAttribute(prefix)) ||
615
- (params &&
616
- targetElements[i] !== element &&
617
- !element.contains(targetElements[i]))
618
- )
619
- targetElements[i][prefix]();
620
- } else {
621
- this.setValue(
622
- prefix,
623
- targetElements[i],
624
- targetAttribute,
625
- values,
626
- targetKey,
627
- deactivate,
628
- events,
629
- targetPosition
630
- );
631
- }
632
- }
633
- }
634
-
635
- el.dispatchEvent(
636
- new CustomEvent(`${prefix}End`, {
637
- detail: {}
638
- })
639
- );
640
- },
641
-
642
- setValue: async function (
643
- prefix,
644
- element,
645
- attrName,
646
- values,
647
- key,
648
- deactivate,
649
- events,
650
- targetPosition
651
- ) {
652
- if (events && events.includes("mouseout")) {
653
- if (element.matches(":hover")) {
654
- element.addEventListener(
655
- "mouseout",
656
- () => {
657
- this.setValue(
658
- prefix,
659
- element,
660
- attrName,
661
- values,
662
- key,
663
- deactivate,
664
- events,
665
- targetPosition
666
- );
667
- },
668
- { once: true }
669
- );
670
- return;
671
- }
672
- }
673
-
674
- let attrValues, oldValue;
675
- if (key) {
676
- key = `{{${key}}}`;
677
- // let value = values[0]
678
- replaceKey(prefix, element, key, values);
679
- } else {
680
- // TODO: if html in iframe
681
- let domTextEditor;
682
- if (element.parentElement) {
683
- domTextEditor =
684
- element.parentElement.closest("[contenteditable]");
685
- }
686
-
687
- if (attrName === "style") {
688
- if (element.getAttribute(attrName)) {
689
- attrValues = element
690
- .getAttribute(attrName)
691
- .split(";")
692
- .map((x) => x.trim());
693
- attrValues = attrValues.map(function (item, index) {
694
- return item.replace(" ", "");
695
- });
696
- oldValue =
697
- values.filter((x) => attrValues.includes(x))[0] || "";
698
- }
699
- let newValue = this.__getNextValue(values, oldValue);
700
-
701
- if (oldValue) {
702
- let [property, value] = oldValue.split(":");
703
- element.style[property] = "";
704
- }
705
-
706
- if (newValue != "") {
707
- newValue = newValue.split(";").map((x) => x.trim());
708
- for (let style of newValue) {
709
- let [property, value] = style.split(":");
710
- element.style[property] = value;
711
- }
712
- }
713
- } else {
714
- if (attrName === "value") {
715
- oldValue = await element.getValue();
716
- } else if (
717
- attrName === "text" ||
718
- attrName === "html" ||
719
- element.getAttribute(attrName)
720
- ) {
721
- attrValues = element
722
- .getAttribute(attrName)
723
- .split(" ")
724
- .map((x) => x.trim());
725
- oldValue =
726
- values.filter((x) => attrValues.includes(x))[0] || "";
727
- }
728
-
729
- let newValue = this.__getNextValue(values, oldValue);
730
- if (deactivate) newValue = "";
731
- if (attrName === "class") {
732
- if (oldValue) {
733
- oldValue.split(" ").forEach((className) => {
734
- element.classList.remove(className);
735
- });
736
- if (values.length === 1) {
737
- return;
738
- }
739
- }
740
-
741
- if (newValue) {
742
- newValue.split(" ").forEach((className) => {
743
- element.classList.add(className);
744
- });
745
- }
746
- } else if (attrName === "value") {
747
- element.setValue(newValue, false);
748
- } else if (
749
- ["$click", "$focus", "$blur", "$save", "$read"].includes(
750
- attrName
751
- )
752
- ) {
753
- element[attrName.substring(1)]();
754
- } else if (attrName) {
755
- // TODO: removeAttribute vs setting empty value, how best to define. operator $remove ???
756
- if (newValue === "" && element.hasAttribute(attrName)) {
757
- element.removeAttribute(attrName);
758
- } else {
759
- element.setAttribute(attrName, newValue);
760
- }
761
- } else if (targetPosition) {
762
- if (domTextEditor) {
763
- CoCreate.text.insertAdjacentElement({
764
- domTextEditor,
765
- position: targetPosition,
766
- target: element,
767
- elementValue: newValue
768
- });
769
- } else if (targetPosition === "replace") {
770
- element.insertAdjacentHTML("beforebegin", newValue); // Insert before oldElement
771
- element.remove(); // Remove oldElement
772
- } else {
773
- element.insertAdjacentHTML(targetPosition, newValue);
774
- }
775
- } else {
776
- if (
777
- !attrName &&
778
- ["click", "focus", "blur"].includes(newValue)
779
- ) {
780
- element[newValue]();
781
- } else if (["click", "focus", "blur"].includes(attrName)) {
782
- element[attrName]();
783
- }
784
- }
785
- }
786
- }
787
- },
788
-
789
- __getNextValue: function (values, val) {
790
- let size = values.length;
791
- let nn = values.indexOf(val);
792
- if (nn == -1) {
793
- return values[0];
794
- } else {
795
- return values[(nn + 1) % size];
796
- }
797
- }
379
+ // Recursive call
380
+ this.__updateElements(rootElement, prefix, events, params);
381
+ }, intervalMs)
382
+ };
383
+ }
384
+ } else if (currentInterval) {
385
+ // Clear interval if set to 0
386
+ clearInterval(currentInterval.id);
387
+ delete sourceElement.eventsInterval[prefix];
388
+ }
389
+ }
390
+
391
+ // setTimeout
392
+ const delayAttr = sourceElement.getAttribute(`${prefix}-set-timeout`);
393
+ if (delayAttr) {
394
+ const delayTime = parseInt(delayAttr, 10) || 0;
395
+ if (delayTime > 0) {
396
+ await new Promise(resolve => setTimeout(resolve, delayTime));
397
+ }
398
+ }
399
+
400
+ if (!sourceElement.isConnected) return;
401
+
402
+ // once
403
+ const onceAttr = sourceElement.getAttribute(`${prefix}-once`);
404
+ if (onceAttr !== null) {
405
+ if (!sourceElement.eventsOnce) {
406
+ sourceElement.eventsOnce = [prefix];
407
+ } else if (sourceElement.eventsOnce.includes(prefix)) {
408
+ continue; // Skip if already ran
409
+ } else {
410
+ sourceElement.eventsOnce.push(prefix);
411
+ }
412
+ }
413
+
414
+ // --- Resolve Targets ---
415
+ const querySelector = sourceElement.getAttribute(`${prefix}-query`);
416
+ let targetElements = [sourceElement];
417
+
418
+ if (querySelector || params) {
419
+ targetElements = queryElements({
420
+ element: sourceElement,
421
+ prefix: prefix,
422
+ selector: querySelector || params
423
+ });
424
+ }
425
+
426
+ // --- Execute Action on Targets ---
427
+ for (const targetElement of targetElements) {
428
+ await this.setValue(
429
+ rootElement,
430
+ sourceElement,
431
+ targetElement,
432
+ prefix,
433
+ events
434
+ );
435
+ }
436
+ }
437
+
438
+ rootElement.dispatchEvent(
439
+ new CustomEvent(`${prefix}End`, { detail: {} })
440
+ );
441
+ },
442
+
443
+ setValue: async function (rootElement, sourceElement, targetElement, prefix, events) {
444
+ // Handle Mouseout toggle logic
445
+ if (events && events.includes("mouseout")) {
446
+ if (targetElement.matches(":hover")) {
447
+ targetElement.addEventListener(
448
+ "mouseout",
449
+ () => {
450
+ this.setValue(rootElement, sourceElement, targetElement, prefix, events);
451
+ },
452
+ { once: true }
453
+ );
454
+ return;
455
+ }
456
+ }
457
+
458
+ const deactivate = (rootElement !== sourceElement);
459
+
460
+ // Iterate Attributes
461
+ for (const attribute of sourceElement.attributes) {
462
+ const name = attribute.name;
463
+
464
+ // Optimization: Skip if shorter than prefix + dot + min char
465
+ if (name.length <= prefix.length + 1) continue;
466
+
467
+ // STRICT RULE: Only process attributes with DOT separator.
468
+ // All options use Dash, so they will be ignored here.
469
+ if (!name.startsWith(prefix) || name[prefix.length] !== '.') continue;
470
+
471
+ // Extract target name (e.g., 'value', 'style', 'src', 'data-something')
472
+ const targetName = name.substring(prefix.length + 1);
473
+
474
+ // Get Value
475
+ let value = sourceElement.getAttribute(name);
476
+ if (!value && sourceElement.getValue) {
477
+ value = await sourceElement.getValue();
478
+ }
479
+
480
+ // Parse CSV for rotation
481
+ let values = [];
482
+ if (typeof value === 'string') {
483
+ values = value.split(',').map(v => v.trim());
484
+ } else if (Array.isArray(value)) {
485
+ values = value;
486
+ } else {
487
+ values = [value];
488
+ }
489
+
490
+ let oldValue = "";
491
+ let newValue = "";
492
+
493
+ switch (targetName) {
494
+ case 'style':
495
+ if (targetElement.getAttribute("style")) {
496
+ // Normalize style string for comparison
497
+ const styles = targetElement.getAttribute("style")
498
+ .split(";")
499
+ .map(x => x.trim().replace(/\s/g, ""));
500
+
501
+ oldValue = values.find(x => styles.includes(x.replace(/\s/g, ""))) || "";
502
+ }
503
+ newValue = this.__getNextValue(values, oldValue, deactivate);
504
+
505
+ if (oldValue) {
506
+ const [prop] = oldValue.split(":");
507
+ if (prop) targetElement.style[prop.trim()] = "";
508
+ }
509
+
510
+ if (newValue) {
511
+ const styles = newValue.split(";");
512
+ for (const s of styles) {
513
+ const [prop, val] = s.split(":");
514
+ if (prop && val) targetElement.style[prop.trim()] = val.trim();
515
+ }
516
+ }
517
+ break;
518
+
519
+ case 'class':
520
+ const currentClasses = Array.from(targetElement.classList);
521
+ oldValue = values.find(val => {
522
+ const requiredClasses = val.split(" ");
523
+ return requiredClasses.every(c => currentClasses.includes(c));
524
+ }) || "";
525
+
526
+ newValue = this.__getNextValue(values, oldValue, deactivate);
527
+
528
+ if (oldValue) {
529
+ oldValue.split(" ").forEach(c => targetElement.classList.remove(c));
530
+ if (values.length === 1 && !newValue) return;
531
+ }
532
+
533
+ if (newValue) {
534
+ newValue.split(" ").forEach(c => targetElement.classList.add(c));
535
+ }
536
+ break;
537
+
538
+ case 'insertadjacenthtml':
539
+ case 'insertadjacenttext':
540
+ case 'insertAdjacentHTML':
541
+ case 'insertAdjacentText':
542
+ const position = sourceElement.getAttribute(name);;
543
+ if (!position) continue;
544
+
545
+ let content = "";
546
+ if (sourceElement.getValue) {
547
+ content = await sourceElement.getValue();
548
+ }
549
+
550
+ // Check for CoCreate Rich Text integration
551
+ let domTextEditor = targetElement.parentElement?.closest("[contenteditable]");
552
+
553
+ if (domTextEditor && typeof CoCreate !== 'undefined' && CoCreate.text) {
554
+ CoCreate.text.insertAdjacentElement({
555
+ domTextEditor,
556
+ position,
557
+ target: targetElement,
558
+ elementValue: content
559
+ });
560
+ } else {
561
+ const isHtml = targetName.toLowerCase().includes('html');
562
+ if (isHtml) {
563
+ targetElement.insertAdjacentHTML(position, content);
564
+ } else {
565
+ targetElement.insertAdjacentText(position, content);
566
+ }
567
+ }
568
+ break;
569
+
570
+ case 'value':
571
+ if (targetElement.getValue) {
572
+ oldValue = await targetElement.getValue();
573
+ } else {
574
+ oldValue = targetElement.value || "";
575
+ }
576
+
577
+ if (!values.includes(oldValue)) oldValue = "";
578
+ newValue = this.__getNextValue(values, oldValue, deactivate);
579
+
580
+ if (targetElement.setValue) {
581
+ targetElement.setValue(newValue);
582
+ } else {
583
+ targetElement.value = newValue;
584
+ }
585
+ break;
586
+
587
+ default:
588
+ // Generic handling: Try Property first, then Attribute
589
+
590
+ // 1. Try Property
591
+ if (targetElement[targetName] !== undefined) {
592
+ oldValue = targetElement[targetName];
593
+ if (!values.includes(oldValue) && typeof oldValue !== 'function') oldValue = "";
594
+
595
+ newValue = this.__getNextValue(values, oldValue, deactivate);
596
+
597
+ if (typeof targetElement[targetName] === 'function') {
598
+ targetElement[targetName](newValue);
599
+ } else {
600
+ targetElement[targetName] = newValue;
601
+ }
602
+ }
603
+ // 2. Fallback to Attribute
604
+ else {
605
+ oldValue = targetElement.getAttribute(targetName);
606
+ if (!values.includes(oldValue)) oldValue = "";
607
+
608
+ newValue = this.__getNextValue(values, oldValue, deactivate);
609
+
610
+ // Handle Namespaced Attributes (e.g. svg:href) if encoded in name
611
+ if (targetName.includes('.')) {
612
+ const realName = targetName.replace('.', ':');
613
+ targetElement.setAttribute(realName, newValue);
614
+ } else {
615
+ targetElement.setAttribute(targetName, newValue);
616
+ }
617
+ }
618
+ break;
619
+ }
620
+ }
621
+ },
622
+
623
+ __getNextValue: function (values, val, deactivate) {
624
+ if (deactivate) return "";
625
+ const size = values.length;
626
+ const nn = values.indexOf(val);
627
+ if (nn === -1) {
628
+ return values[0];
629
+ } else {
630
+ return values[(nn + 1) % size];
631
+ }
632
+ }
798
633
  };
799
634
 
800
- const eventElements = new Map();
801
- const eventKeys = new Map();
802
- const elementAttributes = new Map();
803
-
804
- function replaceKey(prefix, element, key, values) {
805
- let currentValue = key;
806
-
807
- if (eventElements.has(element)) {
808
- currentValue = eventElements.get(element).get(key);
809
- }
810
- let value = CoCreateEvents.__getNextValue(values, currentValue);
811
- eventKeys.set(key, value);
812
- eventElements.set(element, eventKeys);
813
-
814
- let attributes = elementAttributes.get(element);
815
- if (!attributes) {
816
- attributes = {};
817
- elementAttributes.set(element, attributes);
818
- }
819
-
820
- for (let attribute of element.attributes) {
821
- let newName = attribute.name;
822
- let newValue = attribute.value;
823
- let attrName = attribute.name;
824
- let attrValue = attribute.value;
825
- let setAttr = false;
826
-
827
- if (attrName === "clone-data") console.log("clone-data");
828
- if (
829
- attrValue.includes(currentValue) ||
830
- (attributes[attrName] &&
831
- attributes[attrName].valueKeys &&
832
- attributes[attrName].valueKeys[key])
833
- ) {
834
- if (!attributes[attrName])
835
- attributes[attrName] = { originalValue: attrValue };
836
- else if (!attributes[attrName].originalValue)
837
- attributes[attrName].originalValue = attrValue;
838
- if (!attributes[attrName].valueKeys)
839
- attributes[attrName].valueKeys = { [key]: value };
840
- else attributes[attrName].valueKeys[key] = value;
841
-
842
- newValue = attributes[attrName].originalValue;
843
- for (let valueKey of Object.keys(attributes[attrName].valueKeys)) {
844
- const valueKeyRegex = new RegExp(valueKey, "g");
845
- newValue = newValue.replace(valueKeyRegex, value);
846
- }
847
- setAttr = true;
848
- }
849
- // if (attrName.includes(currentValue) || attributes[attrName] && attributes[attrName].nameKeys && attributes[attrName].nameKeys[key]){
850
- // if (!attributes[attrName])
851
- // attributes[attrName] = { originalName: attrName }
852
- // else if (!attributes[attrName].originalName)
853
- // attributes[attrName].originalName = attrName
854
-
855
- // if (!attributes[attrName].nameKeys)
856
- // attributes[attrName].nameKeys = {[key]: value}
857
-
858
- // newName = attributes[attrName].originalName
859
- // for (let nameKey of Object.keys(attributes[attrName].nameKeys)) {
860
- // let r
861
- // if (attrName === nameKey)
862
- // r = nameKey
863
- // else
864
- // r = attributes[attrName].nameKeys[nameKey]
865
-
866
- // const nameKeyRegex = new RegExp(r, "g");
867
- // newName = newName.replace(nameKeyRegex, value);
868
- // }
869
-
870
- // attributes[attrName].nameKeys[key] = value;
871
-
872
- // element.removeAttribute(attrName);
873
- // setAttr = true;
874
-
875
- // }
876
- if (setAttr) element.setAttribute(newName, newValue);
877
- }
878
- let html = element.innerHTML;
879
- if (
880
- html.indexOf(currentValue) !== -1 ||
881
- (element.htmlKeys && element.htmlKeys[key])
882
- ) {
883
- if (!element.originalHtml) element.originalHtml = html;
884
- if (!element.htmlKeys) element.htmlKeys = { [key]: value };
885
- else element.htmlKeys[key] = value;
886
-
887
- let newHTML = element.originalHtml;
888
- if (newHTML) {
889
- for (let elementKey of Object.keys(element.htmlKeys)) {
890
- const htmlKeyRegex = new RegExp(elementKey, "g");
891
- newHTML = newHTML.replace(htmlKeyRegex, value);
892
- }
893
-
894
- element.innerHTML = newHTML;
895
- }
896
- }
897
- }
898
-
899
- function evaluateCondition(condition, values) {
900
- let orConditions = condition.split(/\s*\|\|\s*/);
901
-
902
- if (orConditions.length > 1) {
903
- return orConditions.some((cond) => checkCondition(cond, values));
904
- } else {
905
- let andConditions = condition.split(/\s*&&\s*/);
906
- return andConditions.every((cond) => checkCondition(cond, values));
907
- }
908
- }
909
-
910
- function checkCondition(condition, value) {
911
- let isNegated = condition.startsWith("!");
912
- if (isNegated) {
913
- condition = condition.substring(1);
914
- }
915
- const operatorMatch = condition.match(/(<=|>=|<|>)(.+)/);
916
- if (operatorMatch) condition = operatorMatch[2].trim();
917
-
918
- let parse = true;
919
- if (condition === "true") {
920
- return !!value[0];
921
- } else if (condition === "false") {
922
- return !value[0];
923
- }
924
- // else if (condition === "[]" && typeof value[0] === "string") {
925
- // parse = false;
926
- // }
927
-
928
- // TODO: why parse updated conditin to boolean false
929
- // if (parse && condition !== "false") condition = parseCondition(condition);
930
-
931
- if (
932
- typeof condition === "number" ||
933
- (typeof condition === "string" && /\d/.test(condition))
934
- ) {
935
- condition = parseNumberCondition(condition);
936
- } else if (typeof value[0] === "object" && typeof condition === "string") {
937
- condition = parseJsonCondition(condition);
938
- }
939
-
940
- let result;
941
-
942
- if (condition === "$template") {
943
- result = /{{\s*([\w\W]+)\s*}}/g.test(value[0]);
944
- } else if (Array.isArray(value) && !(typeof condition === "object")) {
945
- if (operatorMatch) {
946
- condition = parseFloat(condition);
947
- result = value.some((v) => {
948
- v = parseFloat(v);
949
- switch (operatorMatch[1]) {
950
- case "<":
951
- return v < condition;
952
- case ">":
953
- return v > condition;
954
- case "<=":
955
- return v <= condition;
956
- case ">=":
957
- return v >= condition;
958
- default:
959
- return false;
960
- }
961
- });
962
- } else result = value.includes(condition);
963
- } else if (Array.isArray(value)) {
964
- // TODO: handle comparing array to array, vs querying the array items for a match
965
- result = queryData(value, condition);
966
- } else if (
967
- typeof value === "object" &&
968
- value !== null &&
969
- typeof parsedCond === "object"
970
- ) {
971
- result = queryData(value, condition);
972
- } else {
973
- result = value === condition;
974
- }
975
-
976
- result = isNegated ? !result : result;
977
- return result;
978
- }
979
-
980
- function parseJsonCondition(condition) {
981
- try {
982
- // Attempt to parse the condition as JSON
983
- let parsedJson = JSON.parse(condition);
984
- return parsedJson;
985
- } catch (e) {
986
- return condition;
987
- }
988
- }
989
- function parseNumberCondition(condition) {
990
- if (typeof condition === "string") {
991
- const num = parseFloat(condition);
992
- return !isNaN(num) ? num : condition;
993
- }
994
- return condition;
995
- }
996
-
997
- // Helper function for delay using Promises
998
- const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
999
-
1000
- /**
1001
- * Waits for a specific element property to meet a readiness condition,
1002
- * based on retry attributes defined on the element.
1003
- * Incorporates optional exponential backoff for retry delays.
1004
- * Returns a Promise that resolves to true if the condition is met within
1005
- * the allowed attempts, and false otherwise.
1006
- *
1007
- * @param {HTMLElement} element The element with the onload-retry-* attributes.
1008
- * @returns {Promise<boolean>} A Promise resolving to true if ready, false if timed out.
1009
- */
1010
- async function checkElementReadinessWithBackoff(element) {
1011
- const propertyName = element.getAttribute("onload-retry-property");
1012
-
1013
- if (!propertyName) {
1014
- return true; // No retry needed
1015
- }
1016
-
1017
- // --- Retry Parameters ---
1018
- const maxAttempts =
1019
- parseInt(element.getAttribute("onload-retry-attempts") || "3", 10) || 3;
1020
- // Initial delay (used for the first wait and as the base for backoff)
1021
- const initialDelayMs =
1022
- parseInt(element.getAttribute("onload-retry-delay") || "100", 10) ||
1023
- 100;
1024
- // Exponential backoff factor (e.g., 2 for doubling). <= 1 means fixed delay.
1025
- // Default to 1 (no backoff) if missing or invalid.
1026
- const backoffFactor =
1027
- parseFloat(
1028
- element.getAttribute("onload-retry-backoff-factor") || "1"
1029
- ) || 1;
1030
- // Optional maximum delay cap. Use Infinity if missing, invalid, or <= 0.
1031
- const maxDelayMs = parseInt(
1032
- element.getAttribute("onload-retry-max-delay") || "0",
1033
- 10
1034
- );
1035
- const effectiveMaxDelay =
1036
- maxDelayMs && maxDelayMs > 0 ? maxDelayMs : Infinity;
1037
-
1038
- console.log(
1039
- `Checking readiness for property "${propertyName}" on element ${
1040
- element.id || ""
1041
- }. Max attempts: ${maxAttempts}, Initial Delay: ${initialDelayMs}ms, Backoff Factor: ${backoffFactor}, Max Delay: ${
1042
- effectiveMaxDelay === Infinity ? "None" : effectiveMaxDelay + "ms"
1043
- }.`
1044
- );
1045
-
1046
- // Initialize the delay to be used for the *first* wait.
1047
- let currentDelay = initialDelayMs;
1048
-
1049
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1050
- console.log(
1051
- `Attempt ${attempt}/${maxAttempts}: Checking "${propertyName}"...`
1052
- );
1053
- try {
1054
- const propertyValue = element[propertyName];
1055
- // Example check: Adjust condition as needed
1056
- if (propertyValue !== undefined && propertyValue > 0) {
1057
- console.log(
1058
- `Property "${propertyName}" ready on attempt ${attempt}. Value: ${propertyValue}`
1059
- );
1060
- return true; // Condition met
1061
- }
1062
- } catch (error) {
1063
- console.error(
1064
- `Error accessing property "${propertyName}" on attempt ${attempt}:`,
1065
- error
1066
- );
1067
- }
1068
-
1069
- // If condition not met and more attempts remain:
1070
- if (attempt < maxAttempts) {
1071
- // Determine the actual delay for this wait, applying the cap
1072
- const delayForThisWait = Math.min(currentDelay, effectiveMaxDelay);
1073
-
1074
- console.log(
1075
- `Waiting ${Math.round(
1076
- delayForThisWait
1077
- )}ms before next attempt...`
1078
- );
1079
- await delay(delayForThisWait);
1080
-
1081
- // Calculate the delay for the *next* potential wait, only if backoff is active
1082
- if (backoffFactor > 1) {
1083
- currentDelay = currentDelay * backoffFactor;
1084
- // Note: We apply the cap just before the actual delay,
1085
- // but the base 'currentDelay' keeps growing exponentially.
1086
- // Alternatively, you could cap 'currentDelay' itself here:
1087
- // currentDelay = Math.min(currentDelay * backoffFactor, effectiveMaxDelay);
1088
- // Let's stick to capping just before delay() for now.
1089
- }
1090
- // If backoffFactor <= 1, currentDelay remains initialDelayMs (implicitly handled by initialization)
1091
- }
1092
- }
1093
-
1094
- // If loop finishes without condition being met
1095
- console.warn(
1096
- `Readiness check failed for property "${propertyName}" on element ${
1097
- element.id || ""
1098
- } after ${maxAttempts} attempts.`
1099
- );
1100
- return false; // Timed out
1101
- }
1102
-
1103
- // --- Example Usage (calling code doesn't change) ---
1104
-
1105
- async function handleElementProcessingWithBackoff(element) {
1106
- // Call the function that now supports backoff
1107
- const isReady = await checkElementReadinessWithBackoff(element);
1108
-
1109
- if (isReady) {
1110
- console.log(
1111
- `Element ${element.id || ""} is ready. Proceeding with actions.`
1112
- );
1113
- // --- Execute subsequent actions ---
1114
- if (element.hasAttribute("onload-attribute")) {
1115
- /* ... */
1116
- }
1117
- if (element.hasAttribute("onload-query")) {
1118
- /* ... */
1119
- }
1120
- } else {
1121
- console.warn(
1122
- `Skipping actions for element ${
1123
- element.id || ""
1124
- } because readiness check failed.`
1125
- );
1126
- }
1127
- }
1128
-
1129
- // --- Example Elements ---
1130
-
1131
- // With Backoff (Delays: 100, 200, 400, 800 - capped)
1132
- // <span id="elem1" onload-retry-property="scrollWidth"
1133
- // onload-retry-attempts="5" onload-retry-delay="100"
1134
- // onload-retry-backoff-factor="2" onload-retry-max-delay="500">
1135
- // Content 1
1136
- // </span>
1137
- // Waits would be: 100ms, 200ms, 400ms, 500ms (capped)
1138
-
1139
- // No Backoff (factor missing/invalid/<=1)
1140
- // <span id="elem2" onload-retry-property="clientHeight"
1141
- // onload-retry-attempts="4" onload-retry-delay="300">
1142
- // Content 2
1143
- // </span>
1144
- // Waits would be: 300ms, 300ms, 300ms
1145
-
1146
- // const element1 = document.getElementById('elem1');
1147
- // if (element1) handleElementProcessingWithBackoff(element1);
1148
- // const element2 = document.getElementById('elem2');
1149
- // if (element2) handleElementProcessingWithBackoff(element2);
1150
-
1151
635
  CoCreateEvents.init();
1152
636
 
1153
- export default CoCreateEvents;
637
+ export default CoCreateEvents;