@cocreate/events 1.36.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,14 +18,29 @@
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
- import { queryElements } from "@cocreate/utils";
23
+ import { queryElements, resolveProperty, astAsync } from "@cocreate/utils";
24
24
  import action from "@cocreate/actions";
25
25
  import observer from "@cocreate/observer";
26
26
  import "@cocreate/element-prototype";
27
27
  import { getAttribute } from "@cocreate/element-prototype/src/getAttribute.js";
28
28
 
29
+ function resolvePropertyPath(object, path) {
30
+ const segments = path.split(".");
31
+ let target = object;
32
+
33
+ for (let index = 0; index < segments.length; index++) {
34
+ if (target == null) return null;
35
+ const key = resolveProperty(target, segments[index]) || segments[index];
36
+ if (index === segments.length - 1)
37
+ return { target, key, value: target[key] };
38
+ target = target[key];
39
+ }
40
+
41
+ return null;
42
+ }
43
+
29
44
  const CoCreateEvents = {
30
45
  elements: new Map(),
31
46
 
@@ -50,6 +65,11 @@ const CoCreateEvents = {
50
65
  "save"
51
66
  ],
52
67
 
68
+ /**
69
+ *
70
+ * @public
71
+ * @environment client
72
+ */
53
73
  init: function (prefix, events) {
54
74
  if (prefix && events) {
55
75
  this.initPrefix(prefix, events);
@@ -111,6 +131,11 @@ const CoCreateEvents = {
111
131
  });
112
132
  },
113
133
 
134
+ /**
135
+ *
136
+ * @public
137
+ * @environment client
138
+ */
114
139
  initPrefix: function (prefix, events) {
115
140
  action.init({
116
141
  name: prefix,
@@ -129,11 +154,11 @@ const CoCreateEvents = {
129
154
  // Updated selector to use dash for options: prefix-group, prefix-query
130
155
  const knownTargets = this.knownTargets.map(target => `[${prefix}\\.${target}]`);
131
156
  const selector = [
132
- `[${prefix}]`,
133
- `[${prefix}-group]`,
134
- `[${prefix}-query]`,
135
- `[${prefix}-element]`,
136
- ...knownTargets
157
+ `[${prefix}]`,
158
+ `[${prefix}-group]`,
159
+ `[${prefix}-query]`,
160
+ `[${prefix}-element]`,
161
+ ...knownTargets
137
162
  ].join(", ");
138
163
 
139
164
  observer.init({
@@ -151,10 +176,20 @@ const CoCreateEvents = {
151
176
  }
152
177
  });
153
178
 
179
+ /**
180
+ *
181
+ * @public
182
+ * @environment client
183
+ */
154
184
  const elements = document.querySelectorAll(selector);
155
185
  this.initElements(elements, prefix, events);
156
186
  },
157
187
 
188
+ /**
189
+ *
190
+ * @public
191
+ * @environment client
192
+ */
158
193
  initElements: function (elements, prefix, events = []) {
159
194
  for (const el of elements) {
160
195
  let isEventable = true;
@@ -235,11 +270,26 @@ const CoCreateEvents = {
235
270
 
236
271
  const prefixData = elData[prefix];
237
272
 
273
+ // A new checked state must propagate even after an earlier
274
+ // input/change event; CRUD updates use the same event path.
275
+ const selectionChanged =
276
+ (target.type === "checkbox" || target.type === "radio") &&
277
+ prefixData.checked !== target.checked;
278
+ if (
279
+ event.detail?.skip === true && prefixData.prev === event.type &&
280
+ ["input", "change"].includes(event.type) && !selectionChanged
281
+ ) {
282
+ prefixData.prev = "";
283
+ return
284
+ }
285
+
238
286
  // Debounce/Prevent duplicate mouse events if needed
239
287
  if (prefixData.prev === event.type && ["mouseover", "mouseout"].includes(event.type)) {
240
288
  return;
241
289
  }
242
290
  prefixData.prev = event.type;
291
+ if (target.type === "checkbox" || target.type === "radio")
292
+ prefixData.checked = target.checked;
243
293
 
244
294
  // Check for 'actions' blocking
245
295
  const actionsAttr = target.attributes.getNamedItem("actions")?.value || "";
@@ -263,6 +313,11 @@ const CoCreateEvents = {
263
313
  }
264
314
  },
265
315
 
316
+ /**
317
+ *
318
+ * @public
319
+ * @environment client
320
+ */
266
321
  _initObserver: function (el, prefix) {
267
322
  // Updated to dash: prefix-selector, prefix-query, prefix-attribute-filter, prefix-types
268
323
  let selector = el.getAttribute(`${prefix}-selector`) || el.getAttribute(`${prefix}-query`) || "";
@@ -298,6 +353,11 @@ const CoCreateEvents = {
298
353
  }
299
354
  },
300
355
 
356
+ /**
357
+ *
358
+ * @public
359
+ * @environment client
360
+ */
301
361
  _initResizeObserver: function (el, prefix) {
302
362
  const resizeObserver = new ResizeObserver((entries) => {
303
363
  const lastEntry = entries[entries.length - 1];
@@ -311,6 +371,11 @@ const CoCreateEvents = {
311
371
  resizeObserver.observe(el);
312
372
  },
313
373
 
374
+ /**
375
+ *
376
+ * @public
377
+ * @environment client
378
+ */
314
379
  _initIntersectionObserver: function (el, prefix) {
315
380
  // Updated to dash
316
381
  const rootSelector = el.getAttribute(`${prefix}-root`);
@@ -352,6 +417,11 @@ const CoCreateEvents = {
352
417
  intersectionObserver.observe(el);
353
418
  },
354
419
 
420
+ /**
421
+ *
422
+ * @public
423
+ * @environment client
424
+ */
355
425
  __updateElements: async function (rootElement, prefix, events, params) {
356
426
  if (!rootElement.isConnected) return;
357
427
 
@@ -473,6 +543,11 @@ const CoCreateEvents = {
473
543
  );
474
544
  },
475
545
 
546
+ /**
547
+ *
548
+ * @public
549
+ * @environment client
550
+ */
476
551
  setValue: async function (rootElement, sourceElement, targetElement, prefix, events) {
477
552
  // Handle Mouseout toggle logic
478
553
  if (events && events.includes("mouseout")) {
@@ -490,8 +565,40 @@ const CoCreateEvents = {
490
565
 
491
566
  const deactivate = (rootElement !== sourceElement);
492
567
 
493
- // Iterate Attributes
568
+ // Expand the prefix object in place, preserving attribute order.
569
+ const attributes = [];
494
570
  for (const attribute of sourceElement.attributes) {
571
+ if (attribute.name === prefix) {
572
+ const rawValue = attribute.value?.trim();
573
+ let assignments = null;
574
+ let assignmentsFromAst = false;
575
+
576
+ // An empty prefix remains an initialization marker.
577
+ if (!rawValue) continue;
578
+ if (rawValue?.startsWith("$")) {
579
+ assignments = await sourceElement.getAttributeAsync(attribute.name);
580
+ assignmentsFromAst = true;
581
+ } else if (rawValue.startsWith("{")) {
582
+ assignments = JSON.parse(rawValue);
583
+ } else {
584
+ continue;
585
+ }
586
+ if (!assignments || typeof assignments !== 'object') continue;
587
+
588
+ for (const [name, value] of Object.entries(assignments)) {
589
+ attributes.push({
590
+ name: `${prefix}.${name}`,
591
+ value,
592
+ fromObject: !assignmentsFromAst
593
+ });
594
+ }
595
+ } else {
596
+ attributes.push(attribute);
597
+ }
598
+ }
599
+
600
+ // Execute both forms through the existing assignment handlers.
601
+ for (const attribute of attributes) {
495
602
  const name = attribute.name;
496
603
 
497
604
  // Optimization: Skip if shorter than prefix + dot + min char
@@ -501,39 +608,27 @@ const CoCreateEvents = {
501
608
  // All options use Dash, so they will be ignored here.
502
609
  if (!name.startsWith(prefix) || name[prefix.length] !== '.') continue;
503
610
 
504
- // Extract target name (e.g., 'value', 'style', 'src', 'data-something')
505
- let targetName = name.substring(prefix.length + 1);
506
- const explicitMethod = targetName.startsWith("$");
611
+ // Extract target name (e.g., 'value', 'style', 'src', 'data-something')
612
+ let targetName = name.substring(prefix.length + 1);
613
+ const explicitProperty = targetName.startsWith("$");
507
614
 
508
615
  let values = [];
509
- let value = attribute.value;
510
- if (value.startsWith("(") && value.endsWith(")")) {
511
- let depth = 0;
512
- let start = 0;
513
- for (let i = 0; i < value.length; i++) {
514
- const char = value.charCodeAt(i);
515
- if (char === 40) {
516
- depth++;
517
- } else if (char === 41) {
518
- depth--;
519
- } else if (char === 44 && depth === 0) {
520
- values.push(value.slice(start, i).trim().slice(1, -1));
521
- start = i + 1;
522
- }
523
- }
524
- values.push(value.slice(start).trim().slice(1, -1));
616
+ const value = attribute.fromObject
617
+ ? await astAsync({
618
+ executeAST: attribute.value,
619
+ node: sourceElement,
620
+ sandbox: { $this: sourceElement }
621
+ })
622
+ : sourceElement.getAttribute(name);
623
+ // Parse CSV for rotation
624
+ if (typeof value === 'string') {
625
+ values = value.split(',').map(v => v.trim());
626
+ } else if (Array.isArray(value)) {
627
+ values = value;
525
628
  } else {
526
- value = sourceElement.getAttribute(name);
527
- // Parse CSV for rotation
528
- if (typeof value === 'string') {
529
- values = value.split(',').map(v => v.trim());
530
- } else if (Array.isArray(value)) {
531
- values = value;
532
- } else {
533
- values = [value];
534
- }
629
+ values = [value];
535
630
  }
536
- // getAttribute has AST built in and resolves a wide range of operators.
631
+ // Object values and dotted attribute values resolve through AST.
537
632
 
538
633
  let oldValue = "";
539
634
  let newValue = "";
@@ -589,28 +684,62 @@ const CoCreateEvents = {
589
684
  }
590
685
  break;
591
686
 
592
- case 'insertadjacenthtml':
593
- case 'insertadjacenttext':
594
- case 'insertAdjacentHTML':
595
- case 'insertAdjacentText':
596
- const position = sourceElement.getAttribute(name);;
597
- if (!position) continue;
687
+ case 'insertadjacenthtml':
688
+ case 'insertadjacenttext':
689
+ case 'insertAdjacentHTML':
690
+ case 'insertAdjacentText':
691
+ if (!value) continue;
692
+ const position = String(value).toLowerCase();
598
693
 
599
694
  let content = "";
600
- if (sourceElement.getValue) {
601
- content = await sourceElement.getValue();
695
+ if (sourceElement.hasAttribute(`${prefix}-value`)) {
696
+ content = sourceElement.getAttribute(`${prefix}-value`);
697
+ } else {
698
+ const contentSelector = sourceElement.attributes
699
+ .getNamedItem(`${prefix}-element`)?.value;
700
+ let contentSource = sourceElement;
701
+ if (contentSelector !== undefined) {
702
+ [contentSource] = queryElements({
703
+ element: sourceElement,
704
+ prefix,
705
+ selector: contentSelector
706
+ });
707
+ if (!contentSource) continue;
708
+ }
709
+ if (contentSource.getValue) content = contentSource.getValue();
602
710
  }
603
-
604
- // Check for CoCreate Rich Text integration
605
- let domTextEditor = targetElement.parentElement?.closest("[contenteditable]");
606
-
607
- if (domTextEditor && typeof CoCreate !== 'undefined' && CoCreate.text) {
608
- CoCreate.text.insertAdjacentElement({
609
- domTextEditor,
610
- position,
611
- target: targetElement,
612
- elementValue: content
613
- });
711
+ if (content && typeof content.then === 'function') {
712
+ content = await content;
713
+ }
714
+ content ??= "";
715
+
716
+ // Resolve adjacency to canonical source coordinates for editors.
717
+ const currentEditor = targetElement.editor;
718
+ const editorDetails = typeof currentEditor?.getStart === "function"
719
+ ? currentEditor : targetElement.editorDetails;
720
+ const editorElement = editorDetails?.element || editorDetails?.editorElement;
721
+ if (editorElement?.editor?.updateText) {
722
+ const point = editorDetails.getInsertionPoint?.(position);
723
+ if (point) {
724
+ await point.editor.updateText({ start: point.offset, length: 0, value: String(content) });
725
+ break;
726
+ }
727
+ if (typeof editorDetails.getInsertionPoint === "function")
728
+ throw new RangeError("Editor insertion position is not valid for this element.");
729
+ const outerRange = editorDetails.getOuterRange?.();
730
+ const contentRange = editorDetails.getContentRange?.();
731
+ const legacyBoundary = {
732
+ beforebegin: "start", afterbegin: "contentStart",
733
+ beforeend: "contentEnd", afterend: "end"
734
+ }[position];
735
+ const start = position === "beforebegin" ? outerRange?.start ?? editorDetails.start
736
+ : position === "afterbegin" ? contentRange?.start ?? editorDetails.contentStart
737
+ : position === "beforeend" ? contentRange?.end ?? editorDetails.contentEnd
738
+ : position === "afterend" ? outerRange?.end ?? editorDetails.end
739
+ : undefined;
740
+ if (!legacyBoundary || !Number.isInteger(start))
741
+ throw new RangeError("Editor insertion requires a current source range for the target.");
742
+ await editorElement.editor.updateText({ start, length: 0, value: String(content) });
614
743
  } else {
615
744
  const isHtml = targetName.toLowerCase().includes('html');
616
745
  if (isHtml) {
@@ -618,34 +747,34 @@ const CoCreateEvents = {
618
747
  } else {
619
748
  targetElement.insertAdjacentText(position, content);
620
749
  }
621
- }
622
- break;
623
-
624
- case 'insertadjacentelement':
625
- case 'insertAdjacentElement':
626
- const elementPosition = sourceElement.getAttribute(name);
627
- const elementSelector =
628
- sourceElement.attributes.getNamedItem(`${prefix}-element`)?.value;
629
- if (!elementPosition || !elementSelector) continue;
630
-
631
- const elementsToInsert = queryElements({
632
- element: sourceElement,
633
- prefix,
634
- selector: elementSelector
635
- });
636
- const elementToInsert = elementsToInsert[0];
637
- if (
638
- elementToInsert &&
639
- typeof targetElement.insertAdjacentElement === 'function'
640
- ) {
641
- targetElement.insertAdjacentElement(
642
- elementPosition,
643
- elementToInsert
644
- );
645
- }
646
- break;
647
-
648
- case 'value':
750
+ }
751
+ break;
752
+
753
+ case 'insertadjacentelement':
754
+ case 'insertAdjacentElement':
755
+ const elementPosition = value;
756
+ const elementSelector =
757
+ sourceElement.attributes.getNamedItem(`${prefix}-element`)?.value;
758
+ if (!elementPosition || !elementSelector) continue;
759
+
760
+ const elementsToInsert = queryElements({
761
+ element: sourceElement,
762
+ prefix,
763
+ selector: elementSelector
764
+ });
765
+ const elementToInsert = elementsToInsert[0];
766
+ if (
767
+ elementToInsert &&
768
+ typeof targetElement.insertAdjacentElement === 'function'
769
+ ) {
770
+ targetElement.insertAdjacentElement(
771
+ elementPosition,
772
+ elementToInsert
773
+ );
774
+ }
775
+ break;
776
+
777
+ case 'value':
649
778
  if (targetElement.getValue) {
650
779
  oldValue = await targetElement.getValue();
651
780
  } else {
@@ -656,7 +785,14 @@ const CoCreateEvents = {
656
785
  newValue = this.__getNextValue(values, oldValue, deactivate);
657
786
 
658
787
  if (targetElement.setValue) {
659
- targetElement.setValue(newValue);
788
+ if (
789
+ targetElement === sourceElement &&
790
+ ["input", "change"].includes(prefix)
791
+ ) {
792
+ targetElement.setValue(newValue, true);
793
+ } else {
794
+ targetElement.setValue(newValue);
795
+ }
660
796
  } else {
661
797
  targetElement.value = newValue;
662
798
  }
@@ -665,27 +801,46 @@ const CoCreateEvents = {
665
801
  default:
666
802
  // Generic handling: Try Property first, then Attribute
667
803
 
668
- // 1. Try Property
669
- if (targetElement[targetName] !== undefined || explicitMethod) {
670
- if (explicitMethod)
671
- targetName = targetName.substring(1)
672
- oldValue = targetElement[targetName];
804
+ // $ always selects a property, including one not yet defined.
805
+ let propertyName;
806
+ let propertyTarget = targetElement;
807
+ let propertyValue;
808
+ if (explicitProperty || (
809
+ propertyName = resolveProperty(targetElement, targetName)
810
+ ) !== undefined) {
811
+ if (explicitProperty) {
812
+ const propertyPath = resolvePropertyPath(
813
+ targetElement,
814
+ targetName.substring(1)
815
+ );
816
+ if (propertyPath) {
817
+ propertyTarget = propertyPath.target;
818
+ propertyName = propertyPath.key;
819
+ propertyValue = propertyPath.value;
820
+ } else {
821
+ targetName = targetName.substring(1);
822
+ propertyName = resolveProperty(targetElement, targetName);
823
+ }
824
+ }
825
+ targetName = propertyName ?? targetName;
826
+ propertyValue ??= propertyTarget[targetName];
827
+ oldValue = propertyValue;
673
828
  if (!values.includes(oldValue) && typeof oldValue !== 'function') oldValue = "";
674
829
 
675
- newValue = this.__getNextValue(values, oldValue, deactivate);
676
-
677
- if (typeof targetElement[targetName] === 'function') {
678
- // A non-$ method binding with value=false disables
679
- // the action. Explicit $method bindings may still
680
- // intentionally pass false as an argument.
681
- if (
682
- explicitMethod ||
683
- (newValue !== false && newValue !== "false")
684
- ) {
685
- targetElement[targetName](newValue);
686
- }
687
- } else {
688
- targetElement[targetName] = newValue;
830
+ newValue = this.__getNextValue(values, oldValue, deactivate);
831
+
832
+ if (typeof propertyValue === 'function') {
833
+ // A non-$ method binding with value=false disables
834
+ // the action. Explicit $ bindings may still
835
+ // intentionally pass false as an argument.
836
+ if (
837
+ explicitProperty ||
838
+ (newValue !== false && newValue !== "false")
839
+ ) {
840
+ propertyValue.call(propertyTarget, newValue);
841
+ }
842
+ } else {
843
+ propertyTarget[targetName] = newValue;
689
844
  }
690
845
  }
691
846
  // 2. Fallback to Attribute
@@ -703,6 +858,11 @@ const CoCreateEvents = {
703
858
  }
704
859
  },
705
860
 
861
+ /**
862
+ *
863
+ * @public
864
+ * @environment client
865
+ */
706
866
  __getNextValue: function (values, val, deactivate) {
707
867
  if (deactivate) return "";
708
868
  const size = values.length;
@@ -718,3 +878,49 @@ const CoCreateEvents = {
718
878
  CoCreateEvents.init();
719
879
 
720
880
  export default CoCreateEvents;
881
+
882
+ /**
883
+ *
884
+ * @name event-name
885
+ * @kind member
886
+ * @attribute
887
+ * @environment client
888
+ */
889
+
890
+ /**
891
+ *
892
+ * @name style
893
+ * @kind member
894
+ * @attribute
895
+ * @environment client
896
+ */
897
+
898
+ /**
899
+ *
900
+ * @name type
901
+ * @kind member
902
+ * @attribute
903
+ * @environment client
904
+ */
905
+
906
+ /**
907
+ * A simple events component in vanilla javascript. Easily configured using HTML5 attributes and/or JavaScript API.
908
+ *
909
+ * @module events
910
+ * @environment client
911
+ */
912
+
913
+ /**
914
+ * Declarative selectors recognized by this module.
915
+ *
916
+ * @name declarative-selectors
917
+ * @kind member
918
+ * @selectors
919
+ * @environment client
920
+ * @example
921
+ * ```json
922
+ * [
923
+ * "[event-name]"
924
+ * ]
925
+ * ```
926
+ */