@elementor/editor-canvas 0.1.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.
@@ -0,0 +1,22 @@
1
+
2
+ > @elementor/editor-canvas@0.1.0 build
3
+ > tsup --config=../../tsup.build.ts
4
+
5
+ CLI Building entry: src/index.ts
6
+ CLI Using tsconfig: ../../../tsconfig.json
7
+ CLI tsup v8.3.5
8
+ CLI Using tsup config: /home/runner/work/elementor-packages/elementor-packages/tsup.build.ts
9
+ CLI Target: esnext
10
+ CLI Cleaning output folder
11
+ ESM Build start
12
+ CJS Build start
13
+ ESM dist/index.mjs 4.77 KB
14
+ ESM dist/index.mjs.map 8.61 KB
15
+ ESM ⚡️ Build success in 48ms
16
+ CJS dist/index.js 6.13 KB
17
+ CJS dist/index.js.map 8.63 KB
18
+ CJS ⚡️ Build success in 49ms
19
+ DTS Build start
20
+ DTS ⚡️ Build success in 11882ms
21
+ DTS dist/index.d.mts 13.00 B
22
+ DTS dist/index.d.ts 13.00 B
package/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # @elementor/editor-canvas
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d21c5c3: Introduce editor canvas to render element overlays for atomic elements
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [e7f4706]
12
+ - Updated dependencies [7781969]
13
+ - Updated dependencies [e4c6e3b]
14
+ - Updated dependencies [d21c5c3]
15
+ - Updated dependencies [7781969]
16
+ - Updated dependencies [0c6bcb6]
17
+ - @elementor/editor-elements@0.3.0
18
+ - @elementor/editor@0.17.0
19
+ - @elementor/editor-v1-adapters@0.8.4
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+
25
+ // src/init.tsx
26
+ var import_editor = require("@elementor/editor");
27
+
28
+ // src/components/elements-overlays.tsx
29
+ var React2 = __toESM(require("react"));
30
+ var import_editor_elements = require("@elementor/editor-elements");
31
+ var import_editor_v1_adapters = require("@elementor/editor-v1-adapters");
32
+
33
+ // src/components/element-overlay.tsx
34
+ var React = __toESM(require("react"));
35
+ var import_ui = require("@elementor/ui");
36
+ var import_react4 = require("@floating-ui/react");
37
+
38
+ // src/hooks/use-bind-react-props-to-element.ts
39
+ var import_react = require("react");
40
+ function useBindReactPropsToElement(element, getProps) {
41
+ (0, import_react.useEffect)(() => {
42
+ const el = element;
43
+ const { events, attrs } = groupProps(getProps());
44
+ events.forEach(([eventName, listener]) => el.addEventListener(eventName, listener));
45
+ attrs.forEach(([attrName, attrValue]) => el.setAttribute(attrName, attrValue));
46
+ return () => {
47
+ events.forEach(([eventName, listener]) => el.removeEventListener(eventName, listener));
48
+ attrs.forEach(([attrName]) => el.removeAttribute(attrName));
49
+ };
50
+ }, [getProps, element]);
51
+ }
52
+ function groupProps(props) {
53
+ const eventRegex = /^on(?=[A-Z])/;
54
+ return Object.entries(props).reduce(
55
+ (acc, [propName, propValue]) => {
56
+ if (!eventRegex.test(propName)) {
57
+ acc.attrs.push([propName, propValue]);
58
+ return acc;
59
+ }
60
+ const eventName = propName.replace(eventRegex, "").toLowerCase();
61
+ const listener = propValue;
62
+ acc.events.push([eventName, listener]);
63
+ return acc;
64
+ },
65
+ {
66
+ events: [],
67
+ attrs: []
68
+ }
69
+ );
70
+ }
71
+
72
+ // src/hooks/use-floating-on-element.ts
73
+ var import_react2 = require("react");
74
+ var import_react3 = require("@floating-ui/react");
75
+ function useFloatingOnElement({ element, isSelected }) {
76
+ const [isOpen, setIsOpen] = (0, import_react2.useState)(false);
77
+ const { refs, floatingStyles, context } = (0, import_react3.useFloating)({
78
+ // Must be controlled for interactions (like hover) to work.
79
+ open: isOpen || isSelected,
80
+ onOpenChange: setIsOpen,
81
+ // Add an animation frame to support scroll events (without it the floating element will stay in the same position).
82
+ whileElementsMounted: (...args) => (0, import_react3.autoUpdate)(...args, { animationFrame: true }),
83
+ // The first element in the canvas is `display: contents` so we need to use the first child.
84
+ elements: { reference: element.firstElementChild },
85
+ middleware: [
86
+ // Match the floating element's size to the reference element.
87
+ (0, import_react3.size)({
88
+ apply({ elements, rects }) {
89
+ Object.assign(elements.floating.style, {
90
+ width: `${rects.reference.width}px`,
91
+ height: `${rects.reference.height}px`
92
+ });
93
+ }
94
+ }),
95
+ // Center the floating element on the reference element.
96
+ (0, import_react3.offset)(({ rects }) => -rects.reference.height / 2 - rects.floating.height / 2)
97
+ ]
98
+ });
99
+ return {
100
+ isVisible: isOpen || isSelected,
101
+ context,
102
+ floating: {
103
+ setRef: refs.setFloating,
104
+ ref: refs.floating,
105
+ styles: floatingStyles
106
+ }
107
+ };
108
+ }
109
+
110
+ // src/components/element-overlay.tsx
111
+ var CANVAS_WRAPPER_ID = "elementor-preview-responsive-wrapper";
112
+ var OverlayBox = (0, import_ui.styled)(import_ui.Box, { shouldForwardProp: (prop) => prop !== "isSelected" })(({ theme, isSelected }) => ({
113
+ outline: `${isSelected ? "2px" : "1px"} solid ${theme.palette.primary.light}`,
114
+ outlineOffset: isSelected ? "-2px" : "-1px",
115
+ pointerEvents: "none"
116
+ }));
117
+ function ElementOverlay({ element, isSelected }) {
118
+ const { context, floating, isVisible } = useFloatingOnElement({ element, isSelected });
119
+ const { getFloatingProps, getReferenceProps } = (0, import_react4.useInteractions)([(0, import_react4.useHover)(context)]);
120
+ useBindReactPropsToElement(element, getReferenceProps);
121
+ return isVisible && /* @__PURE__ */ React.createElement(import_react4.FloatingPortal, { id: CANVAS_WRAPPER_ID }, /* @__PURE__ */ React.createElement(
122
+ OverlayBox,
123
+ {
124
+ ref: floating.setRef,
125
+ isSelected,
126
+ style: floating.styles,
127
+ "data-element-overlay": element.dataset.id,
128
+ role: "presentation",
129
+ ...getFloatingProps()
130
+ }
131
+ ));
132
+ }
133
+
134
+ // src/components/elements-overlays.tsx
135
+ function ElementsOverlays() {
136
+ const selected = (0, import_editor_elements.useSelectedElement)();
137
+ const domElements = (0, import_editor_elements.useElementsDomRef)();
138
+ const isPreviewMode = (0, import_editor_v1_adapters.__privateUseIsPreviewMode)();
139
+ const isKitRouteActive = (0, import_editor_v1_adapters.__privateUseIsRouteActive)("panel/global");
140
+ const isActive = !isPreviewMode && !isKitRouteActive;
141
+ return isActive && domElements.map((el) => /* @__PURE__ */ React2.createElement(
142
+ ElementOverlay,
143
+ {
144
+ element: el,
145
+ key: el.dataset.id,
146
+ isSelected: selected.element?.id === el.dataset.id
147
+ }
148
+ ));
149
+ }
150
+
151
+ // src/init.tsx
152
+ function init() {
153
+ (0, import_editor.injectIntoTop)({
154
+ id: "elements-overlays",
155
+ component: ElementsOverlays
156
+ });
157
+ }
158
+
159
+ // src/index.ts
160
+ init();
161
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/init.tsx","../src/components/elements-overlays.tsx","../src/components/element-overlay.tsx","../src/hooks/use-bind-react-props-to-element.ts","../src/hooks/use-floating-on-element.ts","../src/index.ts"],"sourcesContent":["import { injectIntoTop } from '@elementor/editor';\n\nimport { ElementsOverlays } from './components/elements-overlays';\n\nexport function init() {\n\tinjectIntoTop( {\n\t\tid: 'elements-overlays',\n\t\tcomponent: ElementsOverlays,\n\t} );\n}\n","import * as React from 'react';\nimport { useElementsDomRef, useSelectedElement } from '@elementor/editor-elements';\nimport {\n\t__privateUseIsPreviewMode as useIsPreviewMode,\n\t__privateUseIsRouteActive as useIsRouteActive,\n} from '@elementor/editor-v1-adapters';\n\nimport { ElementOverlay } from './element-overlay';\n\nexport function ElementsOverlays() {\n\tconst selected = useSelectedElement();\n\tconst domElements = useElementsDomRef();\n\n\tconst isPreviewMode = useIsPreviewMode();\n\tconst isKitRouteActive = useIsRouteActive( 'panel/global' );\n\n\tconst isActive = ! isPreviewMode && ! isKitRouteActive;\n\n\treturn (\n\t\tisActive &&\n\t\tdomElements.map( ( el ) => (\n\t\t\t<ElementOverlay\n\t\t\t\telement={ el }\n\t\t\t\tkey={ el.dataset.id }\n\t\t\t\tisSelected={ selected.element?.id === el.dataset.id }\n\t\t\t/>\n\t\t) )\n\t);\n}\n","import * as React from 'react';\nimport { Box, styled } from '@elementor/ui';\nimport { FloatingPortal, useHover, useInteractions } from '@floating-ui/react';\n\nimport { useBindReactPropsToElement } from '../hooks/use-bind-react-props-to-element';\nimport { useFloatingOnElement } from '../hooks/use-floating-on-element';\n\nexport const CANVAS_WRAPPER_ID = 'elementor-preview-responsive-wrapper';\n\ntype Props = {\n\telement: HTMLElement;\n\tisSelected: boolean;\n};\n\nconst OverlayBox = styled( Box, { shouldForwardProp: ( prop ) => prop !== 'isSelected' } )<\n\tPick< Props, 'isSelected' >\n>( ( { theme, isSelected } ) => ( {\n\toutline: `${ isSelected ? '2px' : '1px' } solid ${ theme.palette.primary.light }`,\n\toutlineOffset: isSelected ? '-2px' : '-1px',\n\tpointerEvents: 'none',\n} ) );\n\nexport function ElementOverlay( { element, isSelected }: Props ) {\n\tconst { context, floating, isVisible } = useFloatingOnElement( { element, isSelected } );\n\tconst { getFloatingProps, getReferenceProps } = useInteractions( [ useHover( context ) ] );\n\n\tuseBindReactPropsToElement( element, getReferenceProps );\n\n\treturn (\n\t\tisVisible && (\n\t\t\t<FloatingPortal id={ CANVAS_WRAPPER_ID }>\n\t\t\t\t<OverlayBox\n\t\t\t\t\tref={ floating.setRef }\n\t\t\t\t\tisSelected={ isSelected }\n\t\t\t\t\tstyle={ floating.styles }\n\t\t\t\t\tdata-element-overlay={ element.dataset.id }\n\t\t\t\t\trole=\"presentation\"\n\t\t\t\t\t{ ...getFloatingProps() }\n\t\t\t\t/>\n\t\t\t</FloatingPortal>\n\t\t)\n\t);\n}\n","import { useEffect } from 'react';\n\ntype Props = Record< string, unknown >;\n\nexport function useBindReactPropsToElement( element: HTMLElement, getProps: () => Props ) {\n\tuseEffect( () => {\n\t\tconst el = element;\n\n\t\tconst { events, attrs } = groupProps( getProps() );\n\n\t\tevents.forEach( ( [ eventName, listener ] ) => el.addEventListener( eventName, listener ) );\n\t\tattrs.forEach( ( [ attrName, attrValue ] ) => el.setAttribute( attrName, attrValue ) );\n\n\t\treturn () => {\n\t\t\tevents.forEach( ( [ eventName, listener ] ) => el.removeEventListener( eventName, listener ) );\n\t\t\tattrs.forEach( ( [ attrName ] ) => el.removeAttribute( attrName ) );\n\t\t};\n\t}, [ getProps, element ] );\n}\n\ntype GroupedProps = {\n\tevents: Array< [ string, () => void ] >;\n\tattrs: Array< [ string, string ] >;\n};\n\nfunction groupProps( props: Props ) {\n\tconst eventRegex = /^on(?=[A-Z])/;\n\n\treturn Object.entries( props ).reduce< GroupedProps >(\n\t\t( acc, [ propName, propValue ] ) => {\n\t\t\tif ( ! eventRegex.test( propName ) ) {\n\t\t\t\tacc.attrs.push( [ propName, propValue as string ] );\n\n\t\t\t\treturn acc;\n\t\t\t}\n\n\t\t\tconst eventName = propName.replace( eventRegex, '' ).toLowerCase();\n\t\t\tconst listener = propValue as () => void;\n\n\t\t\tacc.events.push( [ eventName, listener ] );\n\n\t\t\treturn acc;\n\t\t},\n\t\t{\n\t\t\tevents: [],\n\t\t\tattrs: [],\n\t\t}\n\t);\n}\n","import { useState } from 'react';\nimport { autoUpdate, offset, size, useFloating } from '@floating-ui/react';\n\ntype Options = {\n\telement: HTMLElement;\n\tisSelected: boolean;\n};\n\nexport function useFloatingOnElement( { element, isSelected }: Options ) {\n\tconst [ isOpen, setIsOpen ] = useState( false );\n\n\tconst { refs, floatingStyles, context } = useFloating( {\n\t\t// Must be controlled for interactions (like hover) to work.\n\t\topen: isOpen || isSelected,\n\t\tonOpenChange: setIsOpen,\n\n\t\t// Add an animation frame to support scroll events (without it the floating element will stay in the same position).\n\t\twhileElementsMounted: ( ...args ) => autoUpdate( ...args, { animationFrame: true } ),\n\n\t\t// The first element in the canvas is `display: contents` so we need to use the first child.\n\t\telements: { reference: element.firstElementChild },\n\n\t\tmiddleware: [\n\t\t\t// Match the floating element's size to the reference element.\n\t\t\tsize( {\n\t\t\t\tapply( { elements, rects } ) {\n\t\t\t\t\tObject.assign( elements.floating.style, {\n\t\t\t\t\t\twidth: `${ rects.reference.width }px`,\n\t\t\t\t\t\theight: `${ rects.reference.height }px`,\n\t\t\t\t\t} );\n\t\t\t\t},\n\t\t\t} ),\n\n\t\t\t// Center the floating element on the reference element.\n\t\t\toffset( ( { rects } ) => -rects.reference.height / 2 - rects.floating.height / 2 ),\n\t\t],\n\t} );\n\n\treturn {\n\t\tisVisible: isOpen || isSelected,\n\t\tcontext,\n\t\tfloating: {\n\t\t\tsetRef: refs.setFloating,\n\t\t\tref: refs.floating,\n\t\t\tstyles: floatingStyles,\n\t\t},\n\t};\n}\n","import { init } from './init';\n\ninit();\n"],"mappings":"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","names":["React","import_react","import_react","useIsPreviewMode","useIsRouteActive"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,140 @@
1
+ // src/init.tsx
2
+ import { injectIntoTop } from "@elementor/editor";
3
+
4
+ // src/components/elements-overlays.tsx
5
+ import * as React2 from "react";
6
+ import { useElementsDomRef, useSelectedElement } from "@elementor/editor-elements";
7
+ import {
8
+ __privateUseIsPreviewMode as useIsPreviewMode,
9
+ __privateUseIsRouteActive as useIsRouteActive
10
+ } from "@elementor/editor-v1-adapters";
11
+
12
+ // src/components/element-overlay.tsx
13
+ import * as React from "react";
14
+ import { Box, styled } from "@elementor/ui";
15
+ import { FloatingPortal, useHover, useInteractions } from "@floating-ui/react";
16
+
17
+ // src/hooks/use-bind-react-props-to-element.ts
18
+ import { useEffect } from "react";
19
+ function useBindReactPropsToElement(element, getProps) {
20
+ useEffect(() => {
21
+ const el = element;
22
+ const { events, attrs } = groupProps(getProps());
23
+ events.forEach(([eventName, listener]) => el.addEventListener(eventName, listener));
24
+ attrs.forEach(([attrName, attrValue]) => el.setAttribute(attrName, attrValue));
25
+ return () => {
26
+ events.forEach(([eventName, listener]) => el.removeEventListener(eventName, listener));
27
+ attrs.forEach(([attrName]) => el.removeAttribute(attrName));
28
+ };
29
+ }, [getProps, element]);
30
+ }
31
+ function groupProps(props) {
32
+ const eventRegex = /^on(?=[A-Z])/;
33
+ return Object.entries(props).reduce(
34
+ (acc, [propName, propValue]) => {
35
+ if (!eventRegex.test(propName)) {
36
+ acc.attrs.push([propName, propValue]);
37
+ return acc;
38
+ }
39
+ const eventName = propName.replace(eventRegex, "").toLowerCase();
40
+ const listener = propValue;
41
+ acc.events.push([eventName, listener]);
42
+ return acc;
43
+ },
44
+ {
45
+ events: [],
46
+ attrs: []
47
+ }
48
+ );
49
+ }
50
+
51
+ // src/hooks/use-floating-on-element.ts
52
+ import { useState } from "react";
53
+ import { autoUpdate, offset, size, useFloating } from "@floating-ui/react";
54
+ function useFloatingOnElement({ element, isSelected }) {
55
+ const [isOpen, setIsOpen] = useState(false);
56
+ const { refs, floatingStyles, context } = useFloating({
57
+ // Must be controlled for interactions (like hover) to work.
58
+ open: isOpen || isSelected,
59
+ onOpenChange: setIsOpen,
60
+ // Add an animation frame to support scroll events (without it the floating element will stay in the same position).
61
+ whileElementsMounted: (...args) => autoUpdate(...args, { animationFrame: true }),
62
+ // The first element in the canvas is `display: contents` so we need to use the first child.
63
+ elements: { reference: element.firstElementChild },
64
+ middleware: [
65
+ // Match the floating element's size to the reference element.
66
+ size({
67
+ apply({ elements, rects }) {
68
+ Object.assign(elements.floating.style, {
69
+ width: `${rects.reference.width}px`,
70
+ height: `${rects.reference.height}px`
71
+ });
72
+ }
73
+ }),
74
+ // Center the floating element on the reference element.
75
+ offset(({ rects }) => -rects.reference.height / 2 - rects.floating.height / 2)
76
+ ]
77
+ });
78
+ return {
79
+ isVisible: isOpen || isSelected,
80
+ context,
81
+ floating: {
82
+ setRef: refs.setFloating,
83
+ ref: refs.floating,
84
+ styles: floatingStyles
85
+ }
86
+ };
87
+ }
88
+
89
+ // src/components/element-overlay.tsx
90
+ var CANVAS_WRAPPER_ID = "elementor-preview-responsive-wrapper";
91
+ var OverlayBox = styled(Box, { shouldForwardProp: (prop) => prop !== "isSelected" })(({ theme, isSelected }) => ({
92
+ outline: `${isSelected ? "2px" : "1px"} solid ${theme.palette.primary.light}`,
93
+ outlineOffset: isSelected ? "-2px" : "-1px",
94
+ pointerEvents: "none"
95
+ }));
96
+ function ElementOverlay({ element, isSelected }) {
97
+ const { context, floating, isVisible } = useFloatingOnElement({ element, isSelected });
98
+ const { getFloatingProps, getReferenceProps } = useInteractions([useHover(context)]);
99
+ useBindReactPropsToElement(element, getReferenceProps);
100
+ return isVisible && /* @__PURE__ */ React.createElement(FloatingPortal, { id: CANVAS_WRAPPER_ID }, /* @__PURE__ */ React.createElement(
101
+ OverlayBox,
102
+ {
103
+ ref: floating.setRef,
104
+ isSelected,
105
+ style: floating.styles,
106
+ "data-element-overlay": element.dataset.id,
107
+ role: "presentation",
108
+ ...getFloatingProps()
109
+ }
110
+ ));
111
+ }
112
+
113
+ // src/components/elements-overlays.tsx
114
+ function ElementsOverlays() {
115
+ const selected = useSelectedElement();
116
+ const domElements = useElementsDomRef();
117
+ const isPreviewMode = useIsPreviewMode();
118
+ const isKitRouteActive = useIsRouteActive("panel/global");
119
+ const isActive = !isPreviewMode && !isKitRouteActive;
120
+ return isActive && domElements.map((el) => /* @__PURE__ */ React2.createElement(
121
+ ElementOverlay,
122
+ {
123
+ element: el,
124
+ key: el.dataset.id,
125
+ isSelected: selected.element?.id === el.dataset.id
126
+ }
127
+ ));
128
+ }
129
+
130
+ // src/init.tsx
131
+ function init() {
132
+ injectIntoTop({
133
+ id: "elements-overlays",
134
+ component: ElementsOverlays
135
+ });
136
+ }
137
+
138
+ // src/index.ts
139
+ init();
140
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/init.tsx","../src/components/elements-overlays.tsx","../src/components/element-overlay.tsx","../src/hooks/use-bind-react-props-to-element.ts","../src/hooks/use-floating-on-element.ts","../src/index.ts"],"sourcesContent":["import { injectIntoTop } from '@elementor/editor';\n\nimport { ElementsOverlays } from './components/elements-overlays';\n\nexport function init() {\n\tinjectIntoTop( {\n\t\tid: 'elements-overlays',\n\t\tcomponent: ElementsOverlays,\n\t} );\n}\n","import * as React from 'react';\nimport { useElementsDomRef, useSelectedElement } from '@elementor/editor-elements';\nimport {\n\t__privateUseIsPreviewMode as useIsPreviewMode,\n\t__privateUseIsRouteActive as useIsRouteActive,\n} from '@elementor/editor-v1-adapters';\n\nimport { ElementOverlay } from './element-overlay';\n\nexport function ElementsOverlays() {\n\tconst selected = useSelectedElement();\n\tconst domElements = useElementsDomRef();\n\n\tconst isPreviewMode = useIsPreviewMode();\n\tconst isKitRouteActive = useIsRouteActive( 'panel/global' );\n\n\tconst isActive = ! isPreviewMode && ! isKitRouteActive;\n\n\treturn (\n\t\tisActive &&\n\t\tdomElements.map( ( el ) => (\n\t\t\t<ElementOverlay\n\t\t\t\telement={ el }\n\t\t\t\tkey={ el.dataset.id }\n\t\t\t\tisSelected={ selected.element?.id === el.dataset.id }\n\t\t\t/>\n\t\t) )\n\t);\n}\n","import * as React from 'react';\nimport { Box, styled } from '@elementor/ui';\nimport { FloatingPortal, useHover, useInteractions } from '@floating-ui/react';\n\nimport { useBindReactPropsToElement } from '../hooks/use-bind-react-props-to-element';\nimport { useFloatingOnElement } from '../hooks/use-floating-on-element';\n\nexport const CANVAS_WRAPPER_ID = 'elementor-preview-responsive-wrapper';\n\ntype Props = {\n\telement: HTMLElement;\n\tisSelected: boolean;\n};\n\nconst OverlayBox = styled( Box, { shouldForwardProp: ( prop ) => prop !== 'isSelected' } )<\n\tPick< Props, 'isSelected' >\n>( ( { theme, isSelected } ) => ( {\n\toutline: `${ isSelected ? '2px' : '1px' } solid ${ theme.palette.primary.light }`,\n\toutlineOffset: isSelected ? '-2px' : '-1px',\n\tpointerEvents: 'none',\n} ) );\n\nexport function ElementOverlay( { element, isSelected }: Props ) {\n\tconst { context, floating, isVisible } = useFloatingOnElement( { element, isSelected } );\n\tconst { getFloatingProps, getReferenceProps } = useInteractions( [ useHover( context ) ] );\n\n\tuseBindReactPropsToElement( element, getReferenceProps );\n\n\treturn (\n\t\tisVisible && (\n\t\t\t<FloatingPortal id={ CANVAS_WRAPPER_ID }>\n\t\t\t\t<OverlayBox\n\t\t\t\t\tref={ floating.setRef }\n\t\t\t\t\tisSelected={ isSelected }\n\t\t\t\t\tstyle={ floating.styles }\n\t\t\t\t\tdata-element-overlay={ element.dataset.id }\n\t\t\t\t\trole=\"presentation\"\n\t\t\t\t\t{ ...getFloatingProps() }\n\t\t\t\t/>\n\t\t\t</FloatingPortal>\n\t\t)\n\t);\n}\n","import { useEffect } from 'react';\n\ntype Props = Record< string, unknown >;\n\nexport function useBindReactPropsToElement( element: HTMLElement, getProps: () => Props ) {\n\tuseEffect( () => {\n\t\tconst el = element;\n\n\t\tconst { events, attrs } = groupProps( getProps() );\n\n\t\tevents.forEach( ( [ eventName, listener ] ) => el.addEventListener( eventName, listener ) );\n\t\tattrs.forEach( ( [ attrName, attrValue ] ) => el.setAttribute( attrName, attrValue ) );\n\n\t\treturn () => {\n\t\t\tevents.forEach( ( [ eventName, listener ] ) => el.removeEventListener( eventName, listener ) );\n\t\t\tattrs.forEach( ( [ attrName ] ) => el.removeAttribute( attrName ) );\n\t\t};\n\t}, [ getProps, element ] );\n}\n\ntype GroupedProps = {\n\tevents: Array< [ string, () => void ] >;\n\tattrs: Array< [ string, string ] >;\n};\n\nfunction groupProps( props: Props ) {\n\tconst eventRegex = /^on(?=[A-Z])/;\n\n\treturn Object.entries( props ).reduce< GroupedProps >(\n\t\t( acc, [ propName, propValue ] ) => {\n\t\t\tif ( ! eventRegex.test( propName ) ) {\n\t\t\t\tacc.attrs.push( [ propName, propValue as string ] );\n\n\t\t\t\treturn acc;\n\t\t\t}\n\n\t\t\tconst eventName = propName.replace( eventRegex, '' ).toLowerCase();\n\t\t\tconst listener = propValue as () => void;\n\n\t\t\tacc.events.push( [ eventName, listener ] );\n\n\t\t\treturn acc;\n\t\t},\n\t\t{\n\t\t\tevents: [],\n\t\t\tattrs: [],\n\t\t}\n\t);\n}\n","import { useState } from 'react';\nimport { autoUpdate, offset, size, useFloating } from '@floating-ui/react';\n\ntype Options = {\n\telement: HTMLElement;\n\tisSelected: boolean;\n};\n\nexport function useFloatingOnElement( { element, isSelected }: Options ) {\n\tconst [ isOpen, setIsOpen ] = useState( false );\n\n\tconst { refs, floatingStyles, context } = useFloating( {\n\t\t// Must be controlled for interactions (like hover) to work.\n\t\topen: isOpen || isSelected,\n\t\tonOpenChange: setIsOpen,\n\n\t\t// Add an animation frame to support scroll events (without it the floating element will stay in the same position).\n\t\twhileElementsMounted: ( ...args ) => autoUpdate( ...args, { animationFrame: true } ),\n\n\t\t// The first element in the canvas is `display: contents` so we need to use the first child.\n\t\telements: { reference: element.firstElementChild },\n\n\t\tmiddleware: [\n\t\t\t// Match the floating element's size to the reference element.\n\t\t\tsize( {\n\t\t\t\tapply( { elements, rects } ) {\n\t\t\t\t\tObject.assign( elements.floating.style, {\n\t\t\t\t\t\twidth: `${ rects.reference.width }px`,\n\t\t\t\t\t\theight: `${ rects.reference.height }px`,\n\t\t\t\t\t} );\n\t\t\t\t},\n\t\t\t} ),\n\n\t\t\t// Center the floating element on the reference element.\n\t\t\toffset( ( { rects } ) => -rects.reference.height / 2 - rects.floating.height / 2 ),\n\t\t],\n\t} );\n\n\treturn {\n\t\tisVisible: isOpen || isSelected,\n\t\tcontext,\n\t\tfloating: {\n\t\t\tsetRef: refs.setFloating,\n\t\t\tref: refs.floating,\n\t\t\tstyles: floatingStyles,\n\t\t},\n\t};\n}\n","import { init } from './init';\n\ninit();\n"],"mappings":"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","names":["React"]}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@elementor/editor-canvas",
3
+ "description": "Elementor Editor Canvas",
4
+ "version": "0.1.0",
5
+ "private": false,
6
+ "author": "Elementor Team",
7
+ "homepage": "https://elementor.com/",
8
+ "license": "GPL-3.0-or-later",
9
+ "main": "dist/index.js",
10
+ "module": "dist/index.mjs",
11
+ "types": "dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.mjs",
16
+ "require": "./dist/index.js"
17
+ },
18
+ "./package.json": "./package.json"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/elementor/elementor-packages.git",
23
+ "directory": "packages/core/editor-canvas"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/elementor/elementor-packages/issues"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "scripts": {
32
+ "build": "tsup --config=../../tsup.build.ts",
33
+ "dev": "tsup --config=../../tsup.dev.ts"
34
+ },
35
+ "peerDependencies": {
36
+ "react": "^18.3.1"
37
+ },
38
+ "dependencies": {
39
+ "@elementor/editor": "^0.17.0",
40
+ "@elementor/ui": "^1.21.9",
41
+ "@floating-ui/react": "^0.26.28",
42
+ "@elementor/editor-elements": "^0.3.0",
43
+ "@elementor/editor-v1-adapters": "^0.8.4"
44
+ }
45
+ }
@@ -0,0 +1,127 @@
1
+ import * as React from 'react';
2
+ import { createDOMElement, createMockElementType } from 'test-utils';
3
+ import { useElementsDomRef, useSelectedElement } from '@elementor/editor-elements';
4
+ import {
5
+ __privateUseIsPreviewMode as useIsPreviewMode,
6
+ __privateUseIsRouteActive as useIsRouteActive,
7
+ } from '@elementor/editor-v1-adapters';
8
+ import { render, screen } from '@testing-library/react';
9
+
10
+ import { CANVAS_WRAPPER_ID } from '../element-overlay';
11
+ import { ElementsOverlays } from '../elements-overlays';
12
+
13
+ jest.mock( '@elementor/editor-elements' );
14
+ jest.mock( '@elementor/editor-v1-adapters' );
15
+
16
+ describe( '<ElementsOverlays />', () => {
17
+ beforeEach( () => {
18
+ const atomicElements = [
19
+ createDOMElement( {
20
+ tag: 'div',
21
+ attrs: {
22
+ 'data-id': '10',
23
+ 'data-atomic': '',
24
+ },
25
+ } ),
26
+ createDOMElement( {
27
+ tag: 'span',
28
+ attrs: {
29
+ 'data-id': '20',
30
+ 'data-atomic': '',
31
+ },
32
+ } ),
33
+ ];
34
+
35
+ window.document.body.appendChild(
36
+ createDOMElement( {
37
+ tag: 'div',
38
+ attrs: {
39
+ 'data-testid': CANVAS_WRAPPER_ID,
40
+ id: CANVAS_WRAPPER_ID,
41
+ },
42
+ children: [
43
+ createDOMElement( {
44
+ tag: 'iframe',
45
+ children: atomicElements,
46
+ } ),
47
+ ],
48
+ } )
49
+ );
50
+
51
+ jest.mocked( useIsPreviewMode ).mockReturnValue( false );
52
+ jest.mocked( useIsRouteActive ).mockReturnValue( false );
53
+
54
+ jest.mocked( useElementsDomRef ).mockImplementation( () => atomicElements );
55
+ jest.mocked( useSelectedElement ).mockReturnValue( { element: null, elementType: null } );
56
+ } );
57
+
58
+ afterEach( () => {
59
+ window.document.body.innerHTML = '';
60
+ } );
61
+
62
+ it( 'should render an overlay on atomic element when the element is selected', () => {
63
+ // Arrange.
64
+ jest.mocked( useSelectedElement ).mockReturnValue( {
65
+ element: { id: '20', type: 'widget' },
66
+ elementType: createMockElementType(),
67
+ } );
68
+
69
+ // Act.
70
+ render( <ElementsOverlays /> );
71
+
72
+ // Assert.
73
+ const overlay = screen.getByRole( 'presentation' );
74
+
75
+ expect( overlay ).toHaveAttribute( 'data-element-overlay', '20' );
76
+ expect( screen.getByTestId( CANVAS_WRAPPER_ID ) ).toContainElement( overlay );
77
+ } );
78
+
79
+ it.each( [
80
+ {
81
+ message: 'preview mode is active',
82
+ payload: {
83
+ isPreviewMode: true,
84
+ isKitRouteActive: false,
85
+ selected: { id: '20', type: 'widget' },
86
+ },
87
+ },
88
+ {
89
+ message: 'kit route is active',
90
+ payload: {
91
+ isPreviewMode: false,
92
+ isKitRouteActive: true,
93
+ selected: { id: '20', type: 'widget' },
94
+ },
95
+ },
96
+ {
97
+ message: 'nothing selected or hovered',
98
+ payload: {
99
+ isPreviewMode: false,
100
+ isKitRouteActive: false,
101
+ selected: null,
102
+ },
103
+ },
104
+ ] )( 'should not render overlays if $message', ( { payload } ) => {
105
+ // Arrange.
106
+ jest.mocked( useIsPreviewMode ).mockReturnValue( payload.isPreviewMode );
107
+ jest.mocked( useIsRouteActive ).mockReturnValue( payload.isKitRouteActive );
108
+
109
+ jest.mocked( useSelectedElement ).mockReturnValue(
110
+ payload.selected
111
+ ? {
112
+ element: payload.selected,
113
+ elementType: createMockElementType(),
114
+ }
115
+ : {
116
+ element: null,
117
+ elementType: null,
118
+ }
119
+ );
120
+
121
+ // Act.
122
+ render( <ElementsOverlays /> );
123
+
124
+ // Assert.
125
+ expect( screen.queryByRole( 'presentation' ) ).not.toBeInTheDocument();
126
+ } );
127
+ } );
@@ -0,0 +1,43 @@
1
+ import * as React from 'react';
2
+ import { Box, styled } from '@elementor/ui';
3
+ import { FloatingPortal, useHover, useInteractions } from '@floating-ui/react';
4
+
5
+ import { useBindReactPropsToElement } from '../hooks/use-bind-react-props-to-element';
6
+ import { useFloatingOnElement } from '../hooks/use-floating-on-element';
7
+
8
+ export const CANVAS_WRAPPER_ID = 'elementor-preview-responsive-wrapper';
9
+
10
+ type Props = {
11
+ element: HTMLElement;
12
+ isSelected: boolean;
13
+ };
14
+
15
+ const OverlayBox = styled( Box, { shouldForwardProp: ( prop ) => prop !== 'isSelected' } )<
16
+ Pick< Props, 'isSelected' >
17
+ >( ( { theme, isSelected } ) => ( {
18
+ outline: `${ isSelected ? '2px' : '1px' } solid ${ theme.palette.primary.light }`,
19
+ outlineOffset: isSelected ? '-2px' : '-1px',
20
+ pointerEvents: 'none',
21
+ } ) );
22
+
23
+ export function ElementOverlay( { element, isSelected }: Props ) {
24
+ const { context, floating, isVisible } = useFloatingOnElement( { element, isSelected } );
25
+ const { getFloatingProps, getReferenceProps } = useInteractions( [ useHover( context ) ] );
26
+
27
+ useBindReactPropsToElement( element, getReferenceProps );
28
+
29
+ return (
30
+ isVisible && (
31
+ <FloatingPortal id={ CANVAS_WRAPPER_ID }>
32
+ <OverlayBox
33
+ ref={ floating.setRef }
34
+ isSelected={ isSelected }
35
+ style={ floating.styles }
36
+ data-element-overlay={ element.dataset.id }
37
+ role="presentation"
38
+ { ...getFloatingProps() }
39
+ />
40
+ </FloatingPortal>
41
+ )
42
+ );
43
+ }
@@ -0,0 +1,29 @@
1
+ import * as React from 'react';
2
+ import { useElementsDomRef, useSelectedElement } from '@elementor/editor-elements';
3
+ import {
4
+ __privateUseIsPreviewMode as useIsPreviewMode,
5
+ __privateUseIsRouteActive as useIsRouteActive,
6
+ } from '@elementor/editor-v1-adapters';
7
+
8
+ import { ElementOverlay } from './element-overlay';
9
+
10
+ export function ElementsOverlays() {
11
+ const selected = useSelectedElement();
12
+ const domElements = useElementsDomRef();
13
+
14
+ const isPreviewMode = useIsPreviewMode();
15
+ const isKitRouteActive = useIsRouteActive( 'panel/global' );
16
+
17
+ const isActive = ! isPreviewMode && ! isKitRouteActive;
18
+
19
+ return (
20
+ isActive &&
21
+ domElements.map( ( el ) => (
22
+ <ElementOverlay
23
+ element={ el }
24
+ key={ el.dataset.id }
25
+ isSelected={ selected.element?.id === el.dataset.id }
26
+ />
27
+ ) )
28
+ );
29
+ }
@@ -0,0 +1,49 @@
1
+ import { useEffect } from 'react';
2
+
3
+ type Props = Record< string, unknown >;
4
+
5
+ export function useBindReactPropsToElement( element: HTMLElement, getProps: () => Props ) {
6
+ useEffect( () => {
7
+ const el = element;
8
+
9
+ const { events, attrs } = groupProps( getProps() );
10
+
11
+ events.forEach( ( [ eventName, listener ] ) => el.addEventListener( eventName, listener ) );
12
+ attrs.forEach( ( [ attrName, attrValue ] ) => el.setAttribute( attrName, attrValue ) );
13
+
14
+ return () => {
15
+ events.forEach( ( [ eventName, listener ] ) => el.removeEventListener( eventName, listener ) );
16
+ attrs.forEach( ( [ attrName ] ) => el.removeAttribute( attrName ) );
17
+ };
18
+ }, [ getProps, element ] );
19
+ }
20
+
21
+ type GroupedProps = {
22
+ events: Array< [ string, () => void ] >;
23
+ attrs: Array< [ string, string ] >;
24
+ };
25
+
26
+ function groupProps( props: Props ) {
27
+ const eventRegex = /^on(?=[A-Z])/;
28
+
29
+ return Object.entries( props ).reduce< GroupedProps >(
30
+ ( acc, [ propName, propValue ] ) => {
31
+ if ( ! eventRegex.test( propName ) ) {
32
+ acc.attrs.push( [ propName, propValue as string ] );
33
+
34
+ return acc;
35
+ }
36
+
37
+ const eventName = propName.replace( eventRegex, '' ).toLowerCase();
38
+ const listener = propValue as () => void;
39
+
40
+ acc.events.push( [ eventName, listener ] );
41
+
42
+ return acc;
43
+ },
44
+ {
45
+ events: [],
46
+ attrs: [],
47
+ }
48
+ );
49
+ }
@@ -0,0 +1,48 @@
1
+ import { useState } from 'react';
2
+ import { autoUpdate, offset, size, useFloating } from '@floating-ui/react';
3
+
4
+ type Options = {
5
+ element: HTMLElement;
6
+ isSelected: boolean;
7
+ };
8
+
9
+ export function useFloatingOnElement( { element, isSelected }: Options ) {
10
+ const [ isOpen, setIsOpen ] = useState( false );
11
+
12
+ const { refs, floatingStyles, context } = useFloating( {
13
+ // Must be controlled for interactions (like hover) to work.
14
+ open: isOpen || isSelected,
15
+ onOpenChange: setIsOpen,
16
+
17
+ // Add an animation frame to support scroll events (without it the floating element will stay in the same position).
18
+ whileElementsMounted: ( ...args ) => autoUpdate( ...args, { animationFrame: true } ),
19
+
20
+ // The first element in the canvas is `display: contents` so we need to use the first child.
21
+ elements: { reference: element.firstElementChild },
22
+
23
+ middleware: [
24
+ // Match the floating element's size to the reference element.
25
+ size( {
26
+ apply( { elements, rects } ) {
27
+ Object.assign( elements.floating.style, {
28
+ width: `${ rects.reference.width }px`,
29
+ height: `${ rects.reference.height }px`,
30
+ } );
31
+ },
32
+ } ),
33
+
34
+ // Center the floating element on the reference element.
35
+ offset( ( { rects } ) => -rects.reference.height / 2 - rects.floating.height / 2 ),
36
+ ],
37
+ } );
38
+
39
+ return {
40
+ isVisible: isOpen || isSelected,
41
+ context,
42
+ floating: {
43
+ setRef: refs.setFloating,
44
+ ref: refs.floating,
45
+ styles: floatingStyles,
46
+ },
47
+ };
48
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { init } from './init';
2
+
3
+ init();
package/src/init.tsx ADDED
@@ -0,0 +1,10 @@
1
+ import { injectIntoTop } from '@elementor/editor';
2
+
3
+ import { ElementsOverlays } from './components/elements-overlays';
4
+
5
+ export function init() {
6
+ injectIntoTop( {
7
+ id: 'elements-overlays',
8
+ component: ElementsOverlays,
9
+ } );
10
+ }