@elementor/editor-canvas 3.33.0-98 → 3.34.2
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 +133 -10
- package/dist/index.d.ts +133 -10
- package/dist/index.js +1413 -212
- package/dist/index.mjs +1399 -180
- package/package.json +18 -14
- package/src/__tests__/settings-props-resolver.test.ts +0 -40
- package/src/__tests__/styles-prop-resolver.test.ts +13 -0
- package/src/components/__tests__/__snapshots__/style-renderer.test.tsx.snap +2 -6
- package/src/components/__tests__/elements-overlays.test.tsx +96 -12
- package/src/components/__tests__/inline-editor-overlay.test.tsx +245 -0
- package/src/components/__tests__/style-renderer.test.tsx +2 -2
- package/src/components/elements-overlays.tsx +33 -10
- package/src/components/inline-editor-overlay.tsx +79 -0
- package/src/components/interactions-renderer.tsx +33 -0
- package/src/components/{element-overlay.tsx → outline-overlay.tsx} +8 -7
- package/src/components/style-renderer.tsx +2 -4
- package/src/hooks/__tests__/use-has-overlapping.test.ts +187 -0
- package/src/hooks/use-floating-on-element.ts +11 -8
- package/src/hooks/use-has-overlapping.ts +21 -0
- package/src/hooks/use-interactions-items.ts +108 -0
- package/src/hooks/use-style-items.ts +34 -8
- package/src/index.ts +9 -0
- package/src/init-settings-transformers.ts +4 -0
- package/src/init.tsx +18 -0
- package/src/legacy/create-templated-element-type.ts +67 -42
- package/src/legacy/init-legacy-views.ts +27 -5
- package/src/legacy/types.ts +44 -4
- package/src/mcp/canvas-mcp.ts +17 -0
- package/src/mcp/mcp-description.ts +40 -0
- package/src/mcp/resources/widgets-schema-resource.ts +173 -0
- package/src/mcp/tools/build-composition/prompt.ts +128 -0
- package/src/mcp/tools/build-composition/schema.ts +31 -0
- package/src/mcp/tools/build-composition/tool.ts +163 -0
- package/src/mcp/tools/configure-element/prompt.ts +93 -0
- package/src/mcp/tools/configure-element/schema.ts +25 -0
- package/src/mcp/tools/configure-element/tool.ts +67 -0
- package/src/mcp/tools/get-element-config/tool.ts +69 -0
- package/src/mcp/utils/do-update-element-property.ts +129 -0
- package/src/mcp/utils/generate-available-tags.ts +23 -0
- package/src/renderers/__tests__/__snapshots__/create-styles-renderer.test.ts.snap +2 -0
- package/src/renderers/__tests__/create-styles-renderer.test.ts +25 -0
- package/src/renderers/create-props-resolver.ts +8 -1
- package/src/renderers/create-styles-renderer.ts +20 -9
- package/src/renderers/errors.ts +6 -0
- package/src/sync/drag-element-from-panel.ts +49 -0
- package/src/sync/types.ts +32 -1
- package/src/transformers/settings/__tests__/attributes-transformer.test.ts +15 -0
- package/src/transformers/settings/__tests__/classes-transformer.test.ts +83 -0
- package/src/transformers/settings/attributes-transformer.ts +1 -23
- package/src/transformers/settings/classes-transformer.ts +21 -21
- package/src/transformers/settings/date-time-transformer.ts +12 -0
- package/src/transformers/settings/query-transformer.ts +10 -0
- package/src/transformers/styles/__tests__/transform-origin-transformer.test.ts +24 -0
- package/src/transformers/styles/__tests__/transition-transformer.test.ts +52 -0
- package/src/transformers/styles/background-transformer.ts +3 -1
- package/src/transformers/styles/transform-origin-transformer.ts +12 -2
- package/src/transformers/styles/transition-transformer.ts +34 -4
- package/src/types/element-overlay.ts +18 -0
- package/src/utils/inline-editing-utils.ts +43 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elementor/editor-canvas",
|
|
3
3
|
"description": "Elementor Editor Canvas",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.34.2",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Elementor Team",
|
|
7
7
|
"homepage": "https://elementor.com/",
|
|
@@ -37,19 +37,23 @@
|
|
|
37
37
|
"react-dom": "^18.3.1"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@elementor/editor": "3.
|
|
41
|
-
"@elementor/editor-
|
|
42
|
-
"@elementor/editor-documents": "3.
|
|
43
|
-
"@elementor/editor-elements": "3.
|
|
44
|
-
"@elementor/editor-
|
|
45
|
-
"@elementor/editor-
|
|
46
|
-
"@elementor/editor-
|
|
47
|
-
"@elementor/editor-
|
|
48
|
-
"@elementor/editor-
|
|
49
|
-
"@elementor/
|
|
50
|
-
"@elementor/
|
|
51
|
-
"@elementor/
|
|
52
|
-
"@elementor/
|
|
40
|
+
"@elementor/editor": "3.34.2",
|
|
41
|
+
"@elementor/editor-controls": "3.34.2",
|
|
42
|
+
"@elementor/editor-documents": "3.34.2",
|
|
43
|
+
"@elementor/editor-elements": "3.34.2",
|
|
44
|
+
"@elementor/editor-interactions": "3.34.2",
|
|
45
|
+
"@elementor/editor-notifications": "3.34.2",
|
|
46
|
+
"@elementor/editor-props": "3.34.2",
|
|
47
|
+
"@elementor/editor-responsive": "3.34.2",
|
|
48
|
+
"@elementor/editor-styles": "3.34.2",
|
|
49
|
+
"@elementor/editor-styles-repository": "3.34.2",
|
|
50
|
+
"@elementor/editor-v1-adapters": "3.34.2",
|
|
51
|
+
"@elementor/editor-mcp": "3.34.2",
|
|
52
|
+
"@elementor/schema": "3.34.2",
|
|
53
|
+
"@elementor/twing": "3.34.2",
|
|
54
|
+
"@elementor/ui": "1.36.17",
|
|
55
|
+
"@elementor/utils": "3.34.2",
|
|
56
|
+
"@elementor/wp-media": "3.34.2",
|
|
53
57
|
"@floating-ui/react": "^0.27.5",
|
|
54
58
|
"@wordpress/i18n": "^5.13.0"
|
|
55
59
|
},
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { createMockStyleDefinition, createMockStylesProvider } from 'test-utils';
|
|
2
1
|
import {
|
|
3
2
|
booleanPropTypeUtil,
|
|
4
|
-
classesPropTypeUtil,
|
|
5
3
|
imageAttachmentIdPropType,
|
|
6
4
|
imagePropTypeUtil,
|
|
7
5
|
imageSrcPropTypeUtil,
|
|
@@ -12,7 +10,6 @@ import {
|
|
|
12
10
|
stringPropTypeUtil,
|
|
13
11
|
urlPropTypeUtil,
|
|
14
12
|
} from '@elementor/editor-props';
|
|
15
|
-
import { stylesRepository } from '@elementor/editor-styles-repository';
|
|
16
13
|
import { getMediaAttachment } from '@elementor/wp-media';
|
|
17
14
|
|
|
18
15
|
import { initSettingsTransformers } from '../init-settings-transformers';
|
|
@@ -21,7 +18,6 @@ import { settingsTransformersRegistry } from '../settings-transformers-registry'
|
|
|
21
18
|
import { mockAttachmentData } from './mock-attachment-data';
|
|
22
19
|
import {
|
|
23
20
|
booleanPropType,
|
|
24
|
-
classesPropType,
|
|
25
21
|
imagePropType,
|
|
26
22
|
linkPropType,
|
|
27
23
|
numberPropType,
|
|
@@ -69,42 +65,6 @@ describe( 'settings props resolver', () => {
|
|
|
69
65
|
boolean: true,
|
|
70
66
|
},
|
|
71
67
|
},
|
|
72
|
-
{
|
|
73
|
-
name: 'classes',
|
|
74
|
-
props: {
|
|
75
|
-
classes: classesPropTypeUtil.create( [
|
|
76
|
-
'test-1',
|
|
77
|
-
'test-2-suffix',
|
|
78
|
-
'without-provider',
|
|
79
|
-
'',
|
|
80
|
-
null as unknown as string,
|
|
81
|
-
undefined as unknown as string,
|
|
82
|
-
] ),
|
|
83
|
-
},
|
|
84
|
-
prepare: () => {
|
|
85
|
-
jest.mocked( stylesRepository.getProviders ).mockReturnValue( [
|
|
86
|
-
createMockStylesProvider( {
|
|
87
|
-
key: 'test-1-provider',
|
|
88
|
-
actions: {
|
|
89
|
-
all: () => [ createMockStyleDefinition( { id: 'test-1' } ) ],
|
|
90
|
-
},
|
|
91
|
-
} ),
|
|
92
|
-
createMockStylesProvider( {
|
|
93
|
-
key: 'test-2-provider',
|
|
94
|
-
actions: {
|
|
95
|
-
resolveCssName: ( id ) => `${ id }-suffix`,
|
|
96
|
-
all: () => [ createMockStyleDefinition( { id: 'test-2' } ) ],
|
|
97
|
-
},
|
|
98
|
-
} ),
|
|
99
|
-
] );
|
|
100
|
-
},
|
|
101
|
-
schema: {
|
|
102
|
-
classes: classesPropType(),
|
|
103
|
-
},
|
|
104
|
-
expected: {
|
|
105
|
-
classes: [ 'test-1', 'test-2-suffix', 'without-provider' ],
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
68
|
{
|
|
109
69
|
name: 'link',
|
|
110
70
|
props: {
|
|
@@ -316,6 +316,7 @@ describe( 'styles prop resolver', () => {
|
|
|
316
316
|
props: {
|
|
317
317
|
background: backgroundPropTypeUtil.create( {
|
|
318
318
|
color: colorPropTypeUtil.create( '#000' ),
|
|
319
|
+
clip: stringPropTypeUtil.create( 'text' ),
|
|
319
320
|
'background-overlay': backgroundOverlayPropTypeUtil.create( [
|
|
320
321
|
backgroundColorOverlayPropTypeUtil.create( {
|
|
321
322
|
color: colorPropTypeUtil.create( 'blue' ),
|
|
@@ -370,6 +371,7 @@ describe( 'styles prop resolver', () => {
|
|
|
370
371
|
},
|
|
371
372
|
expected: {
|
|
372
373
|
'background-color': '#000',
|
|
374
|
+
'background-clip': 'text',
|
|
373
375
|
'background-attachment': 'scroll,scroll,scroll,fixed',
|
|
374
376
|
'background-image':
|
|
375
377
|
'linear-gradient(blue, blue),linear-gradient(yellow, yellow),url(thumbnail-image-url-123),url(medium_large-image-url-123)',
|
|
@@ -378,6 +380,15 @@ describe( 'styles prop resolver', () => {
|
|
|
378
380
|
'background-size': 'auto auto,auto auto,1400px auto,auto',
|
|
379
381
|
},
|
|
380
382
|
},
|
|
383
|
+
{
|
|
384
|
+
name: 'mix-blend-mode',
|
|
385
|
+
props: {
|
|
386
|
+
'mix-blend-mode': stringPropTypeUtil.create( 'multiply' ),
|
|
387
|
+
},
|
|
388
|
+
expected: {
|
|
389
|
+
'mix-blend-mode': 'multiply',
|
|
390
|
+
},
|
|
391
|
+
},
|
|
381
392
|
{
|
|
382
393
|
name: 'filter',
|
|
383
394
|
props: {
|
|
@@ -560,6 +571,7 @@ describe( 'styles prop resolver', () => {
|
|
|
560
571
|
props: {
|
|
561
572
|
background: backgroundPropTypeUtil.create( {
|
|
562
573
|
color: colorPropTypeUtil.create( '#000' ),
|
|
574
|
+
clip: stringPropTypeUtil.create( 'border-box' ),
|
|
563
575
|
'background-overlay': backgroundOverlayPropTypeUtil.create( [
|
|
564
576
|
backgroundImageOverlayPropTypeUtil.create( {
|
|
565
577
|
image: imagePropTypeUtil.create( {
|
|
@@ -581,6 +593,7 @@ describe( 'styles prop resolver', () => {
|
|
|
581
593
|
},
|
|
582
594
|
expected: {
|
|
583
595
|
'background-color': '#000',
|
|
596
|
+
'background-clip': 'border-box',
|
|
584
597
|
'background-image': 'url(original-image-url-123)',
|
|
585
598
|
'background-repeat': 'repeat',
|
|
586
599
|
'background-size': '1400px auto',
|
|
@@ -4,14 +4,10 @@ exports[`<StyleRenderer /> should render styles and links in portal when contain
|
|
|
4
4
|
<div
|
|
5
5
|
data-testid="portal"
|
|
6
6
|
>
|
|
7
|
-
<style
|
|
8
|
-
data-e-style-id="style1"
|
|
9
|
-
>
|
|
7
|
+
<style>
|
|
10
8
|
.test { color: red; }
|
|
11
9
|
</style>
|
|
12
|
-
<style
|
|
13
|
-
data-e-style-id="style2"
|
|
14
|
-
>
|
|
10
|
+
<style>
|
|
15
11
|
.test2 { color: blue; }
|
|
16
12
|
</style>
|
|
17
13
|
<link
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { createDOMElement, createMockElement, createMockElementType, renderWithTheme } from 'test-utils';
|
|
3
3
|
import { getElements, useSelectedElement } from '@elementor/editor-elements';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import {
|
|
5
|
+
__privateUseIsRouteActive as useIsRouteActive,
|
|
6
|
+
isExperimentActive,
|
|
7
|
+
useEditMode,
|
|
8
|
+
} from '@elementor/editor-v1-adapters';
|
|
9
|
+
import { screen, waitFor } from '@testing-library/react';
|
|
10
|
+
|
|
11
|
+
import { hasInlineEditableProperty } from '../../utils/inline-editing-utils';
|
|
8
12
|
import { ElementsOverlays } from '../elements-overlays';
|
|
13
|
+
import { CANVAS_WRAPPER_ID } from '../outline-overlay';
|
|
9
14
|
|
|
10
15
|
jest.mock( '@elementor/editor-elements' );
|
|
11
16
|
jest.mock( '@elementor/editor-v1-adapters', () => ( {
|
|
12
17
|
...jest.requireActual( '@elementor/editor-v1-adapters' ),
|
|
13
18
|
useEditMode: jest.fn(),
|
|
14
19
|
__privateUseIsRouteActive: jest.fn(),
|
|
20
|
+
isExperimentActive: jest.fn(),
|
|
15
21
|
} ) );
|
|
22
|
+
jest.mock( '../../utils/inline-editing-utils' );
|
|
16
23
|
|
|
17
24
|
describe( '<ElementsOverlays />', () => {
|
|
18
25
|
beforeEach( () => {
|
|
@@ -23,6 +30,8 @@ describe( '<ElementsOverlays />', () => {
|
|
|
23
30
|
|
|
24
31
|
jest.mocked( useEditMode ).mockReturnValue( 'edit' );
|
|
25
32
|
jest.mocked( useIsRouteActive ).mockReturnValue( false );
|
|
33
|
+
jest.mocked( hasInlineEditableProperty ).mockReturnValue( false );
|
|
34
|
+
jest.mocked( isExperimentActive ).mockReturnValue( true );
|
|
26
35
|
|
|
27
36
|
jest.mocked( getElements ).mockReturnValue( [
|
|
28
37
|
createMockElement( {
|
|
@@ -68,16 +77,15 @@ describe( '<ElementsOverlays />', () => {
|
|
|
68
77
|
} );
|
|
69
78
|
|
|
70
79
|
// Act.
|
|
71
|
-
|
|
72
|
-
await act( () => renderWithTheme( <ElementsOverlays /> ) );
|
|
80
|
+
renderWithTheme( <ElementsOverlays /> );
|
|
73
81
|
|
|
74
82
|
// Assert.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
await waitFor( () => {
|
|
84
|
+
const overlay = screen.getByRole( 'presentation' );
|
|
85
|
+
expect( overlay ).toHaveAttribute( 'data-element-overlay', 'atomic2' );
|
|
86
|
+
// eslint-disable-next-line testing-library/no-test-id-queries
|
|
87
|
+
expect( screen.getByTestId( CANVAS_WRAPPER_ID ) ).toContainElement( overlay );
|
|
88
|
+
} );
|
|
81
89
|
} );
|
|
82
90
|
|
|
83
91
|
it.each( [
|
|
@@ -128,4 +136,80 @@ describe( '<ElementsOverlays />', () => {
|
|
|
128
136
|
// Assert.
|
|
129
137
|
expect( screen.queryByRole( 'presentation' ) ).not.toBeInTheDocument();
|
|
130
138
|
} );
|
|
139
|
+
|
|
140
|
+
it( 'should return null when editor is not in edit mode', () => {
|
|
141
|
+
// Arrange
|
|
142
|
+
jest.mocked( useEditMode ).mockReturnValue( 'preview' );
|
|
143
|
+
jest.mocked( useIsRouteActive ).mockReturnValue( false );
|
|
144
|
+
jest.mocked( useSelectedElement ).mockReturnValue( {
|
|
145
|
+
element: { id: 'atomic1', type: 'widget' },
|
|
146
|
+
elementType: createMockElementType(),
|
|
147
|
+
} );
|
|
148
|
+
|
|
149
|
+
// Act
|
|
150
|
+
const { container } = renderWithTheme( <ElementsOverlays /> );
|
|
151
|
+
|
|
152
|
+
// Assert
|
|
153
|
+
expect( container ).toBeEmptyDOMElement();
|
|
154
|
+
expect( screen.queryByRole( 'presentation' ) ).not.toBeInTheDocument();
|
|
155
|
+
} );
|
|
156
|
+
|
|
157
|
+
it( 'should return null when panel/global route is active', () => {
|
|
158
|
+
// Arrange
|
|
159
|
+
jest.mocked( useEditMode ).mockReturnValue( 'edit' );
|
|
160
|
+
jest.mocked( useIsRouteActive ).mockReturnValue( true );
|
|
161
|
+
jest.mocked( useSelectedElement ).mockReturnValue( {
|
|
162
|
+
element: { id: 'atomic1', type: 'widget' },
|
|
163
|
+
elementType: createMockElementType(),
|
|
164
|
+
} );
|
|
165
|
+
|
|
166
|
+
// Act
|
|
167
|
+
const { container } = renderWithTheme( <ElementsOverlays /> );
|
|
168
|
+
|
|
169
|
+
// Assert
|
|
170
|
+
expect( container ).toBeEmptyDOMElement();
|
|
171
|
+
expect( screen.queryByRole( 'presentation' ) ).not.toBeInTheDocument();
|
|
172
|
+
} );
|
|
173
|
+
|
|
174
|
+
it( 'should render OutlineOverlay for atomic elements', async () => {
|
|
175
|
+
// Arrange
|
|
176
|
+
jest.mocked( useSelectedElement ).mockReturnValue( {
|
|
177
|
+
element: { id: 'atomic1', type: 'widget' },
|
|
178
|
+
elementType: createMockElementType(),
|
|
179
|
+
} );
|
|
180
|
+
|
|
181
|
+
// Act
|
|
182
|
+
renderWithTheme( <ElementsOverlays /> );
|
|
183
|
+
|
|
184
|
+
// Assert
|
|
185
|
+
const overlay = await screen.findByRole( 'presentation' );
|
|
186
|
+
expect( overlay ).toBeInTheDocument();
|
|
187
|
+
expect( overlay ).toHaveAttribute( 'data-element-overlay', 'atomic1' );
|
|
188
|
+
} );
|
|
189
|
+
|
|
190
|
+
it( 'should render InlineEditorOverlay only for selected elements that support inline editing', async () => {
|
|
191
|
+
// Arrange
|
|
192
|
+
const headingEl = createDOMElement( { tag: 'div', attrs: { 'data-atomic': '', id: '50' } } );
|
|
193
|
+
|
|
194
|
+
jest.mocked( getElements ).mockReturnValue( [
|
|
195
|
+
createMockElement( {
|
|
196
|
+
model: { id: 'heading-element' },
|
|
197
|
+
view: { el: headingEl, getDomElement: () => ( { get: () => headingEl } ) },
|
|
198
|
+
} ),
|
|
199
|
+
] );
|
|
200
|
+
|
|
201
|
+
jest.mocked( useSelectedElement ).mockReturnValue( {
|
|
202
|
+
element: { id: 'heading-element', type: 'widget' },
|
|
203
|
+
elementType: createMockElementType(),
|
|
204
|
+
} );
|
|
205
|
+
|
|
206
|
+
// Act
|
|
207
|
+
renderWithTheme( <ElementsOverlays /> );
|
|
208
|
+
|
|
209
|
+
// Assert
|
|
210
|
+
await waitFor( () => {
|
|
211
|
+
const overlay = screen.getByRole( 'presentation' );
|
|
212
|
+
expect( overlay ).toHaveAttribute( 'data-element-overlay', 'heading-element' );
|
|
213
|
+
} );
|
|
214
|
+
} );
|
|
131
215
|
} );
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { renderWithTheme } from 'test-utils';
|
|
3
|
+
import { getContainer, updateElementSettings, useElementSetting } from '@elementor/editor-elements';
|
|
4
|
+
import { htmlPropTypeUtil } from '@elementor/editor-props';
|
|
5
|
+
import { debounce } from '@elementor/utils';
|
|
6
|
+
import { act, screen } from '@testing-library/react';
|
|
7
|
+
|
|
8
|
+
import { useFloatingOnElement } from '../../hooks/use-floating-on-element';
|
|
9
|
+
import { getInlineEditablePropertyName } from '../../utils/inline-editing-utils';
|
|
10
|
+
import { InlineEditorOverlay } from '../inline-editor-overlay';
|
|
11
|
+
|
|
12
|
+
jest.mock( '@elementor/editor-elements' );
|
|
13
|
+
jest.mock( '@elementor/editor-props' );
|
|
14
|
+
jest.mock( '@elementor/utils' );
|
|
15
|
+
jest.mock( '@elementor/editor-controls', () => ( {
|
|
16
|
+
InlineEditor: jest.fn( ( { value, setValue } ) => (
|
|
17
|
+
<div aria-label="inline editor container">
|
|
18
|
+
<input aria-label="inline editor" value={ value } onChange={ ( e ) => setValue( e.target.value ) } />
|
|
19
|
+
</div>
|
|
20
|
+
) ),
|
|
21
|
+
} ) );
|
|
22
|
+
jest.mock( '../../hooks/use-floating-on-element' );
|
|
23
|
+
jest.mock( '../../utils/inline-editing-utils' );
|
|
24
|
+
|
|
25
|
+
describe( '<InlineEditorOverlay />', () => {
|
|
26
|
+
const mockElement = document.createElement( 'div' );
|
|
27
|
+
const mockId = 'test-element-id';
|
|
28
|
+
const mockPropertyName = 'title';
|
|
29
|
+
const mockValue = '<p>Test content</p>';
|
|
30
|
+
|
|
31
|
+
beforeEach( () => {
|
|
32
|
+
jest.mocked( useFloatingOnElement ).mockReturnValue( {
|
|
33
|
+
floating: {
|
|
34
|
+
setRef: jest.fn(),
|
|
35
|
+
ref: { current: null },
|
|
36
|
+
styles: { position: 'absolute', top: 0, left: 0 },
|
|
37
|
+
},
|
|
38
|
+
isVisible: true,
|
|
39
|
+
context: {} as never,
|
|
40
|
+
} );
|
|
41
|
+
|
|
42
|
+
jest.mocked( getContainer ).mockReturnValue( {
|
|
43
|
+
model: {
|
|
44
|
+
get: jest.fn().mockReturnValue( 'e-heading' ),
|
|
45
|
+
},
|
|
46
|
+
} as unknown as ReturnType< typeof getContainer > );
|
|
47
|
+
|
|
48
|
+
jest.mocked( getInlineEditablePropertyName ).mockReturnValue( mockPropertyName );
|
|
49
|
+
|
|
50
|
+
jest.mocked( useElementSetting ).mockReturnValue( {
|
|
51
|
+
$$type: 'html',
|
|
52
|
+
value: mockValue,
|
|
53
|
+
} );
|
|
54
|
+
|
|
55
|
+
jest.mocked( htmlPropTypeUtil.extract ).mockReturnValue( mockValue );
|
|
56
|
+
|
|
57
|
+
jest.mocked( htmlPropTypeUtil.create ).mockImplementation( ( value ) => ( {
|
|
58
|
+
$$type: 'html',
|
|
59
|
+
value: typeof value === 'function' ? value( null ) : value,
|
|
60
|
+
} ) );
|
|
61
|
+
|
|
62
|
+
const mockDebouncedFn = jest.fn( ( fn: ( ...args: unknown[] ) => void ) => {
|
|
63
|
+
const debouncedFn = ( ...args: unknown[] ) => fn( ...args );
|
|
64
|
+
(
|
|
65
|
+
debouncedFn as typeof debouncedFn & {
|
|
66
|
+
cancel: jest.Mock;
|
|
67
|
+
flush: jest.Mock;
|
|
68
|
+
pending: jest.Mock;
|
|
69
|
+
}
|
|
70
|
+
).cancel = jest.fn();
|
|
71
|
+
(
|
|
72
|
+
debouncedFn as typeof debouncedFn & {
|
|
73
|
+
cancel: jest.Mock;
|
|
74
|
+
flush: jest.Mock;
|
|
75
|
+
pending: jest.Mock;
|
|
76
|
+
}
|
|
77
|
+
).flush = jest.fn( ( ...args: unknown[] ) => fn( ...args ) );
|
|
78
|
+
(
|
|
79
|
+
debouncedFn as typeof debouncedFn & {
|
|
80
|
+
cancel: jest.Mock;
|
|
81
|
+
flush: jest.Mock;
|
|
82
|
+
pending: jest.Mock;
|
|
83
|
+
}
|
|
84
|
+
).pending = jest.fn().mockReturnValue( false );
|
|
85
|
+
return debouncedFn;
|
|
86
|
+
} );
|
|
87
|
+
jest.mocked( debounce ).mockImplementation( mockDebouncedFn as unknown as typeof debounce );
|
|
88
|
+
} );
|
|
89
|
+
|
|
90
|
+
afterEach( () => {
|
|
91
|
+
jest.clearAllMocks();
|
|
92
|
+
} );
|
|
93
|
+
|
|
94
|
+
it( 'should render InlineEditor when visible', () => {
|
|
95
|
+
renderWithTheme( <InlineEditorOverlay element={ mockElement } isSelected={ true } id={ mockId } /> );
|
|
96
|
+
|
|
97
|
+
const input = screen.getByRole( 'textbox', { name: 'inline editor' } );
|
|
98
|
+
expect( input ).toBeInTheDocument();
|
|
99
|
+
expect( input ).toHaveValue( mockValue );
|
|
100
|
+
} );
|
|
101
|
+
|
|
102
|
+
it( 'should not render when not visible', () => {
|
|
103
|
+
jest.mocked( useFloatingOnElement ).mockReturnValue( {
|
|
104
|
+
floating: {
|
|
105
|
+
setRef: jest.fn(),
|
|
106
|
+
ref: { current: null },
|
|
107
|
+
styles: {},
|
|
108
|
+
},
|
|
109
|
+
isVisible: false,
|
|
110
|
+
context: {} as never,
|
|
111
|
+
} );
|
|
112
|
+
|
|
113
|
+
renderWithTheme( <InlineEditorOverlay element={ mockElement } isSelected={ false } id={ mockId } /> );
|
|
114
|
+
|
|
115
|
+
expect( screen.queryByRole( 'textbox', { name: 'inline editor' } ) ).not.toBeInTheDocument();
|
|
116
|
+
} );
|
|
117
|
+
|
|
118
|
+
it( 'should get container and property name on mount', () => {
|
|
119
|
+
renderWithTheme( <InlineEditorOverlay element={ mockElement } isSelected={ true } id={ mockId } /> );
|
|
120
|
+
|
|
121
|
+
expect( getContainer ).toHaveBeenCalledWith( mockId );
|
|
122
|
+
expect( getInlineEditablePropertyName ).toHaveBeenCalled();
|
|
123
|
+
} );
|
|
124
|
+
|
|
125
|
+
it( 'should extract value from contentProp using htmlPropTypeUtil', () => {
|
|
126
|
+
renderWithTheme( <InlineEditorOverlay element={ mockElement } isSelected={ true } id={ mockId } /> );
|
|
127
|
+
|
|
128
|
+
expect( htmlPropTypeUtil.extract ).toHaveBeenCalledWith( {
|
|
129
|
+
$$type: 'html',
|
|
130
|
+
value: mockValue,
|
|
131
|
+
} );
|
|
132
|
+
} );
|
|
133
|
+
|
|
134
|
+
it( 'should call updateElementSettings when value changes', async () => {
|
|
135
|
+
const newValue = '<p>New content</p>';
|
|
136
|
+
const mockDebounceFn = jest.fn();
|
|
137
|
+
const debouncedFn = jest.fn( ( fn: ( ...args: unknown[] ) => void ) => {
|
|
138
|
+
mockDebounceFn.mockImplementation( fn );
|
|
139
|
+
return mockDebounceFn;
|
|
140
|
+
} );
|
|
141
|
+
jest.mocked( debounce ).mockImplementation( debouncedFn as unknown as typeof debounce );
|
|
142
|
+
|
|
143
|
+
renderWithTheme( <InlineEditorOverlay element={ mockElement } isSelected={ true } id={ mockId } /> );
|
|
144
|
+
|
|
145
|
+
const input = screen.getByRole( 'textbox', { name: 'inline editor' } ) as HTMLInputElement;
|
|
146
|
+
|
|
147
|
+
await act( async () => {
|
|
148
|
+
input.dispatchEvent( new Event( 'change', { bubbles: true } ) );
|
|
149
|
+
Object.defineProperty( input, 'value', { value: newValue, writable: true } );
|
|
150
|
+
} );
|
|
151
|
+
|
|
152
|
+
await act( async () => {
|
|
153
|
+
mockDebounceFn( newValue );
|
|
154
|
+
} );
|
|
155
|
+
|
|
156
|
+
expect( updateElementSettings ).toHaveBeenCalledWith( {
|
|
157
|
+
id: mockId,
|
|
158
|
+
props: {
|
|
159
|
+
[ mockPropertyName ]: {
|
|
160
|
+
$$type: 'html',
|
|
161
|
+
value: newValue,
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
withHistory: true,
|
|
165
|
+
} );
|
|
166
|
+
} );
|
|
167
|
+
|
|
168
|
+
it( 'should save when content is empty', async () => {
|
|
169
|
+
const emptyValue = '';
|
|
170
|
+
const mockDebounceFn = jest.fn();
|
|
171
|
+
const debouncedFn = jest.fn( ( fn: ( ...args: unknown[] ) => void ) => {
|
|
172
|
+
mockDebounceFn.mockImplementation( fn );
|
|
173
|
+
return mockDebounceFn;
|
|
174
|
+
} );
|
|
175
|
+
jest.mocked( debounce ).mockImplementation( debouncedFn as unknown as typeof debounce );
|
|
176
|
+
|
|
177
|
+
renderWithTheme( <InlineEditorOverlay element={ mockElement } isSelected={ true } id={ mockId } /> );
|
|
178
|
+
|
|
179
|
+
await act( async () => {
|
|
180
|
+
mockDebounceFn( emptyValue );
|
|
181
|
+
} );
|
|
182
|
+
|
|
183
|
+
expect( updateElementSettings ).toHaveBeenCalledWith( {
|
|
184
|
+
id: mockId,
|
|
185
|
+
props: {
|
|
186
|
+
[ mockPropertyName ]: {
|
|
187
|
+
$$type: 'html',
|
|
188
|
+
value: ' ',
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
withHistory: true,
|
|
192
|
+
} );
|
|
193
|
+
} );
|
|
194
|
+
|
|
195
|
+
it( 'should update and display new value after editing', async () => {
|
|
196
|
+
const initialValue = '<p>Initial content</p>';
|
|
197
|
+
const newValue = '<p>Updated content</p>';
|
|
198
|
+
|
|
199
|
+
jest.mocked( useElementSetting ).mockReturnValue( {
|
|
200
|
+
$$type: 'html',
|
|
201
|
+
value: initialValue,
|
|
202
|
+
} );
|
|
203
|
+
jest.mocked( htmlPropTypeUtil.extract ).mockReturnValue( initialValue );
|
|
204
|
+
|
|
205
|
+
const mockDebounceFn = jest.fn();
|
|
206
|
+
const debouncedFn = jest.fn( ( fn: ( ...args: unknown[] ) => void ) => {
|
|
207
|
+
mockDebounceFn.mockImplementation( fn );
|
|
208
|
+
return mockDebounceFn;
|
|
209
|
+
} );
|
|
210
|
+
jest.mocked( debounce ).mockImplementation( debouncedFn as unknown as typeof debounce );
|
|
211
|
+
|
|
212
|
+
const { rerender } = renderWithTheme(
|
|
213
|
+
<InlineEditorOverlay element={ mockElement } isSelected={ true } id={ mockId } />
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const input = screen.getByRole( 'textbox', { name: 'inline editor' } ) as HTMLInputElement;
|
|
217
|
+
expect( input ).toHaveValue( initialValue );
|
|
218
|
+
|
|
219
|
+
await act( async () => {
|
|
220
|
+
mockDebounceFn( newValue );
|
|
221
|
+
} );
|
|
222
|
+
|
|
223
|
+
expect( updateElementSettings ).toHaveBeenCalledWith( {
|
|
224
|
+
id: mockId,
|
|
225
|
+
props: {
|
|
226
|
+
[ mockPropertyName ]: {
|
|
227
|
+
$$type: 'html',
|
|
228
|
+
value: newValue,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
withHistory: true,
|
|
232
|
+
} );
|
|
233
|
+
|
|
234
|
+
jest.mocked( useElementSetting ).mockReturnValue( {
|
|
235
|
+
$$type: 'html',
|
|
236
|
+
value: newValue,
|
|
237
|
+
} );
|
|
238
|
+
jest.mocked( htmlPropTypeUtil.extract ).mockReturnValue( newValue );
|
|
239
|
+
|
|
240
|
+
rerender( <InlineEditorOverlay element={ mockElement } isSelected={ true } id={ mockId } /> );
|
|
241
|
+
|
|
242
|
+
const updatedInput = screen.getByRole( 'textbox', { name: 'inline editor' } ) as HTMLInputElement;
|
|
243
|
+
expect( updatedInput ).toHaveValue( newValue );
|
|
244
|
+
} );
|
|
245
|
+
} );
|
|
@@ -46,8 +46,8 @@ describe( '<StyleRenderer />', () => {
|
|
|
46
46
|
const mockContainer = document.createElement( 'div' );
|
|
47
47
|
|
|
48
48
|
const mockCssItems = [
|
|
49
|
-
{ id: 'style1', value: '.test { color: red; }', breakpoint: 'desktop' },
|
|
50
|
-
{ id: 'style2', value: '.test2 { color: blue; }', breakpoint: 'desktop' },
|
|
49
|
+
{ id: 'style1', value: '.test { color: red; }', breakpoint: 'desktop', state: null },
|
|
50
|
+
{ id: 'style2', value: '.test2 { color: blue; }', breakpoint: 'desktop', state: null },
|
|
51
51
|
];
|
|
52
52
|
|
|
53
53
|
const mockLinkAttrs = [
|
|
@@ -3,11 +3,29 @@ import { getElements, useSelectedElement } from '@elementor/editor-elements';
|
|
|
3
3
|
import {
|
|
4
4
|
__privateUseIsRouteActive as useIsRouteActive,
|
|
5
5
|
__privateUseListenTo as useListenTo,
|
|
6
|
+
isExperimentActive,
|
|
6
7
|
useEditMode,
|
|
7
8
|
windowEvent,
|
|
8
9
|
} from '@elementor/editor-v1-adapters';
|
|
9
10
|
|
|
10
|
-
import {
|
|
11
|
+
import type { ElementOverlayConfig } from '../types/element-overlay';
|
|
12
|
+
import { hasInlineEditableProperty } from '../utils/inline-editing-utils';
|
|
13
|
+
import { InlineEditorOverlay } from './inline-editor-overlay';
|
|
14
|
+
import { OutlineOverlay } from './outline-overlay';
|
|
15
|
+
|
|
16
|
+
const ELEMENTS_DATA_ATTR = 'atomic';
|
|
17
|
+
|
|
18
|
+
const overlayRegistry: ElementOverlayConfig[] = [
|
|
19
|
+
{
|
|
20
|
+
component: OutlineOverlay,
|
|
21
|
+
shouldRender: () => true,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
component: InlineEditorOverlay,
|
|
25
|
+
shouldRender: ( { id, isSelected } ) =>
|
|
26
|
+
isSelected && hasInlineEditableProperty( id ) && isExperimentActive( 'v4-inline-text-editing' ),
|
|
27
|
+
},
|
|
28
|
+
];
|
|
11
29
|
|
|
12
30
|
export function ElementsOverlays() {
|
|
13
31
|
const selected = useSelectedElement();
|
|
@@ -16,18 +34,23 @@ export function ElementsOverlays() {
|
|
|
16
34
|
|
|
17
35
|
const isEditMode = currentEditMode === 'edit';
|
|
18
36
|
const isKitRouteActive = useIsRouteActive( 'panel/global' );
|
|
19
|
-
|
|
20
37
|
const isActive = isEditMode && ! isKitRouteActive;
|
|
21
38
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<ElementOverlay key={ id } id={ id } element={ element } isSelected={ selected.element?.id === id } />
|
|
26
|
-
) )
|
|
27
|
-
);
|
|
28
|
-
}
|
|
39
|
+
if ( ! isActive ) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
29
42
|
|
|
30
|
-
|
|
43
|
+
return elements.map( ( [ id, element ] ) => {
|
|
44
|
+
const isSelected = selected.element?.id === id;
|
|
45
|
+
|
|
46
|
+
return overlayRegistry.map(
|
|
47
|
+
( { shouldRender, component: Overlay }, index ) =>
|
|
48
|
+
shouldRender( { id, element, isSelected } ) && (
|
|
49
|
+
<Overlay key={ `${ id }-${ index }` } id={ id } element={ element } isSelected={ isSelected } />
|
|
50
|
+
)
|
|
51
|
+
);
|
|
52
|
+
} );
|
|
53
|
+
}
|
|
31
54
|
|
|
32
55
|
type IdElementTuple = [ string, HTMLElement ];
|
|
33
56
|
|