@elementor/editor-canvas 0.13.1 → 0.15.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 (34) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +33 -0
  3. package/dist/index.d.mts +9 -8
  4. package/dist/index.d.ts +9 -8
  5. package/dist/index.js +184 -31
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +192 -34
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +9 -8
  10. package/src/__tests__/__mocks__/styles-schema.ts +3 -3
  11. package/src/__tests__/settings-props-resolver.test.ts +1 -1
  12. package/src/__tests__/styles-prop-resolver.test.ts +7 -7
  13. package/src/components/__tests__/elements-overlays.test.tsx +40 -35
  14. package/src/components/element-overlay.tsx +3 -2
  15. package/src/components/elements-overlays.tsx +26 -9
  16. package/src/hooks/use-floating-on-element.ts +9 -6
  17. package/src/init-settings-transformers.ts +1 -5
  18. package/src/init-style-transformers.ts +2 -6
  19. package/src/init-styles-renderer.ts +1 -1
  20. package/src/legacy/__tests__/signalized-process.test.ts +80 -0
  21. package/src/legacy/create-element-type.ts +2 -2
  22. package/src/legacy/create-templated-element-type.ts +131 -0
  23. package/src/legacy/init-legacy-views.ts +7 -1
  24. package/src/legacy/signalized-process.ts +35 -0
  25. package/src/legacy/types.ts +27 -3
  26. package/src/renderers/__tests__/create-dom-renderer.test.ts +66 -0
  27. package/src/renderers/__tests__/create-props-resolver.test.ts +123 -15
  28. package/src/renderers/create-dom-renderer.ts +56 -0
  29. package/src/renderers/create-props-resolver.ts +10 -8
  30. package/src/renderers/render-styles.ts +4 -0
  31. package/src/transformers/create-transformers-registry.ts +16 -5
  32. package/src/transformers/styles/background-image-position-offset-transformer.ts +1 -1
  33. package/src/transformers/styles/background-image-size-scale-transformer.ts +1 -1
  34. package/src/transformers/types.ts +1 -5
package/dist/index.mjs CHANGED
@@ -3,8 +3,13 @@ import { injectIntoTop } from "@elementor/editor";
3
3
 
4
4
  // src/components/elements-overlays.tsx
5
5
  import * as React2 from "react";
6
- import { useElementsDomRef, useSelectedElement } from "@elementor/editor-elements";
7
- import { __privateUseIsRouteActive as useIsRouteActive, useEditMode } from "@elementor/editor-v1-adapters";
6
+ import { getElements, useSelectedElement } from "@elementor/editor-elements";
7
+ import {
8
+ __privateUseIsRouteActive as useIsRouteActive,
9
+ __privateUseListenTo as useListenTo,
10
+ useEditMode,
11
+ windowEvent
12
+ } from "@elementor/editor-v1-adapters";
8
13
 
9
14
  // src/components/element-overlay.tsx
10
15
  import * as React from "react";
@@ -46,7 +51,7 @@ function groupProps(props) {
46
51
  }
47
52
 
48
53
  // src/hooks/use-floating-on-element.ts
49
- import { useState } from "react";
54
+ import { useEffect as useEffect2, useState } from "react";
50
55
  import { autoUpdate, offset, size, useFloating } from "@floating-ui/react";
51
56
  function useFloatingOnElement({ element, isSelected }) {
52
57
  const [isOpen, setIsOpen] = useState(false);
@@ -54,10 +59,7 @@ function useFloatingOnElement({ element, isSelected }) {
54
59
  // Must be controlled for interactions (like hover) to work.
55
60
  open: isOpen || isSelected,
56
61
  onOpenChange: setIsOpen,
57
- // Add an animation frame to support scroll events (without it the floating element will stay in the same position).
58
- whileElementsMounted: (...args) => autoUpdate(...args, { animationFrame: true }),
59
- // The first element in the canvas is `display: contents` so we need to use the first child.
60
- elements: { reference: element.firstElementChild },
62
+ whileElementsMounted: autoUpdate,
61
63
  middleware: [
62
64
  // Match the floating element's size to the reference element.
63
65
  size({
@@ -72,6 +74,9 @@ function useFloatingOnElement({ element, isSelected }) {
72
74
  offset(({ rects }) => -rects.reference.height / 2 - rects.floating.height / 2)
73
75
  ]
74
76
  });
77
+ useEffect2(() => {
78
+ refs.setReference(element);
79
+ }, [element, refs]);
75
80
  return {
76
81
  isVisible: isOpen || isSelected,
77
82
  context,
@@ -90,7 +95,7 @@ var OverlayBox = styled(Box, { shouldForwardProp: (prop) => prop !== "isSelected
90
95
  outlineOffset: isSelected ? "-2px" : "-1px",
91
96
  pointerEvents: "none"
92
97
  }));
93
- function ElementOverlay({ element, isSelected }) {
98
+ function ElementOverlay({ element, isSelected, id }) {
94
99
  const { context, floating, isVisible } = useFloatingOnElement({ element, isSelected });
95
100
  const { getFloatingProps, getReferenceProps } = useInteractions([useHover(context)]);
96
101
  useBindReactPropsToElement(element, getReferenceProps);
@@ -100,7 +105,7 @@ function ElementOverlay({ element, isSelected }) {
100
105
  ref: floating.setRef,
101
106
  isSelected,
102
107
  style: floating.styles,
103
- "data-element-overlay": element.dataset.id,
108
+ "data-element-overlay": id,
104
109
  role: "presentation",
105
110
  ...getFloatingProps()
106
111
  }
@@ -110,31 +115,38 @@ function ElementOverlay({ element, isSelected }) {
110
115
  // src/components/elements-overlays.tsx
111
116
  function ElementsOverlays() {
112
117
  const selected = useSelectedElement();
113
- const domElements = useElementsDomRef();
118
+ const elements = useElementsDom();
114
119
  const currentEditMode = useEditMode();
115
120
  const isEditMode = currentEditMode === "edit";
116
121
  const isKitRouteActive = useIsRouteActive("panel/global");
117
122
  const isActive = isEditMode && !isKitRouteActive;
118
- return isActive && domElements.map((el) => /* @__PURE__ */ React2.createElement(
119
- ElementOverlay,
120
- {
121
- element: el,
122
- key: el.dataset.id,
123
- isSelected: selected.element?.id === el.dataset.id
123
+ return isActive && elements.map(([id, element]) => /* @__PURE__ */ React2.createElement(ElementOverlay, { key: id, id, element, isSelected: selected.element?.id === id }));
124
+ }
125
+ var ELEMENTS_DATA_ATTR = "atomic";
126
+ function useElementsDom() {
127
+ return useListenTo(
128
+ [windowEvent("elementor/editor/element-rendered"), windowEvent("elementor/editor/element-destroyed")],
129
+ () => {
130
+ return getElements().filter((el) => ELEMENTS_DATA_ATTR in (el.view?.el?.dataset ?? {})).map((element) => [element.id, element.view?.getDomElement?.()?.get?.(0)]).filter((item) => !!item[1]);
124
131
  }
125
- ));
132
+ );
126
133
  }
127
134
 
128
135
  // src/transformers/create-transformers-registry.ts
129
136
  function createTransformersRegistry() {
130
137
  const transformers = {};
138
+ let fallbackTransformer = null;
131
139
  return {
132
- register(name, transformer) {
133
- transformers[name] = transformer;
140
+ register(type, transformer) {
141
+ transformers[type] = transformer;
142
+ return this;
143
+ },
144
+ registerFallback(transformer) {
145
+ fallbackTransformer = transformer;
134
146
  return this;
135
147
  },
136
- all() {
137
- return transformers;
148
+ get(type) {
149
+ return transformers[type] ?? fallbackTransformer;
138
150
  }
139
151
  };
140
152
  }
@@ -200,7 +212,7 @@ var plainTransformer = createTransformer((value) => {
200
212
 
201
213
  // src/init-settings-transformers.ts
202
214
  function initSettingsTransformers() {
203
- settingsTransformersRegistry.register("string", plainTransformer).register("url", plainTransformer).register("number", plainTransformer).register("boolean", plainTransformer).register("classes", arrayTransformer).register("link", linkTransformer).register("image", imageTransformer).register("image-src", imageSrcTransformer).register("image-attachment-id", plainTransformer);
215
+ settingsTransformersRegistry.register("classes", arrayTransformer).register("link", linkTransformer).register("image", imageTransformer).register("image-src", imageSrcTransformer).registerFallback(plainTransformer);
204
216
  }
205
217
 
206
218
  // src/style-transformers-registry.ts
@@ -242,12 +254,12 @@ var backgroundImageOverlayTransformer = createTransformer((value) => {
242
254
 
243
255
  // src/transformers/styles/background-image-position-offset-transformer.ts
244
256
  var backgroundImagePositionOffsetTransformer = createTransformer(
245
- ({ x = "0px", y = "0px" }) => `${x} ${y}`
257
+ ({ x, y }) => `${x ?? "0px"} ${y ?? "0px"}`
246
258
  );
247
259
 
248
260
  // src/transformers/styles/background-image-size-scale-transformer.ts
249
261
  var backgroundImageSizeScaleTransformer = createTransformer(
250
- ({ width = "auto", height = "auto" }) => `${width} ${height}`
262
+ ({ width, height }) => `${width ?? "auto"} ${height ?? "auto"}`
251
263
  );
252
264
 
253
265
  // src/transformers/styles/background-transformer.ts
@@ -317,7 +329,7 @@ function initStyleTransformers() {
317
329
  ["block-start", "block-end", "inline-start", "inline-end"],
318
330
  ({ propKey, key }) => `${propKey}-${key}`
319
331
  )
320
- ).register("color", plainTransformer).register("number", plainTransformer).register("string", plainTransformer).register("url", plainTransformer).register("box-shadow", createCombineArrayTransformer(",")).register("background", backgroundTransformer).register("background-overlay", createCombineArrayTransformer(",")).register("background-color-overlay", backgroundColorOverlayTransformer).register("background-image-overlay", backgroundImageOverlayTransformer).register("background-gradient-overlay", backgroundGradientOverlayTransformer).register("gradient-color-stop", createCombineArrayTransformer(",")).register("color-stop", colorStopTransformer).register("background-image-position-offset", backgroundImagePositionOffsetTransformer).register("background-image-size-scale", backgroundImageSizeScaleTransformer).register("image-attachment-id", plainTransformer).register("image-src", imageSrcTransformer).register("image", imageTransformer).register(
332
+ ).register("box-shadow", createCombineArrayTransformer(",")).register("background", backgroundTransformer).register("background-overlay", createCombineArrayTransformer(",")).register("background-color-overlay", backgroundColorOverlayTransformer).register("background-image-overlay", backgroundImageOverlayTransformer).register("background-gradient-overlay", backgroundGradientOverlayTransformer).register("gradient-color-stop", createCombineArrayTransformer(",")).register("color-stop", colorStopTransformer).register("background-image-position-offset", backgroundImagePositionOffsetTransformer).register("background-image-size-scale", backgroundImageSizeScaleTransformer).register("image-src", imageSrcTransformer).register("image", imageTransformer).register(
321
333
  "layout-direction",
322
334
  createMultiPropsTransformer(["row", "column"], ({ propKey, key }) => `${key}-${propKey}`)
323
335
  ).register(
@@ -332,7 +344,7 @@ function initStyleTransformers() {
332
344
  ["start-start", "start-end", "end-start", "end-end"],
333
345
  ({ key }) => `border-${key}-radius`
334
346
  )
335
- );
347
+ ).registerFallback(plainTransformer);
336
348
  }
337
349
 
338
350
  // src/init-styles-renderer.ts
@@ -353,9 +365,6 @@ function createPropsResolver({ transformers, schema: initialSchema, onPropResolv
353
365
  Object.entries(schema).map(async ([key, type]) => {
354
366
  const value = props[key] ?? type.default;
355
367
  const transformed = await transform({ value, key, type, signal });
356
- if (transformed === null) {
357
- return;
358
- }
359
368
  onPropResolve?.({ key, value: transformed });
360
369
  if (isMultiProps(transformed)) {
361
370
  return getMultiPropsValue(transformed);
@@ -384,6 +393,9 @@ function createPropsResolver({ transformers, schema: initialSchema, onPropResolv
384
393
  return null;
385
394
  }
386
395
  }
396
+ if (value.$$type !== type.key) {
397
+ return null;
398
+ }
387
399
  let resolvedValue = value.value;
388
400
  if (type.kind === "object") {
389
401
  resolvedValue = await resolve({
@@ -399,7 +411,7 @@ function createPropsResolver({ transformers, schema: initialSchema, onPropResolv
399
411
  )
400
412
  );
401
413
  }
402
- const transformer = transformers[value.$$type];
414
+ const transformer = transformers.get(value.$$type);
403
415
  if (!transformer) {
404
416
  return null;
405
417
  }
@@ -472,6 +484,9 @@ function createStyleWrapper(value = "", wrapper) {
472
484
  async function propsToCss({ props, resolve, signal }) {
473
485
  const transformed = await resolve({ props, signal });
474
486
  return Object.entries(transformed).reduce((acc, [propName, propValue]) => {
487
+ if (propValue === null) {
488
+ return acc;
489
+ }
475
490
  acc.push(propName + ":" + propValue + ";");
476
491
  return acc;
477
492
  }, []).join("");
@@ -499,7 +514,7 @@ function initStylesRenderer() {
499
514
  listenTo(v1ReadyEvent(), () => {
500
515
  let abortController = null;
501
516
  const resolve = createPropsResolver({
502
- transformers: styleTransformersRegistry.all(),
517
+ transformers: styleTransformersRegistry,
503
518
  schema: getStylesSchema(),
504
519
  onPropResolve: enqueueUsedFonts
505
520
  });
@@ -550,6 +565,51 @@ function enqueueUsedFonts({ key, value }) {
550
565
  import { getWidgetsCache } from "@elementor/editor-elements";
551
566
  import { __privateListenTo, v1ReadyEvent as v1ReadyEvent2 } from "@elementor/editor-v1-adapters";
552
567
 
568
+ // src/renderers/create-dom-renderer.ts
569
+ import { createArrayLoader, createEnvironment } from "@elementor/twing";
570
+ function createDomRenderer() {
571
+ const loader = createArrayLoader({});
572
+ const environment = createEnvironment(loader);
573
+ environment.registerEscapingStrategy(escapeHtmlTag, "html_tag");
574
+ environment.registerEscapingStrategy(escapeURL, "full_url");
575
+ return {
576
+ register: loader.setTemplate,
577
+ render: environment.render
578
+ };
579
+ }
580
+ function escapeHtmlTag(value) {
581
+ const allowedTags = [
582
+ "a",
583
+ "article",
584
+ "aside",
585
+ "button",
586
+ "div",
587
+ "footer",
588
+ "h1",
589
+ "h2",
590
+ "h3",
591
+ "h4",
592
+ "h5",
593
+ "h6",
594
+ "header",
595
+ "main",
596
+ "nav",
597
+ "p",
598
+ "section",
599
+ "span"
600
+ ];
601
+ return allowedTags.includes(value) ? value : "div";
602
+ }
603
+ function escapeURL(value) {
604
+ const allowedProtocols = ["http:", "https:", "mailto:", "tel:"];
605
+ try {
606
+ const parsed = new URL(value);
607
+ return allowedProtocols.includes(parsed.protocol) ? value : "";
608
+ } catch {
609
+ return "";
610
+ }
611
+ }
612
+
553
613
  // src/legacy/create-element-type.ts
554
614
  function createElementType(type) {
555
615
  const legacyWindow = window;
@@ -558,11 +618,11 @@ function createElementType(type) {
558
618
  return type;
559
619
  }
560
620
  getView() {
561
- return createElementView();
621
+ return createElementViewClassDeclaration();
562
622
  }
563
623
  };
564
624
  }
565
- function createElementView() {
625
+ function createElementViewClassDeclaration() {
566
626
  const legacyWindow = window;
567
627
  return class extends legacyWindow.elementor.modules.elements.views.Widget {
568
628
  // Dispatch `render` event so the overlay layer will be updated
@@ -613,16 +673,114 @@ function createElementView() {
613
673
  };
614
674
  }
615
675
 
676
+ // src/legacy/signalized-process.ts
677
+ function signalizedProcess(signal, steps = []) {
678
+ return {
679
+ then: (cb) => {
680
+ steps.push(cb);
681
+ return signalizedProcess(signal, steps);
682
+ },
683
+ execute: async () => {
684
+ let lastResult;
685
+ for (const step of steps) {
686
+ if (signal.aborted) {
687
+ break;
688
+ }
689
+ lastResult = await step(lastResult, signal);
690
+ }
691
+ }
692
+ };
693
+ }
694
+
695
+ // src/legacy/create-templated-element-type.ts
696
+ function createTemplatedElementType({ type, renderer, element }) {
697
+ const legacyWindow = window;
698
+ Object.entries(element.twig_templates).forEach(([key, template]) => {
699
+ renderer.register(key, template);
700
+ });
701
+ const propsResolver = createPropsResolver({
702
+ transformers: settingsTransformersRegistry,
703
+ schema: element.atomic_props_schema
704
+ });
705
+ return class extends legacyWindow.elementor.modules.elements.types.Widget {
706
+ getType() {
707
+ return type;
708
+ }
709
+ getView() {
710
+ return createTemplatedElementViewClassDeclaration({
711
+ type,
712
+ renderer,
713
+ propsResolver,
714
+ baseStylesDictionary: element.base_styles_dictionary,
715
+ templateKey: element.twig_main_template
716
+ });
717
+ }
718
+ };
719
+ }
720
+ function canBeTemplated(element) {
721
+ return !!(element.atomic_props_schema && element.twig_templates && element.twig_main_template && element.base_styles_dictionary);
722
+ }
723
+ function createTemplatedElementViewClassDeclaration({
724
+ type,
725
+ renderer,
726
+ propsResolver: resolveProps,
727
+ templateKey,
728
+ baseStylesDictionary
729
+ }) {
730
+ const BaseView = createElementViewClassDeclaration();
731
+ return class extends BaseView {
732
+ #abortController = null;
733
+ getTemplateType() {
734
+ return "twig";
735
+ }
736
+ renderOnChange() {
737
+ this.render();
738
+ }
739
+ // Overriding Marionette original render method to inject our renderer.
740
+ async _renderTemplate() {
741
+ this.#beforeRenderTemplate();
742
+ this.#abortController?.abort();
743
+ this.#abortController = new AbortController();
744
+ const process = signalizedProcess(this.#abortController.signal).then((_, signal) => {
745
+ const settings = this.model.get("settings").toJSON();
746
+ return resolveProps({
747
+ props: settings,
748
+ signal
749
+ });
750
+ }).then((resolvedSettings) => {
751
+ const context = {
752
+ id: this.model.get("id"),
753
+ type,
754
+ settings: resolvedSettings,
755
+ base_styles: baseStylesDictionary
756
+ };
757
+ return renderer.render(templateKey, context);
758
+ }).then((html) => this.$el.html(html));
759
+ await process.execute();
760
+ this.#afterRenderTemplate();
761
+ }
762
+ // Emulating the original Marionette behavior.
763
+ #beforeRenderTemplate() {
764
+ this.triggerMethod("before:render:template");
765
+ }
766
+ #afterRenderTemplate() {
767
+ this.bindUIElements();
768
+ this.triggerMethod("render:template");
769
+ }
770
+ };
771
+ }
772
+
616
773
  // src/legacy/init-legacy-views.ts
617
774
  function initLegacyViews() {
618
775
  __privateListenTo(v1ReadyEvent2(), () => {
619
776
  const config = getWidgetsCache() ?? {};
620
777
  const legacyWindow = window;
778
+ const renderer = createDomRenderer();
621
779
  Object.entries(config).forEach(([type, element]) => {
622
780
  if (!element.atomic) {
623
781
  return;
624
782
  }
625
- const ElementType = createElementType(type);
783
+ const ElementType = canBeTemplated(element) ? createTemplatedElementType({ type, renderer, element }) : createElementType(type);
626
784
  legacyWindow.elementor.elementsManager.registerElementType(new ElementType());
627
785
  });
628
786
  });