@elementor/editor-components 4.0.0-663 → 4.0.0-665

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elementor/editor-components",
3
3
  "description": "Elementor editor components",
4
- "version": "4.0.0-663",
4
+ "version": "4.0.0-665",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -40,31 +40,31 @@
40
40
  "dev": "tsup --config=../../tsup.dev.ts"
41
41
  },
42
42
  "dependencies": {
43
- "@elementor/editor": "4.0.0-663",
44
- "@elementor/editor-canvas": "4.0.0-663",
45
- "@elementor/editor-controls": "4.0.0-663",
46
- "@elementor/editor-documents": "4.0.0-663",
47
- "@elementor/editor-editing-panel": "4.0.0-663",
48
- "@elementor/editor-elements": "4.0.0-663",
49
- "@elementor/editor-elements-panel": "4.0.0-663",
50
- "@elementor/editor-mcp": "4.0.0-663",
51
- "@elementor/editor-templates": "4.0.0-663",
52
- "@elementor/editor-panels": "4.0.0-663",
53
- "@elementor/editor-props": "4.0.0-663",
54
- "@elementor/editor-styles-repository": "4.0.0-663",
55
- "@elementor/editor-ui": "4.0.0-663",
56
- "@elementor/editor-v1-adapters": "4.0.0-663",
57
- "@elementor/http-client": "4.0.0-663",
43
+ "@elementor/editor": "4.0.0-665",
44
+ "@elementor/editor-canvas": "4.0.0-665",
45
+ "@elementor/editor-controls": "4.0.0-665",
46
+ "@elementor/editor-documents": "4.0.0-665",
47
+ "@elementor/editor-editing-panel": "4.0.0-665",
48
+ "@elementor/editor-elements": "4.0.0-665",
49
+ "@elementor/editor-elements-panel": "4.0.0-665",
50
+ "@elementor/editor-mcp": "4.0.0-665",
51
+ "@elementor/editor-templates": "4.0.0-665",
52
+ "@elementor/editor-panels": "4.0.0-665",
53
+ "@elementor/editor-props": "4.0.0-665",
54
+ "@elementor/editor-styles-repository": "4.0.0-665",
55
+ "@elementor/editor-ui": "4.0.0-665",
56
+ "@elementor/editor-v1-adapters": "4.0.0-665",
57
+ "@elementor/http-client": "4.0.0-665",
58
58
  "@elementor/icons": "^1.68.0",
59
- "@elementor/events": "4.0.0-663",
60
- "@elementor/query": "4.0.0-663",
61
- "@elementor/schema": "4.0.0-663",
62
- "@elementor/store": "4.0.0-663",
59
+ "@elementor/events": "4.0.0-665",
60
+ "@elementor/query": "4.0.0-665",
61
+ "@elementor/schema": "4.0.0-665",
62
+ "@elementor/store": "4.0.0-665",
63
63
  "@elementor/ui": "1.36.17",
64
- "@elementor/utils": "4.0.0-663",
64
+ "@elementor/utils": "4.0.0-665",
65
65
  "@wordpress/i18n": "^5.13.0",
66
- "@elementor/editor-notifications": "4.0.0-663",
67
- "@elementor/editor-current-user": "4.0.0-663"
66
+ "@elementor/editor-notifications": "4.0.0-665",
67
+ "@elementor/editor-current-user": "4.0.0-665"
68
68
  },
69
69
  "peerDependencies": {
70
70
  "react": "^18.3.1",
@@ -0,0 +1,50 @@
1
+ import * as React from 'react';
2
+ import { closeDialog, ConfirmationDialog, openDialog } from '@elementor/editor-ui';
3
+ import { AlertTriangleFilledIcon } from '@elementor/icons';
4
+ import { __ } from '@wordpress/i18n';
5
+
6
+ type DetachInstanceConfirmationDialogProps = {
7
+ open: boolean;
8
+ onClose: () => void;
9
+ onConfirm: () => void;
10
+ };
11
+
12
+ export function DetachInstanceConfirmationDialog( {
13
+ open,
14
+ onClose,
15
+ onConfirm,
16
+ }: DetachInstanceConfirmationDialogProps ) {
17
+ return (
18
+ <ConfirmationDialog open={ open } onClose={ onClose }>
19
+ <ConfirmationDialog.Title icon={ AlertTriangleFilledIcon } iconColor="secondary">
20
+ { __( 'Detach from Component?', 'elementor' ) }
21
+ </ConfirmationDialog.Title>
22
+ <ConfirmationDialog.Content>
23
+ <ConfirmationDialog.ContentText>
24
+ { __(
25
+ 'Detaching this instance will break its link to the Component. Changes to the Component will no longer apply. Continue?',
26
+ 'elementor'
27
+ ) }
28
+ </ConfirmationDialog.ContentText>
29
+ </ConfirmationDialog.Content>
30
+ <ConfirmationDialog.Actions
31
+ onClose={ onClose }
32
+ onConfirm={ onConfirm }
33
+ confirmLabel={ __( 'Detach', 'elementor' ) }
34
+ color="primary"
35
+ />
36
+ </ConfirmationDialog>
37
+ );
38
+ }
39
+
40
+ // Used imperatively from the context menu (Marionette view).
41
+ export function openDetachConfirmDialog( onConfirm: () => void ) {
42
+ const handleConfirm = () => {
43
+ closeDialog();
44
+ onConfirm();
45
+ };
46
+
47
+ openDialog( {
48
+ component: <DetachInstanceConfirmationDialog open onClose={ closeDialog } onConfirm={ handleConfirm } />,
49
+ } );
50
+ }
@@ -0,0 +1,76 @@
1
+ import * as React from 'react';
2
+ import { useState } from 'react';
3
+ import { notify } from '@elementor/editor-notifications';
4
+ import { DetachIcon } from '@elementor/icons';
5
+ import { __ } from '@wordpress/i18n';
6
+
7
+ import { type ExtendedWindow } from '../../types';
8
+ import { detachComponentInstance } from '../../utils/detach-component-instance';
9
+ import { DetachInstanceConfirmationDialog } from '../detach-instance-confirmation-dialog';
10
+ import { EditComponentAction } from './instance-panel-header';
11
+
12
+ export const DetachAction = ( {
13
+ componentInstanceId,
14
+ componentId,
15
+ }: {
16
+ componentInstanceId: string;
17
+ componentId: number;
18
+ } ) => {
19
+ const [ isDetachDialogOpen, setIsDetachDialogOpen ] = useState( false );
20
+
21
+ const handleDetachConfirm = async () => {
22
+ setIsDetachDialogOpen( false );
23
+
24
+ try {
25
+ await detachComponentInstance( {
26
+ instanceId: componentInstanceId,
27
+ componentId,
28
+ trackingInfo: getDetachTrackingInfo(),
29
+ } );
30
+ } catch {
31
+ notify( {
32
+ type: 'error',
33
+ message: __( 'Failed to detach component instance.', 'elementor' ),
34
+ id: 'detach-component-instance-failed',
35
+ } );
36
+ }
37
+ };
38
+
39
+ const handleDetachCancel = () => {
40
+ setIsDetachDialogOpen( false );
41
+ };
42
+
43
+ const handleDetachClick = () => {
44
+ setIsDetachDialogOpen( true );
45
+ };
46
+
47
+ const detachLabel = __( 'Detach from Component', 'elementor' );
48
+
49
+ return (
50
+ <>
51
+ <EditComponentAction label={ detachLabel } icon={ DetachIcon } onClick={ handleDetachClick } />
52
+ <DetachInstanceConfirmationDialog
53
+ open={ isDetachDialogOpen }
54
+ onClose={ handleDetachCancel }
55
+ onConfirm={ handleDetachConfirm }
56
+ />
57
+ </>
58
+ );
59
+ };
60
+
61
+ function getDetachTrackingInfo() {
62
+ const extendedWindow = window as unknown as ExtendedWindow;
63
+ const config = extendedWindow?.elementorCommon?.eventsManager?.config;
64
+
65
+ if ( ! config ) {
66
+ return {
67
+ location: '',
68
+ trigger: '',
69
+ };
70
+ }
71
+
72
+ return {
73
+ location: ( config.locations.components as Record< string, string > ).instanceEditingPanel,
74
+ trigger: config.triggers.click,
75
+ };
76
+ }
@@ -1,15 +1,18 @@
1
1
  import * as React from 'react';
2
2
  import { PencilIcon } from '@elementor/icons';
3
- import { Box } from '@elementor/ui';
3
+ import { Box, Stack } from '@elementor/ui';
4
4
  import { __ } from '@wordpress/i18n';
5
5
 
6
+ import { useComponentsPermissions } from '../../hooks/use-components-permissions';
6
7
  import { ComponentInstanceProvider } from '../../provider/component-instance-context';
8
+ import { DetachAction } from './detach-action';
7
9
  import { EmptyState } from './empty-state';
8
10
  import { InstancePanelBody } from './instance-panel-body';
9
11
  import { EditComponentAction, InstancePanelHeader } from './instance-panel-header';
10
12
  import { useInstancePanelData } from './use-instance-panel-data';
11
13
 
12
14
  export function InstanceEditingPanel() {
15
+ const { canEdit } = useComponentsPermissions();
13
16
  const data = useInstancePanelData();
14
17
 
15
18
  if ( ! data ) {
@@ -21,6 +24,13 @@ export function InstanceEditingPanel() {
21
24
  /* translators: %s: component name. */
22
25
  const panelTitle = __( 'Edit %s', 'elementor' ).replace( '%s', component.name );
23
26
 
27
+ const actions = (
28
+ <Stack direction="row" gap={ 0.5 }>
29
+ <DetachAction componentInstanceId={ componentInstanceId } componentId={ componentId } />
30
+ { canEdit && <EditComponentAction disabled label={ panelTitle } icon={ PencilIcon } /> }
31
+ </Stack>
32
+ );
33
+
24
34
  return (
25
35
  <Box data-testid="instance-editing-panel">
26
36
  <ComponentInstanceProvider
@@ -28,10 +38,7 @@ export function InstanceEditingPanel() {
28
38
  overrides={ overrides }
29
39
  overridableProps={ overridableProps }
30
40
  >
31
- <InstancePanelHeader
32
- componentName={ component.name }
33
- actions={ <EditComponentAction disabled label={ panelTitle } icon={ PencilIcon } /> }
34
- />
41
+ <InstancePanelHeader componentName={ component.name } actions={ actions } />
35
42
  <InstancePanelBody
36
43
  groups={ groups }
37
44
  isEmpty={ isEmpty }
@@ -13,7 +13,7 @@ type InstancePanelData = {
13
13
  overridableProps: NonNullable< ReturnType< typeof useSanitizeOverridableProps > >;
14
14
  groups: OverridablePropsGroup[];
15
15
  isEmpty: boolean;
16
- componentInstanceId: string | undefined;
16
+ componentInstanceId: string;
17
17
  };
18
18
 
19
19
  export function useInstancePanelData(): InstancePanelData | null {
@@ -29,7 +29,7 @@ export function useInstancePanelData(): InstancePanelData | null {
29
29
 
30
30
  const overridableProps = useSanitizeOverridableProps( componentId ?? null, componentInstanceId );
31
31
 
32
- if ( ! componentId || ! overridableProps || ! component ) {
32
+ if ( ! componentId || ! overridableProps || ! component || ! componentInstanceId ) {
33
33
  return null;
34
34
  }
35
35
 
package/src/consts.ts ADDED
@@ -0,0 +1 @@
1
+ export const COMPONENT_WIDGET_TYPE = 'e-component';
@@ -2,6 +2,7 @@ import {
2
2
  type BackboneModel,
3
3
  type BackboneModelConstructor,
4
4
  type ContextMenuAction,
5
+ type ContextMenuEventData,
5
6
  type CreateTemplatedElementTypeOptions,
6
7
  createTemplatedElementView,
7
8
  type ElementModel,
@@ -22,15 +23,14 @@ import { apiClient } from './api';
22
23
  import { type ComponentInstanceProp } from './prop-types/component-instance-prop-type';
23
24
  import { type ComponentsSlice, selectComponentByUid } from './store/store';
24
25
  import { type ComponentRenderContext, type ExtendedWindow } from './types';
26
+ import { detachComponentInstance } from './utils/detach-component-instance';
25
27
  import { formatComponentElementsId } from './utils/format-component-elements-id';
26
28
  import { switchToComponent } from './utils/switch-to-component';
27
29
  import { trackComponentEvent } from './utils/tracking';
28
30
 
29
- type ContextMenuEventData = { location: string; secondaryLocation: string; trigger: string };
30
-
31
31
  type ContextMenuGroupConfig = {
32
32
  disable: Record< string, string[] >;
33
- add: Record< string, { index: number; action: ContextMenuAction } >;
33
+ add: Record< string, { index: number; actions: ContextMenuAction[] } >;
34
34
  };
35
35
 
36
36
  type ContextMenuGroup = {
@@ -88,18 +88,21 @@ const updateGroups = ( groups: ContextMenuGroup[], config: ContextMenuGroupConfi
88
88
  disabledActions.includes( action.name ) ? { ...action, isEnabled: () => false } : action
89
89
  );
90
90
 
91
- // Insert additional action if needed
91
+ // Insert additional actions if needed
92
92
  if ( addConfig ) {
93
- updatedActions.splice( addConfig.index, 0, addConfig.action );
93
+ updatedActions.splice( addConfig.index, 0, ...addConfig.actions );
94
94
  }
95
95
 
96
96
  return { ...group, actions: updatedActions };
97
97
  } );
98
98
  };
99
99
 
100
- export function createComponentType(
101
- options: CreateTemplatedElementTypeOptions & { showLockedByModal?: ( lockedBy: string ) => void }
102
- ): typeof ElementType {
100
+ type ComponentTypeOptions = CreateTemplatedElementTypeOptions & {
101
+ showLockedByModal?: ( lockedBy: string ) => void;
102
+ showDetachConfirmDialog?: ( onConfirm: () => void ) => void;
103
+ };
104
+
105
+ export function createComponentType( options: ComponentTypeOptions ): typeof ElementType {
103
106
  const legacyWindow = window as unknown as LegacyWindow;
104
107
  const WidgetType = legacyWindow.elementor.modules.elements.types.Widget;
105
108
 
@@ -120,11 +123,7 @@ export function createComponentType(
120
123
  };
121
124
  }
122
125
 
123
- function createComponentView(
124
- options: CreateTemplatedElementTypeOptions & {
125
- showLockedByModal?: ( lockedBy: string ) => void;
126
- }
127
- ): typeof ElementView {
126
+ function createComponentView( options: ComponentTypeOptions ): typeof ElementView {
128
127
  const legacyWindow = window as unknown as LegacyWindow & ExtendedWindow;
129
128
 
130
129
  return class extends createTemplatedElementView( options ) {
@@ -241,17 +240,29 @@ function createComponentView(
241
240
  const badgeClass = 'elementor-context-menu-list__item__shortcut__new-badge';
242
241
  const proBadge = `<a href="${ EDIT_COMPONENT_UPGRADE_URL }" target="_blank" onclick="event.stopPropagation()" class="${ badgeClass }">${ proLabel }</a>`;
243
242
 
243
+ const editComponentAction: ContextMenuAction = {
244
+ name: 'edit component',
245
+ icon: 'eicon-edit',
246
+ title: () => __( 'Edit Component', 'elementor' ),
247
+ ...( ! hasPro && { shortcut: proBadge, hasShortcutAction: true } ),
248
+ isEnabled: () => hasPro,
249
+ callback: ( _: unknown, eventData: ContextMenuEventData ) => this.editComponent( eventData ),
250
+ };
251
+
252
+ const detachInstanceAction: ContextMenuAction = {
253
+ name: 'detach instance',
254
+ icon: 'eicon-chain-broken',
255
+ title: () => __( 'Detach from Component', 'elementor' ),
256
+ isEnabled: () => true,
257
+ callback: ( _: unknown, eventData: ContextMenuEventData ) => this.detachInstance( eventData ),
258
+ };
259
+
260
+ const actions = isAdministrator ? [ editComponentAction, detachInstanceAction ] : [ detachInstanceAction ];
261
+
244
262
  const addedGroup = {
245
263
  general: {
246
264
  index: 1,
247
- action: {
248
- name: 'edit component',
249
- icon: 'eicon-edit',
250
- title: () => __( 'Edit Component', 'elementor' ),
251
- ...( ! hasPro && { shortcut: proBadge, hasShortcutAction: true } ),
252
- isEnabled: () => hasPro,
253
- callback: ( _: unknown, eventData: ContextMenuEventData ) => this.editComponent( eventData ),
254
- },
265
+ actions,
255
266
  },
256
267
  };
257
268
 
@@ -259,7 +270,7 @@ function createComponentView(
259
270
  clipboard: [ 'pasteStyle', 'resetStyle' ],
260
271
  };
261
272
 
262
- return { add: isAdministrator ? addedGroup : {}, disable: disabledGroup };
273
+ return { add: addedGroup, disable: disabledGroup };
263
274
  }
264
275
 
265
276
  async switchDocument() {
@@ -297,6 +308,33 @@ function createComponentView(
297
308
  } );
298
309
  }
299
310
 
311
+ detachInstance( { trigger, location, secondaryLocation }: ContextMenuEventData ) {
312
+ const componentId = this.getComponentId();
313
+ const instanceId = this.model.get( 'id' );
314
+
315
+ if ( ! componentId || ! instanceId ) {
316
+ return;
317
+ }
318
+
319
+ const handleConfirm = async () => {
320
+ try {
321
+ await detachComponentInstance( {
322
+ instanceId,
323
+ componentId,
324
+ trackingInfo: { location, secondaryLocation, trigger },
325
+ } );
326
+ } catch {
327
+ notify( {
328
+ type: 'error',
329
+ message: __( 'Failed to detach component instance.', 'elementor' ),
330
+ id: 'detach-component-instance-failed',
331
+ } );
332
+ }
333
+ };
334
+
335
+ options.showDetachConfirmDialog?.( handleConfirm );
336
+ }
337
+
300
338
  handleDblClick( e: MouseEvent ) {
301
339
  e.stopPropagation();
302
340
 
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ export { EmptyState as InstanceEmptyState } from './components/instance-editing-
17
17
  export { InstancePanelBody } from './components/instance-editing-panel/instance-panel-body';
18
18
  export { EditComponentAction, InstancePanelHeader } from './components/instance-editing-panel/instance-panel-header';
19
19
  export { useInstancePanelData } from './components/instance-editing-panel/use-instance-panel-data';
20
+ export { DetachAction } from './components/instance-editing-panel/detach-action';
20
21
 
21
22
  export { COMPONENT_WIDGET_TYPE } from './create-component-type';
22
23
 
package/src/init.ts CHANGED
@@ -18,6 +18,7 @@ import { componentInstanceTransformer } from './component-instance-transformer';
18
18
  import { componentOverridableTransformer } from './component-overridable-transformer';
19
19
  import { componentOverrideTransformer } from './component-override-transformer';
20
20
  import { Components } from './components/components-tab/components';
21
+ import { openDetachConfirmDialog } from './components/detach-instance-confirmation-dialog';
21
22
  import { openEditModeDialog } from './components/in-edit-mode';
22
23
  import { InstanceEditingPanel } from './components/instance-editing-panel/instance-editing-panel';
23
24
  import { LoadTemplateComponents } from './components/load-template-components';
@@ -42,7 +43,11 @@ export function init() {
42
43
  registerSlice( slice );
43
44
 
44
45
  registerElementType( COMPONENT_WIDGET_TYPE, ( options: CreateTemplatedElementTypeOptions ) =>
45
- createComponentType( { ...options, showLockedByModal: openEditModeDialog } )
46
+ createComponentType( {
47
+ ...options,
48
+ showLockedByModal: openEditModeDialog,
49
+ showDetachConfirmDialog: openDetachConfirmDialog,
50
+ } )
46
51
  );
47
52
 
48
53
  ( window as unknown as ExtendedWindow ).elementorCommon.__beforeSave = beforeSave;
package/src/types.ts CHANGED
@@ -74,7 +74,7 @@ export type ExtendedWindow = Window & {
74
74
  elementorCommon: Record< string, unknown > & {
75
75
  eventsManager: {
76
76
  config: {
77
- locations: Record< string, string >;
77
+ locations: Record< string, string | Record< string, string > >;
78
78
  secondaryLocations: Record< string, string >;
79
79
  triggers: Record< string, string >;
80
80
  };
@@ -0,0 +1,172 @@
1
+ import { getContainer, replaceElement, type V1Element, type V1ElementModelProps } from '@elementor/editor-elements';
2
+ import { undoable } from '@elementor/editor-v1-adapters';
3
+ import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
4
+ import { __ } from '@wordpress/i18n';
5
+
6
+ import { componentInstanceOverridesPropTypeUtil } from '../../prop-types/component-instance-overrides-prop-type';
7
+ import {
8
+ type ComponentInstanceProp,
9
+ componentInstancePropTypeUtil,
10
+ } from '../../prop-types/component-instance-prop-type';
11
+ import { selectComponent, selectCurrentComponentId, selectOverridableProps, slice } from '../../store/store';
12
+ import { type OverridableProps } from '../../types';
13
+ import { getComponentDocumentData } from '../component-document-data';
14
+ import { trackComponentEvent } from '../tracking';
15
+ import { resolveDetachedInstance } from './resolve-detached-instance';
16
+
17
+ type DetachParams = {
18
+ instanceId: string;
19
+ componentId: number;
20
+ trackingInfo: {
21
+ location: string;
22
+ secondaryLocation?: string;
23
+ trigger: string;
24
+ };
25
+ };
26
+
27
+ type DoReturn = {
28
+ detachedElement: V1Element;
29
+ detachedInstanceElementData: V1ElementModelProps;
30
+ editedComponentOnDetach: number | null;
31
+ overridablePropsBeforeDetach: OverridableProps | null;
32
+ originalInstanceModel: V1ElementModelProps;
33
+ };
34
+
35
+ export async function detachComponentInstance( {
36
+ instanceId,
37
+ componentId,
38
+ trackingInfo,
39
+ }: DetachParams ): Promise< DoReturn > {
40
+ const instanceContainer = getContainer( instanceId );
41
+
42
+ if ( ! instanceContainer ) {
43
+ throw new Error( `Instance container with ID "${ instanceId }" not found.` );
44
+ }
45
+
46
+ const componentData = await getComponentDocumentData( componentId );
47
+
48
+ if ( ! componentData ) {
49
+ throw new Error( `Component with ID "${ componentId }" not found.` );
50
+ }
51
+
52
+ const rootElement = componentData.elements?.[ 0 ];
53
+
54
+ if ( ! rootElement ) {
55
+ throw new Error( `Component with ID "${ componentId }" has no root element.` );
56
+ }
57
+
58
+ const undoableDetach = undoable(
59
+ {
60
+ do: (): DoReturn => {
61
+ const overrides = extractInstanceOverrides( instanceContainer );
62
+ const detachedInstanceElementData = resolveDetachedInstance(
63
+ rootElement,
64
+ overrides
65
+ ) as V1ElementModelProps;
66
+
67
+ const editedComponentOnDetach = selectCurrentComponentId( getState() );
68
+ // We need to store the overridable props of the current component before detach to restore them on undo.
69
+ const overridablePropsBeforeDetach = editedComponentOnDetach
70
+ ? selectOverridableProps( getState(), editedComponentOnDetach ) ?? null
71
+ : null;
72
+
73
+ const originalInstanceModel = instanceContainer.model.toJSON();
74
+
75
+ const detachedElement = replaceElement( {
76
+ currentElementId: instanceId,
77
+ newElement: detachedInstanceElementData,
78
+ withHistory: false,
79
+ } );
80
+
81
+ const componentUid = selectComponent( getState(), componentId )?.uid;
82
+ trackComponentEvent( {
83
+ action: 'detached',
84
+ source: 'user',
85
+ component_uid: componentUid,
86
+ instance_id: instanceId,
87
+ location: trackingInfo.location,
88
+ secondary_location: trackingInfo.secondaryLocation,
89
+ trigger: trackingInfo.trigger,
90
+ } );
91
+
92
+ return {
93
+ detachedElement,
94
+ detachedInstanceElementData,
95
+ editedComponentOnDetach,
96
+ overridablePropsBeforeDetach,
97
+ originalInstanceModel,
98
+ };
99
+ },
100
+ undo: (
101
+ _: undefined,
102
+ {
103
+ detachedElement,
104
+ originalInstanceModel,
105
+ overridablePropsBeforeDetach,
106
+ editedComponentOnDetach,
107
+ }: DoReturn
108
+ ): V1Element => {
109
+ const restoredInstance = replaceElement( {
110
+ currentElementId: detachedElement.id,
111
+ newElement: originalInstanceModel,
112
+ withHistory: false,
113
+ } );
114
+
115
+ const currentComponentId = selectCurrentComponentId( getState() );
116
+ if (
117
+ currentComponentId &&
118
+ currentComponentId === editedComponentOnDetach &&
119
+ overridablePropsBeforeDetach
120
+ ) {
121
+ dispatch(
122
+ slice.actions.setOverridableProps( {
123
+ componentId: currentComponentId,
124
+ overridableProps: overridablePropsBeforeDetach,
125
+ } )
126
+ );
127
+ }
128
+
129
+ return restoredInstance;
130
+ },
131
+ redo: ( _: undefined, doReturn: DoReturn, restoredInstance: V1Element ) => {
132
+ const { detachedInstanceElementData } = doReturn;
133
+
134
+ const editedComponentOnDetach = selectCurrentComponentId( getState() );
135
+ // We need to store the overridable props of the current component before detach to restore them on undo.
136
+ const overridablePropsBeforeDetach = editedComponentOnDetach
137
+ ? selectOverridableProps( getState(), editedComponentOnDetach ) ?? null
138
+ : null;
139
+
140
+ const detachedElement = replaceElement( {
141
+ currentElementId: restoredInstance.id,
142
+ newElement: detachedInstanceElementData,
143
+ withHistory: false,
144
+ } );
145
+
146
+ return {
147
+ ...doReturn,
148
+ detachedElement,
149
+ editedComponentOnDetach,
150
+ overridablePropsBeforeDetach,
151
+ };
152
+ },
153
+ },
154
+ {
155
+ title: __( 'Detach from Component', 'elementor' ),
156
+ subtitle: __( 'Instance detached', 'elementor' ),
157
+ }
158
+ );
159
+
160
+ return undoableDetach();
161
+ }
162
+
163
+ function extractInstanceOverrides( instanceContainer: NonNullable< ReturnType< typeof getContainer > > ) {
164
+ const settings = instanceContainer.model.toJSON().settings;
165
+ const componentInstance = componentInstancePropTypeUtil.extract(
166
+ settings?.component_instance as ComponentInstanceProp
167
+ );
168
+
169
+ const overrides = componentInstanceOverridesPropTypeUtil.extract( componentInstance?.overrides );
170
+
171
+ return overrides ?? [];
172
+ }
@@ -0,0 +1 @@
1
+ export { detachComponentInstance } from './detach-component-instance';
@@ -0,0 +1,53 @@
1
+ import { generateElementId, type V1ElementData, type V1ElementSettingsProps } from '@elementor/editor-elements';
2
+ import { classesPropTypeUtil, type ClassesPropValue, type PropValue } from '@elementor/editor-props';
3
+ import { type StyleDefinition, type StyleDefinitionID } from '@elementor/editor-styles';
4
+
5
+ // Ts version for atomic-widgets/assets/js/editor/utils/regenerate-local-style-ids.js
6
+ export function regenerateLocalStyleIds( element: V1ElementData ): {
7
+ styles: Record< StyleDefinitionID, StyleDefinition > | undefined;
8
+ settings: V1ElementSettingsProps | undefined;
9
+ } {
10
+ const originalStyles = element.styles;
11
+
12
+ if ( ! originalStyles || Object.keys( originalStyles ).length === 0 ) {
13
+ return { styles: undefined, settings: undefined };
14
+ }
15
+
16
+ const newStyles: Record< string, StyleDefinition > = {};
17
+ const styleIdMapping: Record< string, string > = {};
18
+
19
+ for ( const [ originalStyleId, style ] of Object.entries( originalStyles ) ) {
20
+ const newStyleId = generateLocalStyleId( element.id );
21
+
22
+ newStyles[ newStyleId ] = { ...style, id: newStyleId };
23
+ styleIdMapping[ originalStyleId ] = newStyleId;
24
+ }
25
+
26
+ const settings = element.settings;
27
+ if ( ! settings || Object.keys( settings ).length === 0 ) {
28
+ return { styles: newStyles, settings: undefined };
29
+ }
30
+
31
+ const updatedSettings = { ...settings };
32
+
33
+ for ( const [ propKey, propValue ] of Object.entries( updatedSettings ) ) {
34
+ if ( isClassesProp( propValue ) && propValue.value.length > 0 ) {
35
+ const updatedClasses = propValue.value.map( ( classId ) => styleIdMapping[ classId ] ?? classId );
36
+
37
+ updatedSettings[ propKey ] = classesPropTypeUtil.create( updatedClasses );
38
+ }
39
+ }
40
+
41
+ return {
42
+ styles: newStyles,
43
+ settings: updatedSettings,
44
+ };
45
+ }
46
+
47
+ function isClassesProp( prop: PropValue ): prop is ClassesPropValue {
48
+ return classesPropTypeUtil.isValid( prop );
49
+ }
50
+
51
+ function generateLocalStyleId( elementId: string ): string {
52
+ return `e-${ elementId }-${ generateElementId() }`;
53
+ }