@elementor/editor-canvas 4.0.0-manual → 4.0.1
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 +11 -2
- package/dist/index.d.ts +11 -2
- package/dist/index.js +307 -128
- package/dist/index.mjs +273 -94
- package/package.json +19 -18
- package/src/components/__tests__/style-renderer.test.tsx +4 -0
- package/src/hooks/__tests__/use-style-items.test.ts +125 -0
- package/src/hooks/use-style-items.ts +40 -16
- package/src/index.ts +1 -1
- package/src/init-settings-transformers.ts +2 -0
- package/src/legacy/create-nested-templated-element-type.ts +15 -2
- package/src/legacy/create-templated-element-type.ts +8 -0
- package/src/legacy/replacements/base.ts +4 -0
- package/src/legacy/replacements/inline-editing/canvas-inline-editor.tsx +49 -27
- package/src/legacy/replacements/inline-editing/inline-editing-elements.tsx +16 -10
- package/src/legacy/replacements/inline-editing/inline-editing-utils.ts +12 -1
- package/src/legacy/replacements/manager.ts +13 -0
- package/src/legacy/types.ts +6 -1
- package/src/mcp/canvas-mcp.ts +5 -7
- package/src/mcp/resources/breakpoints-resource.ts +11 -4
- package/src/mcp/resources/document-structure-resource.ts +18 -13
- package/src/mcp/tools/build-composition/schema.ts +1 -1
- package/src/mcp/tools/build-composition/tool.ts +5 -1
- package/src/mcp/utils/__tests__/get-composition-target-container.test.ts +59 -0
- package/src/mcp/utils/get-composition-target-container.ts +15 -0
- package/src/renderers/__tests__/create-styles-renderer.test.ts +117 -0
- package/src/renderers/create-styles-renderer.ts +13 -3
- package/src/style-commands/__tests__/paste-style.test.ts +5 -3
- package/src/style-commands/__tests__/reset-style.test.ts +3 -3
- package/src/style-commands/paste-style.ts +7 -1
- package/src/style-commands/reset-style.ts +1 -1
- package/src/style-commands/undoable-actions/paste-element-style.ts +1 -1
- package/src/style-commands/undoable-actions/reset-element-style.ts +1 -1
- package/src/transformers/shared/__tests__/svg-src-transformer.test.ts +184 -0
- package/src/transformers/shared/svg-src-transformer.ts +87 -0
- package/src/transformers/styles/__tests__/size-transformer.test.ts +24 -0
- package/src/transformers/styles/size-transformer.ts +3 -0
- /package/src/{style-commands/utils.ts → utils/command-utils.ts} +0 -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": "4.0.
|
|
4
|
+
"version": "4.0.1",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Elementor Team",
|
|
7
7
|
"homepage": "https://elementor.com/",
|
|
@@ -37,24 +37,25 @@
|
|
|
37
37
|
"react-dom": "^18.3.1"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@elementor/editor": "4.0.
|
|
41
|
-
"
|
|
42
|
-
"@elementor/editor-
|
|
43
|
-
"@elementor/editor-
|
|
44
|
-
"@elementor/editor-
|
|
45
|
-
"@elementor/editor-
|
|
46
|
-
"@elementor/editor-
|
|
47
|
-
"@elementor/editor-
|
|
48
|
-
"@elementor/editor-
|
|
49
|
-
"@elementor/editor-
|
|
50
|
-
"@elementor/editor-styles
|
|
51
|
-
"@elementor/editor-
|
|
52
|
-
"@elementor/editor-
|
|
53
|
-
"@elementor/
|
|
54
|
-
"@elementor/
|
|
40
|
+
"@elementor/editor": "4.0.1",
|
|
41
|
+
"dompurify": "^3.2.6",
|
|
42
|
+
"@elementor/editor-controls": "4.0.1",
|
|
43
|
+
"@elementor/editor-documents": "4.0.1",
|
|
44
|
+
"@elementor/editor-elements": "4.0.1",
|
|
45
|
+
"@elementor/editor-interactions": "4.0.1",
|
|
46
|
+
"@elementor/editor-mcp": "4.0.1",
|
|
47
|
+
"@elementor/editor-notifications": "4.0.1",
|
|
48
|
+
"@elementor/editor-props": "4.0.1",
|
|
49
|
+
"@elementor/editor-responsive": "4.0.1",
|
|
50
|
+
"@elementor/editor-styles": "4.0.1",
|
|
51
|
+
"@elementor/editor-styles-repository": "4.0.1",
|
|
52
|
+
"@elementor/editor-ui": "4.0.1",
|
|
53
|
+
"@elementor/editor-v1-adapters": "4.0.1",
|
|
54
|
+
"@elementor/schema": "4.0.1",
|
|
55
|
+
"@elementor/twing": "4.0.1",
|
|
55
56
|
"@elementor/ui": "1.36.17",
|
|
56
|
-
"@elementor/utils": "4.0.
|
|
57
|
-
"@elementor/wp-media": "4.0.
|
|
57
|
+
"@elementor/utils": "4.0.1",
|
|
58
|
+
"@elementor/wp-media": "4.0.1",
|
|
58
59
|
"@floating-ui/react": "^0.27.5",
|
|
59
60
|
"@wordpress/i18n": "^5.13.0"
|
|
60
61
|
},
|
|
@@ -13,6 +13,10 @@ jest.mock( '@elementor/editor-v1-adapters', () => ( {
|
|
|
13
13
|
commandEndEvent: jest.fn(),
|
|
14
14
|
} ) );
|
|
15
15
|
|
|
16
|
+
jest.mock( '@elementor/editor-documents', () => ( {
|
|
17
|
+
getCurrentDocument: jest.fn().mockReturnValue( { id: 1 } ),
|
|
18
|
+
} ) );
|
|
19
|
+
|
|
16
20
|
jest.mock( '@elementor/ui', () => ( {
|
|
17
21
|
Portal: jest.fn( ( { children } ) => <div data-testid="portal">{ children }</div> ),
|
|
18
22
|
} ) );
|
|
@@ -128,6 +128,9 @@ describe( 'useStyleItems', () => {
|
|
|
128
128
|
|
|
129
129
|
jest.mocked( stylesRepository ).getProviders.mockReturnValue( [ mockProvider1, mockProvider2 ] );
|
|
130
130
|
|
|
131
|
+
const provider1OriginalOrder = mockProvider1.actions.all().map( ( s ) => s.id );
|
|
132
|
+
const provider2OriginalOrder = mockProvider2.actions.all().map( ( s ) => s.id );
|
|
133
|
+
|
|
131
134
|
let attachPreviewCallback: () => Promise< void >;
|
|
132
135
|
|
|
133
136
|
jest.mocked( registerDataHook ).mockImplementation( ( position, command, callback ) => {
|
|
@@ -156,6 +159,9 @@ describe( 'useStyleItems', () => {
|
|
|
156
159
|
{ id: 'style2', breakpoint: 'desktop' },
|
|
157
160
|
{ id: 'style1', breakpoint: 'desktop' },
|
|
158
161
|
] );
|
|
162
|
+
|
|
163
|
+
expect( mockProvider1.actions.all().map( ( s ) => s.id ) ).toEqual( provider1OriginalOrder );
|
|
164
|
+
expect( mockProvider2.actions.all().map( ( s ) => s.id ) ).toEqual( provider2OriginalOrder );
|
|
159
165
|
} );
|
|
160
166
|
|
|
161
167
|
it( 'should return style items ordered by provider priority and breakpoint', async () => {
|
|
@@ -283,6 +289,125 @@ describe( 'useStyleItems', () => {
|
|
|
283
289
|
expect( result.current ).toHaveLength( 2 );
|
|
284
290
|
} );
|
|
285
291
|
|
|
292
|
+
it( 'should maintain breakpoint order after style update', async () => {
|
|
293
|
+
// Arrange.
|
|
294
|
+
const renderStylesMock = jest.fn().mockImplementation( ( { styles } ) =>
|
|
295
|
+
Promise.resolve(
|
|
296
|
+
styles.map( ( style: StyleDefinition ) => ( {
|
|
297
|
+
id: style.id,
|
|
298
|
+
breakpoint: style?.variants[ 0 ]?.meta.breakpoint || 'desktop',
|
|
299
|
+
} ) )
|
|
300
|
+
)
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
jest.mocked( useStyleRenderer ).mockReturnValue( renderStylesMock );
|
|
304
|
+
|
|
305
|
+
const mockProvider = createMockStylesProvider( { key: 'provider1', priority: 1 }, [
|
|
306
|
+
createMockStyleDefinitionWithVariants( {
|
|
307
|
+
id: 'style1',
|
|
308
|
+
variants: [
|
|
309
|
+
{ meta: { breakpoint: null, state: null }, props: { padding: '10px' }, custom_css: null },
|
|
310
|
+
{ meta: { breakpoint: 'tablet', state: null }, props: { padding: '8px' }, custom_css: null },
|
|
311
|
+
{ meta: { breakpoint: 'mobile', state: null }, props: { padding: '5px' }, custom_css: null },
|
|
312
|
+
],
|
|
313
|
+
} ),
|
|
314
|
+
] );
|
|
315
|
+
|
|
316
|
+
jest.mocked( stylesRepository ).getProviders.mockReturnValue( [ mockProvider ] );
|
|
317
|
+
|
|
318
|
+
// Act - initial render.
|
|
319
|
+
const { result } = renderHook( () => useStyleItems() );
|
|
320
|
+
|
|
321
|
+
await act( async () => {
|
|
322
|
+
mockProvider.actions.updateProps?.( {
|
|
323
|
+
id: 'style1',
|
|
324
|
+
meta: { breakpoint: 'tablet', state: null },
|
|
325
|
+
props: { padding: '12px' },
|
|
326
|
+
} );
|
|
327
|
+
} );
|
|
328
|
+
|
|
329
|
+
// Assert - items should be ordered by breakpoint (desktop, tablet, mobile).
|
|
330
|
+
const breakpointOrder = result.current.map( ( item ) => item.breakpoint );
|
|
331
|
+
expect( breakpointOrder ).toEqual( [ 'desktop', 'tablet', 'mobile' ] );
|
|
332
|
+
|
|
333
|
+
// Act - update again (should maintain order).
|
|
334
|
+
await act( async () => {
|
|
335
|
+
mockProvider.actions.update?.( { id: 'style1', label: 'Updated' } );
|
|
336
|
+
} );
|
|
337
|
+
|
|
338
|
+
// Assert - order should still be maintained.
|
|
339
|
+
const breakpointOrderAfterUpdate = result.current.map( ( item ) => item.breakpoint );
|
|
340
|
+
expect( breakpointOrderAfterUpdate ).toEqual( [ 'desktop', 'tablet', 'mobile' ] );
|
|
341
|
+
} );
|
|
342
|
+
|
|
343
|
+
it( 'should recover and render styles when a provider key becomes available after initial failure', async () => {
|
|
344
|
+
// Arrange.
|
|
345
|
+
let shouldThrow = true;
|
|
346
|
+
|
|
347
|
+
const dynamicKey: () => string = () => {
|
|
348
|
+
if ( shouldThrow ) {
|
|
349
|
+
throw new Error( 'Document not ready' );
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return 'late-provider';
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const failingThenSucceedingProvider = createMockStylesProvider(
|
|
356
|
+
{
|
|
357
|
+
key: dynamicKey,
|
|
358
|
+
priority: 2,
|
|
359
|
+
},
|
|
360
|
+
[ createMockStyleDefinition( { id: 'late-style1' } ), createMockStyleDefinition( { id: 'late-style2' } ) ]
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
const stableProvider = createMockStylesProvider(
|
|
364
|
+
{
|
|
365
|
+
key: 'stable-provider',
|
|
366
|
+
priority: 1,
|
|
367
|
+
},
|
|
368
|
+
[
|
|
369
|
+
createMockStyleDefinition( { id: 'stable-style1' } ),
|
|
370
|
+
createMockStyleDefinition( { id: 'stable-style2' } ),
|
|
371
|
+
]
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
jest.mocked( stylesRepository ).getProviders.mockReturnValue( [
|
|
375
|
+
failingThenSucceedingProvider,
|
|
376
|
+
stableProvider,
|
|
377
|
+
] );
|
|
378
|
+
|
|
379
|
+
let attachPreviewCallback: () => Promise< void >;
|
|
380
|
+
|
|
381
|
+
jest.mocked( registerDataHook ).mockImplementation( ( position, command, callback ) => {
|
|
382
|
+
if ( command === 'editor/documents/attach-preview' && position === 'after' ) {
|
|
383
|
+
attachPreviewCallback = callback as never;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return null as never;
|
|
387
|
+
} );
|
|
388
|
+
|
|
389
|
+
// Act.
|
|
390
|
+
const { result } = renderHook( () => useStyleItems() );
|
|
391
|
+
|
|
392
|
+
// Assert - hook should not crash, should return empty initially.
|
|
393
|
+
expect( result.current ).toEqual( [] );
|
|
394
|
+
|
|
395
|
+
// Act - simulate document becoming ready, then trigger attach-preview.
|
|
396
|
+
shouldThrow = false;
|
|
397
|
+
|
|
398
|
+
await act( async () => {
|
|
399
|
+
await attachPreviewCallback?.();
|
|
400
|
+
} );
|
|
401
|
+
|
|
402
|
+
// Assert - both providers' styles should render in correct priority order.
|
|
403
|
+
expect( result.current ).toEqual( [
|
|
404
|
+
{ id: 'stable-style2', breakpoint: 'desktop' },
|
|
405
|
+
{ id: 'stable-style1', breakpoint: 'desktop' },
|
|
406
|
+
{ id: 'late-style2', breakpoint: 'desktop' },
|
|
407
|
+
{ id: 'late-style1', breakpoint: 'desktop' },
|
|
408
|
+
] );
|
|
409
|
+
} );
|
|
410
|
+
|
|
286
411
|
it( 'should only re-render changed styles on differential update', async () => {
|
|
287
412
|
// Arrange.
|
|
288
413
|
const renderStylesMock = jest.fn().mockImplementation( ( { styles } ) =>
|
|
@@ -36,25 +36,35 @@ export function useStyleItems() {
|
|
|
36
36
|
const styleItemsCacheRef = useRef< Map< string, StyleItemsCache > >( new Map() );
|
|
37
37
|
|
|
38
38
|
const providerAndSubscribers = useMemo( () => {
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
const createEmptyCache = () => {
|
|
40
|
+
return { orderedIds: [], itemsById: new Map() };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const getCache = ( provider: StylesProvider ): StyleItemsCache => {
|
|
44
|
+
const providerKey = safeGetKey( provider );
|
|
45
|
+
|
|
46
|
+
if ( ! providerKey ) {
|
|
47
|
+
return createEmptyCache();
|
|
48
|
+
}
|
|
41
49
|
|
|
42
50
|
if ( ! styleItemsCacheRef.current.has( providerKey ) ) {
|
|
43
|
-
styleItemsCacheRef.current.set( providerKey,
|
|
51
|
+
styleItemsCacheRef.current.set( providerKey, createEmptyCache() );
|
|
44
52
|
}
|
|
45
53
|
|
|
46
|
-
|
|
54
|
+
return styleItemsCacheRef.current.get( providerKey ) as StyleItemsCache;
|
|
55
|
+
};
|
|
47
56
|
|
|
48
|
-
|
|
57
|
+
return stylesRepository.getProviders().map(
|
|
58
|
+
( provider ): ProviderAndSubscriber => ( {
|
|
49
59
|
provider,
|
|
50
60
|
subscriber: createProviderSubscriber( {
|
|
51
61
|
provider,
|
|
52
62
|
renderStyles,
|
|
53
63
|
setStyleItems,
|
|
54
|
-
|
|
64
|
+
getCache: () => getCache( provider ),
|
|
55
65
|
} ),
|
|
56
|
-
}
|
|
57
|
-
|
|
66
|
+
} )
|
|
67
|
+
);
|
|
58
68
|
}, [ renderStyles ] );
|
|
59
69
|
|
|
60
70
|
useEffect( () => {
|
|
@@ -122,25 +132,34 @@ function createBreakpointSorter( breakpointsOrder: BreakpointId[] ) {
|
|
|
122
132
|
breakpointsOrder.indexOf( breakpointB as BreakpointId );
|
|
123
133
|
}
|
|
124
134
|
|
|
135
|
+
function safeGetKey( provider: StylesProvider ): string | null {
|
|
136
|
+
try {
|
|
137
|
+
return provider.getKey();
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
125
143
|
type CreateProviderSubscriberArgs = {
|
|
126
144
|
provider: StylesProvider;
|
|
127
145
|
renderStyles: StyleRenderer;
|
|
128
146
|
setStyleItems: Dispatch< SetStateAction< ProviderAndStyleItemsMap > >;
|
|
129
|
-
|
|
147
|
+
getCache: () => StyleItemsCache;
|
|
130
148
|
};
|
|
131
149
|
|
|
132
|
-
function createProviderSubscriber( { provider, renderStyles, setStyleItems,
|
|
150
|
+
function createProviderSubscriber( { provider, renderStyles, setStyleItems, getCache }: CreateProviderSubscriberArgs ) {
|
|
133
151
|
return abortPreviousRuns( ( abortController, previous?: StylesCollection, current?: StylesCollection ) =>
|
|
134
152
|
signalizedProcess( abortController.signal )
|
|
135
153
|
.then( ( _, signal ) => {
|
|
154
|
+
const cache = getCache();
|
|
136
155
|
const hasDiffInfo = current !== undefined && previous !== undefined;
|
|
137
156
|
const hasCache = cache.orderedIds.length > 0;
|
|
138
157
|
|
|
139
158
|
if ( hasDiffInfo && hasCache ) {
|
|
140
|
-
return updateItems( previous, current, signal );
|
|
159
|
+
return updateItems( cache, previous, current, signal );
|
|
141
160
|
}
|
|
142
161
|
|
|
143
|
-
return createItems( signal );
|
|
162
|
+
return createItems( cache, signal );
|
|
144
163
|
} )
|
|
145
164
|
.then( ( items ) => {
|
|
146
165
|
setStyleItems( ( prev ) => ( {
|
|
@@ -151,7 +170,12 @@ function createProviderSubscriber( { provider, renderStyles, setStyleItems, cach
|
|
|
151
170
|
.execute()
|
|
152
171
|
);
|
|
153
172
|
|
|
154
|
-
async function updateItems(
|
|
173
|
+
async function updateItems(
|
|
174
|
+
cache: StyleItemsCache,
|
|
175
|
+
previous: StylesCollection,
|
|
176
|
+
current: StylesCollection,
|
|
177
|
+
signal: AbortSignal
|
|
178
|
+
) {
|
|
155
179
|
const changedIds = getChangedStyleIds( previous, current );
|
|
156
180
|
|
|
157
181
|
cache.orderedIds = provider.actions
|
|
@@ -178,10 +202,10 @@ function createProviderSubscriber( { provider, renderStyles, setStyleItems, cach
|
|
|
178
202
|
return getOrderedItems( cache );
|
|
179
203
|
}
|
|
180
204
|
|
|
181
|
-
async function createItems( signal: AbortSignal ) {
|
|
205
|
+
async function createItems( cache: StyleItemsCache, signal: AbortSignal ) {
|
|
182
206
|
const allStyles = provider.actions.all();
|
|
183
207
|
|
|
184
|
-
const styles = allStyles.reverse().map( ( style ) => {
|
|
208
|
+
const styles = [ ...allStyles ].reverse().map( ( style ) => {
|
|
185
209
|
return {
|
|
186
210
|
...style,
|
|
187
211
|
cssName: provider.actions.resolveCssName( style.id ),
|
|
@@ -191,7 +215,7 @@ function createProviderSubscriber( { provider, renderStyles, setStyleItems, cach
|
|
|
191
215
|
return renderStyles( { styles: breakToBreakpoints( styles ), signal } ).then( ( rendered ) => {
|
|
192
216
|
rebuildCache( cache, allStyles, rendered );
|
|
193
217
|
|
|
194
|
-
return
|
|
218
|
+
return getOrderedItems( cache );
|
|
195
219
|
} );
|
|
196
220
|
}
|
|
197
221
|
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { BREAKPOINTS_SCHEMA_URI } from './mcp/resources/breakpoints-resource';
|
|
|
2
2
|
export { STYLE_SCHEMA_URI } from './mcp/resources/widgets-schema-resource';
|
|
3
3
|
|
|
4
4
|
export { init } from './init';
|
|
5
|
-
export { isAtomicWidget } from './
|
|
5
|
+
export { isAtomicWidget } from './utils/command-utils';
|
|
6
6
|
|
|
7
7
|
export {
|
|
8
8
|
createTemplatedElementView,
|
|
@@ -9,6 +9,7 @@ import { queryTransformer } from './transformers/settings/query-transformer';
|
|
|
9
9
|
import { imageSrcTransformer } from './transformers/shared/image-src-transformer';
|
|
10
10
|
import { imageTransformer } from './transformers/shared/image-transformer';
|
|
11
11
|
import { plainTransformer } from './transformers/shared/plain-transformer';
|
|
12
|
+
import { svgSrcTransformer } from './transformers/shared/svg-src-transformer';
|
|
12
13
|
import { videoSrcTransformer } from './transformers/shared/video-src-transformer';
|
|
13
14
|
|
|
14
15
|
export function initSettingsTransformers() {
|
|
@@ -18,6 +19,7 @@ export function initSettingsTransformers() {
|
|
|
18
19
|
.register( 'query', queryTransformer )
|
|
19
20
|
.register( 'image', imageTransformer )
|
|
20
21
|
.register( 'image-src', imageSrcTransformer )
|
|
22
|
+
.register( 'svg-src', svgSrcTransformer )
|
|
21
23
|
.register( 'video-src', videoSrcTransformer )
|
|
22
24
|
.register( 'attributes', attributesTransformer )
|
|
23
25
|
.register( 'date-time', dateTimeTransformer )
|
|
@@ -61,13 +61,14 @@ export function createNestedTemplatedElementType( {
|
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
function buildEditorAttributes( model:
|
|
64
|
+
function buildEditorAttributes( model: ElementView[ 'model' ] ): string {
|
|
65
65
|
const id = model.get( 'id' );
|
|
66
|
+
const originId = model.get( 'originId' );
|
|
66
67
|
const cid = model.cid ?? '';
|
|
67
68
|
|
|
68
69
|
const attrs: Record< string, string > = {
|
|
69
70
|
'data-model-cid': cid,
|
|
70
|
-
'data-interaction-id': id,
|
|
71
|
+
'data-interaction-id': originId ?? id,
|
|
71
72
|
'x-ignore': 'true',
|
|
72
73
|
};
|
|
73
74
|
|
|
@@ -116,6 +117,10 @@ export function createNestedTemplatedElementView( {
|
|
|
116
117
|
this._lastResolvedSettingsHash = null;
|
|
117
118
|
},
|
|
118
119
|
|
|
120
|
+
renderOnChange() {
|
|
121
|
+
this.render();
|
|
122
|
+
},
|
|
123
|
+
|
|
119
124
|
render() {
|
|
120
125
|
this._abortController?.abort();
|
|
121
126
|
this._abortController = new AbortController();
|
|
@@ -181,6 +186,7 @@ export function createNestedTemplatedElementView( {
|
|
|
181
186
|
|
|
182
187
|
const context = {
|
|
183
188
|
id: model.get( 'id' ),
|
|
189
|
+
interaction_id: this.getInteractionId(),
|
|
184
190
|
type,
|
|
185
191
|
settings,
|
|
186
192
|
base_styles: baseStylesDictionary,
|
|
@@ -361,5 +367,12 @@ export function createNestedTemplatedElementView( {
|
|
|
361
367
|
_openEditingPanel( options?: { scrollIntoView: boolean } ) {
|
|
362
368
|
this._doAfterRender( () => parentOpenEditingPanel.call( this, options ) );
|
|
363
369
|
},
|
|
370
|
+
|
|
371
|
+
getInteractionId() {
|
|
372
|
+
const originId = this.model.get( 'originId' );
|
|
373
|
+
const id = this.model.get( 'id' );
|
|
374
|
+
|
|
375
|
+
return originId ?? id;
|
|
376
|
+
},
|
|
364
377
|
} ) as unknown as typeof ElementView;
|
|
365
378
|
}
|
|
@@ -162,6 +162,7 @@ export function createTemplatedElementView( {
|
|
|
162
162
|
|
|
163
163
|
const context = {
|
|
164
164
|
id: this.model.get( 'id' ),
|
|
165
|
+
interaction_id: this.getInteractionId(),
|
|
165
166
|
type,
|
|
166
167
|
settings,
|
|
167
168
|
base_styles: baseStylesDictionary,
|
|
@@ -206,5 +207,12 @@ export function createTemplatedElementView( {
|
|
|
206
207
|
_openEditingPanel( options?: { scrollIntoView: boolean } ) {
|
|
207
208
|
this._doAfterRender( () => super._openEditingPanel( options ) );
|
|
208
209
|
}
|
|
210
|
+
|
|
211
|
+
getInteractionId() {
|
|
212
|
+
const originId = this.model.get( 'originId' );
|
|
213
|
+
const id = this.model.get( 'id' );
|
|
214
|
+
|
|
215
|
+
return originId ?? id;
|
|
216
|
+
}
|
|
209
217
|
};
|
|
210
218
|
}
|
|
@@ -26,6 +26,8 @@ export class ReplacementBase implements ReplacementBaseInterface {
|
|
|
26
26
|
protected type: ReplacementSettings[ 'type' ];
|
|
27
27
|
protected id: ReplacementSettings[ 'id' ];
|
|
28
28
|
protected refreshView: ReplacementSettings[ 'refreshView' ];
|
|
29
|
+
protected reactRoot: ReplacementSettings[ 'reactRoot' ];
|
|
30
|
+
protected reactContainer: ReplacementSettings[ 'reactContainer' ];
|
|
29
31
|
|
|
30
32
|
constructor( settings: ReplacementSettings ) {
|
|
31
33
|
this.getSetting = settings.getSetting;
|
|
@@ -34,6 +36,8 @@ export class ReplacementBase implements ReplacementBaseInterface {
|
|
|
34
36
|
this.type = settings.type;
|
|
35
37
|
this.id = settings.id;
|
|
36
38
|
this.refreshView = settings.refreshView;
|
|
39
|
+
this.reactRoot = settings.reactRoot;
|
|
40
|
+
this.reactContainer = settings.reactContainer;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
static getTypes(): string[] | null {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { useEffect, useLayoutEffect, useState } from 'react';
|
|
2
|
+
import { useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
|
3
3
|
import { InlineEditor, InlineEditorToolbar } from '@elementor/editor-controls';
|
|
4
4
|
import { Box, ThemeProvider } from '@elementor/ui';
|
|
5
5
|
import { autoUpdate, flip, FloatingPortal, useFloating } from '@floating-ui/react';
|
|
@@ -9,68 +9,83 @@ import {
|
|
|
9
9
|
type Editor,
|
|
10
10
|
getInlineEditorElement,
|
|
11
11
|
horizontalShifterMiddleware as horizontalShifter,
|
|
12
|
-
removeToolbarAnchor,
|
|
13
12
|
useOnClickOutsideIframe,
|
|
14
13
|
useRenderToolbar,
|
|
15
14
|
} from './inline-editing-utils';
|
|
16
15
|
|
|
17
|
-
const EDITOR_WRAPPER_SELECTOR = 'inline-editor-wrapper';
|
|
18
|
-
|
|
19
16
|
export const CanvasInlineEditor = ( {
|
|
20
17
|
elementClasses,
|
|
21
18
|
initialValue,
|
|
22
19
|
expectedTag,
|
|
23
20
|
rootElement,
|
|
21
|
+
contentElement,
|
|
24
22
|
id,
|
|
25
23
|
setValue,
|
|
26
|
-
|
|
24
|
+
requestDestroy,
|
|
27
25
|
}: {
|
|
28
26
|
elementClasses: string;
|
|
29
27
|
initialValue: string | null;
|
|
30
28
|
expectedTag: string | null;
|
|
31
29
|
rootElement: HTMLElement;
|
|
30
|
+
contentElement: HTMLElement;
|
|
32
31
|
id: string;
|
|
33
32
|
setValue: ( value: string | null ) => void;
|
|
34
|
-
|
|
33
|
+
requestDestroy: () => void;
|
|
35
34
|
} ) => {
|
|
35
|
+
const [ active, setActive ] = useState( true );
|
|
36
36
|
const [ editor, setEditor ] = useState< Editor | null >( null );
|
|
37
|
-
const { onSelectionEnd, anchor: toolbarAnchor } = useRenderToolbar( rootElement.ownerDocument, id );
|
|
37
|
+
const { onSelectionEnd, anchor: toolbarAnchor, clearAnchor } = useRenderToolbar( rootElement.ownerDocument, id );
|
|
38
|
+
|
|
39
|
+
useEffect( () => {
|
|
40
|
+
if ( ! active ) {
|
|
41
|
+
clearAnchor();
|
|
42
|
+
requestDestroy();
|
|
43
|
+
}
|
|
44
|
+
}, [ active, clearAnchor, requestDestroy ] );
|
|
45
|
+
|
|
46
|
+
const dismiss = useCallback( () => {
|
|
47
|
+
setEditor( null );
|
|
48
|
+
setActive( false );
|
|
49
|
+
}, [] );
|
|
38
50
|
|
|
39
|
-
|
|
40
|
-
removeToolbarAnchor( rootElement.ownerDocument, id );
|
|
51
|
+
useOnClickOutsideIframe( dismiss );
|
|
41
52
|
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
useEffect( () => {
|
|
54
|
+
const ownerDocument = contentElement.ownerDocument;
|
|
55
|
+
|
|
56
|
+
const handleClickAway = ( event: MouseEvent ) => {
|
|
57
|
+
if ( contentElement.contains( event.target as Node ) ) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
44
60
|
|
|
45
|
-
|
|
61
|
+
dismiss();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
ownerDocument.addEventListener( 'mousedown', handleClickAway );
|
|
65
|
+
|
|
66
|
+
return () => ownerDocument.removeEventListener( 'mousedown', handleClickAway );
|
|
67
|
+
}, [ contentElement, dismiss ] );
|
|
68
|
+
|
|
69
|
+
if ( ! active ) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
46
72
|
|
|
47
73
|
return (
|
|
48
74
|
<ThemeProvider>
|
|
49
75
|
<InlineEditingOverlay expectedTag={ expectedTag } rootElement={ rootElement } id={ id } />
|
|
50
|
-
<style>
|
|
51
|
-
{ `
|
|
52
|
-
.ProseMirror > * {
|
|
53
|
-
height: 100%;
|
|
54
|
-
}
|
|
55
|
-
.${ EDITOR_WRAPPER_SELECTOR } .ProseMirror > button[contenteditable="true"] {
|
|
56
|
-
height: auto;
|
|
57
|
-
cursor: text;
|
|
58
|
-
}
|
|
59
|
-
` }
|
|
60
|
-
</style>
|
|
61
76
|
<InlineEditor
|
|
62
77
|
onEditorCreate={ setEditor }
|
|
78
|
+
mountElement={ contentElement }
|
|
63
79
|
editorProps={ {
|
|
64
80
|
attributes: {
|
|
65
|
-
style: 'outline: none;
|
|
81
|
+
style: 'outline: none; display: inherit; justify-content: inherit; align-items: inherit; flex-direction: inherit; text-align: inherit;',
|
|
66
82
|
},
|
|
67
83
|
} }
|
|
68
84
|
elementClasses={ elementClasses }
|
|
69
85
|
value={ initialValue }
|
|
70
86
|
setValue={ setValue }
|
|
71
|
-
onBlur={
|
|
87
|
+
onBlur={ dismiss }
|
|
72
88
|
autofocus
|
|
73
|
-
expectedTag={ expectedTag }
|
|
74
89
|
onSelectionEnd={ onSelectionEnd }
|
|
75
90
|
/>
|
|
76
91
|
{ toolbarAnchor && editor && <InlineEditingToolbar anchor={ toolbarAnchor } editor={ editor } id={ id } /> }
|
|
@@ -114,7 +129,14 @@ const InlineEditingToolbar = ( { anchor, editor, id }: { anchor: HTMLElement; ed
|
|
|
114
129
|
|
|
115
130
|
return (
|
|
116
131
|
<FloatingPortal id={ CANVAS_WRAPPER_ID }>
|
|
117
|
-
<Box
|
|
132
|
+
<Box
|
|
133
|
+
ref={ refs.setFloating }
|
|
134
|
+
role="presentation"
|
|
135
|
+
style={ {
|
|
136
|
+
...floatingStyles,
|
|
137
|
+
pointerEvents: 'none',
|
|
138
|
+
} }
|
|
139
|
+
>
|
|
118
140
|
<InlineEditorToolbar editor={ editor } elementId={ id } />
|
|
119
141
|
</Box>
|
|
120
142
|
</FloatingPortal>
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import { createRoot, type Root } from 'react-dom/client';
|
|
3
2
|
import { getContainer, getElementLabel, getElementType } from '@elementor/editor-elements';
|
|
4
3
|
import {
|
|
5
4
|
htmlV3PropTypeUtil,
|
|
@@ -25,8 +24,8 @@ type TagPropType = PropType< 'tag' > & {
|
|
|
25
24
|
const HISTORY_DEBOUNCE_WAIT = 800;
|
|
26
25
|
|
|
27
26
|
export default class InlineEditingReplacement extends ReplacementBase {
|
|
28
|
-
private inlineEditorRoot: Root | null = null;
|
|
29
27
|
private handlerAttached = false;
|
|
28
|
+
private editing = false;
|
|
30
29
|
|
|
31
30
|
getReplacementKey() {
|
|
32
31
|
return 'inline-editing';
|
|
@@ -37,7 +36,7 @@ export default class InlineEditingReplacement extends ReplacementBase {
|
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
isEditingModeActive() {
|
|
40
|
-
return
|
|
39
|
+
return this.editing;
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
shouldRenderReplacement() {
|
|
@@ -91,8 +90,8 @@ export default class InlineEditingReplacement extends ReplacementBase {
|
|
|
91
90
|
resetInlineEditorRoot() {
|
|
92
91
|
this.element.removeEventListener( 'click', this.handleRenderInlineEditor );
|
|
93
92
|
this.handlerAttached = false;
|
|
94
|
-
this.
|
|
95
|
-
this.
|
|
93
|
+
this.reactRoot.render( null );
|
|
94
|
+
this.editing = false;
|
|
96
95
|
}
|
|
97
96
|
|
|
98
97
|
unmountInlineEditor() {
|
|
@@ -239,22 +238,29 @@ export default class InlineEditingReplacement extends ReplacementBase {
|
|
|
239
238
|
this.resetInlineEditorRoot();
|
|
240
239
|
}
|
|
241
240
|
|
|
242
|
-
const
|
|
241
|
+
const contentElement = this.element.children?.[ 0 ] as HTMLElement | undefined;
|
|
242
|
+
|
|
243
|
+
if ( ! contentElement ) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const elementClasses = contentElement.classList.toString();
|
|
243
248
|
const propValue = this.getExtractedContentValue();
|
|
244
249
|
const expectedTag = this.getExpectedTag();
|
|
245
250
|
|
|
246
|
-
|
|
251
|
+
contentElement.innerHTML = '';
|
|
252
|
+
this.editing = true;
|
|
247
253
|
|
|
248
|
-
this.
|
|
249
|
-
this.inlineEditorRoot.render(
|
|
254
|
+
this.reactRoot.render(
|
|
250
255
|
<CanvasInlineEditor
|
|
251
256
|
elementClasses={ elementClasses }
|
|
252
257
|
initialValue={ propValue }
|
|
253
258
|
expectedTag={ expectedTag }
|
|
254
259
|
rootElement={ this.element }
|
|
260
|
+
contentElement={ contentElement }
|
|
255
261
|
id={ this.id }
|
|
256
262
|
setValue={ this.setContentValue.bind( this ) }
|
|
257
|
-
|
|
263
|
+
requestDestroy={ this.unmountInlineEditor.bind( this ) }
|
|
258
264
|
/>
|
|
259
265
|
);
|
|
260
266
|
}
|
|
@@ -43,6 +43,7 @@ export const INLINE_EDITING_PROPERTY_PER_TYPE: Record< string, string > = {
|
|
|
43
43
|
'e-form-label': 'text',
|
|
44
44
|
'e-heading': 'title',
|
|
45
45
|
'e-paragraph': 'paragraph',
|
|
46
|
+
'e-form-submit-button': 'text',
|
|
46
47
|
};
|
|
47
48
|
|
|
48
49
|
export const legacyWindow = window as unknown as LegacyWindow;
|
|
@@ -79,6 +80,12 @@ export const useOnClickOutsideIframe = ( handleUnmount: () => void ) => {
|
|
|
79
80
|
export const useRenderToolbar = ( ownerDocument: Document, id: string ) => {
|
|
80
81
|
const [ anchor, setAnchor ] = useState< HTMLElement | null >( null );
|
|
81
82
|
|
|
83
|
+
useEffect( () => {
|
|
84
|
+
if ( ! anchor ) {
|
|
85
|
+
removeToolbarAnchor( ownerDocument, id );
|
|
86
|
+
}
|
|
87
|
+
}, [ anchor, ownerDocument, id ] );
|
|
88
|
+
|
|
82
89
|
const onSelectionEnd = ( view: EditorView ) => {
|
|
83
90
|
const hasSelection = ! view.state.selection.empty;
|
|
84
91
|
|
|
@@ -91,7 +98,11 @@ export const useRenderToolbar = ( ownerDocument: Document, id: string ) => {
|
|
|
91
98
|
}
|
|
92
99
|
};
|
|
93
100
|
|
|
94
|
-
|
|
101
|
+
const clearAnchor = useCallback( () => {
|
|
102
|
+
setAnchor( null );
|
|
103
|
+
}, [] );
|
|
104
|
+
|
|
105
|
+
return { onSelectionEnd, anchor, clearAnchor };
|
|
95
106
|
};
|
|
96
107
|
|
|
97
108
|
const createAnchorBasedOnSelection = ( ownerDocument: Document, id: string ): HTMLElement | null => {
|