@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.
Files changed (38) hide show
  1. package/dist/index.d.mts +11 -2
  2. package/dist/index.d.ts +11 -2
  3. package/dist/index.js +307 -128
  4. package/dist/index.mjs +273 -94
  5. package/package.json +19 -18
  6. package/src/components/__tests__/style-renderer.test.tsx +4 -0
  7. package/src/hooks/__tests__/use-style-items.test.ts +125 -0
  8. package/src/hooks/use-style-items.ts +40 -16
  9. package/src/index.ts +1 -1
  10. package/src/init-settings-transformers.ts +2 -0
  11. package/src/legacy/create-nested-templated-element-type.ts +15 -2
  12. package/src/legacy/create-templated-element-type.ts +8 -0
  13. package/src/legacy/replacements/base.ts +4 -0
  14. package/src/legacy/replacements/inline-editing/canvas-inline-editor.tsx +49 -27
  15. package/src/legacy/replacements/inline-editing/inline-editing-elements.tsx +16 -10
  16. package/src/legacy/replacements/inline-editing/inline-editing-utils.ts +12 -1
  17. package/src/legacy/replacements/manager.ts +13 -0
  18. package/src/legacy/types.ts +6 -1
  19. package/src/mcp/canvas-mcp.ts +5 -7
  20. package/src/mcp/resources/breakpoints-resource.ts +11 -4
  21. package/src/mcp/resources/document-structure-resource.ts +18 -13
  22. package/src/mcp/tools/build-composition/schema.ts +1 -1
  23. package/src/mcp/tools/build-composition/tool.ts +5 -1
  24. package/src/mcp/utils/__tests__/get-composition-target-container.test.ts +59 -0
  25. package/src/mcp/utils/get-composition-target-container.ts +15 -0
  26. package/src/renderers/__tests__/create-styles-renderer.test.ts +117 -0
  27. package/src/renderers/create-styles-renderer.ts +13 -3
  28. package/src/style-commands/__tests__/paste-style.test.ts +5 -3
  29. package/src/style-commands/__tests__/reset-style.test.ts +3 -3
  30. package/src/style-commands/paste-style.ts +7 -1
  31. package/src/style-commands/reset-style.ts +1 -1
  32. package/src/style-commands/undoable-actions/paste-element-style.ts +1 -1
  33. package/src/style-commands/undoable-actions/reset-element-style.ts +1 -1
  34. package/src/transformers/shared/__tests__/svg-src-transformer.test.ts +184 -0
  35. package/src/transformers/shared/svg-src-transformer.ts +87 -0
  36. package/src/transformers/styles/__tests__/size-transformer.test.ts +24 -0
  37. package/src/transformers/styles/size-transformer.ts +3 -0
  38. /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.0-manual",
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.0-manual",
41
- "@elementor/editor-controls": "4.0.0-manual",
42
- "@elementor/editor-documents": "4.0.0-manual",
43
- "@elementor/editor-elements": "4.0.0-manual",
44
- "@elementor/editor-interactions": "4.0.0-manual",
45
- "@elementor/editor-mcp": "4.0.0-manual",
46
- "@elementor/editor-notifications": "4.0.0-manual",
47
- "@elementor/editor-props": "4.0.0-manual",
48
- "@elementor/editor-responsive": "4.0.0-manual",
49
- "@elementor/editor-styles": "4.0.0-manual",
50
- "@elementor/editor-styles-repository": "4.0.0-manual",
51
- "@elementor/editor-ui": "4.0.0-manual",
52
- "@elementor/editor-v1-adapters": "4.0.0-manual",
53
- "@elementor/schema": "4.0.0-manual",
54
- "@elementor/twing": "4.0.0-manual",
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.0-manual",
57
- "@elementor/wp-media": "4.0.0-manual",
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
- return stylesRepository.getProviders().map( ( provider ): ProviderAndSubscriber => {
40
- const providerKey = provider.getKey();
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, { orderedIds: [], itemsById: new Map() } );
51
+ styleItemsCacheRef.current.set( providerKey, createEmptyCache() );
44
52
  }
45
53
 
46
- const cache = styleItemsCacheRef.current.get( providerKey ) as StyleItemsCache;
54
+ return styleItemsCacheRef.current.get( providerKey ) as StyleItemsCache;
55
+ };
47
56
 
48
- return {
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
- cache,
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
- cache: StyleItemsCache;
147
+ getCache: () => StyleItemsCache;
130
148
  };
131
149
 
132
- function createProviderSubscriber( { provider, renderStyles, setStyleItems, cache }: CreateProviderSubscriberArgs ) {
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( previous: StylesCollection, current: StylesCollection, signal: AbortSignal ) {
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 rendered;
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 './style-commands/utils';
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: { get: ( key: 'id' ) => string; cid?: string } ): string {
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
- ...props
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
- onBlur: () => void;
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
- const onBlur = () => {
40
- removeToolbarAnchor( rootElement.ownerDocument, id );
51
+ useOnClickOutsideIframe( dismiss );
41
52
 
42
- props.onBlur();
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
- useOnClickOutsideIframe( onBlur );
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;overflow-wrap: normal;height:100%',
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={ 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 ref={ refs.setFloating } role="presentation" style={ { ...floatingStyles, pointerEvents: 'none' } }>
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 !! this.inlineEditorRoot;
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.inlineEditorRoot?.unmount?.();
95
- this.inlineEditorRoot = null;
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 elementClasses = this.element.children?.[ 0 ]?.classList.toString() ?? '';
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
- this.element.innerHTML = '';
251
+ contentElement.innerHTML = '';
252
+ this.editing = true;
247
253
 
248
- this.inlineEditorRoot = createRoot( this.element );
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
- onBlur={ this.unmountInlineEditor.bind( this ) }
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
- return { onSelectionEnd, anchor };
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 => {