@elementor/editor-controls 0.16.0 → 0.17.0

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": "0.16.0",
4
+ "version": "0.17.0",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -40,7 +40,9 @@
40
40
  "dev": "tsup --config=../../tsup.dev.ts"
41
41
  },
42
42
  "dependencies": {
43
- "@elementor/editor-props": "0.10.0",
43
+ "@elementor/editor-current-user": "0.3.0",
44
+ "@elementor/editor-elements": "0.6.3",
45
+ "@elementor/editor-props": "0.11.0",
44
46
  "@elementor/env": "0.3.5",
45
47
  "@elementor/http": "0.1.4",
46
48
  "@elementor/icons": "1.37.0",
@@ -48,9 +50,9 @@
48
50
  "@elementor/session": "0.1.0",
49
51
  "@elementor/ui": "1.26.0",
50
52
  "@elementor/utils": "0.4.0",
51
- "@elementor/wp-media": "0.5.0",
52
- "@wordpress/i18n": "^5.13.0",
53
- "@tanstack/react-virtual": "3.13.3"
53
+ "@elementor/wp-media": "0.6.0",
54
+ "@tanstack/react-virtual": "3.13.3",
55
+ "@wordpress/i18n": "^5.13.0"
54
56
  },
55
57
  "devDependencies": {
56
58
  "tsup": "^8.3.5"
@@ -43,6 +43,8 @@ type Props< TValue > = {
43
43
  }
44
44
  );
45
45
 
46
+ // The maximum number of buttons that can be displayed in the group
47
+ const MAX_VISIBLE_ITEMS = 4;
46
48
  export const ControlToggleButtonGroup = < TValue, >( {
47
49
  justify = 'end',
48
50
  size = 'tiny',
@@ -69,6 +71,9 @@ export const ControlToggleButtonGroup = < TValue, >( {
69
71
  exclusive={ exclusive }
70
72
  sx={ {
71
73
  direction: isRtl ? 'rtl /* @noflip */' : 'ltr /* @noflip */',
74
+ display: 'grid',
75
+ gridTemplateColumns: `repeat(${ items.length }, 1fr)`,
76
+ width: `${ ( items.length / MAX_VISIBLE_ITEMS ) * 100 }%`,
72
77
  } }
73
78
  >
74
79
  { items.map( ( { label, value: buttonValue, renderContent: Content, showTooltip } ) =>
@@ -0,0 +1,130 @@
1
+ import * as React from 'react';
2
+ import { useState } from 'react';
3
+ import { useCurrentUserCapabilities } from '@elementor/editor-current-user';
4
+ import {
5
+ Button,
6
+ CircularProgress,
7
+ Dialog,
8
+ DialogActions,
9
+ DialogContent,
10
+ DialogContentText,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ Divider,
14
+ } from '@elementor/ui';
15
+ import { __ } from '@wordpress/i18n';
16
+
17
+ import { useUpdateUnfilteredFilesUpload } from '../hooks/use-unfiltered-files-upload';
18
+
19
+ type EnableUnfilteredModalProps = {
20
+ open: boolean;
21
+ onClose: ( enabled: boolean ) => void;
22
+ };
23
+
24
+ type LocalModalProps = {
25
+ open: boolean;
26
+ onClose: ( enabled: boolean ) => void;
27
+ isPending?: boolean;
28
+ isError?: boolean;
29
+ handleEnable: () => void;
30
+ };
31
+
32
+ const ADMIN_TITLE_TEXT = __( 'Enable Unfiltered Uploads', 'elementor' );
33
+ const ADMIN_CONTENT_TEXT = __(
34
+ 'Before you enable unfiltered files upload, note that such files include a security risk. Elementor does run a process to remove possible malicious code, but there is still risk involved when using such files.',
35
+ 'elementor'
36
+ );
37
+ const NON_ADMIN_TITLE_TEXT = __( "Sorry, you can't upload that file yet", 'elementor' );
38
+ const NON_ADMIN_CONTENT_TEXT = __(
39
+ 'This is because this file type may pose a security risk. To upload them anyway, ask the site administrator to enable unfiltered file uploads.',
40
+ 'elementor'
41
+ );
42
+
43
+ const ADMIN_FAILED_CONTENT_TEXT_PT1 = __( 'Failed to enable unfiltered files upload.', 'elementor' );
44
+
45
+ const ADMIN_FAILED_CONTENT_TEXT_PT2 = __(
46
+ 'You can try again, if the problem persists, please contact support.',
47
+ 'elementor'
48
+ );
49
+
50
+ const WAIT_FOR_CLOSE_TIMEOUT_MS = 300;
51
+
52
+ export const EnableUnfilteredModal = ( props: EnableUnfilteredModalProps ) => {
53
+ const { mutateAsync, isPending } = useUpdateUnfilteredFilesUpload();
54
+ const { canUser } = useCurrentUserCapabilities();
55
+ const [ isError, setIsError ] = useState( false );
56
+ const canManageOptions = canUser( 'manage_options' );
57
+
58
+ const onClose = ( enabled: boolean ) => {
59
+ props.onClose( enabled );
60
+ setTimeout( () => setIsError( false ), WAIT_FOR_CLOSE_TIMEOUT_MS );
61
+ };
62
+
63
+ const handleEnable = async () => {
64
+ try {
65
+ const response = await mutateAsync( { allowUnfilteredFilesUpload: true } );
66
+ if ( response?.data?.success === false ) {
67
+ setIsError( true );
68
+ } else {
69
+ props.onClose( true );
70
+ }
71
+ } catch {
72
+ setIsError( true );
73
+ }
74
+ };
75
+
76
+ const dialogProps = { ...props, isPending, handleEnable, isError, onClose };
77
+
78
+ return canManageOptions ? <AdminDialog { ...dialogProps } /> : <NonAdminDialog { ...dialogProps } />;
79
+ };
80
+
81
+ const AdminDialog = ( { open, onClose, handleEnable, isPending, isError }: LocalModalProps ) => (
82
+ <Dialog open={ open } maxWidth={ 'sm' } onClose={ () => onClose( false ) }>
83
+ <DialogHeader logo={ false }>
84
+ <DialogTitle>{ ADMIN_TITLE_TEXT }</DialogTitle>
85
+ </DialogHeader>
86
+ <Divider />
87
+ <DialogContent>
88
+ <DialogContentText>
89
+ { isError ? (
90
+ <>
91
+ { ADMIN_FAILED_CONTENT_TEXT_PT1 } <br /> { ADMIN_FAILED_CONTENT_TEXT_PT2 }
92
+ </>
93
+ ) : (
94
+ ADMIN_CONTENT_TEXT
95
+ ) }
96
+ </DialogContentText>
97
+ </DialogContent>
98
+ <DialogActions>
99
+ <Button size={ 'medium' } color="secondary" onClick={ () => onClose( false ) }>
100
+ { __( 'Cancel', 'elementor' ) }
101
+ </Button>
102
+ <Button
103
+ size={ 'medium' }
104
+ onClick={ () => handleEnable() }
105
+ variant="contained"
106
+ color="primary"
107
+ disabled={ isPending }
108
+ >
109
+ { isPending ? <CircularProgress size={ 24 } /> : __( 'Enable', 'elementor' ) }
110
+ </Button>
111
+ </DialogActions>
112
+ </Dialog>
113
+ );
114
+
115
+ const NonAdminDialog = ( { open, onClose }: LocalModalProps ) => (
116
+ <Dialog open={ open } maxWidth={ 'sm' } onClose={ () => onClose( false ) }>
117
+ <DialogHeader logo={ false }>
118
+ <DialogTitle>{ NON_ADMIN_TITLE_TEXT }</DialogTitle>
119
+ </DialogHeader>
120
+ <Divider />
121
+ <DialogContent>
122
+ <DialogContentText>{ NON_ADMIN_CONTENT_TEXT }</DialogContentText>
123
+ </DialogContent>
124
+ <DialogActions>
125
+ <Button size={ 'medium' } onClick={ () => onClose( false ) } variant="contained" color="primary">
126
+ { __( 'Got it', 'elementor' ) }
127
+ </Button>
128
+ </DialogActions>
129
+ </Dialog>
130
+ );
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { useRef, useState } from 'react';
2
+ import { useEffect, useState } from 'react';
3
3
  import { type PropKey } from '@elementor/editor-props';
4
4
  import { CopyIcon, EyeIcon, EyeOffIcon, PlusIcon, XIcon } from '@elementor/icons';
5
5
  import {
@@ -33,6 +33,7 @@ type RepeaterProps< T > = {
33
33
  label: string;
34
34
  values?: T[];
35
35
  addToBottom?: boolean;
36
+ openOnAdd?: boolean;
36
37
  setValues: ( newValue: T[] ) => void;
37
38
  itemSettings: {
38
39
  initialValues: T;
@@ -49,10 +50,13 @@ type RepeaterProps< T > = {
49
50
  export const Repeater = < T, >( {
50
51
  label,
51
52
  itemSettings,
53
+ openOnAdd = false,
52
54
  addToBottom = false,
53
55
  values: repeaterValues = [],
54
56
  setValues: setRepeaterValues,
55
57
  }: RepeaterProps< Item< T > > ) => {
58
+ const [ openItem, setOpenItem ] = useState( -1 );
59
+
56
60
  const [ items, setItems ] = useSyncExternalState( {
57
61
  external: repeaterValues,
58
62
  // @ts-expect-error - as long as persistWhen => true, value will never be null
@@ -77,6 +81,10 @@ export const Repeater = < T, >( {
77
81
  setItems( [ newItem, ...items ] );
78
82
  setUniqueKeys( [ newKey, ...uniqueKeys ] );
79
83
  }
84
+
85
+ if ( openOnAdd ) {
86
+ setOpenItem( newKey );
87
+ }
80
88
  };
81
89
 
82
90
  const duplicateRepeaterItem = ( index: number ) => {
@@ -158,6 +166,7 @@ export const Repeater = < T, >( {
158
166
  removeItem={ () => removeRepeaterItem( index ) }
159
167
  duplicateItem={ () => duplicateRepeaterItem( index ) }
160
168
  toggleDisableItem={ () => toggleDisableRepeaterItem( index ) }
169
+ openOnMount={ openOnAdd && openItem === key }
161
170
  >
162
171
  { ( props ) => (
163
172
  <itemSettings.Content { ...props } value={ value } bind={ String( index ) } />
@@ -181,25 +190,21 @@ type RepeaterItemProps = {
181
190
  duplicateItem: () => void;
182
191
  toggleDisableItem: () => void;
183
192
  children: ( { anchorEl }: { anchorEl: AnchorEl } ) => React.ReactNode;
193
+ openOnMount: boolean;
184
194
  };
185
195
 
186
196
  const RepeaterItem = ( {
187
197
  label,
188
- bind,
189
198
  disabled,
190
199
  startIcon,
191
200
  children,
192
201
  removeItem,
193
202
  duplicateItem,
194
203
  toggleDisableItem,
204
+ openOnMount,
195
205
  }: RepeaterItemProps ) => {
196
- const popupId = `repeater-popup-${ bind }`;
197
- const controlRef = useRef< HTMLElement >( null );
198
206
  const [ anchorEl, setAnchorEl ] = useState< AnchorEl >( null );
199
-
200
- const popoverState = usePopupState( { popupId, variant: 'popover' } );
201
-
202
- const popoverProps = bindPopover( popoverState );
207
+ const { popoverState, popoverProps, ref, setRef } = usePopover( openOnMount );
203
208
 
204
209
  const duplicateLabel = __( 'Duplicate', 'elementor' );
205
210
  const toggleLabel = disabled ? __( 'Show', 'elementor' ) : __( 'Hide', 'elementor' );
@@ -211,7 +216,7 @@ const RepeaterItem = ( {
211
216
  label={ label }
212
217
  showActionsOnHover
213
218
  fullWidth
214
- ref={ controlRef }
219
+ ref={ setRef }
215
220
  variant="outlined"
216
221
  aria-label={ __( 'Open item', 'elementor' ) }
217
222
  { ...bindTrigger( popoverState ) }
@@ -242,14 +247,38 @@ const RepeaterItem = ( {
242
247
  slotProps={ {
243
248
  paper: {
244
249
  ref: setAnchorEl,
245
- sx: { mt: 0.5, width: controlRef.current?.getBoundingClientRect().width },
250
+ sx: { mt: 0.5, width: ref?.getBoundingClientRect().width },
246
251
  },
247
252
  } }
248
253
  anchorOrigin={ { vertical: 'bottom', horizontal: 'left' } }
249
254
  { ...popoverProps }
255
+ anchorEl={ ref }
250
256
  >
251
257
  <Box>{ children( { anchorEl } ) }</Box>
252
258
  </Popover>
253
259
  </>
254
260
  );
255
261
  };
262
+
263
+ const usePopover = ( openOnMount: boolean ) => {
264
+ const [ ref, setRef ] = useState< HTMLElement | null >( null );
265
+
266
+ const popoverState = usePopupState( { variant: 'popover' } );
267
+
268
+ const popoverProps = bindPopover( popoverState );
269
+
270
+ useEffect( () => {
271
+ if ( openOnMount && ref ) {
272
+ popoverState.open( ref );
273
+ }
274
+ // eslint-disable-next-line react-compiler/react-compiler
275
+ // eslint-disable-next-line react-hooks/exhaustive-deps
276
+ }, [ ref ] );
277
+
278
+ return {
279
+ popoverState,
280
+ ref,
281
+ setRef,
282
+ popoverProps,
283
+ };
284
+ };
@@ -14,13 +14,8 @@ import {
14
14
 
15
15
  export const SortableProvider = < T extends number >( props: UnstableSortableProviderProps< T > ) => {
16
16
  return (
17
- <List sx={ { p: 0, m: 0 } }>
18
- <UnstableSortableProvider
19
- restrictAxis={ true }
20
- disableDragOverlay={ false }
21
- variant={ 'static' }
22
- { ...props }
23
- />
17
+ <List sx={ { p: 0, my: -0.5, mx: 0 } }>
18
+ <UnstableSortableProvider restrictAxis disableDragOverlay={ false } variant={ 'static' } { ...props } />
24
19
  </List>
25
20
  );
26
21
  };
@@ -90,7 +90,7 @@ export const initialBackgroundGradientOverlay: BackgroundOverlayItemPropValue =
90
90
  angle: numberPropTypeUtil.create( 180 ),
91
91
  stops: gradientColorStopPropTypeUtil.create( [
92
92
  colorStopPropTypeUtil.create( {
93
- color: stringPropTypeUtil.create( 'var(--primary-color)' ),
93
+ color: colorPropTypeUtil.create( 'rgb(0,0,0)' ),
94
94
  offset: numberPropTypeUtil.create( 0 ),
95
95
  } ),
96
96
  colorStopPropTypeUtil.create( {
@@ -4,6 +4,7 @@ import {
4
4
  backgroundImageOverlayPropTypeUtil,
5
5
  type BackgroundOverlayItemPropValue,
6
6
  backgroundOverlayPropTypeUtil,
7
+ colorPropTypeUtil,
7
8
  type PropKey,
8
9
  } from '@elementor/editor-props';
9
10
  import { Box, CardMedia, Grid, Tab, TabPanel, Tabs, UnstableColorIndicator } from '@elementor/ui';
@@ -29,10 +30,13 @@ import { BackgroundImageOverlaySize } from './background-image-overlay/backgroun
29
30
  import { type BackgroundImageOverlay } from './types';
30
31
  import { useBackgroundTabsHistory } from './use-background-tabs-history';
31
32
 
32
- export const initialBackgroundColorOverlay: BackgroundOverlayItemPropValue = {
33
- $$type: 'background-color-overlay',
34
- value: '#00000033',
35
- };
33
+ const DEFAULT_BACKGROUND_COLOR_OVERLAY_COLOR = '#00000033';
34
+
35
+ export const initialBackgroundColorOverlay: BackgroundOverlayItemPropValue = backgroundColorOverlayPropTypeUtil.create(
36
+ {
37
+ color: colorPropTypeUtil.create( DEFAULT_BACKGROUND_COLOR_OVERLAY_COLOR ),
38
+ }
39
+ );
36
40
 
37
41
  export const getInitialBackgroundOverlay = (): BackgroundOverlayItemPropValue => ( {
38
42
  $$type: 'background-image-overlay',
@@ -72,6 +76,7 @@ export const BackgroundOverlayRepeaterControl = createControl( () => {
72
76
  return (
73
77
  <PropProvider propType={ propType } value={ overlayValues } setValue={ setValue }>
74
78
  <Repeater
79
+ openOnAdd
75
80
  values={ overlayValues ?? [] }
76
81
  setValues={ setValue }
77
82
  label={ __( 'Overlay', 'elementor' ) }
@@ -118,8 +123,10 @@ const Content = () => {
118
123
  <TabPanel sx={ { p: 1.5 } } { ...getTabPanelProps( 'gradient' ) }>
119
124
  <BackgroundGradientColorControl />
120
125
  </TabPanel>
121
- <TabPanel { ...getTabPanelProps( 'color' ) } sx={ { p: 1.5 } }>
122
- <ColorControl propTypeUtil={ backgroundColorOverlayPropTypeUtil } />
126
+ <TabPanel sx={ { p: 1.5 } } { ...getTabPanelProps( 'color' ) }>
127
+ <PopoverContent>
128
+ <ColorOverlayContent />
129
+ </PopoverContent>
123
130
  </TabPanel>
124
131
  </Box>
125
132
  );
@@ -138,8 +145,17 @@ const ItemIcon = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
138
145
  }
139
146
  };
140
147
 
141
- const ItemIconColor = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
142
- return <UnstableColorIndicator size="inherit" component="span" value={ value.value } />;
148
+ const extractColorFrom = ( prop: BackgroundOverlayItemPropValue ) => {
149
+ if ( prop?.value?.color?.value ) {
150
+ return prop.value.color.value;
151
+ }
152
+
153
+ return '';
154
+ };
155
+
156
+ const ItemIconColor = ( { value: prop }: { value: BackgroundOverlayItemPropValue } ) => {
157
+ const color = extractColorFrom( prop );
158
+ return <UnstableColorIndicator size="inherit" component="span" value={ color } />;
143
159
  };
144
160
 
145
161
  const ItemIconImage = ( { value }: { value: BackgroundImageOverlay } ) => {
@@ -167,8 +183,9 @@ const ItemLabel = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
167
183
  }
168
184
  };
169
185
 
170
- const ItemLabelColor = ( { value }: { value: BackgroundOverlayItemPropValue } ) => {
171
- return <span>{ value.value }</span>;
186
+ const ItemLabelColor = ( { value: prop }: { value: BackgroundOverlayItemPropValue } ) => {
187
+ const color = extractColorFrom( prop );
188
+ return <span>{ color }</span>;
172
189
  };
173
190
 
174
191
  const ItemLabelImage = ( { value }: { value: BackgroundImageOverlay } ) => {
@@ -185,6 +202,17 @@ const ItemLabelGradient = ( { value }: { value: BackgroundOverlayItemPropValue }
185
202
  return <span>{ __( 'Radial Gradient', 'elementor' ) }</span>;
186
203
  };
187
204
 
205
+ const ColorOverlayContent = () => {
206
+ const propContext = useBoundProp( backgroundColorOverlayPropTypeUtil );
207
+ return (
208
+ <PropProvider { ...propContext }>
209
+ <PropKeyProvider bind={ 'color' }>
210
+ <ColorControl />
211
+ </PropKeyProvider>
212
+ </PropProvider>
213
+ );
214
+ };
215
+
188
216
  const ImageOverlayContent = () => {
189
217
  const propContext = useBoundProp( backgroundImageOverlayPropTypeUtil );
190
218
 
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { boxShadowPropTypeUtil, type PropKey, shadowPropTypeUtil, type ShadowPropValue } from '@elementor/editor-props';
3
- import { Grid, Typography, UnstableColorIndicator } from '@elementor/ui';
3
+ import { Grid, type SxProps, type Theme, Typography, UnstableColorIndicator } from '@elementor/ui';
4
4
  import { __ } from '@wordpress/i18n';
5
5
 
6
6
  import { PropKeyProvider, PropProvider, useBoundProp } from '../bound-prop-context';
@@ -18,6 +18,7 @@ export const BoxShadowRepeaterControl = createControl( () => {
18
18
  return (
19
19
  <PropProvider propType={ propType } value={ value } setValue={ setValue }>
20
20
  <Repeater
21
+ openOnAdd
21
22
  values={ value ?? [] }
22
23
  setValues={ setValue }
23
24
  label={ __( 'Box shadow', 'elementor' ) }
@@ -68,7 +69,7 @@ const Content = ( { anchorEl }: { anchorEl: HTMLElement | null } ) => {
68
69
  } }
69
70
  />
70
71
  </Control>
71
- <Control bind="position" label={ __( 'Position', 'elementor' ) }>
72
+ <Control bind="position" label={ __( 'Position', 'elementor' ) } sx={ { overflow: 'hidden' } }>
72
73
  <SelectControl
73
74
  options={ [
74
75
  { label: __( 'Inset', 'elementor' ), value: 'inset' },
@@ -98,9 +99,19 @@ const Content = ( { anchorEl }: { anchorEl: HTMLElement | null } ) => {
98
99
  );
99
100
  };
100
101
 
101
- const Control = ( { label, bind, children }: { bind: string; label: string; children: React.ReactNode } ) => (
102
+ const Control = ( {
103
+ label,
104
+ bind,
105
+ children,
106
+ sx,
107
+ }: {
108
+ bind: string;
109
+ label: string;
110
+ children: React.ReactNode;
111
+ sx?: SxProps< Theme >;
112
+ } ) => (
102
113
  <PropKeyProvider bind={ bind }>
103
- <Grid item xs={ 6 } sx={ { overflow: 'hidden' } }>
114
+ <Grid item xs={ 6 } sx={ sx }>
104
115
  <Grid container gap={ 1 } alignItems="center">
105
116
  <Grid item xs={ 12 }>
106
117
  <Typography component="label" variant="caption" color="text.secondary">
@@ -144,8 +144,8 @@ export function EqualUnequalSizesControl< TMultiPropType extends string, TPropVa
144
144
  <MultiSizeValueControl item={ items[ 1 ] } />
145
145
  </PopoverGridContainer>
146
146
  <PopoverGridContainer>
147
- <MultiSizeValueControl item={ items[ 3 ] } />
148
147
  <MultiSizeValueControl item={ items[ 2 ] } />
148
+ <MultiSizeValueControl item={ items[ 3 ] } />
149
149
  </PopoverGridContainer>
150
150
  </PopoverContent>
151
151
  </PropProvider>
@@ -31,8 +31,13 @@ import { enqueueFont } from './enqueue-font';
31
31
 
32
32
  const SIZE = 'tiny';
33
33
 
34
+ export type FontCategory = {
35
+ label: string;
36
+ fonts: string[];
37
+ };
38
+
34
39
  type FontFamilyControlProps = {
35
- fontFamilies: Record< string, string[] >;
40
+ fontFamilies: FontCategory[];
36
41
  };
37
42
 
38
43
  export const FontFamilyControl = createControl( ( { fontFamilies }: FontFamilyControlProps ) => {
@@ -278,7 +283,7 @@ const StyledMenuList = styled( MenuList )( ( { theme } ) => ( {
278
283
  '& > [role="option"]': {
279
284
  ...theme.typography.caption,
280
285
  lineHeight: 'inherit',
281
- padding: theme.spacing( 0.75, 2 ),
286
+ padding: theme.spacing( 0.75, 2, 0.75, 4 ),
282
287
  '&:hover, &:focus': {
283
288
  backgroundColor: theme.palette.action.hover,
284
289
  },
@@ -26,8 +26,10 @@ import {
26
26
  import { ControlLabel } from '../components/control-label';
27
27
  import ControlActions from '../control-actions/control-actions';
28
28
  import { createControl } from '../create-control';
29
+ import { getLinkRestriction } from '../utils/link-restriction';
30
+ import { type ControlProps } from '../utils/types';
29
31
 
30
- type Props = {
32
+ type Props = ControlProps< {
31
33
  queryOptions: {
32
34
  requestParams: Record< string, unknown >;
33
35
  endpoint: string;
@@ -35,7 +37,7 @@ type Props = {
35
37
  allowCustomValues?: boolean;
36
38
  minInputLength?: number;
37
39
  placeholder?: string;
38
- };
40
+ } >;
39
41
 
40
42
  type LinkSessionValue = {
41
43
  value?: LinkPropValue[ 'value' ] | null;
@@ -58,6 +60,7 @@ export const LinkControl = createControl( ( props: Props ) => {
58
60
  queryOptions: { endpoint = '', requestParams = {} },
59
61
  placeholder,
60
62
  minInputLength = 2,
63
+ context: { elementId },
61
64
  } = props || {};
62
65
 
63
66
  const [ options, setOptions ] = useState< FlatOption[] | CategorizedOption[] >(
@@ -65,6 +68,12 @@ export const LinkControl = createControl( ( props: Props ) => {
65
68
  );
66
69
 
67
70
  const onEnabledChange = () => {
71
+ const { shouldRestrict } = getLinkRestriction( elementId );
72
+
73
+ if ( shouldRestrict && ! isEnabled ) {
74
+ return;
75
+ }
76
+
68
77
  setIsEnabled( ( prevState ) => ! prevState );
69
78
  setValue( isEnabled ? null : linkSessionValue?.value ?? null );
70
79
  setLinkSessionValue( { value, meta: { isEnabled: ! isEnabled } } );
@@ -1,4 +1,5 @@
1
1
  import * as React from 'react';
2
+ import { useState } from 'react';
2
3
  import { imageSrcPropTypeUtil } from '@elementor/editor-props';
3
4
  import { UploadIcon } from '@elementor/icons';
4
5
  import { Button, Card, CardMedia, CardOverlay, CircularProgress, Stack, styled } from '@elementor/ui';
@@ -7,6 +8,7 @@ import { __ } from '@wordpress/i18n';
7
8
 
8
9
  import { useBoundProp } from '../bound-prop-context';
9
10
  import { ControlLabel } from '../components/control-label';
11
+ import { EnableUnfilteredModal } from '../components/enable-unfiltered-modal';
10
12
  import ControlActions from '../control-actions/control-actions';
11
13
  import { createControl } from '../create-control';
12
14
  import { useUnfilteredFilesUpload } from '../hooks/use-unfiltered-files-upload';
@@ -36,12 +38,16 @@ const StyledCardMediaContainer = styled( Stack )`
36
38
  background-color: rgba( 255, 255, 255, 0.37 );
37
39
  `;
38
40
 
41
+ const MODE_BROWSE: OpenOptions = { mode: 'browse' };
42
+ const MODE_UPLOAD: OpenOptions = { mode: 'upload' };
43
+
39
44
  export const SvgMediaControl = createControl( () => {
40
45
  const { value, setValue } = useBoundProp( imageSrcPropTypeUtil );
41
46
  const { id, url } = value ?? {};
42
47
  const { data: attachment, isFetching } = useWpMediaAttachment( id?.value || null );
43
48
  const src = attachment?.url ?? url?.value ?? null;
44
49
  const { data: allowSvgUpload } = useUnfilteredFilesUpload();
50
+ const [ unfilteredModalOpenState, setUnfilteredModalOpenState ] = useState( false );
45
51
 
46
52
  const { open } = useWpMediaFrame( {
47
53
  mediaTypes: [ 'svg' ],
@@ -58,16 +64,25 @@ export const SvgMediaControl = createControl( () => {
58
64
  },
59
65
  } );
60
66
 
67
+ const onCloseUnfilteredModal = ( enabled: boolean ) => {
68
+ setUnfilteredModalOpenState( false );
69
+
70
+ if ( enabled ) {
71
+ open( MODE_UPLOAD );
72
+ }
73
+ };
74
+
61
75
  const handleClick = ( openOptions?: OpenOptions ) => {
62
- if ( allowSvgUpload ) {
63
- open( openOptions );
76
+ if ( ! allowSvgUpload && openOptions === MODE_UPLOAD ) {
77
+ setUnfilteredModalOpenState( true );
64
78
  } else {
65
- // TODO open upload SVG confirmation modal
79
+ open( openOptions );
66
80
  }
67
81
  };
68
82
 
69
83
  return (
70
84
  <Stack gap={ 1 }>
85
+ <EnableUnfilteredModal open={ unfilteredModalOpenState } onClose={ onCloseUnfilteredModal } />
71
86
  <ControlLabel> { __( 'SVG', 'elementor' ) } </ControlLabel>
72
87
  <ControlActions>
73
88
  <StyledCard variant="outlined">
@@ -95,7 +110,7 @@ export const SvgMediaControl = createControl( () => {
95
110
  size="tiny"
96
111
  color="inherit"
97
112
  variant="outlined"
98
- onClick={ () => handleClick( { mode: 'browse' } ) }
113
+ onClick={ () => handleClick( MODE_BROWSE ) }
99
114
  >
100
115
  { __( 'Select SVG', 'elementor' ) }
101
116
  </Button>
@@ -104,7 +119,7 @@ export const SvgMediaControl = createControl( () => {
104
119
  variant="text"
105
120
  color="inherit"
106
121
  startIcon={ <UploadIcon /> }
107
- onClick={ () => handleClick( { mode: 'upload' } ) }
122
+ onClick={ () => handleClick( MODE_UPLOAD ) }
108
123
  >
109
124
  { __( 'Upload', 'elementor' ) }
110
125
  </Button>