@elementor/editor-controls 4.4.0-1131 → 4.4.0-1132

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": "4.4.0-1131",
4
+ "version": "4.4.0-1132",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -40,23 +40,23 @@
40
40
  "dev": "tsup --config=../../tsup.dev.ts"
41
41
  },
42
42
  "dependencies": {
43
- "@elementor/editor-current-user": "4.4.0-1131",
44
- "@elementor/editor-elements": "4.4.0-1131",
45
- "@elementor/editor-props": "4.4.0-1131",
46
- "@elementor/editor-responsive": "4.4.0-1131",
47
- "@elementor/editor-ui": "4.4.0-1131",
48
- "@elementor/editor-v1-adapters": "4.4.0-1131",
49
- "@elementor/env": "4.4.0-1131",
50
- "@elementor/events": "4.4.0-1131",
51
- "@elementor/http-client": "4.4.0-1131",
43
+ "@elementor/editor-current-user": "4.4.0-1132",
44
+ "@elementor/editor-elements": "4.4.0-1132",
45
+ "@elementor/editor-props": "4.4.0-1132",
46
+ "@elementor/editor-responsive": "4.4.0-1132",
47
+ "@elementor/editor-ui": "4.4.0-1132",
48
+ "@elementor/editor-v1-adapters": "4.4.0-1132",
49
+ "@elementor/env": "4.4.0-1132",
50
+ "@elementor/events": "4.4.0-1132",
51
+ "@elementor/http-client": "4.4.0-1132",
52
52
  "@elementor/icons": "~1.78.1",
53
- "@elementor/locations": "4.4.0-1131",
54
- "@elementor/query": "4.4.0-1131",
55
- "@elementor/schema": "4.4.0-1131",
56
- "@elementor/session": "4.4.0-1131",
53
+ "@elementor/locations": "4.4.0-1132",
54
+ "@elementor/query": "4.4.0-1132",
55
+ "@elementor/schema": "4.4.0-1132",
56
+ "@elementor/session": "4.4.0-1132",
57
57
  "@elementor/ui": "1.41.1",
58
- "@elementor/utils": "4.4.0-1131",
59
- "@elementor/wp-media": "4.4.0-1131",
58
+ "@elementor/utils": "4.4.0-1132",
59
+ "@elementor/wp-media": "4.4.0-1132",
60
60
  "@monaco-editor/react": "^4.7.0",
61
61
  "@tanstack/react-virtual": "^3.13.3",
62
62
  "@tiptap/extension-bold": "^3.11.1",
@@ -10,33 +10,40 @@ import { ControlLabel } from '../components/control-label';
10
10
  import { createControl } from '../create-control';
11
11
  import { useUnfilteredFilesUpload } from '../hooks/use-unfiltered-files-upload';
12
12
  import { ImageMediaControl } from './image-media-control';
13
+ import { ImageOptimizeTrigger, type ImagePromotion } from './image-optimize-trigger';
13
14
  import { SelectControl } from './select-control';
14
15
 
15
16
  type ImageControlProps = {
16
17
  sizes: { label: string; value: string }[];
17
18
  label?: string;
19
+ promotion?: ImagePromotion;
18
20
  };
19
21
 
20
- export const ImageControl = createControl( ( { sizes, label = __( 'Image', 'elementor' ) }: ImageControlProps ) => {
21
- const propContext = useBoundProp( imagePropTypeUtil );
22
+ export const ImageControl = createControl(
23
+ ( { sizes, promotion, label = __( 'Image', 'elementor' ) }: ImageControlProps ) => {
24
+ const propContext = useBoundProp( imagePropTypeUtil );
22
25
 
23
- return (
24
- <PropProvider { ...propContext }>
25
- <Stack gap={ 1.5 }>
26
- <ControlLabel>{ label }</ControlLabel>
27
- <ImageSrcControl label={ label } />
28
- <Grid container gap={ 1.5 } alignItems="center" flexWrap="nowrap">
29
- <Grid item xs={ 6 }>
30
- <ControlFormLabel>{ __( 'Resolution', 'elementor' ) }</ControlFormLabel>
31
- </Grid>
32
- <Grid item xs={ 6 } sx={ { overflow: 'hidden' } }>
33
- <ImageSizeControl sizes={ sizes } />
26
+ return (
27
+ <PropProvider { ...propContext }>
28
+ <Stack gap={ 1.5 }>
29
+ <Stack direction="row" alignItems="center" justifyContent="space-between">
30
+ <ControlLabel>{ label }</ControlLabel>
31
+ <ImageOptimizeTrigger promotion={ promotion } />
32
+ </Stack>
33
+ <ImageSrcControl label={ label } />
34
+ <Grid container gap={ 1.5 } alignItems="center" flexWrap="nowrap">
35
+ <Grid item xs={ 6 }>
36
+ <ControlFormLabel>{ __( 'Resolution', 'elementor' ) }</ControlFormLabel>
37
+ </Grid>
38
+ <Grid item xs={ 6 } sx={ { overflow: 'hidden' } }>
39
+ <ImageSizeControl sizes={ sizes } />
40
+ </Grid>
34
41
  </Grid>
35
- </Grid>
36
- </Stack>
37
- </PropProvider>
38
- );
39
- } );
42
+ </Stack>
43
+ </PropProvider>
44
+ );
45
+ }
46
+ );
40
47
 
41
48
  const ImageSrcControl = ( { label }: { label: string } ) => {
42
49
  const { data: allowSvgUpload } = useUnfilteredFilesUpload();
@@ -0,0 +1,215 @@
1
+ import * as React from 'react';
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { ajax } from '@elementor/editor-v1-adapters';
4
+ import { BulbIcon } from '@elementor/icons';
5
+ import { Box, Button, CloseButton, Infotip, Stack, Typography, useTheme } from '@elementor/ui';
6
+ import { __ } from '@wordpress/i18n';
7
+
8
+ import { isNoticeDismissed, markNoticeAsDismissedInSession } from './notice-control';
9
+
10
+ const POPOVER_WIDTH = 296;
11
+ const POPOVER_OFFSET: [ number, number ] = [ -24, 12 ];
12
+
13
+ type InfotipWithPopperProps = React.ComponentProps< typeof Infotip > & {
14
+ PopperProps?: {
15
+ modifiers?: Array< { name: string; options?: { offset: [ number, number ] } } >;
16
+ };
17
+ };
18
+
19
+ const OptimizeInfotip = Infotip as React.FC< InfotipWithPopperProps >;
20
+
21
+ export type ImagePromotion = {
22
+ label?: string;
23
+ title?: string;
24
+ content: string;
25
+ buttonText?: string;
26
+ buttonUrl?: string;
27
+ dismissible?: string;
28
+ };
29
+
30
+ type PreviewWindow = Window & {
31
+ elementor?: {
32
+ $preview?: ArrayLike< { contentDocument?: Document | null } >;
33
+ };
34
+ };
35
+
36
+ const isEventInside = ( target: EventTarget | null, anchor: HTMLElement | null ) => {
37
+ if ( ! ( target instanceof Element ) ) {
38
+ return false;
39
+ }
40
+
41
+ return !! anchor?.contains( target ) || !! target.closest( '[data-image-optimize-popover], .MuiTooltip-popper' );
42
+ };
43
+
44
+ export const ImageOptimizeTrigger = ( { promotion }: { promotion?: ImagePromotion } ) => {
45
+ const [ open, setOpen ] = useState( false );
46
+ const [ dismissed, setDismissed ] = useState( () => isNoticeDismissed( promotion?.dismissible ) );
47
+ const anchorRef = useRef< HTMLButtonElement >( null );
48
+ const theme = useTheme();
49
+ const placement = 'rtl' === theme.direction ? 'left-start' : 'right-start';
50
+
51
+ useEffect( () => {
52
+ if ( ! open ) {
53
+ return undefined;
54
+ }
55
+
56
+ const closeOnPointerDown = ( event: Event ) => {
57
+ if ( isEventInside( event.target, anchorRef.current ) ) {
58
+ return;
59
+ }
60
+
61
+ setOpen( false );
62
+ };
63
+
64
+ const closeOnEscape = ( event: KeyboardEvent ) => {
65
+ if ( 'Escape' !== event.key ) {
66
+ return;
67
+ }
68
+
69
+ event.preventDefault();
70
+ event.stopPropagation();
71
+ event.stopImmediatePropagation();
72
+
73
+ if ( 'keydown' === event.type ) {
74
+ setOpen( false );
75
+ }
76
+ };
77
+
78
+ const iframeDocument = ( window as PreviewWindow ).elementor?.$preview?.[ 0 ]?.contentDocument;
79
+
80
+ document.addEventListener( 'mousedown', closeOnPointerDown, true );
81
+ document.addEventListener( 'keydown', closeOnEscape, true );
82
+ document.addEventListener( 'keyup', closeOnEscape, true );
83
+ iframeDocument?.addEventListener( 'mousedown', closeOnPointerDown, true );
84
+ iframeDocument?.addEventListener( 'keydown', closeOnEscape, true );
85
+ iframeDocument?.addEventListener( 'keyup', closeOnEscape, true );
86
+
87
+ return () => {
88
+ document.removeEventListener( 'mousedown', closeOnPointerDown, true );
89
+ document.removeEventListener( 'keydown', closeOnEscape, true );
90
+ document.removeEventListener( 'keyup', closeOnEscape, true );
91
+ iframeDocument?.removeEventListener( 'mousedown', closeOnPointerDown, true );
92
+ iframeDocument?.removeEventListener( 'keydown', closeOnEscape, true );
93
+ iframeDocument?.removeEventListener( 'keyup', closeOnEscape, true );
94
+ };
95
+ }, [ open ] );
96
+
97
+ if ( dismissed || ! promotion?.content ) {
98
+ return null;
99
+ }
100
+
101
+ const dismiss = () => {
102
+ setDismissed( true );
103
+ setOpen( false );
104
+
105
+ if ( ! promotion.dismissible ) {
106
+ return;
107
+ }
108
+
109
+ markNoticeAsDismissedInSession( promotion.dismissible );
110
+
111
+ ajax.load( {
112
+ action: 'dismissed_editor_notices',
113
+ unique_id: `dismiss-editor-notice-${ promotion.dismissible }`,
114
+ data: { dismissId: promotion.dismissible },
115
+ } ).catch( () => {} );
116
+ };
117
+
118
+ const hideForSession = () => {
119
+ setDismissed( true );
120
+ setOpen( false );
121
+
122
+ if ( promotion.dismissible ) {
123
+ markNoticeAsDismissedInSession( promotion.dismissible );
124
+ }
125
+ };
126
+
127
+ return (
128
+ <OptimizeInfotip
129
+ arrow
130
+ disableFocusListener
131
+ disableHoverListener
132
+ disableTouchListener
133
+ content={
134
+ <Box className="dialog-widget" data-image-optimize-popover="" sx={ { width: POPOVER_WIDTH } }>
135
+ <Stack direction="row" alignItems="center" gap={ 1 } py={ 1 } px={ 2 }>
136
+ <BulbIcon fontSize="small" color="info" />
137
+ <Typography variant="subtitle2">
138
+ { promotion.title || __( 'Optimize your images', 'elementor' ) }
139
+ </Typography>
140
+ <CloseButton
141
+ edge="end"
142
+ sx={ { ml: 'auto' } }
143
+ slotProps={ { icon: { fontSize: 'small' } } }
144
+ onClick={ () => setOpen( false ) }
145
+ />
146
+ </Stack>
147
+ <Stack px={ 2 }>
148
+ <Typography variant="body2" color="secondary" sx={ { pt: 0.5, pb: 1 } }>
149
+ { promotion.content }
150
+ </Typography>
151
+ </Stack>
152
+ <Stack direction="row" alignItems="center" pt={ 1 } pb={ 1.5 } px={ 2 }>
153
+ <Button variant="text" size="small" color="secondary" onClick={ dismiss } sx={ { ml: -1 } }>
154
+ { __( 'Dismiss', 'elementor' ) }
155
+ </Button>
156
+ { promotion.buttonText && promotion.buttonUrl && (
157
+ <Button
158
+ variant="contained"
159
+ size="small"
160
+ color="info"
161
+ href={ promotion.buttonUrl }
162
+ target="_blank"
163
+ rel="noopener noreferrer"
164
+ onClick={ hideForSession }
165
+ sx={ { ml: 'auto' } }
166
+ >
167
+ { promotion.buttonText }
168
+ </Button>
169
+ ) }
170
+ </Stack>
171
+ </Box>
172
+ }
173
+ open={ open }
174
+ placement={ placement }
175
+ PopperProps={ {
176
+ modifiers: [
177
+ {
178
+ name: 'offset',
179
+ options: { offset: POPOVER_OFFSET },
180
+ },
181
+ ],
182
+ } }
183
+ >
184
+ <Button
185
+ ref={ anchorRef }
186
+ variant="text"
187
+ size="small"
188
+ color="info"
189
+ aria-expanded={ open }
190
+ onClick={ () => setOpen( ( current ) => ! current ) }
191
+ startIcon={
192
+ <BulbIcon fontSize="tiny" sx={ { display: 'block', position: 'relative', top: '-3px' } } />
193
+ }
194
+ sx={ {
195
+ display: 'inline-flex',
196
+ alignItems: 'center',
197
+ minWidth: 0,
198
+ minHeight: 0,
199
+ px: 0.75,
200
+ py: 0.25,
201
+ lineHeight: 1,
202
+ fontWeight: 400,
203
+ '& .MuiButton-startIcon': {
204
+ display: 'inline-flex',
205
+ alignItems: 'center',
206
+ margin: 0,
207
+ mr: 0.5,
208
+ },
209
+ } }
210
+ >
211
+ { promotion.label || __( 'Optimize', 'elementor' ) }
212
+ </Button>
213
+ </OptimizeInfotip>
214
+ );
215
+ };
@@ -1,109 +1,129 @@
1
1
  import * as React from 'react';
2
2
  import { type ComponentProps, useCallback, useState } from 'react';
3
3
  import { escapedHtmlPropTypeUtil } from '@elementor/editor-props';
4
- import { Box, type SxProps, type Theme } from '@elementor/ui';
4
+ import { Box, Stack, type SxProps, type Theme } from '@elementor/ui';
5
5
  import { type Editor } from '@tiptap/react';
6
6
 
7
7
  import { useBoundProp, usePropKeyContext } from '../bound-prop-context';
8
+ import { ControlLabel } from '../components/control-label';
8
9
  import { InlineEditor } from '../components/inline-editor';
9
10
  import { InlineEditorToolbar } from '../components/inline-editor-toolbar';
10
11
  import ControlActions from '../control-actions/control-actions';
11
12
  import { createControl } from '../create-control';
12
13
  import { extractInlineHtmlContent } from '../utils/inline-editing';
13
14
  import { type ControlProps } from '../utils/types';
15
+ import { ImageOptimizeTrigger, type ImagePromotion } from './image-optimize-trigger';
14
16
 
15
17
  type Props = ControlProps< {
16
18
  sx?: SxProps< Theme >;
17
19
  attributes?: Record< string, string >;
18
20
  props?: ComponentProps< 'div' >;
21
+ label?: string;
22
+ promotion?: ImagePromotion;
19
23
  } >;
20
24
 
21
- export const InlineEditingControl = createControl( ( { sx, attributes, props, context: { elementId } }: Props ) => {
22
- const { setValue, placeholder, value } = useBoundProp( escapedHtmlPropTypeUtil );
23
- const { value: rawValue } = usePropKeyContext();
24
- const content = value ?? extractInlineHtmlContent( rawValue );
25
- const [ editor, setEditor ] = useState< Editor | null >( null );
25
+ export const InlineEditingControl = createControl(
26
+ ( { sx, attributes, props, label, promotion, context: { elementId } }: Props ) => {
27
+ const { setValue, placeholder, value } = useBoundProp( escapedHtmlPropTypeUtil );
28
+ const { value: rawValue } = usePropKeyContext();
29
+ const content = value ?? extractInlineHtmlContent( rawValue );
30
+ const [ editor, setEditor ] = useState< Editor | null >( null );
26
31
 
27
- const handleChange = useCallback(
28
- ( newValue: unknown ) => {
29
- const html = ( newValue ?? '' ) as string;
32
+ const handleChange = useCallback(
33
+ ( newValue: unknown ) => {
34
+ const html = ( newValue ?? '' ) as string;
30
35
 
31
- setValue( html );
32
- },
33
- [ setValue ]
34
- );
36
+ setValue( html );
37
+ },
38
+ [ setValue ]
39
+ );
35
40
 
36
- return (
37
- <ControlActions>
38
- <Box sx={ { position: 'relative' } }>
39
- { editor && editor.isEditable && (
40
- <InlineEditorToolbar
41
- editor={ editor }
42
- elementId={ elementId }
41
+ const editorField = (
42
+ <ControlActions>
43
+ <Box sx={ { position: 'relative' } }>
44
+ { editor && editor.isEditable && (
45
+ <InlineEditorToolbar
46
+ editor={ editor }
47
+ elementId={ elementId }
48
+ sx={ ( theme: Theme ) => ( {
49
+ boxShadow: 'none',
50
+ border: '1px solid',
51
+ borderColor: theme.palette.text.secondary,
52
+ mb: 0.5,
53
+ } ) }
54
+ inControlPanel={ true }
55
+ />
56
+ ) }
57
+ <Box
43
58
  sx={ ( theme: Theme ) => ( {
44
- boxShadow: 'none',
59
+ p: 0.8,
45
60
  border: '1px solid',
46
61
  borderColor: theme.palette.text.secondary,
47
- mb: 0.5,
48
- } ) }
49
- inControlPanel={ true }
50
- />
51
- ) }
52
- <Box
53
- sx={ ( theme: Theme ) => ( {
54
- p: 0.8,
55
- border: '1px solid',
56
- borderColor: theme.palette.text.secondary,
57
- borderRadius: '8px',
58
- transition: 'border-color .2s ease, box-shadow .2s ease',
59
- '&:hover': {
60
- borderColor: theme.palette.text.primary,
61
- },
62
- '&:focus-within': {
63
- borderColor: theme.palette.text.primary,
64
- boxShadow: `0 0 0 1px ${ theme.palette.text.primary }`,
65
- },
66
- '& .ProseMirror:focus': {
67
- outline: 'none',
68
- },
69
- '& .ProseMirror': {
70
- minHeight: '100px',
71
- fontSize: '12px',
72
- '& a': {
73
- color: 'inherit',
62
+ borderRadius: '8px',
63
+ transition: 'border-color .2s ease, box-shadow .2s ease',
64
+ '&:hover': {
65
+ borderColor: theme.palette.text.primary,
66
+ },
67
+ '&:focus-within': {
68
+ borderColor: theme.palette.text.primary,
69
+ boxShadow: `0 0 0 1px ${ theme.palette.text.primary }`,
70
+ },
71
+ '& .ProseMirror:focus': {
72
+ outline: 'none',
74
73
  },
75
- '& .elementor-inline-editor-reset': {
76
- margin: 0,
77
- padding: 0,
74
+ '& .ProseMirror': {
75
+ minHeight: '100px',
76
+ fontSize: '12px',
77
+ '& a': {
78
+ color: 'inherit',
79
+ },
80
+ '& .elementor-inline-editor-reset': {
81
+ margin: 0,
82
+ padding: 0,
83
+ },
84
+ '&.is-empty::before': {
85
+ content: 'attr(data-placeholder)',
86
+ color: 'text.tertiary',
87
+ pointerEvents: 'none',
88
+ position: 'absolute',
89
+ opacity: 0.6,
90
+ },
78
91
  },
79
- '&.is-empty::before': {
80
- content: 'attr(data-placeholder)',
81
- color: 'text.tertiary',
82
- pointerEvents: 'none',
83
- position: 'absolute',
84
- opacity: 0.6,
92
+ '.strip-styles *': {
93
+ all: 'unset',
85
94
  },
86
- },
87
- '.strip-styles *': {
88
- all: 'unset',
89
- },
90
- ...sx,
91
- } ) }
92
- { ...attributes }
93
- { ...props }
94
- >
95
- <InlineEditor
96
- value={ content }
97
- setValue={ handleChange }
98
- placeholder={ placeholder ?? null }
99
- onEditorCreate={ setEditor }
100
- onEditorDestroy={ () => setEditor( null ) }
101
- sx={ {
102
- paddingBlockStart: 5,
103
- } }
104
- />
95
+ ...sx,
96
+ } ) }
97
+ { ...attributes }
98
+ { ...props }
99
+ >
100
+ <InlineEditor
101
+ value={ content }
102
+ setValue={ handleChange }
103
+ placeholder={ placeholder ?? null }
104
+ onEditorCreate={ setEditor }
105
+ onEditorDestroy={ () => setEditor( null ) }
106
+ sx={ {
107
+ paddingBlockStart: 5,
108
+ } }
109
+ />
110
+ </Box>
105
111
  </Box>
106
- </Box>
107
- </ControlActions>
108
- );
109
- } );
112
+ </ControlActions>
113
+ );
114
+
115
+ if ( ! promotion ) {
116
+ return editorField;
117
+ }
118
+
119
+ return (
120
+ <Stack gap={ 1 }>
121
+ <Stack direction="row" alignItems="center" justifyContent="space-between">
122
+ <ControlLabel>{ label }</ControlLabel>
123
+ <ImageOptimizeTrigger promotion={ promotion } />
124
+ </Stack>
125
+ { editorField }
126
+ </Stack>
127
+ );
128
+ }
129
+ );
@@ -37,7 +37,7 @@ type ExtendedWindow = Window & {
37
37
  };
38
38
  };
39
39
 
40
- const markNoticeAsDismissedInSession = ( dismissId: string ) => {
40
+ export const markNoticeAsDismissedInSession = ( dismissId: string ) => {
41
41
  const dismissedNotices = ( window as ExtendedWindow ).elementor?.config?.user?.dismissed_editor_notices;
42
42
 
43
43
  if ( dismissedNotices && ! dismissedNotices.includes( dismissId ) ) {
@@ -45,7 +45,7 @@ const markNoticeAsDismissedInSession = ( dismissId: string ) => {
45
45
  }
46
46
  };
47
47
 
48
- const isNoticeDismissed = ( dismissId?: string ) => {
48
+ export const isNoticeDismissed = ( dismissId?: string ) => {
49
49
  if ( ! dismissId ) {
50
50
  return false;
51
51
  }