@elementor/editor-components 4.0.0-662 → 4.0.0-664

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-662",
4
+ "version": "4.0.0-664",
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-662",
44
- "@elementor/editor-canvas": "4.0.0-662",
45
- "@elementor/editor-controls": "4.0.0-662",
46
- "@elementor/editor-documents": "4.0.0-662",
47
- "@elementor/editor-editing-panel": "4.0.0-662",
48
- "@elementor/editor-elements": "4.0.0-662",
49
- "@elementor/editor-elements-panel": "4.0.0-662",
50
- "@elementor/editor-mcp": "4.0.0-662",
51
- "@elementor/editor-templates": "4.0.0-662",
52
- "@elementor/editor-panels": "4.0.0-662",
53
- "@elementor/editor-props": "4.0.0-662",
54
- "@elementor/editor-styles-repository": "4.0.0-662",
55
- "@elementor/editor-ui": "4.0.0-662",
56
- "@elementor/editor-v1-adapters": "4.0.0-662",
57
- "@elementor/http-client": "4.0.0-662",
43
+ "@elementor/editor": "4.0.0-664",
44
+ "@elementor/editor-canvas": "4.0.0-664",
45
+ "@elementor/editor-controls": "4.0.0-664",
46
+ "@elementor/editor-documents": "4.0.0-664",
47
+ "@elementor/editor-editing-panel": "4.0.0-664",
48
+ "@elementor/editor-elements": "4.0.0-664",
49
+ "@elementor/editor-elements-panel": "4.0.0-664",
50
+ "@elementor/editor-mcp": "4.0.0-664",
51
+ "@elementor/editor-templates": "4.0.0-664",
52
+ "@elementor/editor-panels": "4.0.0-664",
53
+ "@elementor/editor-props": "4.0.0-664",
54
+ "@elementor/editor-styles-repository": "4.0.0-664",
55
+ "@elementor/editor-ui": "4.0.0-664",
56
+ "@elementor/editor-v1-adapters": "4.0.0-664",
57
+ "@elementor/http-client": "4.0.0-664",
58
58
  "@elementor/icons": "^1.68.0",
59
- "@elementor/events": "4.0.0-662",
60
- "@elementor/query": "4.0.0-662",
61
- "@elementor/schema": "4.0.0-662",
62
- "@elementor/store": "4.0.0-662",
59
+ "@elementor/events": "4.0.0-664",
60
+ "@elementor/query": "4.0.0-664",
61
+ "@elementor/schema": "4.0.0-664",
62
+ "@elementor/store": "4.0.0-664",
63
63
  "@elementor/ui": "1.36.17",
64
- "@elementor/utils": "4.0.0-662",
64
+ "@elementor/utils": "4.0.0-664",
65
65
  "@wordpress/i18n": "^5.13.0",
66
- "@elementor/editor-notifications": "4.0.0-662",
67
- "@elementor/editor-current-user": "4.0.0-662"
66
+ "@elementor/editor-notifications": "4.0.0-664",
67
+ "@elementor/editor-current-user": "4.0.0-664"
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,
@@ -13,6 +14,7 @@ import {
13
14
  } from '@elementor/editor-canvas';
14
15
  import { getCurrentDocument } from '@elementor/editor-documents';
15
16
  import { type V1ElementData } from '@elementor/editor-elements';
17
+ import { notify } from '@elementor/editor-notifications';
16
18
  import { __getState as getState } from '@elementor/store';
17
19
  import { hasProInstalled } from '@elementor/utils';
18
20
  import { __ } from '@wordpress/i18n';
@@ -21,15 +23,14 @@ import { apiClient } from './api';
21
23
  import { type ComponentInstanceProp } from './prop-types/component-instance-prop-type';
22
24
  import { type ComponentsSlice, selectComponentByUid } from './store/store';
23
25
  import { type ComponentRenderContext, type ExtendedWindow } from './types';
26
+ import { detachComponentInstance } from './utils/detach-component-instance';
24
27
  import { formatComponentElementsId } from './utils/format-component-elements-id';
25
28
  import { switchToComponent } from './utils/switch-to-component';
26
29
  import { trackComponentEvent } from './utils/tracking';
27
30
 
28
- type ContextMenuEventData = { location: string; secondaryLocation: string; trigger: string };
29
-
30
31
  type ContextMenuGroupConfig = {
31
32
  disable: Record< string, string[] >;
32
- add: Record< string, { index: number; action: ContextMenuAction } >;
33
+ add: Record< string, { index: number; actions: ContextMenuAction[] } >;
33
34
  };
34
35
 
35
36
  type ContextMenuGroup = {
@@ -54,6 +55,26 @@ export const COMPONENT_WIDGET_TYPE = 'e-component';
54
55
 
55
56
  const EDIT_COMPONENT_UPGRADE_URL = 'https://go.elementor.com/go-pro-components-edit/';
56
57
 
58
+ const COMPONENT_EDIT_UPGRADE_NOTIFICATION_ID = 'component-edit-upgrade';
59
+
60
+ function notifyComponentEditUpgrade() {
61
+ notify( {
62
+ type: 'promotion',
63
+ id: COMPONENT_EDIT_UPGRADE_NOTIFICATION_ID,
64
+ message: __( 'Your Pro subscription has expired. Renew to edit components.', 'elementor' ),
65
+ additionalActionProps: [
66
+ {
67
+ size: 'small',
68
+ variant: 'contained',
69
+ color: 'promotion',
70
+ href: EDIT_COMPONENT_UPGRADE_URL,
71
+ target: '_blank',
72
+ children: __( 'Upgrade Now', 'elementor' ),
73
+ },
74
+ ],
75
+ } );
76
+ }
77
+
57
78
  const updateGroups = ( groups: ContextMenuGroup[], config: ContextMenuGroupConfig ): ContextMenuGroup[] => {
58
79
  const disableMap = new Map( Object.entries( config.disable ?? {} ) );
59
80
  const addMap = new Map( Object.entries( config.add ?? {} ) );
@@ -67,18 +88,21 @@ const updateGroups = ( groups: ContextMenuGroup[], config: ContextMenuGroupConfi
67
88
  disabledActions.includes( action.name ) ? { ...action, isEnabled: () => false } : action
68
89
  );
69
90
 
70
- // Insert additional action if needed
91
+ // Insert additional actions if needed
71
92
  if ( addConfig ) {
72
- updatedActions.splice( addConfig.index, 0, addConfig.action );
93
+ updatedActions.splice( addConfig.index, 0, ...addConfig.actions );
73
94
  }
74
95
 
75
96
  return { ...group, actions: updatedActions };
76
97
  } );
77
98
  };
78
99
 
79
- export function createComponentType(
80
- options: CreateTemplatedElementTypeOptions & { showLockedByModal?: ( lockedBy: string ) => void }
81
- ): 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 {
82
106
  const legacyWindow = window as unknown as LegacyWindow;
83
107
  const WidgetType = legacyWindow.elementor.modules.elements.types.Widget;
84
108
 
@@ -99,11 +123,7 @@ export function createComponentType(
99
123
  };
100
124
  }
101
125
 
102
- function createComponentView(
103
- options: CreateTemplatedElementTypeOptions & {
104
- showLockedByModal?: ( lockedBy: string ) => void;
105
- }
106
- ): typeof ElementView {
126
+ function createComponentView( options: ComponentTypeOptions ): typeof ElementView {
107
127
  const legacyWindow = window as unknown as LegacyWindow & ExtendedWindow;
108
128
 
109
129
  return class extends createTemplatedElementView( options ) {
@@ -220,17 +240,29 @@ function createComponentView(
220
240
  const badgeClass = 'elementor-context-menu-list__item__shortcut__new-badge';
221
241
  const proBadge = `<a href="${ EDIT_COMPONENT_UPGRADE_URL }" target="_blank" onclick="event.stopPropagation()" class="${ badgeClass }">${ proLabel }</a>`;
222
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
+
223
262
  const addedGroup = {
224
263
  general: {
225
264
  index: 1,
226
- action: {
227
- name: 'edit component',
228
- icon: 'eicon-edit',
229
- title: () => __( 'Edit Component', 'elementor' ),
230
- ...( ! hasPro && { shortcut: proBadge, hasShortcutAction: true } ),
231
- isEnabled: () => hasPro,
232
- callback: ( _: unknown, eventData: ContextMenuEventData ) => this.editComponent( eventData ),
233
- },
265
+ actions,
234
266
  },
235
267
  };
236
268
 
@@ -238,7 +270,7 @@ function createComponentView(
238
270
  clipboard: [ 'pasteStyle', 'resetStyle' ],
239
271
  };
240
272
 
241
- return { add: isAdministrator ? addedGroup : {}, disable: disabledGroup };
273
+ return { add: addedGroup, disable: disabledGroup };
242
274
  }
243
275
 
244
276
  async switchDocument() {
@@ -276,10 +308,42 @@ function createComponentView(
276
308
  } );
277
309
  }
278
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
+
279
338
  handleDblClick( e: MouseEvent ) {
280
339
  e.stopPropagation();
281
340
 
282
- if ( ! isUserAdministrator() || ! hasProInstalled() ) {
341
+ if ( ! isUserAdministrator() ) {
342
+ return;
343
+ }
344
+
345
+ if ( ! hasProInstalled() ) {
346
+ notifyComponentEditUpgrade();
283
347
  return;
284
348
  }
285
349
 
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';