@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.
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +187 -95
- package/dist/index.mjs +127 -35
- package/package.json +19 -19
- package/src/hooks/__tests__/use-style-prop-resolver.test.ts +35 -26
- package/src/hooks/use-style-prop-resolver.ts +3 -6
- package/src/init-style-transformers.ts +2 -0
- package/src/mcp/resources/document-structure-resource.ts +4 -1
- package/src/mcp/tools/configure-element/tool.ts +17 -4
- package/src/mcp/utils/__tests__/do-update-element-property.test.ts +43 -0
- package/src/mcp/utils/__tests__/resolve-canonical-prop-name.test.ts +111 -0
- package/src/mcp/utils/do-update-element-property.ts +6 -1
- package/src/mcp/utils/resolve-canonical-prop-name.ts +71 -0
- package/src/renderers/__tests__/create-props-resolver.test.ts +12 -2
- package/src/renderers/__tests__/create-styles-renderer.test.ts +30 -0
- package/src/renderers/__tests__/enqueue-font-from-style-prop.test.ts +29 -0
- package/src/renderers/create-props-resolver.ts +2 -2
- package/src/renderers/create-styles-renderer.ts +1 -1
- package/src/renderers/enqueue-font-from-style-prop.ts +34 -0
- package/src/transformers/styles/__tests__/font-family-transformer.test.ts +20 -0
- package/src/transformers/styles/font-family-transformer.ts +18 -0
|
@@ -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,
|
|
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, {
|
|
291
|
-
|
|
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 );
|
|
@@ -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
|
+
} );
|