@elementor/editor-canvas 3.35.0-493 → 3.35.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.
@@ -0,0 +1,215 @@
1
+ import * as React from 'react';
2
+ import { useEffect, useState } from 'react';
3
+ import { InlineEditor, InlineEditorToolbar } from '@elementor/editor-controls';
4
+ import { Box, ThemeProvider } from '@elementor/ui';
5
+ import { FloatingPortal, useInteractions } from '@floating-ui/react';
6
+
7
+ import { CANVAS_WRAPPER_ID, OutlineOverlay } from '../../../components/outline-overlay';
8
+ import { useBindReactPropsToElement } from '../../../hooks/use-bind-react-props-to-element';
9
+ import { useFloatingOnElement } from '../../../hooks/use-floating-on-element';
10
+ import {
11
+ calcSelectionCenterOffsets,
12
+ type Editor,
13
+ type EditorView,
14
+ getComputedStyle,
15
+ type Offsets,
16
+ } from './inline-editing-utils';
17
+
18
+ const TOP_BAR_SELECTOR = '#elementor-editor-wrapper-v2';
19
+ const NAVIGATOR_SELECTOR = '#elementor-navigator';
20
+ const EDITING_PANEL = '#elementor-panel';
21
+
22
+ const EDITOR_ELEMENTS_OUT_OF_IFRAME = [ TOP_BAR_SELECTOR, NAVIGATOR_SELECTOR, EDITING_PANEL ];
23
+
24
+ const EDITOR_WRAPPER_SELECTOR = 'inline-editor-wrapper';
25
+
26
+ export const CanvasInlineEditor = ( {
27
+ elementClasses,
28
+ initialValue,
29
+ expectedTag,
30
+ rootElement,
31
+ id,
32
+ setValue,
33
+ onBlur,
34
+ }: {
35
+ elementClasses: string;
36
+ initialValue: string | null;
37
+ expectedTag: string | null;
38
+ rootElement: HTMLElement;
39
+ id: string;
40
+ setValue: ( value: string | null ) => void;
41
+ onBlur: () => void;
42
+ } ) => {
43
+ const [ selectionOffsets, setSelectionOffsets ] = useState< Offsets | null >( null );
44
+ const [ editor, setEditor ] = useState< Editor | null >( null );
45
+
46
+ const onSelectionEnd = ( view: EditorView ) => {
47
+ const hasSelection = ! view.state.selection.empty;
48
+
49
+ setSelectionOffsets( hasSelection ? calcSelectionCenterOffsets( view ) : null );
50
+ };
51
+
52
+ useOnClickOutsideIframe( onBlur );
53
+
54
+ return (
55
+ <ThemeProvider>
56
+ <InlineEditingOverlay expectedTag={ expectedTag } rootElement={ rootElement } id={ id } />
57
+ <style>
58
+ { `
59
+ .${ EDITOR_WRAPPER_SELECTOR }, .${ EDITOR_WRAPPER_SELECTOR } > * {
60
+ height: 100%;
61
+ }
62
+ .ProseMirror > * {
63
+ height: 100%;
64
+ }
65
+ ` }
66
+ </style>
67
+ <InlineEditor
68
+ onEditorCreate={ setEditor }
69
+ editorProps={ {
70
+ attributes: {
71
+ style: 'outline: none;overflow-wrap: normal;height:100%',
72
+ },
73
+ } }
74
+ elementClasses={ elementClasses }
75
+ value={ initialValue }
76
+ setValue={ setValue }
77
+ onBlur={ onBlur }
78
+ autofocus
79
+ expectedTag={ expectedTag }
80
+ wrapperClassName={ EDITOR_WRAPPER_SELECTOR }
81
+ onSelectionEnd={ onSelectionEnd }
82
+ />
83
+ { selectionOffsets && editor && (
84
+ <InlineEditingToolbarWrapper
85
+ expectedTag={ expectedTag }
86
+ editor={ editor }
87
+ rootElement={ rootElement }
88
+ id={ id }
89
+ selectionOffsets={ selectionOffsets }
90
+ />
91
+ ) }
92
+ </ThemeProvider>
93
+ );
94
+ };
95
+
96
+ const InlineEditingOverlay = ( {
97
+ expectedTag,
98
+ rootElement,
99
+ id,
100
+ }: {
101
+ expectedTag: string | null;
102
+ rootElement: HTMLElement;
103
+ id: string;
104
+ } ) => {
105
+ const inlineEditedElement = getInlineEditorElement( rootElement, expectedTag );
106
+ const [ overlayRefElement, setOverlayElement ] = useState< HTMLDivElement | null >( inlineEditedElement );
107
+
108
+ useEffect( () => {
109
+ setOverlayElement( getInlineEditorElement( rootElement, expectedTag ) );
110
+ }, [ expectedTag, rootElement ] );
111
+
112
+ return overlayRefElement ? <OutlineOverlay element={ overlayRefElement } id={ id } isSelected /> : null;
113
+ };
114
+
115
+ const InlineEditingToolbarWrapper = ( {
116
+ expectedTag,
117
+ editor,
118
+ rootElement,
119
+ id,
120
+ selectionOffsets,
121
+ }: {
122
+ expectedTag: string | null;
123
+ editor: Editor;
124
+ rootElement: HTMLElement;
125
+ id: string;
126
+ selectionOffsets: Offsets;
127
+ } ) => {
128
+ const [ element, setElement ] = useState< HTMLElement | null >( null );
129
+
130
+ useEffect( () => {
131
+ setElement( getInlineEditorElement( rootElement, expectedTag ) );
132
+ }, [ expectedTag, rootElement ] );
133
+
134
+ return element ? (
135
+ <InlineEditingToolbar element={ element } editor={ editor } id={ id } selectionOffsets={ selectionOffsets } />
136
+ ) : null;
137
+ };
138
+
139
+ const InlineEditingToolbar = ( {
140
+ element,
141
+ editor,
142
+ id,
143
+ selectionOffsets,
144
+ }: {
145
+ element: HTMLElement;
146
+ editor: Editor;
147
+ id: string;
148
+ selectionOffsets: Offsets;
149
+ } ) => {
150
+ const { floating } = useFloatingOnElement( {
151
+ element,
152
+ isSelected: true,
153
+ } );
154
+ const { getFloatingProps, getReferenceProps } = useInteractions();
155
+ const style = getComputedStyle( floating.styles, selectionOffsets );
156
+
157
+ useBindReactPropsToElement( element, getReferenceProps );
158
+
159
+ return (
160
+ <FloatingPortal id={ CANVAS_WRAPPER_ID }>
161
+ <Box
162
+ ref={ floating.setRef }
163
+ style={ {
164
+ ...floating.styles,
165
+ pointerEvents: 'none',
166
+ } }
167
+ role="presentation"
168
+ { ...getFloatingProps( { style } ) }
169
+ >
170
+ { floating.styles.transform && (
171
+ <Box
172
+ sx={ {
173
+ position: 'relative',
174
+ transform: 'translateY(-100%)',
175
+ height: 'max-content',
176
+ } }
177
+ >
178
+ <InlineEditorToolbar
179
+ editor={ editor }
180
+ elementId={ id }
181
+ sx={ {
182
+ transform: 'translateX(-50%)',
183
+ } }
184
+ />
185
+ </Box>
186
+ ) }
187
+ </Box>
188
+ </FloatingPortal>
189
+ );
190
+ };
191
+
192
+ const getInlineEditorElement = ( elementWrapper: HTMLElement, expectedTag: string | null ) => {
193
+ return ! expectedTag ? null : ( elementWrapper.querySelector( expectedTag ) as HTMLDivElement );
194
+ };
195
+
196
+ // Elements out of iframe and canvas don't trigger "onClickAway" which unmounts the editor
197
+ // since they are not part of the iframes owner document.
198
+ // We need to manually add listeners to these elements to unmount the editor when they are clicked.
199
+ const useOnClickOutsideIframe = ( handleUnmount: () => void ) => {
200
+ const asyncUnmountInlineEditor = React.useCallback( () => queueMicrotask( handleUnmount ), [ handleUnmount ] );
201
+
202
+ useEffect( () => {
203
+ EDITOR_ELEMENTS_OUT_OF_IFRAME.forEach(
204
+ ( selector ) =>
205
+ document?.querySelector( selector )?.addEventListener( 'mousedown', asyncUnmountInlineEditor )
206
+ );
207
+
208
+ return () =>
209
+ EDITOR_ELEMENTS_OUT_OF_IFRAME.forEach(
210
+ ( selector ) =>
211
+ document?.querySelector( selector )?.removeEventListener( 'mousedown', asyncUnmountInlineEditor )
212
+ );
213
+ // eslint-disable-next-line react-hooks/exhaustive-deps
214
+ }, [] );
215
+ };
@@ -1,19 +1,14 @@
1
1
  import * as React from 'react';
2
- import { useEffect, useRef, useState } from 'react';
3
2
  import { createRoot, type Root } from 'react-dom/client';
4
- import { InlineEditor } from '@elementor/editor-controls';
5
3
  import { getContainer, getElementLabel, getElementType } from '@elementor/editor-elements';
6
4
  import { htmlPropTypeUtil, type PropType, type PropValue, stringPropTypeUtil } from '@elementor/editor-props';
7
- import { __privateRunCommandSync as runCommandSync, isExperimentActive, undoable } from '@elementor/editor-v1-adapters';
8
- import { Box, ThemeProvider } from '@elementor/ui';
5
+ import { __privateRunCommandSync as runCommandSync, getCurrentEditMode, undoable } from '@elementor/editor-v1-adapters';
9
6
  import { __ } from '@wordpress/i18n';
10
7
 
11
- import { OutlineOverlay } from '../../../components/outline-overlay';
12
8
  import { ReplacementBase, TRIGGER_TIMING } from '../base';
9
+ import { CanvasInlineEditor } from './canvas-inline-editor';
13
10
  import { isInlineEditingAllowed } from './inline-editing-eligibility';
14
- import { getInitialPopoverPosition, INLINE_EDITING_PROPERTY_PER_TYPE } from './inline-editing-utils';
15
-
16
- const EXPERIMENT_KEY = 'v4-inline-text-editing';
11
+ import { INLINE_EDITING_PROPERTY_PER_TYPE } from './inline-editing-utils';
17
12
 
18
13
  type TagPropType = PropType< 'tag' > & {
19
14
  settings?: {
@@ -23,13 +18,6 @@ type TagPropType = PropType< 'tag' > & {
23
18
 
24
19
  const HISTORY_DEBOUNCE_WAIT = 800;
25
20
 
26
- const TOP_BAR_SELECTOR = '#elementor-editor-wrapper-v2';
27
- const NAVIGATOR_SELECTOR = '#elementor-navigator';
28
- const V4_EDITING_PANEL = 'main.MuiBox-root';
29
- const V3_EDITING_PANEL = '#elementor-panel-content-wrapper';
30
-
31
- const BLUR_TRIGGERING_SELECTORS = [ TOP_BAR_SELECTOR, NAVIGATOR_SELECTOR, V4_EDITING_PANEL, V3_EDITING_PANEL ];
32
-
33
21
  export default class InlineEditingReplacement extends ReplacementBase {
34
22
  private inlineEditorRoot: Root | null = null;
35
23
  private handlerAttached = false;
@@ -47,7 +35,7 @@ export default class InlineEditingReplacement extends ReplacementBase {
47
35
  }
48
36
 
49
37
  shouldRenderReplacement() {
50
- return isExperimentActive( EXPERIMENT_KEY ) && this.isInlineEditingEligible();
38
+ return this.isInlineEditingEligible() && getCurrentEditMode() === 'edit';
51
39
  }
52
40
 
53
41
  handleRenderInlineEditor = () => {
@@ -134,12 +122,12 @@ export default class InlineEditingReplacement extends ReplacementBase {
134
122
  getExtractedContentValue() {
135
123
  const propValue = this.getInlineEditablePropValue();
136
124
 
137
- return htmlPropTypeUtil.extract( propValue ) ?? stringPropTypeUtil.extract( propValue ) ?? '';
125
+ return htmlPropTypeUtil.extract( propValue ) ?? '';
138
126
  }
139
127
 
140
128
  setContentValue( value: string | null ) {
141
129
  const settingKey = this.getInlineEditablePropertyName();
142
- const valueToSave = value ? htmlPropTypeUtil.create( value ) : null;
130
+ const valueToSave = htmlPropTypeUtil.create( value || '' );
143
131
 
144
132
  undoable(
145
133
  {
@@ -238,77 +226,23 @@ export default class InlineEditingReplacement extends ReplacementBase {
238
226
  this.resetInlineEditorRoot();
239
227
  }
240
228
 
241
- const InlineEditorApp = this.InlineEditorApp;
242
- const wrapperClasses = 'elementor';
243
229
  const elementClasses = this.element.children?.[ 0 ]?.classList.toString() ?? '';
230
+ const propValue = this.getExtractedContentValue();
231
+ const expectedTag = this.getExpectedTag();
244
232
 
245
233
  this.element.innerHTML = '';
246
234
 
247
235
  this.inlineEditorRoot = createRoot( this.element );
248
236
  this.inlineEditorRoot.render(
249
- <InlineEditorApp wrapperClasses={ wrapperClasses } elementClasses={ elementClasses } />
237
+ <CanvasInlineEditor
238
+ elementClasses={ elementClasses }
239
+ initialValue={ propValue }
240
+ expectedTag={ expectedTag }
241
+ rootElement={ this.element }
242
+ id={ this.id }
243
+ setValue={ this.setContentValue.bind( this ) }
244
+ onBlur={ this.unmountInlineEditor.bind( this ) }
245
+ />
250
246
  );
251
247
  }
252
-
253
- InlineEditorApp = ( { wrapperClasses, elementClasses }: { wrapperClasses: string; elementClasses: string } ) => {
254
- const propValue = this.getExtractedContentValue();
255
- const expectedTag = this.getExpectedTag();
256
- const wrapperRef = useRef< HTMLDivElement | null >( null );
257
- const [ isWrapperRendered, setIsWrapperRendered ] = useState( false );
258
-
259
- useEffect( () => {
260
- setIsWrapperRendered( !! wrapperRef.current );
261
- BLUR_TRIGGERING_SELECTORS.forEach(
262
- ( selector ) =>
263
- document?.querySelector( selector )?.addEventListener( 'mousedown', asyncUnmountInlineEditor )
264
- );
265
-
266
- return () =>
267
- BLUR_TRIGGERING_SELECTORS.forEach(
268
- ( selector ) =>
269
- document
270
- ?.querySelector( selector )
271
- ?.removeEventListener( 'mousedown', asyncUnmountInlineEditor )
272
- );
273
- // eslint-disable-next-line react-hooks/exhaustive-deps
274
- }, [] );
275
-
276
- const asyncUnmountInlineEditor = React.useCallback(
277
- () => queueMicrotask( this.unmountInlineEditor.bind( this ) ),
278
- []
279
- );
280
-
281
- return (
282
- <ThemeProvider>
283
- <Box
284
- ref={ wrapperRef }
285
- sx={ {
286
- '& .elementor-inline-editor-reset': {
287
- margin: 0,
288
- padding: 0,
289
- },
290
- } }
291
- >
292
- { isWrapperRendered && (
293
- <OutlineOverlay element={ wrapperRef.current as HTMLDivElement } id={ this.id } isSelected />
294
- ) }
295
- <InlineEditor
296
- attributes={ {
297
- class: wrapperClasses,
298
- style: 'outline: none;',
299
- } }
300
- elementClasses={ elementClasses }
301
- value={ propValue }
302
- setValue={ this.setContentValue.bind( this ) }
303
- onBlur={ this.unmountInlineEditor.bind( this ) }
304
- autofocus
305
- showToolbar
306
- getInitialPopoverPosition={ getInitialPopoverPosition }
307
- expectedTag={ expectedTag }
308
- elementId={ this.id }
309
- />
310
- </Box>
311
- </ThemeProvider>
312
- );
313
- };
314
248
  }
@@ -1,7 +1,17 @@
1
+ import { type CSSProperties } from 'react';
2
+ import { type InlineEditorToolbarProps } from '@elementor/editor-controls';
1
3
  import { type V1Element } from '@elementor/editor-elements';
2
4
 
3
5
  import { type LegacyWindow } from '../../types';
4
6
 
7
+ export type Editor = InlineEditorToolbarProps[ 'editor' ];
8
+ export type EditorView = Editor[ 'view' ];
9
+
10
+ export type Offsets = {
11
+ left: number;
12
+ top: number;
13
+ };
14
+
5
15
  export const INLINE_EDITING_PROPERTY_PER_TYPE: Record< string, string > = {
6
16
  'e-form-label': 'text',
7
17
  'e-heading': 'title',
@@ -14,19 +24,62 @@ export const getWidgetType = ( container: V1Element | null ) => {
14
24
  return container?.model?.get( 'widgetType' ) ?? container?.model?.get( 'elType' ) ?? null;
15
25
  };
16
26
 
17
- export const getInitialPopoverPosition = () => {
18
- const positionFallback = { left: 0, top: 0 };
27
+ export const calcSelectionCenterOffsets = ( view: EditorView ): Offsets | null => {
28
+ const frameWindow = ( view.root as Document )?.defaultView;
29
+ const selection = frameWindow?.getSelection();
30
+ const editorContainer = view.dom;
31
+
32
+ if ( ! selection || ! editorContainer ) {
33
+ return null;
34
+ }
35
+
36
+ const range = selection.getRangeAt( 0 );
37
+ const selectionRect = range.getBoundingClientRect();
38
+ const editorContainerRect = editorContainer.getBoundingClientRect();
39
+
40
+ if ( ! selectionRect || ! editorContainerRect ) {
41
+ return null;
42
+ }
43
+
44
+ const verticalOffset = selectionRect.top - editorContainerRect.top;
45
+
46
+ const selectionCenter = selectionRect?.left + selectionRect?.width / 2;
47
+ const horizontalOffset = selectionCenter - editorContainerRect.left;
48
+
49
+ return { left: horizontalOffset, top: verticalOffset };
50
+ };
51
+
52
+ export const getComputedStyle = ( styles: CSSProperties, offsets: Offsets ): CSSProperties => {
53
+ const transform = extractTransformValue( styles );
54
+
55
+ return transform
56
+ ? {
57
+ ...styles,
58
+ marginLeft: `${ offsets.left }px`,
59
+ marginTop: `${ offsets.top }px`,
60
+ pointerEvents: 'none',
61
+ }
62
+ : {
63
+ display: 'none',
64
+ };
65
+ };
66
+
67
+ const extractTransformValue = ( styles: CSSProperties ) => {
68
+ const translateRegex = /translate\([^)]*\)\s?/g;
69
+ const numericValuesRegex = /(-?\d+\.?\d*)/g;
70
+
71
+ const translateValue = styles?.transform?.match( translateRegex )?.[ 0 ];
72
+ const values = translateValue?.match( numericValuesRegex );
73
+
74
+ if ( ! translateValue || ! values ) {
75
+ return null;
76
+ }
19
77
 
20
- const iFrameElement = legacyWindow?.elementor?.$preview?.get( 0 );
21
- const iFramePosition = iFrameElement?.getBoundingClientRect() ?? positionFallback;
78
+ const [ numericX, numericY ] = values.map( Number );
22
79
 
23
- const previewElement = legacyWindow?.elementor?.$previewWrapper?.get( 0 );
24
- const previewPosition = previewElement
25
- ? { left: previewElement.scrollLeft, top: previewElement.scrollTop }
26
- : positionFallback;
80
+ if ( ! numericX || ! numericY ) {
81
+ return null;
82
+ }
27
83
 
28
- return {
29
- left: iFramePosition.left + previewPosition.left,
30
- top: iFramePosition.top + previewPosition.top,
31
- };
84
+ return styles.transform;
32
85
  };