@elementor/editor-components 3.33.0-234 → 3.33.0-236

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.
@@ -0,0 +1,134 @@
1
+ import * as React from 'react';
2
+ import { type CSSProperties, useEffect } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { __ } from '@wordpress/i18n';
5
+
6
+ import { useCanvasDocument } from '../../hooks/use-canvas-document';
7
+ import { useElementRect } from '../../hooks/use-element-rect';
8
+
9
+ type ModalProps = {
10
+ element: HTMLElement;
11
+ onClose: () => void;
12
+ };
13
+ export function ComponentModal( { element, onClose }: ModalProps ) {
14
+ const canvasDocument = useCanvasDocument();
15
+
16
+ useEffect( () => {
17
+ const handleEsc = ( event: KeyboardEvent ) => {
18
+ if ( event.key === 'Escape' ) {
19
+ onClose();
20
+ }
21
+ };
22
+
23
+ canvasDocument?.body.addEventListener( 'keydown', handleEsc );
24
+
25
+ return () => {
26
+ canvasDocument?.body.removeEventListener( 'keydown', handleEsc );
27
+ };
28
+ }, [ canvasDocument, onClose ] );
29
+
30
+ if ( ! canvasDocument?.body ) {
31
+ return null;
32
+ }
33
+
34
+ return createPortal(
35
+ <>
36
+ <BlockEditPage />
37
+ <Backdrop canvas={ canvasDocument } element={ element } onClose={ onClose } />
38
+ </>,
39
+ canvasDocument.body
40
+ );
41
+ }
42
+
43
+ function Backdrop( { canvas, element, onClose }: { canvas: HTMLDocument; element: HTMLElement; onClose: () => void } ) {
44
+ const rect = useElementRect( element );
45
+ const backdropStyle: CSSProperties = {
46
+ position: 'fixed',
47
+ top: 0,
48
+ left: 0,
49
+ width: '100vw',
50
+ height: '100vh',
51
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
52
+ zIndex: 999,
53
+ pointerEvents: 'painted',
54
+ cursor: 'pointer',
55
+ clipPath: getRoundedRectPath( rect, canvas.defaultView as Window, 5 ),
56
+ };
57
+
58
+ const handleKeyDown = ( event: React.KeyboardEvent ) => {
59
+ if ( event.key === 'Enter' || event.key === ' ' ) {
60
+ event.preventDefault();
61
+ onClose();
62
+ }
63
+ };
64
+
65
+ return (
66
+ <div
67
+ style={ backdropStyle }
68
+ onClick={ onClose }
69
+ onKeyDown={ handleKeyDown }
70
+ role="button"
71
+ tabIndex={ 0 }
72
+ aria-label={ __( 'Exit component editing mode', 'elementor' ) }
73
+ />
74
+ );
75
+ }
76
+
77
+ function getRoundedRectPath( rect: DOMRect, viewport: Window, borderRadius: number ) {
78
+ const padding = borderRadius / 2;
79
+ const { x: originalX, y: originalY, width: originalWidth, height: originalHeight } = rect;
80
+ const x = originalX - padding;
81
+ const y = originalY - padding;
82
+ const width = originalWidth + 2 * padding;
83
+ const height = originalHeight + 2 * padding;
84
+ const radius = Math.min( borderRadius, width / 2, height / 2 );
85
+
86
+ const { innerWidth: vw, innerHeight: vh } = viewport;
87
+
88
+ const path = `path(evenodd, 'M 0 0
89
+ L ${ vw } 0
90
+ L ${ vw } ${ vh }
91
+ L 0 ${ vh }
92
+ Z
93
+ M ${ x + radius } ${ y }
94
+ L ${ x + width - radius } ${ y }
95
+ A ${ radius } ${ radius } 0 0 1 ${ x + width } ${ y + radius }
96
+ L ${ x + width } ${ y + height - radius }
97
+ A ${ radius } ${ radius } 0 0 1 ${ x + width - radius } ${ y + height }
98
+ L ${ x + radius } ${ y + height }
99
+ A ${ radius } ${ radius } 0 0 1 ${ x } ${ y + height - radius }
100
+ L ${ x } ${ y + radius }
101
+ A ${ radius } ${ radius } 0 0 1 ${ x + radius } ${ y }
102
+ Z'
103
+ )`;
104
+
105
+ return path.replace( /\s{2,}/g, ' ' );
106
+ }
107
+
108
+ /**
109
+ * when switching to another document id, we get a document handler when hovering
110
+ * this functionality originates in Pro, and is intended for editing templates, e.g. header/footer
111
+ * in components we don't want that, so the easy way out is to prevent it of being displayed via a CSS rule
112
+ */
113
+ function BlockEditPage() {
114
+ const blockV3DocumentHandlesStyles = `
115
+ .elementor-editor-active {
116
+ & .elementor-section-wrap.ui-sortable {
117
+ display: contents;
118
+ }
119
+
120
+ & *[data-editable-elementor-document]:not(.elementor-edit-mode):hover {
121
+ & .elementor-document-handle:not(.elementor-document-save-back-handle) {
122
+ display: none;
123
+
124
+ &::before,
125
+ & .elementor-document-handle__inner {
126
+ display: none;
127
+ }
128
+ }
129
+ }
130
+ }
131
+ `;
132
+
133
+ return <style data-e-style-id="e-block-v3-document-handles-styles">{ blockV3DocumentHandlesStyles }</style>;
134
+ }
@@ -0,0 +1,119 @@
1
+ import * as React from 'react';
2
+ import { type Dispatch, type SetStateAction, useCallback, useEffect, useState } from 'react';
3
+ import { getV1DocumentsManager, type V1Document } from '@elementor/editor-documents';
4
+ import { type V1Element } from '@elementor/editor-elements';
5
+ import {
6
+ __privateListenTo as listenTo,
7
+ __privateRunCommand as runCommand,
8
+ commandEndEvent,
9
+ } from '@elementor/editor-v1-adapters';
10
+ import { __useSelector as useSelector } from '@elementor/store';
11
+
12
+ import { apiClient } from '../../api';
13
+ import { selectComponentsObject } from '../../store/store';
14
+ import { ComponentModal } from './component-modal';
15
+
16
+ type ComponentsPathItem = {
17
+ instanceId: string | undefined;
18
+ component: V1Document;
19
+ };
20
+ export function EditComponent() {
21
+ const [ componentsPath, setComponentsPath ] = useState< ComponentsPathItem[] >( [] );
22
+
23
+ useHandleDocumentSwitches( componentsPath, setComponentsPath );
24
+ const onBack = useNavigateBack( componentsPath );
25
+
26
+ const currentItem = componentsPath.at( -1 );
27
+ const { component: currentComponent } = currentItem ?? {};
28
+
29
+ const widget = currentComponent?.container as V1Element;
30
+ const container = ( widget?.view?.el?.children?.[ 0 ] ?? null ) as HTMLElement | null;
31
+ const elementDom = container?.children[ 0 ] as HTMLElement | null;
32
+
33
+ if ( ! elementDom ) {
34
+ return null;
35
+ }
36
+
37
+ return <ComponentModal element={ elementDom } onClose={ onBack } />;
38
+ }
39
+
40
+ function useHandleDocumentSwitches(
41
+ path: ComponentsPathItem[],
42
+ setPath: Dispatch< SetStateAction< ComponentsPathItem[] > >
43
+ ) {
44
+ const components = useSelector( selectComponentsObject );
45
+ const documentsManager = getV1DocumentsManager();
46
+
47
+ useEffect(
48
+ () =>
49
+ listenTo( commandEndEvent( 'editor/documents/attach-preview' ), () => {
50
+ const { component: currentComponent } = path.at( -1 ) ?? {};
51
+ const { id: currentComponentId } = currentComponent ?? {};
52
+ const nextDocument = documentsManager.getCurrent();
53
+
54
+ if ( nextDocument.id === currentComponentId ) {
55
+ return;
56
+ }
57
+
58
+ if ( currentComponentId ) {
59
+ apiClient.unlockComponent( currentComponentId );
60
+ }
61
+
62
+ const isComponent = !! components?.[ nextDocument.id ];
63
+
64
+ if ( ! isComponent ) {
65
+ setPath( [] );
66
+
67
+ return;
68
+ }
69
+
70
+ setPath( getUpdatedComponentPath( path, nextDocument ) );
71
+ } ),
72
+ [ path, setPath, components, documentsManager ]
73
+ );
74
+ }
75
+
76
+ function getUpdatedComponentPath( path: ComponentsPathItem[], nextDocument: V1Document ): ComponentsPathItem[] {
77
+ const componentIndex = path.findIndex( ( { component } ) => component.id === nextDocument.id );
78
+
79
+ if ( componentIndex >= 0 ) {
80
+ // When exiting the editing of a nested component - we in fact go back a step
81
+ // so we need to make sure the path is cleaned up of any newer items
82
+ // By doing it with the slice and not a simple pop() - we could jump to any component in the path and make sure it becomes the current one
83
+ return path.slice( 0, componentIndex + 1 );
84
+ }
85
+
86
+ return [
87
+ ...path,
88
+ {
89
+ instanceId: nextDocument?.container.view?.el?.dataset.id,
90
+ component: nextDocument,
91
+ },
92
+ ];
93
+ }
94
+
95
+ function useNavigateBack( path: ComponentsPathItem[] ) {
96
+ const documentsManager = getV1DocumentsManager();
97
+
98
+ return useCallback( () => {
99
+ const { component: prevComponent, instanceId: prevComponentInstanceId } = path.at( -2 ) ?? {};
100
+ const { id: prevComponentId } = prevComponent ?? {};
101
+ const switchToDocument = ( id: number, selector?: string ) => {
102
+ runCommand( 'editor/documents/switch', {
103
+ id,
104
+ selector,
105
+ mode: 'autosave',
106
+ setAsInitial: false,
107
+ shouldScroll: false,
108
+ } );
109
+ };
110
+
111
+ if ( prevComponentId && prevComponentInstanceId ) {
112
+ switchToDocument( prevComponentId, `[data-id="${ prevComponentInstanceId }"]` );
113
+
114
+ return;
115
+ }
116
+
117
+ switchToDocument( documentsManager.getInitialId() );
118
+ }, [ path, documentsManager ] );
119
+ }
@@ -106,15 +106,21 @@ function createComponentView(
106
106
  id: this.getComponentId()?.value as number,
107
107
  mode: 'autosave',
108
108
  selector: `[data-id="${ this.model.get( 'id' ) }"]`,
109
+ shouldScroll: false,
109
110
  } );
110
- apiClient.lockComponent( this.getComponentId()?.value as number );
111
111
  }
112
112
  }
113
113
 
114
+ handleDblClick( e: MouseEvent ) {
115
+ e.stopPropagation();
116
+
117
+ this.switchDocument();
118
+ }
119
+
114
120
  events() {
115
121
  return {
116
122
  ...super.events(),
117
- dblclick: this.switchDocument,
123
+ dblclick: this.handleDblClick,
118
124
  };
119
125
  }
120
126
 
@@ -0,0 +1,6 @@
1
+ import { getCanvasIframeDocument } from '@elementor/editor-canvas';
2
+ import { __privateUseListenTo as useListenTo, commandEndEvent } from '@elementor/editor-v1-adapters';
3
+
4
+ export function useCanvasDocument() {
5
+ return useListenTo( commandEndEvent( 'editor/documents/attach-preview' ), () => getCanvasIframeDocument() );
6
+ }
@@ -0,0 +1,81 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { throttle } from '@elementor/utils';
3
+
4
+ export function useElementRect( element: HTMLElement | null ) {
5
+ const [ rect, setRect ] = useState< DOMRect >( new DOMRect( 0, 0, 0, 0 ) );
6
+
7
+ const onChange = throttle(
8
+ () => {
9
+ setRect( element?.getBoundingClientRect() ?? new DOMRect( 0, 0, 0, 0 ) );
10
+ },
11
+ 20,
12
+ true
13
+ );
14
+
15
+ useScrollListener( { element, onChange } );
16
+ useResizeListener( { element, onChange } );
17
+ useMutationsListener( { element, onChange } );
18
+
19
+ useEffect(
20
+ () => () => {
21
+ onChange.cancel();
22
+ },
23
+ [ onChange ]
24
+ );
25
+
26
+ return rect;
27
+ }
28
+
29
+ type ListenerProps = {
30
+ element: HTMLElement | null;
31
+ onChange: () => void;
32
+ };
33
+
34
+ function useScrollListener( { element, onChange }: ListenerProps ) {
35
+ useEffect( () => {
36
+ if ( ! element ) {
37
+ return;
38
+ }
39
+
40
+ const win = element.ownerDocument?.defaultView;
41
+ win?.addEventListener( 'scroll', onChange, { passive: true } );
42
+
43
+ return () => {
44
+ win?.removeEventListener( 'scroll', onChange );
45
+ };
46
+ }, [ element, onChange ] );
47
+ }
48
+
49
+ function useResizeListener( { element, onChange }: ListenerProps ) {
50
+ useEffect( () => {
51
+ if ( ! element ) {
52
+ return;
53
+ }
54
+
55
+ const resizeObserver = new ResizeObserver( onChange );
56
+ resizeObserver.observe( element );
57
+
58
+ const win = element.ownerDocument?.defaultView;
59
+ win?.addEventListener( 'resize', onChange, { passive: true } );
60
+
61
+ return () => {
62
+ resizeObserver.disconnect();
63
+ win?.removeEventListener( 'resize', onChange );
64
+ };
65
+ }, [ element, onChange ] );
66
+ }
67
+
68
+ function useMutationsListener( { element, onChange }: ListenerProps ) {
69
+ useEffect( () => {
70
+ if ( ! element ) {
71
+ return;
72
+ }
73
+
74
+ const mutationObserver = new MutationObserver( onChange );
75
+ mutationObserver.observe( element, { childList: true, subtree: true } );
76
+
77
+ return () => {
78
+ mutationObserver.disconnect();
79
+ };
80
+ }, [ element, onChange ] );
81
+ }
package/src/init.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  settingsTransformersRegistry,
6
6
  } from '@elementor/editor-canvas';
7
7
  import { getV1CurrentDocument } from '@elementor/editor-documents';
8
+ import { type V1ElementData } from '@elementor/editor-elements';
8
9
  import { injectTab } from '@elementor/editor-elements-panel';
9
10
  import { stylesRepository } from '@elementor/editor-styles-repository';
10
11
  import { __privateListenTo as listenTo, commandStartEvent, registerDataHook } from '@elementor/editor-v1-adapters';
@@ -14,6 +15,7 @@ import { __ } from '@wordpress/i18n';
14
15
  import { componentIdTransformer } from './component-id-transformer';
15
16
  import { Components } from './components/components-tab/components';
16
17
  import { CreateComponentForm } from './components/create-component-form/create-component-form';
18
+ import { EditComponent } from './components/edit-component/edit-component';
17
19
  import { openEditModeDialog } from './components/in-edit-mode';
18
20
  import { createComponentType, TYPE } from './create-component-type';
19
21
  import { PopulateStore } from './populate-store';
@@ -21,7 +23,7 @@ import { componentsStylesProvider } from './store/components-styles-provider';
21
23
  import { loadComponentsStyles } from './store/load-components-styles';
22
24
  import { removeComponentStyles } from './store/remove-component-styles';
23
25
  import { slice } from './store/store';
24
- import { type Element, type ExtendedWindow } from './types';
26
+ import { type ExtendedWindow } from './types';
25
27
  import { beforeSave } from './utils/before-save';
26
28
 
27
29
  const COMPONENT_DOCUMENT_TYPE = 'elementor_component';
@@ -58,6 +60,11 @@ export function init() {
58
60
  component: PopulateStore,
59
61
  } );
60
62
 
63
+ injectIntoTop( {
64
+ id: 'edit-component',
65
+ component: EditComponent,
66
+ } );
67
+
61
68
  listenTo( commandStartEvent( 'editor/documents/attach-preview' ), () => {
62
69
  const { id, config } = getV1CurrentDocument();
63
70
 
@@ -65,7 +72,7 @@ export function init() {
65
72
  removeComponentStyles( id );
66
73
  }
67
74
 
68
- loadComponentsStyles( ( config?.elements as Element[] ) ?? [] );
75
+ loadComponentsStyles( ( config?.elements as V1ElementData[] ) ?? [] );
69
76
  } );
70
77
 
71
78
  settingsTransformersRegistry.register( 'component-id', componentIdTransformer );
@@ -3,11 +3,11 @@ import { type StyleDefinition } from '@elementor/editor-styles';
3
3
  import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
4
4
 
5
5
  import { apiClient } from '../api';
6
- import { type ComponentId, type Element } from '../types';
6
+ import { type ComponentId } from '../types';
7
7
  import { getComponentIds } from '../utils/get-component-ids';
8
8
  import { selectStyles, slice } from './store';
9
9
 
10
- export async function loadComponentsStyles( elements: Element[] ) {
10
+ export async function loadComponentsStyles( elements: V1ElementData[] ) {
11
11
  const componentIds = Array.from( new Set( getComponentIds( elements ) ) );
12
12
 
13
13
  if ( ! componentIds.length ) {
@@ -30,7 +30,7 @@ async function addComponentStyles( ids: ComponentId[] ) {
30
30
  addStyles( newComponents );
31
31
 
32
32
  Object.values( newComponents ).forEach( ( [ , data ] ) => {
33
- loadComponentsStyles( data.elements as Element[] );
33
+ loadComponentsStyles( data.elements ?? ( [] as V1ElementData[] ) );
34
34
  } );
35
35
  }
36
36
 
@@ -94,6 +94,15 @@ export const selectUnpublishedComponents = createSelector(
94
94
  selectUnpublishedData,
95
95
  ( unpublishedData: UnpublishedComponent[] ) => unpublishedData
96
96
  );
97
+ export const selectComponentsObject = createSelector(
98
+ selectData,
99
+ selectUnpublishedData,
100
+ ( data: Component[], unpublishedData: UnpublishedComponent[] ) =>
101
+ data.concat( unpublishedData ).reduce< Record< ComponentId, Component > >( ( acc, component ) => {
102
+ acc[ component.id ] = component;
103
+ return acc;
104
+ }, {} )
105
+ );
97
106
  export const selectLoadIsPending = createSelector( selectLoadStatus, ( status ) => status === 'pending' );
98
107
  export const selectLoadIsError = createSelector( selectLoadStatus, ( status ) => status === 'error' );
99
108
  export const selectStyles = ( state: ComponentsSlice ) => state[ SLICE_NAME ].styles ?? {};
package/src/types.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { type V1ElementModelProps, type V1ElementSettingsProps } from '@elementor/editor-elements';
2
1
  import type { StyleDefinition } from '@elementor/editor-styles';
3
2
 
4
3
  export type ComponentFormValues = {
@@ -16,16 +15,6 @@ export type Component = {
16
15
 
17
16
  export type DocumentStatus = 'publish' | 'draft' | 'autosave';
18
17
 
19
- export type Element = V1ElementModelProps & {
20
- elements?: Element[];
21
- settings?: V1ElementSettingsProps & {
22
- component?: {
23
- $$type: 'component-id';
24
- value: number;
25
- };
26
- };
27
- };
28
-
29
18
  export type ExtendedWindow = Window & {
30
19
  elementorCommon: Record< string, unknown >;
31
20
  };