@elementor/editor-canvas 0.13.0 → 0.14.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.
@@ -1,12 +1,17 @@
1
1
  import * as React from 'react';
2
- import { useElementsDomRef, useSelectedElement } from '@elementor/editor-elements';
3
- import { __privateUseIsRouteActive as useIsRouteActive, useEditMode } from '@elementor/editor-v1-adapters';
2
+ import { getElements, useSelectedElement } from '@elementor/editor-elements';
3
+ import {
4
+ __privateUseIsRouteActive as useIsRouteActive,
5
+ __privateUseListenTo as useListenTo,
6
+ useEditMode,
7
+ windowEvent,
8
+ } from '@elementor/editor-v1-adapters';
4
9
 
5
10
  import { ElementOverlay } from './element-overlay';
6
11
 
7
12
  export function ElementsOverlays() {
8
13
  const selected = useSelectedElement();
9
- const domElements = useElementsDomRef();
14
+ const elements = useElementsDom();
10
15
  const currentEditMode = useEditMode();
11
16
 
12
17
  const isEditMode = currentEditMode === 'edit';
@@ -16,12 +21,24 @@ export function ElementsOverlays() {
16
21
 
17
22
  return (
18
23
  isActive &&
19
- domElements.map( ( el ) => (
20
- <ElementOverlay
21
- element={ el }
22
- key={ el.dataset.id }
23
- isSelected={ selected.element?.id === el.dataset.id }
24
- />
24
+ elements.map( ( [ id, element ] ) => (
25
+ <ElementOverlay key={ id } id={ id } element={ element } isSelected={ selected.element?.id === id } />
25
26
  ) )
26
27
  );
27
28
  }
29
+
30
+ const ELEMENTS_DATA_ATTR = 'atomic';
31
+
32
+ type IdElementTuple = [ string, HTMLElement ];
33
+
34
+ function useElementsDom() {
35
+ return useListenTo(
36
+ [ windowEvent( 'elementor/editor/element-rendered' ), windowEvent( 'elementor/editor/element-destroyed' ) ],
37
+ () => {
38
+ return getElements()
39
+ .filter( ( el ) => ELEMENTS_DATA_ATTR in ( el.view?.el?.dataset ?? {} ) )
40
+ .map( ( element ) => [ element.id, element.view?.getDomElement?.()?.get?.( 0 ) ] )
41
+ .filter( ( item ): item is IdElementTuple => !! item[ 1 ] );
42
+ }
43
+ );
44
+ }
@@ -1,4 +1,4 @@
1
- import { useState } from 'react';
1
+ import { useEffect, useState } from 'react';
2
2
  import { autoUpdate, offset, size, useFloating } from '@floating-ui/react';
3
3
 
4
4
  type Options = {
@@ -14,11 +14,7 @@ export function useFloatingOnElement( { element, isSelected }: Options ) {
14
14
  open: isOpen || isSelected,
15
15
  onOpenChange: setIsOpen,
16
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 },
17
+ whileElementsMounted: autoUpdate,
22
18
 
23
19
  middleware: [
24
20
  // Match the floating element's size to the reference element.
@@ -36,6 +32,13 @@ export function useFloatingOnElement( { element, isSelected }: Options ) {
36
32
  ],
37
33
  } );
38
34
 
35
+ useEffect( () => {
36
+ // Update the reference manually because Floating UI does not recalculate
37
+ // the reference element when it is being used in `option.elements.reference`.
38
+ // @link https://github.com/floating-ui/floating-ui/blob/master/packages/react/src/hooks/useFloatingRootContext.ts
39
+ refs.setReference( element );
40
+ }, [ element, refs ] );
41
+
39
42
  return {
40
43
  isVisible: isOpen || isSelected,
41
44
  context,
@@ -7,13 +7,9 @@ import { plainTransformer } from './transformers/shared/plain-transformer';
7
7
 
8
8
  export function initSettingsTransformers() {
9
9
  settingsTransformersRegistry
10
- .register( 'string', plainTransformer )
11
- .register( 'url', plainTransformer )
12
- .register( 'number', plainTransformer )
13
- .register( 'boolean', plainTransformer )
14
10
  .register( 'classes', arrayTransformer )
15
11
  .register( 'link', linkTransformer )
16
12
  .register( 'image', imageTransformer )
17
13
  .register( 'image-src', imageSrcTransformer )
18
- .register( 'image-attachment-id', plainTransformer );
14
+ .registerFallback( plainTransformer );
19
15
  }
@@ -27,10 +27,6 @@ export function initStyleTransformers() {
27
27
  ( { propKey, key } ) => `${ propKey }-${ key }`
28
28
  )
29
29
  )
30
- .register( 'color', plainTransformer )
31
- .register( 'number', plainTransformer )
32
- .register( 'string', plainTransformer )
33
- .register( 'url', plainTransformer )
34
30
  .register( 'box-shadow', createCombineArrayTransformer( ',' ) )
35
31
  .register( 'background', backgroundTransformer )
36
32
  .register( 'background-overlay', createCombineArrayTransformer( ',' ) )
@@ -41,7 +37,6 @@ export function initStyleTransformers() {
41
37
  .register( 'color-stop', colorStopTransformer )
42
38
  .register( 'background-image-position-offset', backgroundImagePositionOffsetTransformer )
43
39
  .register( 'background-image-size-scale', backgroundImageSizeScaleTransformer )
44
- .register( 'image-attachment-id', plainTransformer )
45
40
  .register( 'image-src', imageSrcTransformer )
46
41
  .register( 'image', imageTransformer )
47
42
  .register(
@@ -61,5 +56,6 @@ export function initStyleTransformers() {
61
56
  [ 'start-start', 'start-end', 'end-start', 'end-end' ],
62
57
  ( { key } ) => `border-${ key }-radius`
63
58
  )
64
- );
59
+ )
60
+ .registerFallback( plainTransformer );
65
61
  }
@@ -17,7 +17,7 @@ export function initStylesRenderer() {
17
17
  let abortController: AbortController | null = null;
18
18
 
19
19
  const resolve = createPropsResolver( {
20
- transformers: styleTransformersRegistry.all(),
20
+ transformers: styleTransformersRegistry,
21
21
  schema: getStylesSchema(),
22
22
  onPropResolve: enqueueUsedFonts,
23
23
  } );
@@ -0,0 +1,66 @@
1
+ /* eslint-disable testing-library/render-result-naming-convention */
2
+ import { createDomRenderer } from '../create-dom-renderer';
3
+
4
+ describe( 'createDomRenderer', () => {
5
+ it.each( [
6
+ {
7
+ title: 'basic string',
8
+ template: 'Hello {{ name }}',
9
+ context: { name: 'StyleShit' },
10
+ expected: 'Hello StyleShit',
11
+ },
12
+ {
13
+ title: 'allowed html tags',
14
+ template: `<{{ tag | e( 'html_tag' ) }}></{{ tag | e( 'html_tag' ) }}>`,
15
+ context: { tag: 'a' },
16
+ expected: '<a></a>',
17
+ },
18
+ {
19
+ title: 'disallowed html tags',
20
+ template: `<{{ tag | e( 'html_tag' ) }}></{{ tag | e( 'html_tag' ) }}>`,
21
+ context: { tag: 'script' },
22
+ expected: '<div></div>',
23
+ },
24
+ {
25
+ title: 'allowed url (http)',
26
+ template: `{{ url | e( 'full_url' ) }}`,
27
+ context: { url: 'http://localhost/test-page' },
28
+ expected: 'http://localhost/test-page',
29
+ },
30
+ {
31
+ title: 'allowed url (https)',
32
+ template: `{{ url | e( 'full_url' ) }}`,
33
+ context: { url: 'https://localhost/test-page' },
34
+ expected: 'https://localhost/test-page',
35
+ },
36
+ {
37
+ title: 'allowed url (tel)',
38
+ template: `{{ url | e( 'full_url' ) }}`,
39
+ context: { url: 'tel:050-1234567' },
40
+ expected: 'tel:050-1234567',
41
+ },
42
+ {
43
+ title: 'allowed url (mailto)',
44
+ template: `{{ url | e( 'full_url' ) }}`,
45
+ context: { url: 'mailto:user@example.com' },
46
+ expected: 'mailto:user@example.com',
47
+ },
48
+ {
49
+ title: 'disallowed url',
50
+ template: `{{ url | e( 'full_url' ) }}`,
51
+ context: { url: 'javascript:alert(123)' },
52
+ expected: '',
53
+ },
54
+ ] )( 'should render template with $title', async ( { template, context, expected } ) => {
55
+ // Arrange.
56
+ const domRenderer = createDomRenderer();
57
+
58
+ domRenderer.register( 'test-template', template );
59
+
60
+ // Act.
61
+ const result = await domRenderer.render( 'test-template', context );
62
+
63
+ // Assert.
64
+ expect( result ).toBe( expected );
65
+ } );
66
+ } );
@@ -1,13 +1,19 @@
1
1
  import { createMockPropType } from 'test-utils';
2
2
 
3
3
  import { createTransformer } from '../../transformers/create-transformer';
4
+ import { createTransformersRegistry } from '../../transformers/create-transformers-registry';
4
5
  import { createPropsResolver } from '../create-props-resolver';
5
6
 
6
7
  describe( 'createPropsResolver', () => {
7
8
  it( 'should resolve simple props', async () => {
8
9
  // Arrange.
10
+ const transformers = createTransformersRegistry().register(
11
+ 'int',
12
+ createTransformer( ( value: number ) => value + 1 )
13
+ );
14
+
9
15
  const resolve = createPropsResolver( {
10
- transformers: { int: createTransformer( ( value: number ) => value + 1 ) },
16
+ transformers,
11
17
  schema: { int: createMockPropType( { kind: 'plain', key: 'int' } ) },
12
18
  } );
13
19
 
@@ -24,8 +30,13 @@ describe( 'createPropsResolver', () => {
24
30
 
25
31
  it( 'should skip disabled props', async () => {
26
32
  // Arrange.
33
+ const transformers = createTransformersRegistry().register(
34
+ 'int',
35
+ createTransformer( ( value: number ) => value + 1 )
36
+ );
37
+
27
38
  const resolve = createPropsResolver( {
28
- transformers: { int: createTransformer( ( value: number ) => value + 1 ) },
39
+ transformers,
29
40
  schema: { int: createMockPropType( { kind: 'plain', key: 'int' } ) },
30
41
  } );
31
42
 
@@ -41,13 +52,18 @@ describe( 'createPropsResolver', () => {
41
52
  } );
42
53
 
43
54
  // Assert.
44
- expect( result ).toEqual( {} );
55
+ expect( result ).toEqual( { int: null } );
45
56
  } );
46
57
 
47
58
  it( 'should fallback to default value when there is no value', async () => {
48
59
  // Arrange.
60
+ const transformers = createTransformersRegistry().register(
61
+ 'int',
62
+ createTransformer( ( value: number ) => value + 1 )
63
+ );
64
+
49
65
  const resolve = createPropsResolver( {
50
- transformers: { int: createTransformer( ( value: number ) => value + 1 ) },
66
+ transformers,
51
67
  schema: {
52
68
  int: createMockPropType( {
53
69
  kind: 'plain',
@@ -66,8 +82,13 @@ describe( 'createPropsResolver', () => {
66
82
 
67
83
  it( 'should skip props that are not in the schema', async () => {
68
84
  // Arrange.
85
+ const transformers = createTransformersRegistry().register(
86
+ 'int',
87
+ createTransformer( ( value: number ) => value + 1 )
88
+ );
89
+
69
90
  const resolve = createPropsResolver( {
70
- transformers: { int: createTransformer( ( value: number ) => value + 1 ) },
91
+ transformers,
71
92
  schema: {
72
93
  int: createMockPropType( { kind: 'plain', key: 'int' } ),
73
94
  },
@@ -93,8 +114,13 @@ describe( 'createPropsResolver', () => {
93
114
 
94
115
  it( "should skip props that don't have a transformer", async () => {
95
116
  // Arrange.
117
+ const transformers = createTransformersRegistry().register(
118
+ 'int',
119
+ createTransformer( ( value: number ) => value + 1 )
120
+ );
121
+
96
122
  const resolve = createPropsResolver( {
97
- transformers: { int: createTransformer( ( value: number ) => value + 1 ) },
123
+ transformers,
98
124
  schema: {
99
125
  int: createMockPropType( { kind: 'plain', key: 'int' } ),
100
126
  invalid: createMockPropType( { kind: 'plain', key: 'invalid' } ),
@@ -116,17 +142,94 @@ describe( 'createPropsResolver', () => {
116
142
  } );
117
143
 
118
144
  // Assert.
119
- expect( result ).toEqual( { int: 2 } );
145
+ expect( result ).toEqual( { int: 2, invalid: null } );
120
146
  } );
121
147
 
122
- it( 'should skip props when their transformer throws an error', async () => {
148
+ it( 'should not skip props if there is a fallback transformer', async () => {
149
+ // Arrange.
150
+ const transformers = createTransformersRegistry()
151
+ .register(
152
+ 'int',
153
+ createTransformer( ( value: number ) => value + 1 )
154
+ )
155
+ .registerFallback( createTransformer( ( value: string ) => value + ' world' ) );
156
+
123
157
  const resolve = createPropsResolver( {
124
- transformers: {
125
- int: createTransformer( ( value: number ) => value + 1 ),
126
- throws: createTransformer< number >( () => {
127
- throw new Error( 'Not Working!' );
128
- } ),
158
+ transformers,
159
+ schema: {
160
+ int: createMockPropType( { kind: 'plain', key: 'int' } ),
161
+ greet: createMockPropType( { kind: 'plain', key: 'string' } ),
162
+ },
163
+ } );
164
+
165
+ // Act.
166
+ const result = await resolve( {
167
+ props: {
168
+ int: {
169
+ $$type: 'int',
170
+ value: 1,
171
+ },
172
+ greet: {
173
+ $$type: 'string',
174
+ value: 'hello',
175
+ },
176
+ },
177
+ } );
178
+
179
+ // Assert.
180
+ expect( result ).toEqual( { int: 2, greet: 'hello world' } );
181
+ } );
182
+
183
+ it( 'should return null if the prop is value is not match the prop type', async () => {
184
+ // Arrange.
185
+ const transformers = createTransformersRegistry()
186
+ .register(
187
+ 'int',
188
+ createTransformer( ( value: number ) => value + 1 )
189
+ )
190
+ .registerFallback( createTransformer( ( value: string ) => value + ' world' ) );
191
+
192
+ const resolve = createPropsResolver( {
193
+ transformers,
194
+ schema: {
195
+ int: createMockPropType( { kind: 'plain', key: 'int' } ),
196
+ greet: createMockPropType( { kind: 'plain', key: 'string' } ),
129
197
  },
198
+ } );
199
+
200
+ // Act.
201
+ const result = await resolve( {
202
+ props: {
203
+ int: {
204
+ $$type: 'int',
205
+ value: 1,
206
+ },
207
+ greet: {
208
+ $$type: 'int',
209
+ value: 2,
210
+ },
211
+ },
212
+ } );
213
+
214
+ // Assert.
215
+ expect( result ).toEqual( { int: 2, greet: null } );
216
+ } );
217
+
218
+ it( 'should skip props when their transformer throws an error', async () => {
219
+ const transformers = createTransformersRegistry()
220
+ .register(
221
+ 'int',
222
+ createTransformer( ( value: number ) => value + 1 )
223
+ )
224
+ .register(
225
+ 'throws',
226
+ createTransformer< number >( () => {
227
+ throw new Error( 'Not Working!' );
228
+ } )
229
+ );
230
+
231
+ const resolve = createPropsResolver( {
232
+ transformers,
130
233
  schema: {
131
234
  int: createMockPropType( { kind: 'plain', key: 'int' } ),
132
235
  invalid: createMockPropType( { kind: 'plain', key: 'throws' } ),
@@ -148,14 +251,19 @@ describe( 'createPropsResolver', () => {
148
251
  } );
149
252
 
150
253
  // Assert.
151
- expect( result ).toEqual( { int: 2 } );
254
+ expect( result ).toEqual( { int: 2, invalid: null } );
152
255
  } );
153
256
 
154
257
  it( 'should trigger onResolve when resolving a prop', async () => {
258
+ const transformers = createTransformersRegistry().register(
259
+ 'int',
260
+ createTransformer( ( value: number ) => value + 1 )
261
+ );
262
+
155
263
  const onResolve = jest.fn();
156
264
 
157
265
  const resolve = createPropsResolver( {
158
- transformers: { int: createTransformer( ( value: number ) => value + 1 ) },
266
+ transformers,
159
267
  schema: {
160
268
  int: createMockPropType( { kind: 'plain', key: 'int' } ),
161
269
  int2: createMockPropType( { kind: 'plain', key: 'int' } ),
@@ -0,0 +1,56 @@
1
+ import { createArrayLoader, createEnvironment, type TwingArrayLoader, type TwingEnvironment } from '@elementor/twing';
2
+
3
+ type DomRenderer = {
4
+ register: TwingArrayLoader[ 'setTemplate' ];
5
+ render: TwingEnvironment[ 'render' ];
6
+ };
7
+
8
+ export function createDomRenderer(): DomRenderer {
9
+ const loader = createArrayLoader( {} );
10
+ const environment = createEnvironment( loader );
11
+
12
+ environment.registerEscapingStrategy( escapeHtmlTag, 'html_tag' );
13
+ environment.registerEscapingStrategy( escapeURL, 'full_url' );
14
+
15
+ return {
16
+ register: loader.setTemplate,
17
+ render: environment.render,
18
+ };
19
+ }
20
+
21
+ function escapeHtmlTag( value: string ) {
22
+ const allowedTags = [
23
+ 'a',
24
+ 'article',
25
+ 'aside',
26
+ 'button',
27
+ 'div',
28
+ 'footer',
29
+ 'h1',
30
+ 'h2',
31
+ 'h3',
32
+ 'h4',
33
+ 'h5',
34
+ 'h6',
35
+ 'header',
36
+ 'main',
37
+ 'nav',
38
+ 'p',
39
+ 'section',
40
+ 'span',
41
+ ];
42
+
43
+ return allowedTags.includes( value ) ? value : 'div';
44
+ }
45
+
46
+ function escapeURL( value: string ) {
47
+ const allowedProtocols = [ 'http:', 'https:', 'mailto:', 'tel:' ];
48
+
49
+ try {
50
+ const parsed = new URL( value );
51
+
52
+ return allowedProtocols.includes( parsed.protocol ) ? value : '';
53
+ } catch {
54
+ return '';
55
+ }
56
+ }
@@ -7,11 +7,11 @@ import {
7
7
  type PropValue,
8
8
  } from '@elementor/editor-props';
9
9
 
10
- import { type TransformersMap } from '../transformers/types';
10
+ import { type TransformersRegistry } from '../transformers/create-transformers-registry';
11
11
  import { getMultiPropsValue, isMultiProps } from './multi-props';
12
12
 
13
13
  type CreatePropResolverArgs = {
14
- transformers: TransformersMap;
14
+ transformers: TransformersRegistry;
15
15
  schema: PropsSchema;
16
16
  onPropResolve?: ( args: { key: string; value: unknown } ) => void;
17
17
  };
@@ -44,10 +44,6 @@ export function createPropsResolver( { transformers, schema: initialSchema, onPr
44
44
 
45
45
  const transformed = await transform( { value, key, type, signal } );
46
46
 
47
- if ( transformed === null ) {
48
- return;
49
- }
50
-
51
47
  onPropResolve?.( { key, value: transformed } );
52
48
 
53
49
  if ( isMultiProps( transformed ) ) {
@@ -86,6 +82,10 @@ export function createPropsResolver( { transformers, schema: initialSchema, onPr
86
82
  }
87
83
  }
88
84
 
85
+ if ( value.$$type !== type.key ) {
86
+ return null;
87
+ }
88
+
89
89
  // Warning: This variable is loosely-typed - use with caution.
90
90
  let resolvedValue = value.value;
91
91
 
@@ -105,7 +105,7 @@ export function createPropsResolver( { transformers, schema: initialSchema, onPr
105
105
  );
106
106
  }
107
107
 
108
- const transformer = transformers[ value.$$type ];
108
+ const transformer = transformers.get( value.$$type );
109
109
 
110
110
  if ( ! transformer ) {
111
111
  return null;
@@ -97,6 +97,10 @@ async function propsToCss( { props, resolve, signal }: PropsToCssArgs ) {
97
97
 
98
98
  return Object.entries( transformed )
99
99
  .reduce< string[] >( ( acc, [ propName, propValue ] ) => {
100
+ if ( propValue === null ) {
101
+ return acc;
102
+ }
103
+
100
104
  acc.push( propName + ':' + propValue + ';' );
101
105
 
102
106
  return acc;
@@ -1,16 +1,27 @@
1
- import { type AnyTransformer, type TransformerName, type TransformersMap } from './types';
1
+ import { type PropTypeKey } from '@elementor/editor-props';
2
+
3
+ import { type AnyTransformer, type TransformersMap } from './types';
4
+
5
+ export type TransformersRegistry = ReturnType< typeof createTransformersRegistry >;
2
6
 
3
7
  export function createTransformersRegistry() {
4
8
  const transformers: TransformersMap = {};
5
9
 
10
+ let fallbackTransformer: AnyTransformer | null = null;
11
+
6
12
  return {
7
- register( name: TransformerName, transformer: AnyTransformer ) {
8
- transformers[ name ] = transformer;
13
+ register( type: PropTypeKey, transformer: AnyTransformer ) {
14
+ transformers[ type ] = transformer;
15
+
16
+ return this;
17
+ },
18
+ registerFallback( transformer: AnyTransformer ) {
19
+ fallbackTransformer = transformer;
9
20
 
10
21
  return this;
11
22
  },
12
- all() {
13
- return transformers;
23
+ get( type: PropTypeKey ): AnyTransformer | null {
24
+ return transformers[ type ] ?? fallbackTransformer;
14
25
  },
15
26
  };
16
27
  }
@@ -6,5 +6,5 @@ type BackgroundImagePositionOffset = {
6
6
  };
7
7
 
8
8
  export const backgroundImagePositionOffsetTransformer = createTransformer(
9
- ( { x = '0px', y = '0px' }: BackgroundImagePositionOffset ) => `${ x } ${ y }`
9
+ ( { x, y }: BackgroundImagePositionOffset ) => `${ x ?? '0px' } ${ y ?? '0px' }`
10
10
  );
@@ -6,5 +6,5 @@ type BackgroundImageSizeScale = {
6
6
  };
7
7
 
8
8
  export const backgroundImageSizeScaleTransformer = createTransformer(
9
- ( { width = 'auto', height = 'auto' }: BackgroundImageSizeScale ) => `${ width } ${ height }`
9
+ ( { width, height }: BackgroundImageSizeScale ) => `${ width ?? 'auto' } ${ height ?? 'auto' }`
10
10
  );
@@ -6,14 +6,10 @@ export type UnbrandedTransformer< TValue > = (
6
6
  }
7
7
  ) => unknown;
8
8
 
9
- const brand = Symbol( 'transformer-brand' );
10
-
11
9
  export type Transformer< TValue > = UnbrandedTransformer< TValue > & {
12
- [ brand ]: true;
10
+ __transformer: true;
13
11
  };
14
12
 
15
- export type TransformerName = string;
16
-
17
13
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
14
  export type AnyTransformer = Transformer< any >;
19
15