@elementor/editor-canvas 4.2.0-938 → 4.2.0-940

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,111 @@
1
+ import { getWidgetsCache } from '@elementor/editor-elements';
2
+
3
+ import { resolveCanonicalPropKeys, resolveCanonicalPropName } from '../resolve-canonical-prop-name';
4
+
5
+ jest.mock( '@elementor/editor-elements', () => ( {
6
+ getWidgetsCache: jest.fn(),
7
+ } ) );
8
+
9
+ const ELEMENT_TYPE = 'e-paragraph';
10
+
11
+ const widgetsCacheFixture = {
12
+ [ ELEMENT_TYPE ]: {
13
+ atomic_props_schema: {
14
+ paragraph: {
15
+ key: 'html-v3',
16
+ meta: {
17
+ aliases: [ 'text', 'content' ],
18
+ },
19
+ },
20
+ tag: {
21
+ key: 'string',
22
+ },
23
+ },
24
+ },
25
+ };
26
+
27
+ describe( 'resolveCanonicalPropName', () => {
28
+ beforeEach( () => {
29
+ // @ts-ignore: Mock values for test
30
+ jest.mocked( getWidgetsCache ).mockReturnValue( widgetsCacheFixture );
31
+ } );
32
+
33
+ it( 'returns canonical property names unchanged', () => {
34
+ // Act
35
+ const resolved = resolveCanonicalPropName( ELEMENT_TYPE, 'paragraph' );
36
+
37
+ // Assert
38
+ expect( resolved ).toBe( 'paragraph' );
39
+ } );
40
+
41
+ it( 'resolves alias property names to canonical keys', () => {
42
+ // Act
43
+ const resolved = resolveCanonicalPropName( ELEMENT_TYPE, 'text' );
44
+
45
+ // Assert
46
+ expect( resolved ).toBe( 'paragraph' );
47
+ } );
48
+
49
+ it( 'returns unknown property names unchanged', () => {
50
+ // Act
51
+ const resolved = resolveCanonicalPropName( ELEMENT_TYPE, 'unknown' );
52
+
53
+ // Assert
54
+ expect( resolved ).toBe( 'unknown' );
55
+ } );
56
+
57
+ it( 'returns property names unchanged when element type has no schema', () => {
58
+ // Arrange
59
+ jest.mocked( getWidgetsCache ).mockReturnValue( null );
60
+
61
+ // Act
62
+ const resolved = resolveCanonicalPropName( 'missing-widget', 'text' );
63
+
64
+ // Assert
65
+ expect( resolved ).toBe( 'text' );
66
+ } );
67
+ } );
68
+
69
+ describe( 'resolveCanonicalPropKeys', () => {
70
+ beforeEach( () => {
71
+ // @ts-ignore: Mock values for test
72
+ jest.mocked( getWidgetsCache ).mockReturnValue( widgetsCacheFixture );
73
+ } );
74
+
75
+ it( 'rewrites alias keys to canonical keys', () => {
76
+ // Act
77
+ const resolved = resolveCanonicalPropKeys( ELEMENT_TYPE, {
78
+ text: 'hello',
79
+ } );
80
+
81
+ // Assert
82
+ expect( resolved ).toEqual( {
83
+ paragraph: 'hello',
84
+ } );
85
+ } );
86
+
87
+ it( 'prefers canonical keys when alias and canonical are both provided', () => {
88
+ // Act
89
+ const resolved = resolveCanonicalPropKeys( ELEMENT_TYPE, {
90
+ paragraph: 'canonical',
91
+ text: 'alias',
92
+ } );
93
+
94
+ // Assert
95
+ expect( resolved ).toEqual( {
96
+ paragraph: 'canonical',
97
+ } );
98
+ } );
99
+
100
+ it( 'passes unknown keys through for downstream validation', () => {
101
+ // Act
102
+ const resolved = resolveCanonicalPropKeys( ELEMENT_TYPE, {
103
+ unknown: 'value',
104
+ } );
105
+
106
+ // Assert
107
+ expect( resolved ).toEqual( {
108
+ unknown: 'value',
109
+ } );
110
+ } );
111
+ } );
@@ -12,6 +12,7 @@ import { type Utils as IUtils } from '@elementor/editor-variables';
12
12
  import { type z } from '@elementor/schema';
13
13
 
14
14
  import { mergeCustomCssText, readStoredCustomCssText } from './merge-custom-css';
15
+ import { resolveCanonicalPropName } from './resolve-canonical-prop-name';
15
16
  import { DYNAMIC_PROP_TYPE_KEY, dynamicTagLLMResolver } from './resolve-dynamic-tag';
16
17
 
17
18
  // TODO: see https://elementor.atlassian.net/browse/ED-22513 for better cross-module access
@@ -47,7 +48,11 @@ export function resolvePropValue( value: unknown, forceKey?: string ): PropValue
47
48
  * Also, it supports updating styles "on-the-way" by checking for "_styles" property with PropValue bag that fits the common style schema.
48
49
  */
49
50
  export const doUpdateElementProperty = ( params: OwnParams ) => {
50
- const { elementId, propertyName, propertyValue, elementType, customCssWriteMode = 'replace' } = params;
51
+ const { elementId, propertyValue, elementType, customCssWriteMode = 'replace' } = params;
52
+ const propertyName =
53
+ params.propertyName === '_styles'
54
+ ? params.propertyName
55
+ : resolveCanonicalPropName( elementType, params.propertyName );
51
56
  if ( propertyName === '_styles' ) {
52
57
  const elementStyles = getElementStyles( elementId ) || {};
53
58
  const propertyMapValue = propertyValue as Record< string, PropValue >;
@@ -0,0 +1,71 @@
1
+ import { getWidgetsCache } from '@elementor/editor-elements';
2
+ import { type PropsSchema } from '@elementor/editor-props';
3
+
4
+ function buildAliasToCanonicalMap( schema: PropsSchema ): Record< string, string > {
5
+ const aliasToCanonical: Record< string, string > = {};
6
+
7
+ for ( const [ canonical, propType ] of Object.entries( schema ) ) {
8
+ const aliases = propType.meta?.aliases;
9
+
10
+ if ( ! Array.isArray( aliases ) ) {
11
+ continue;
12
+ }
13
+
14
+ for ( const alias of aliases ) {
15
+ if ( typeof alias === 'string' && alias ) {
16
+ aliasToCanonical[ alias ] = canonical;
17
+ }
18
+ }
19
+ }
20
+
21
+ return aliasToCanonical;
22
+ }
23
+
24
+ export function resolveCanonicalPropName( elementType: string, propertyName: string ): string {
25
+ const schema = getWidgetsCache()?.[ elementType ]?.atomic_props_schema;
26
+
27
+ if ( ! schema || schema[ propertyName ] ) {
28
+ return propertyName;
29
+ }
30
+
31
+ return buildAliasToCanonicalMap( schema )[ propertyName ] ?? propertyName;
32
+ }
33
+
34
+ export function resolveCanonicalPropKeys(
35
+ elementType: string,
36
+ props: Record< string, unknown >
37
+ ): Record< string, unknown > {
38
+ const schema = getWidgetsCache()?.[ elementType ]?.atomic_props_schema;
39
+
40
+ if ( ! schema ) {
41
+ return { ...props };
42
+ }
43
+
44
+ const aliasToCanonical = buildAliasToCanonicalMap( schema );
45
+ const resolved: Record< string, unknown > = {};
46
+
47
+ for ( const [ key, value ] of Object.entries( props ) ) {
48
+ if ( schema[ key ] ) {
49
+ resolved[ key ] = value;
50
+ }
51
+ }
52
+
53
+ for ( const [ key, value ] of Object.entries( props ) ) {
54
+ if ( schema[ key ] ) {
55
+ continue;
56
+ }
57
+
58
+ const canonical = aliasToCanonical[ key ];
59
+
60
+ if ( ! canonical ) {
61
+ resolved[ key ] = value;
62
+ continue;
63
+ }
64
+
65
+ if ( ! Object.prototype.hasOwnProperty.call( resolved, canonical ) ) {
66
+ resolved[ canonical ] = value;
67
+ }
68
+ }
69
+
70
+ return resolved;
71
+ }
@@ -287,8 +287,18 @@ describe( 'createPropsResolver', () => {
287
287
 
288
288
  // Assert.
289
289
  expect( onResolve ).toHaveBeenCalledTimes( 2 );
290
- expect( onResolve ).toHaveBeenNthCalledWith( 1, { key: 'int', value: 2 } );
291
- expect( onResolve ).toHaveBeenNthCalledWith( 2, { key: 'int2', value: 4 } );
290
+ expect( onResolve ).toHaveBeenNthCalledWith( 1, {
291
+ key: 'int',
292
+ value: 2,
293
+ propValue: { $$type: 'int', value: 1 },
294
+ propType: createMockPropType( { kind: 'plain', key: 'int' } ),
295
+ } );
296
+ expect( onResolve ).toHaveBeenNthCalledWith( 2, {
297
+ key: 'int2',
298
+ value: 4,
299
+ propValue: { $$type: 'int', value: 3 },
300
+ propType: createMockPropType( { kind: 'plain', key: 'int' } ),
301
+ } );
292
302
  } );
293
303
 
294
304
  it( 'should pass renderContext to transformers', async () => {
@@ -79,6 +79,36 @@ describe( 'renderStyles', () => {
79
79
  expect( resolve ).toHaveBeenNthCalledWith( 5, { props: { 'font-size': '50px' } } );
80
80
  } );
81
81
 
82
+ it.each( [
83
+ [ '"Open Sans"', 'font-family:"Open Sans";' ],
84
+ [ 'Arial', 'font-family:Arial;' ],
85
+ [ 'var(--primary-font)', 'font-family:var(--primary-font);' ],
86
+ ] )( 'should pass through font-family "%s" as-is in CSS output', async ( fontFamily, expected ) => {
87
+ // Arrange.
88
+ const styleDef: RendererStyleDefinition = {
89
+ id: 'test',
90
+ type: 'class',
91
+ cssName: 'test',
92
+ label: 'Test',
93
+ variants: [
94
+ {
95
+ meta: { breakpoint: null, state: null },
96
+ props: { 'font-family': fontFamily },
97
+ custom_css: null,
98
+ },
99
+ ],
100
+ };
101
+
102
+ const resolve = jest.fn( ( { props } ) => props );
103
+ const renderStyles = createStylesRenderer( { breakpoints: {} as BreakpointsMap, resolve } );
104
+
105
+ // Act.
106
+ const result = await renderStyles( { styles: [ styleDef ] } );
107
+
108
+ // Assert.
109
+ expect( result[ 0 ].value ).toContain( expected );
110
+ } );
111
+
82
112
  it( 'should add selector prefix to the output', async () => {
83
113
  // Arrange.
84
114
  const styleDef: RendererStyleDefinition = {
@@ -0,0 +1,29 @@
1
+ import { createMockPropType } from 'test-utils';
2
+ import { fontFamilyPropTypeUtil } from '@elementor/editor-props';
3
+ import { enqueueFont } from '@elementor/editor-v1-adapters';
4
+
5
+ import { maybeEnqueueFontFromStyleProp } from '../enqueue-font-from-style-prop';
6
+
7
+ jest.mock( '@elementor/editor-v1-adapters' );
8
+
9
+ describe( 'maybeEnqueueFontFromStyleProp', () => {
10
+ it( 'should enqueue when prop type implements getEnqueueFontFamily', () => {
11
+ maybeEnqueueFontFromStyleProp(
12
+ createMockPropType( { key: 'font-family', kind: 'plain' } ),
13
+ fontFamilyPropTypeUtil.create( 'Open Sans' ),
14
+ enqueueFont
15
+ );
16
+
17
+ expect( enqueueFont ).toHaveBeenCalledWith( 'Open Sans' );
18
+ } );
19
+
20
+ it( 'should not enqueue when prop type does not implement font enqueue', () => {
21
+ maybeEnqueueFontFromStyleProp(
22
+ createMockPropType( { key: 'string', kind: 'plain' } ),
23
+ { $$type: 'string', value: 'red' },
24
+ enqueueFont
25
+ );
26
+
27
+ expect( enqueueFont ).not.toHaveBeenCalled();
28
+ } );
29
+ } );
@@ -15,7 +15,7 @@ import { getMultiPropsValue, isMultiProps } from './multi-props';
15
15
  type CreatePropResolverArgs = {
16
16
  transformers: TransformersRegistry;
17
17
  schema: PropsSchema;
18
- onPropResolve?: ( args: { key: string; value: unknown } ) => void;
18
+ onPropResolve?: ( args: { key: string; value: unknown; propValue: unknown; propType: PropType } ) => void;
19
19
  };
20
20
 
21
21
  type ResolveArgs = {
@@ -48,7 +48,7 @@ export function createPropsResolver( { transformers, schema: initialSchema, onPr
48
48
  const value = props[ key ] ?? type.default;
49
49
  const transformed = ( await transform( { value, key, type, signal, renderContext } ) ) as PropValue;
50
50
 
51
- onPropResolve?.( { key, value: transformed } );
51
+ onPropResolve?.( { key, value: transformed, propValue: value, propType: type } );
52
52
 
53
53
  if ( isMultiProps( transformed ) ) {
54
54
  return getMultiPropsValue( transformed );
@@ -1,4 +1,4 @@
1
- import type { Props } from '@elementor/editor-props';
1
+ import { type Props } from '@elementor/editor-props';
2
2
  import { type Breakpoint, type BreakpointsMap } from '@elementor/editor-responsive';
3
3
  import {
4
4
  type CustomCss,
@@ -0,0 +1,34 @@
1
+ import { getPropSchemaFromCache, isTransformable, type PropType } from '@elementor/editor-props';
2
+
3
+ export const maybeEnqueueFontFromStyleProp = (
4
+ propType: PropType,
5
+ propValue: unknown,
6
+ enqueue: ( font: string ) => void
7
+ ): void => {
8
+ if ( ! isTransformable( propValue ) || propValue.disabled ) {
9
+ return;
10
+ }
11
+
12
+ const typeKey = propType.kind === 'union' ? propValue.$$type : propType.key;
13
+ const propTypeUtil = getPropSchemaFromCache( typeKey );
14
+
15
+ if (
16
+ ! propTypeUtil ||
17
+ ! ( 'getEnqueueFontFamily' in propTypeUtil ) ||
18
+ typeof propTypeUtil.getEnqueueFontFamily !== 'function'
19
+ ) {
20
+ return;
21
+ }
22
+
23
+ const stored = propValue.value;
24
+
25
+ if ( typeof stored !== 'string' ) {
26
+ return;
27
+ }
28
+
29
+ const font = propTypeUtil.getEnqueueFontFamily( stored );
30
+
31
+ if ( font ) {
32
+ enqueue( font );
33
+ }
34
+ };
@@ -0,0 +1,20 @@
1
+ import { fontFamilyTransformer } from '../font-family-transformer';
2
+
3
+ function run( value: string | null ) {
4
+ return fontFamilyTransformer( value, { key: 'font-family', signal: undefined } );
5
+ }
6
+
7
+ describe( 'fontFamilyTransformer', () => {
8
+ it( 'wraps font names in quotes', () => {
9
+ expect( run( ' Open Sans ' ) ).toBe( '"Open Sans"' );
10
+ expect( run( 'Arial' ) ).toBe( '"Arial"' );
11
+ } );
12
+
13
+ it( 'passes through already quoted values', () => {
14
+ expect( run( '"Open Sans"' ) ).toBe( '"Open Sans"' );
15
+ } );
16
+
17
+ it( 'returns null for non-string values', () => {
18
+ expect( run( null ) ).toBeNull();
19
+ } );
20
+ } );
@@ -0,0 +1,18 @@
1
+ import { createTransformer } from '../create-transformer';
2
+
3
+ export const fontFamilyTransformer = createTransformer( ( value: string | null ) => {
4
+ if ( typeof value !== 'string' || ! value.trim() ) {
5
+ return null;
6
+ }
7
+
8
+ const trimmed = value.trim();
9
+
10
+ if (
11
+ ( trimmed.startsWith( '"' ) && trimmed.endsWith( '"' ) ) ||
12
+ ( trimmed.startsWith( "'" ) && trimmed.endsWith( "'" ) )
13
+ ) {
14
+ return trimmed;
15
+ }
16
+
17
+ return `"${ trimmed }"`;
18
+ } );