@elementor/editor-controls 3.35.0-350 → 3.35.0-352

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-controls",
3
3
  "description": "This package contains the controls model and utils for the Elementor editor",
4
- "version": "3.35.0-350",
4
+ "version": "3.35.0-352",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -40,29 +40,31 @@
40
40
  "dev": "tsup --config=../../tsup.dev.ts"
41
41
  },
42
42
  "dependencies": {
43
- "@elementor/editor-current-user": "3.35.0-350",
44
- "@elementor/editor-elements": "3.35.0-350",
45
- "@elementor/editor-props": "3.35.0-350",
46
- "@elementor/editor-responsive": "3.35.0-350",
47
- "@elementor/editor-ui": "3.35.0-350",
48
- "@elementor/editor-v1-adapters": "3.35.0-350",
49
- "@elementor/env": "3.35.0-350",
50
- "@elementor/http-client": "3.35.0-350",
43
+ "@elementor/editor-current-user": "3.35.0-352",
44
+ "@elementor/editor-elements": "3.35.0-352",
45
+ "@elementor/editor-props": "3.35.0-352",
46
+ "@elementor/editor-responsive": "3.35.0-352",
47
+ "@elementor/editor-ui": "3.35.0-352",
48
+ "@elementor/editor-v1-adapters": "3.35.0-352",
49
+ "@elementor/env": "3.35.0-352",
50
+ "@elementor/http-client": "3.35.0-352",
51
51
  "@elementor/icons": "^1.62.0",
52
- "@elementor/locations": "3.35.0-350",
53
- "@elementor/mixpanel": "3.35.0-350",
54
- "@elementor/query": "3.35.0-350",
55
- "@elementor/session": "3.35.0-350",
52
+ "@elementor/locations": "3.35.0-352",
53
+ "@elementor/mixpanel": "3.35.0-352",
54
+ "@elementor/query": "3.35.0-352",
55
+ "@elementor/session": "3.35.0-352",
56
56
  "@elementor/ui": "1.36.17",
57
- "@elementor/utils": "3.35.0-350",
58
- "@elementor/wp-media": "3.35.0-350",
57
+ "@elementor/utils": "3.35.0-352",
58
+ "@elementor/wp-media": "3.35.0-352",
59
59
  "@wordpress/i18n": "^5.13.0",
60
60
  "@monaco-editor/react": "^4.7.0",
61
61
  "dayjs": "^1.11.18",
62
62
  "@tiptap/extension-bold": "^3.11.1",
63
63
  "@tiptap/extension-document": "^3.11.1",
64
64
  "@tiptap/extension-hard-break": "^3.11.1",
65
+ "@tiptap/extension-heading": "^3.10.4",
65
66
  "@tiptap/extension-italic": "^3.11.1",
67
+ "@tiptap/extension-paragraph": "^3.10.4",
66
68
  "@tiptap/extension-strike": "^3.11.1",
67
69
  "@tiptap/extension-subscript": "^3.11.1",
68
70
  "@tiptap/extension-superscript": "^3.11.1",
@@ -105,7 +105,7 @@ export const InlineEditorToolbar = ( { editor }: InlineEditorToolbarProps ) => {
105
105
  const [ urlValue, setUrlValue ] = useState( '' );
106
106
  const [ openInNewTab, setOpenInNewTab ] = useState( false );
107
107
  const toolbarRef = useRef< HTMLDivElement >( null );
108
- const popupState = usePopupState( { variant: 'popover' } );
108
+ const linkPopupState = usePopupState( { variant: 'popover' } );
109
109
 
110
110
  const editorState = useEditorState( {
111
111
  editor,
@@ -118,7 +118,7 @@ export const InlineEditorToolbar = ( { editor }: InlineEditorToolbarProps ) => {
118
118
  const linkAttrs = editor.getAttributes( 'link' );
119
119
  setUrlValue( linkAttrs.href || '' );
120
120
  setOpenInNewTab( linkAttrs.target === '_blank' );
121
- popupState.open( toolbarRef.current );
121
+ linkPopupState.open( toolbarRef.current );
122
122
  };
123
123
 
124
124
  const handleUrlChange = ( event: React.ChangeEvent< HTMLInputElement > ) => {
@@ -142,16 +142,17 @@ export const InlineEditorToolbar = ( { editor }: InlineEditorToolbarProps ) => {
142
142
  } else {
143
143
  editor.chain().focus().unsetLink().run();
144
144
  }
145
- popupState.close();
145
+ linkPopupState.close();
146
146
  };
147
147
 
148
+ React.useEffect( () => {
149
+ editor?.commands?.focus();
150
+ }, [ editor ] );
151
+
148
152
  return (
149
153
  <Box
150
154
  ref={ toolbarRef }
151
155
  sx={ {
152
- position: 'absolute',
153
- left: 0,
154
- top: -40,
155
156
  display: 'inline-flex',
156
157
  gap: 0.5,
157
158
  padding: 0.5,
@@ -159,7 +160,8 @@ export const InlineEditorToolbar = ( { editor }: InlineEditorToolbarProps ) => {
159
160
  backgroundColor: 'background.paper',
160
161
  boxShadow: '0 2px 8px rgba(0, 0, 0, 0.2)',
161
162
  alignItems: 'center',
162
- visibility: popupState.isOpen ? 'hidden' : 'visible',
163
+ visibility: linkPopupState.isOpen ? 'hidden' : 'visible',
164
+ pointerEvents: linkPopupState.isOpen ? 'none' : 'all',
163
165
  } }
164
166
  >
165
167
  <Tooltip title={ clearButton.label } placement="top" sx={ { borderRadius: '8px' } }>
@@ -194,9 +196,15 @@ export const InlineEditorToolbar = ( { editor }: InlineEditorToolbarProps ) => {
194
196
  value={ button.action }
195
197
  aria-label={ button.label }
196
198
  size="tiny"
197
- onClick={ () =>
198
- button.action === 'link' ? handleLinkClick() : button.method?.( editor )
199
- }
199
+ onClick={ () => {
200
+ if ( button.action === 'link' ) {
201
+ handleLinkClick();
202
+ } else {
203
+ button.method?.( editor );
204
+ }
205
+
206
+ editor?.commands?.focus();
207
+ } }
200
208
  >
201
209
  { button.icon }
202
210
  </ToggleButton>
@@ -204,7 +212,7 @@ export const InlineEditorToolbar = ( { editor }: InlineEditorToolbarProps ) => {
204
212
  ) ) }
205
213
  </ToggleButtonGroup>
206
214
  <UrlPopover
207
- popupState={ popupState }
215
+ popupState={ linkPopupState }
208
216
  anchorRef={ toolbarRef }
209
217
  restoreValue={ handleUrlSubmit }
210
218
  value={ urlValue }
@@ -1,28 +1,34 @@
1
1
  import * as React from 'react';
2
- import { type DependencyList, type ForwardedRef, useEffect, useRef } from 'react';
3
- import { Box, type SxProps, type Theme } from '@elementor/ui';
2
+ import { type DependencyList, useEffect, useRef } from 'react';
3
+ import { bindPopover, Box, ClickAwayListener, Popover, type SxProps, type Theme, usePopupState } from '@elementor/ui';
4
4
  import Bold from '@tiptap/extension-bold';
5
5
  import Document from '@tiptap/extension-document';
6
6
  import HardBreak from '@tiptap/extension-hard-break';
7
+ import Heading from '@tiptap/extension-heading';
7
8
  import Italic from '@tiptap/extension-italic';
8
9
  import Link from '@tiptap/extension-link';
10
+ import Paragraph from '@tiptap/extension-paragraph';
9
11
  import Strike from '@tiptap/extension-strike';
10
12
  import Subscript from '@tiptap/extension-subscript';
11
13
  import Superscript from '@tiptap/extension-superscript';
12
14
  import Text from '@tiptap/extension-text';
13
15
  import Underline from '@tiptap/extension-underline';
14
- import { type AnyExtension, EditorContent, useEditor } from '@tiptap/react';
16
+ import { type EditorView } from '@tiptap/pm/view';
17
+ import { EditorContent, useEditor } from '@tiptap/react';
15
18
 
19
+ import { isEmpty } from '../utils/inline-editing';
16
20
  import { InlineEditorToolbar } from './inline-editor-toolbar';
17
21
 
18
22
  type InlineEditorProps = {
19
- value: string;
20
- setValue: ( value: string ) => void;
23
+ value: string | null;
24
+ setValue: ( value: string | null ) => void;
21
25
  attributes?: Record< string, string >;
22
26
  sx?: SxProps< Theme >;
27
+ onBlur?: ( event: Event ) => void;
23
28
  showToolbar?: boolean;
24
- // UnstableFloatingActionBar sends props to be used for event handling for floating actions.
25
- props?: React.ComponentProps< 'div' >;
29
+ autofocus?: boolean;
30
+ getInitialPopoverPosition?: () => { left: number; top: number };
31
+ expectedTag?: string | null;
26
32
  };
27
33
 
28
34
  const useOnUpdate = ( callback: () => void, dependencies: DependencyList ): void => {
@@ -40,24 +46,79 @@ const useOnUpdate = ( callback: () => void, dependencies: DependencyList ): void
40
46
 
41
47
  export const InlineEditor = React.forwardRef(
42
48
  (
43
- { value, setValue, attributes = {}, showToolbar = false, sx, ...props }: InlineEditorProps,
44
- ref: ForwardedRef< HTMLDivElement >
49
+ {
50
+ value,
51
+ setValue,
52
+ attributes = {},
53
+ showToolbar = false,
54
+ autofocus = false,
55
+ sx = {},
56
+ onBlur = undefined,
57
+ getInitialPopoverPosition = undefined,
58
+ expectedTag = null,
59
+ }: InlineEditorProps,
60
+ ref
45
61
  ) => {
62
+ const containerRef = React.useRef< HTMLDivElement >( null );
63
+ const popupState = usePopupState( { variant: 'popover', disableAutoFocus: true } );
64
+ const [ hasSelectedContent, setHasSelectedContent ] = React.useState( false );
65
+ const documentContentSettings = !! expectedTag ? 'block+' : 'inline*';
66
+
67
+ const onSelectionEnd = ( view: EditorView ) => {
68
+ setHasSelectedContent( () => ! view.state.selection.empty );
69
+ queueMicrotask( () => view.focus() );
70
+ };
71
+
72
+ const onKeyDown = ( _: EditorView, event: KeyboardEvent ) => {
73
+ if ( event.key === 'Escape' ) {
74
+ onBlur?.( event );
75
+ }
76
+ };
77
+
78
+ const toolbarRelatedListeners = showToolbar
79
+ ? {
80
+ mouseup: onSelectionEnd,
81
+ keyup: onSelectionEnd,
82
+ keydown: onKeyDown,
83
+ }
84
+ : undefined;
85
+
46
86
  const editor = useEditor( {
47
87
  extensions: [
48
88
  Document.extend( {
49
- content: 'inline*',
89
+ content: documentContentSettings,
90
+ } ),
91
+ Paragraph.extend( {
92
+ renderHTML( { HTMLAttributes } ) {
93
+ const tag = expectedTag ?? 'p';
94
+ return [ tag, { ...HTMLAttributes, style: 'margin:0;padding:0;' }, 0 ];
95
+ },
96
+ } ),
97
+ Heading.extend( {
98
+ renderHTML( { node, HTMLAttributes } ) {
99
+ if ( expectedTag ) {
100
+ return [ expectedTag, { ...HTMLAttributes, style: 'margin:0;padding:0;' }, 0 ];
101
+ }
102
+
103
+ const level = this.options.levels.includes( node.attrs.level )
104
+ ? node.attrs.level
105
+ : this.options.levels[ 0 ];
106
+
107
+ return [ `h${ level }`, { ...HTMLAttributes, style: 'margin:0;padding:0;' }, 0 ];
108
+ },
109
+ } ).configure( {
110
+ levels: [ 1, 2, 3, 4, 5, 6 ],
111
+ } ),
112
+ Link.configure( {
113
+ openOnClick: false,
50
114
  } ),
51
115
  Text,
52
116
  Bold,
53
117
  Italic,
54
118
  Strike,
55
- Underline,
56
119
  Superscript,
57
120
  Subscript,
58
- Link.configure( {
59
- openOnClick: false,
60
- } ),
121
+ Underline,
61
122
  HardBreak.extend( {
62
123
  addKeyboardShortcuts() {
63
124
  return {
@@ -65,9 +126,22 @@ export const InlineEditor = React.forwardRef(
65
126
  };
66
127
  },
67
128
  } ),
68
- ] as AnyExtension[],
129
+ ],
69
130
  content: value,
70
- onUpdate: ( { editor: updatedEditor } ) => setValue( updatedEditor.getHTML() ),
131
+ onUpdate: ( { editor: updatedEditor } ) => {
132
+ const newValue: string | null = updatedEditor.getHTML();
133
+
134
+ setValue( isEmpty( newValue ) ? null : newValue );
135
+ },
136
+ autofocus,
137
+ editorProps: {
138
+ attributes: {
139
+ ...attributes,
140
+ class: attributes.class ?? '',
141
+ role: 'textbox',
142
+ },
143
+ handleDOMEvents: toolbarRelatedListeners,
144
+ },
71
145
  } );
72
146
 
73
147
  useOnUpdate( () => {
@@ -82,40 +156,69 @@ export const InlineEditor = React.forwardRef(
82
156
  }
83
157
  }, [ editor, value ] );
84
158
 
159
+ const computePopupPosition = () => {
160
+ const positionFallback = { left: 0, top: 0 };
161
+ const { left, top } = containerRef.current?.getBoundingClientRect() ?? positionFallback;
162
+ const initial = getInitialPopoverPosition?.() ?? positionFallback;
163
+
164
+ return {
165
+ left: left + initial.left,
166
+ top: top + initial.top,
167
+ };
168
+ };
169
+
170
+ const Wrapper = ( { children }: React.PropsWithChildren ) => {
171
+ const wrappedChildren = (
172
+ <Box ref={ containerRef } { ...sx }>
173
+ { children }
174
+ </Box>
175
+ );
176
+
177
+ return onBlur ? (
178
+ <ClickAwayListener
179
+ onClickAway={ ( event: PointerEvent ) => {
180
+ if (
181
+ containerRef.current?.contains( event.target as Node ) ||
182
+ editor.view.dom.contains( event.target as Node )
183
+ ) {
184
+ return;
185
+ }
186
+
187
+ onBlur?.( event );
188
+ } }
189
+ >
190
+ { wrappedChildren }
191
+ </ClickAwayListener>
192
+ ) : (
193
+ <>{ wrappedChildren }</>
194
+ );
195
+ };
196
+
85
197
  return (
86
- <Box
87
- ref={ ref }
88
- sx={ {
89
- p: 0.8,
90
- border: '1px solid',
91
- borderColor: 'grey.200',
92
- borderRadius: '8px',
93
- transition: 'border-color .2s ease, box-shadow .2s ease',
94
- '&:hover': {
95
- borderColor: 'black',
96
- },
97
- '&:focus-within': {
98
- borderColor: 'black',
99
- boxShadow: '0 0 0 1px black',
100
- },
101
- '& .ProseMirror:focus': {
102
- outline: 'none',
103
- },
104
- '& .ProseMirror': {
105
- minHeight: '70px',
106
- fontSize: '12px',
107
- '& a': {
108
- color: 'inherit',
109
- },
110
- },
111
- ...sx,
112
- } }
113
- { ...attributes }
114
- { ...props }
115
- >
116
- { showToolbar && <InlineEditorToolbar editor={ editor } /> }
117
- <EditorContent editor={ editor } />
118
- </Box>
198
+ <>
199
+ <Wrapper>
200
+ <EditorContent ref={ ref } editor={ editor } />
201
+ </Wrapper>
202
+ { showToolbar && containerRef.current && (
203
+ <Popover
204
+ slotProps={ {
205
+ root: {
206
+ sx: {
207
+ pointerEvents: 'none',
208
+ },
209
+ },
210
+ } }
211
+ { ...bindPopover( popupState ) }
212
+ open={ hasSelectedContent }
213
+ anchorReference="anchorPosition"
214
+ anchorPosition={ computePopupPosition() }
215
+ anchorOrigin={ { vertical: 'top', horizontal: 'left' } }
216
+ transformOrigin={ { vertical: 'bottom', horizontal: 'left' } }
217
+ >
218
+ <InlineEditorToolbar editor={ editor } />
219
+ </Popover>
220
+ ) }
221
+ </>
119
222
  );
120
223
  }
121
224
  );
@@ -1,18 +1,62 @@
1
1
  import * as React from 'react';
2
2
  import { htmlPropTypeUtil } from '@elementor/editor-props';
3
+ import { Box, type SxProps, type Theme } from '@elementor/ui';
3
4
 
4
5
  import { useBoundProp } from '../bound-prop-context';
5
6
  import { InlineEditor } from '../components/inline-editor';
6
7
  import ControlActions from '../control-actions/control-actions';
7
8
  import { createControl } from '../create-control';
8
9
 
9
- export const InlineEditingControl = createControl( () => {
10
- const { value, setValue } = useBoundProp( htmlPropTypeUtil );
11
- const handleChange = ( newValue: unknown ) => setValue( newValue as string );
10
+ export const InlineEditingControl = createControl(
11
+ ( {
12
+ sx,
13
+ attributes,
14
+ props,
15
+ }: {
16
+ sx?: SxProps< Theme >;
17
+ attributes?: Record< string, string >;
18
+ props?: React.ComponentProps< 'div' >;
19
+ } ) => {
20
+ const { value, setValue } = useBoundProp( htmlPropTypeUtil );
21
+ const handleChange = ( newValue: unknown ) => setValue( newValue as string );
12
22
 
13
- return (
14
- <ControlActions>
15
- <InlineEditor value={ value || '' } setValue={ handleChange } />
16
- </ControlActions>
17
- );
18
- } );
23
+ return (
24
+ <ControlActions>
25
+ <Box
26
+ sx={ {
27
+ p: 0.8,
28
+ border: '1px solid',
29
+ borderColor: 'grey.200',
30
+ borderRadius: '8px',
31
+ transition: 'border-color .2s ease, box-shadow .2s ease',
32
+ '&:hover': {
33
+ borderColor: 'black',
34
+ },
35
+ '&:focus-within': {
36
+ borderColor: 'black',
37
+ boxShadow: '0 0 0 1px black',
38
+ },
39
+ '& .ProseMirror:focus': {
40
+ outline: 'none',
41
+ },
42
+ '& .ProseMirror': {
43
+ minHeight: '70px',
44
+ fontSize: '12px',
45
+ '& a': {
46
+ color: 'inherit',
47
+ },
48
+ },
49
+ '.strip-styles *': {
50
+ all: 'unset',
51
+ },
52
+ ...sx,
53
+ } }
54
+ { ...attributes }
55
+ { ...props }
56
+ >
57
+ <InlineEditor value={ value || '' } setValue={ handleChange } />
58
+ </Box>
59
+ </ControlActions>
60
+ );
61
+ }
62
+ );
package/src/index.ts CHANGED
@@ -35,8 +35,6 @@ export { enqueueFont } from './controls/font-family-control/enqueue-font';
35
35
  export { transitionProperties, transitionsItemsList } from './controls/transition-control/data';
36
36
  export { DateTimeControl } from './controls/date-time-control';
37
37
  export { InlineEditingControl } from './controls/inline-editing-control';
38
- export { InlineEditor } from './components/inline-editor';
39
- export { InlineEditorToolbar } from './components/inline-editor-toolbar';
40
38
 
41
39
  // components
42
40
  export { ControlFormLabel } from './components/control-form-label';
@@ -51,6 +49,8 @@ export {
51
49
  } from './components/repeater/repeater';
52
50
  export { FloatingActionsBar } from './components/floating-bar';
53
51
  export { PopoverGridContainer } from './components/popover-grid-container';
52
+ export { InlineEditor } from './components/inline-editor';
53
+ export { InlineEditorToolbar } from './components/inline-editor-toolbar';
54
54
 
55
55
  // types
56
56
  export type { ControlComponent } from './create-control';
@@ -0,0 +1,11 @@
1
+ export function isEmpty( value: string | null = '' ) {
2
+ if ( ! value ) {
3
+ return true;
4
+ }
5
+
6
+ const pseudoElement = document.createElement( 'div' );
7
+
8
+ pseudoElement.innerHTML = value;
9
+
10
+ return ! pseudoElement.textContent?.length;
11
+ }