@elementor/editor-controls 0.21.0 → 0.24.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.21.0",
4
+ "version": "0.24.0",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -41,9 +41,9 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@elementor/editor-current-user": "0.3.0",
44
- "@elementor/editor-elements": "0.7.0",
44
+ "@elementor/editor-elements": "0.8.0",
45
45
  "@elementor/editor-props": "0.11.1",
46
- "@elementor/editor-ui": "0.5.1",
46
+ "@elementor/editor-ui": "0.7.0",
47
47
  "@elementor/env": "0.3.5",
48
48
  "@elementor/http": "0.1.4",
49
49
  "@elementor/icons": "1.37.0",
@@ -48,6 +48,8 @@ type RepeaterProps< T > = {
48
48
  };
49
49
  };
50
50
 
51
+ const EMPTY_OPEN_ITEM = -1;
52
+
51
53
  export const Repeater = < T, >( {
52
54
  label,
53
55
  itemSettings,
@@ -56,7 +58,7 @@ export const Repeater = < T, >( {
56
58
  values: repeaterValues = [],
57
59
  setValues: setRepeaterValues,
58
60
  }: RepeaterProps< Item< T > > ) => {
59
- const [ openItem, setOpenItem ] = useState( -1 );
61
+ const [ openItem, setOpenItem ] = useState( EMPTY_OPEN_ITEM );
60
62
 
61
63
  const [ items, setItems ] = useSyncExternalState( {
62
64
  external: repeaterValues,
@@ -166,7 +168,6 @@ export const Repeater = < T, >( {
166
168
  return (
167
169
  <SortableItem id={ key } key={ `sortable-${ key }` }>
168
170
  <RepeaterItem
169
- bind={ String( index ) }
170
171
  disabled={ value?.disabled }
171
172
  label={ <itemSettings.Label value={ value } /> }
172
173
  startIcon={ <itemSettings.Icon value={ value } /> }
@@ -174,6 +175,7 @@ export const Repeater = < T, >( {
174
175
  duplicateItem={ () => duplicateRepeaterItem( index ) }
175
176
  toggleDisableItem={ () => toggleDisableRepeaterItem( index ) }
176
177
  openOnMount={ openOnAdd && openItem === key }
178
+ onOpen={ () => setOpenItem( EMPTY_OPEN_ITEM ) }
177
179
  >
178
180
  { ( props ) => (
179
181
  <itemSettings.Content { ...props } value={ value } bind={ String( index ) } />
@@ -190,7 +192,6 @@ export const Repeater = < T, >( {
190
192
 
191
193
  type RepeaterItemProps = {
192
194
  label: React.ReactNode;
193
- bind: string;
194
195
  disabled?: boolean;
195
196
  startIcon: UnstableTagProps[ 'startIcon' ];
196
197
  removeItem: () => void;
@@ -198,6 +199,7 @@ type RepeaterItemProps = {
198
199
  toggleDisableItem: () => void;
199
200
  children: ( { anchorEl }: { anchorEl: AnchorEl } ) => React.ReactNode;
200
201
  openOnMount: boolean;
202
+ onOpen: () => void;
201
203
  };
202
204
 
203
205
  const RepeaterItem = ( {
@@ -209,9 +211,10 @@ const RepeaterItem = ( {
209
211
  duplicateItem,
210
212
  toggleDisableItem,
211
213
  openOnMount,
214
+ onOpen,
212
215
  }: RepeaterItemProps ) => {
213
216
  const [ anchorEl, setAnchorEl ] = useState< AnchorEl >( null );
214
- const { popoverState, popoverProps, ref, setRef } = usePopover( openOnMount );
217
+ const { popoverState, popoverProps, ref, setRef } = usePopover( openOnMount, onOpen );
215
218
 
216
219
  const duplicateLabel = __( 'Duplicate', 'elementor' );
217
220
  const toggleLabel = disabled ? __( 'Show', 'elementor' ) : __( 'Hide', 'elementor' );
@@ -247,7 +250,6 @@ const RepeaterItem = ( {
247
250
  </Tooltip>
248
251
  </>
249
252
  }
250
- sx={ { backgroundColor: 'background.paper' } }
251
253
  />
252
254
  <Popover
253
255
  disablePortal
@@ -267,7 +269,7 @@ const RepeaterItem = ( {
267
269
  );
268
270
  };
269
271
 
270
- const usePopover = ( openOnMount: boolean ) => {
272
+ const usePopover = ( openOnMount: boolean, onOpen: () => void ) => {
271
273
  const [ ref, setRef ] = useState< HTMLElement | null >( null );
272
274
 
273
275
  const popoverState = usePopupState( { variant: 'popover' } );
@@ -277,6 +279,7 @@ const usePopover = ( openOnMount: boolean ) => {
277
279
  useEffect( () => {
278
280
  if ( openOnMount && ref ) {
279
281
  popoverState.open( ref );
282
+ onOpen?.();
280
283
  }
281
284
  // eslint-disable-next-line react-compiler/react-compiler
282
285
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -34,16 +34,11 @@ export const SortableItem = ( { id, children }: SortableItemProps ): React.React
34
34
  triggerProps,
35
35
  itemStyle,
36
36
  triggerStyle,
37
- isDragOverlay,
38
37
  showDropIndication,
39
38
  dropIndicationStyle,
40
39
  }: UnstableSortableItemRenderProps ) => {
41
40
  return (
42
- <StyledListItem
43
- { ...itemProps }
44
- style={ itemStyle }
45
- sx={ { backgroundColor: isDragOverlay ? 'background.paper' : undefined } }
46
- >
41
+ <StyledListItem { ...itemProps } style={ itemStyle }>
47
42
  <SortableTrigger { ...triggerProps } style={ triggerStyle } />
48
43
  { children }
49
44
  { showDropIndication && <StyledDivider style={ dropIndicationStyle } /> }
@@ -72,6 +67,11 @@ const StyledListItem = styled( ListItem )`
72
67
  transform: translate( -75%, -50% );
73
68
  }
74
69
 
70
+ &[aria-describedby=''] > .MuiTag-root {
71
+ background-color: ${ ( { theme } ) => theme.palette.background.paper };
72
+ box-shadow: ${ ( { theme } ) => theme.shadows[ 3 ] };
73
+ }
74
+
75
75
  &:hover {
76
76
  & .class-item-sortable-trigger {
77
77
  visibility: visible;
@@ -58,11 +58,11 @@ export const SelectionEndAdornment = < T extends string >( {
58
58
  <InputAdornment position="end">
59
59
  <Button
60
60
  size="small"
61
- color="inherit"
62
- sx={ { font: 'inherit', minWidth: 'initial' } }
61
+ color="secondary"
62
+ sx={ { font: 'inherit', minWidth: 'initial', textTransform: 'uppercase' } }
63
63
  { ...bindTrigger( popupState ) }
64
64
  >
65
- { value.toUpperCase() }
65
+ { value }
66
66
  </Button>
67
67
 
68
68
  <Menu MenuListProps={ { dense: true } } { ...bindMenu( popupState ) }>
@@ -271,7 +271,7 @@ const useImage = ( image: BackgroundImageOverlay ) => {
271
271
  const { data: attachment } = useWpMediaAttachment( imageSrc.id?.value || null );
272
272
 
273
273
  if ( imageSrc.id ) {
274
- const imageFileTypeExtension = attachment?.subtype ? `.${ attachment.subtype }` : '';
274
+ const imageFileTypeExtension = getFileExtensionFromFilename( attachment?.filename );
275
275
  imageTitle = `${ attachment?.title }${ imageFileTypeExtension }` || null;
276
276
  imageUrl = attachment?.url || null;
277
277
  } else if ( imageSrc.url ) {
@@ -282,6 +282,17 @@ const useImage = ( image: BackgroundImageOverlay ) => {
282
282
  return { imageTitle, imageUrl };
283
283
  };
284
284
 
285
+ const getFileExtensionFromFilename = ( filename?: string ) => {
286
+ if ( ! filename ) {
287
+ return '';
288
+ }
289
+
290
+ // get the substring after the last . in the filename
291
+ const extension = filename.substring( filename.lastIndexOf( '.' ) + 1 );
292
+
293
+ return `.${ extension }`;
294
+ };
295
+
285
296
  const getGradientValue = ( value: BackgroundOverlayItemPropValue ) => {
286
297
  const gradient = value.value;
287
298
 
@@ -280,6 +280,8 @@ const StyledMenuList = styled( MenuList )( ( { theme } ) => ( {
280
280
  top: 0,
281
281
  left: 0,
282
282
  width: '100%',
283
+ display: 'flex',
284
+ alignItems: 'center',
283
285
  },
284
286
  '& > [role="option"]': {
285
287
  ...theme.typography.caption,
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
- import { useMemo, useState } from 'react';
3
- import { getAncestorWithAnchorTag, getDescendantWithAnchorTag } from '@elementor/editor-elements';
2
+ import { type PropsWithChildren, useMemo, useState } from 'react';
3
+ import { getLinkInLinkRestriction, type LinkInLinkRestriction, selectElement } from '@elementor/editor-elements';
4
4
  import {
5
5
  booleanPropTypeUtil,
6
6
  linkPropTypeUtil,
@@ -9,10 +9,11 @@ import {
9
9
  stringPropTypeUtil,
10
10
  urlPropTypeUtil,
11
11
  } from '@elementor/editor-props';
12
+ import { InfoTipCard } from '@elementor/editor-ui';
12
13
  import { type HttpResponse, httpService } from '@elementor/http';
13
- import { MinusIcon, PlusIcon } from '@elementor/icons';
14
+ import { AlertTriangleIcon, MinusIcon, PlusIcon } from '@elementor/icons';
14
15
  import { useSessionStorage } from '@elementor/session';
15
- import { Collapse, Divider, Grid, IconButton, Stack, Switch } from '@elementor/ui';
16
+ import { Box, Collapse, Divider, Grid, IconButton, Infotip, Stack, Switch } from '@elementor/ui';
16
17
  import { debounce } from '@elementor/utils';
17
18
  import { __ } from '@wordpress/i18n';
18
19
 
@@ -49,11 +50,15 @@ type LinkSessionValue = {
49
50
  type Response = HttpResponse< { value: FlatOption[] | CategorizedOption[] } >;
50
51
 
51
52
  const SIZE = 'tiny';
53
+ const learnMoreButton = {
54
+ label: __( 'Learn More', 'elementor' ),
55
+ href: 'https://go.elementor.com/element-link-inside-link-infotip',
56
+ };
52
57
 
53
58
  export const LinkControl = createControl( ( props: Props ) => {
54
59
  const { value, path, setValue, ...propContext } = useBoundProp( linkPropTypeUtil );
55
60
  const [ linkSessionValue, setLinkSessionValue ] = useSessionStorage< LinkSessionValue >( path.join( '/' ) );
56
- const [ isEnabled, setIsEnabled ] = useState( !! value );
61
+ const [ isActive, setIsActive ] = useState( !! value );
57
62
 
58
63
  const {
59
64
  allowCustomValues,
@@ -63,20 +68,22 @@ export const LinkControl = createControl( ( props: Props ) => {
63
68
  context: { elementId },
64
69
  } = props || {};
65
70
 
71
+ const [ linkInLinkRestriction, setLinkInLinkRestriction ] = useState( getLinkInLinkRestriction( elementId ) );
66
72
  const [ options, setOptions ] = useState< FlatOption[] | CategorizedOption[] >(
67
73
  generateFirstLoadedOption( value )
68
74
  );
75
+ const shouldDisableAddingLink = ! isActive && linkInLinkRestriction.shouldRestrict;
69
76
 
70
77
  const onEnabledChange = () => {
71
- const shouldRestrict = getAncestorWithAnchorTag( elementId ) || getDescendantWithAnchorTag( elementId );
78
+ setLinkInLinkRestriction( getLinkInLinkRestriction( elementId ) );
72
79
 
73
- if ( shouldRestrict && ! isEnabled ) {
80
+ if ( linkInLinkRestriction.shouldRestrict && ! isActive ) {
74
81
  return;
75
82
  }
76
83
 
77
- setIsEnabled( ( prevState ) => ! prevState );
78
- setValue( isEnabled ? null : linkSessionValue?.value ?? null );
79
- setLinkSessionValue( { value, meta: { isEnabled: ! isEnabled } } );
84
+ setIsActive( ( prevState ) => ! prevState );
85
+ setValue( isActive ? null : linkSessionValue?.value ?? null );
86
+ setLinkSessionValue( { value, meta: { isEnabled: ! isActive } } );
80
87
  };
81
88
 
82
89
  const onOptionChange = ( newValue: number | null ) => {
@@ -145,13 +152,16 @@ export const LinkControl = createControl( ( props: Props ) => {
145
152
  } }
146
153
  >
147
154
  <ControlFormLabel>{ __( 'Link', 'elementor' ) }</ControlFormLabel>
148
- <ToggleIconControl
149
- enabled={ isEnabled }
150
- onIconClick={ onEnabledChange }
151
- label={ __( 'Toggle link', 'elementor' ) }
152
- />
155
+ <ConditionalInfoTip isVisible={ ! isActive } linkInLinkRestriction={ linkInLinkRestriction }>
156
+ <ToggleIconControl
157
+ disabled={ shouldDisableAddingLink }
158
+ active={ isActive }
159
+ onIconClick={ onEnabledChange }
160
+ label={ __( 'Toggle link', 'elementor' ) }
161
+ />
162
+ </ConditionalInfoTip>
153
163
  </Stack>
154
- <Collapse in={ isEnabled } timeout="auto" unmountOnExit>
164
+ <Collapse in={ isActive } timeout="auto" unmountOnExit>
155
165
  <Stack gap={ 1.5 }>
156
166
  <PropKeyProvider bind={ 'destination' }>
157
167
  <ControlActions>
@@ -167,7 +177,7 @@ export const LinkControl = createControl( ( props: Props ) => {
167
177
  </ControlActions>
168
178
  </PropKeyProvider>
169
179
  <PropKeyProvider bind={ 'isTargetBlank' }>
170
- <SwitchControl />
180
+ <SwitchControl disabled={ ! value } />
171
181
  </PropKeyProvider>
172
182
  </Stack>
173
183
  </Collapse>
@@ -177,34 +187,43 @@ export const LinkControl = createControl( ( props: Props ) => {
177
187
  } );
178
188
 
179
189
  type ToggleIconControlProps = {
180
- enabled: boolean;
190
+ disabled: boolean;
191
+ active: boolean;
181
192
  onIconClick: () => void;
182
193
  label?: string;
183
194
  };
184
195
 
185
- const ToggleIconControl = ( { enabled, onIconClick, label }: ToggleIconControlProps ) => {
196
+ const ToggleIconControl = ( { disabled, active, onIconClick, label }: ToggleIconControlProps ) => {
186
197
  return (
187
- <IconButton size={ SIZE } onClick={ onIconClick } aria-label={ label }>
188
- { enabled ? <MinusIcon fontSize={ SIZE } /> : <PlusIcon fontSize={ SIZE } /> }
198
+ <IconButton size={ SIZE } onClick={ onIconClick } aria-label={ label } disabled={ disabled }>
199
+ { active ? <MinusIcon fontSize={ SIZE } /> : <PlusIcon fontSize={ SIZE } /> }
189
200
  </IconButton>
190
201
  );
191
202
  };
192
203
 
193
204
  // @TODO Should be refactored in ED-16323
194
- const SwitchControl = () => {
205
+ const SwitchControl = ( { disabled }: { disabled: boolean } ) => {
195
206
  const { value = false, setValue } = useBoundProp( booleanPropTypeUtil );
196
207
 
197
208
  const onClick = () => {
198
209
  setValue( ! value );
199
210
  };
200
211
 
212
+ const inputProps = disabled
213
+ ? {
214
+ style: {
215
+ opacity: 0,
216
+ },
217
+ }
218
+ : {};
219
+
201
220
  return (
202
221
  <Grid container alignItems="center" flexWrap="nowrap" justifyContent="space-between">
203
222
  <Grid item>
204
223
  <ControlFormLabel>{ __( 'Open in a new tab', 'elementor' ) }</ControlFormLabel>
205
224
  </Grid>
206
225
  <Grid item>
207
- <Switch checked={ value } onClick={ onClick } />
226
+ <Switch checked={ value } onClick={ onClick } disabled={ disabled } inputProps={ inputProps } />
208
227
  </Grid>
209
228
  </Grid>
210
229
  );
@@ -248,3 +267,56 @@ function generateFirstLoadedOption( unionValue: LinkPropValue[ 'value' ] | null
248
267
  ]
249
268
  : [];
250
269
  }
270
+
271
+ interface ConditionalInfoTipType extends PropsWithChildren {
272
+ linkInLinkRestriction: LinkInLinkRestriction;
273
+ isVisible: boolean;
274
+ }
275
+
276
+ const ConditionalInfoTip: React.FC< ConditionalInfoTipType > = ( { linkInLinkRestriction, isVisible, children } ) => {
277
+ const { shouldRestrict, reason, elementId } = linkInLinkRestriction;
278
+
279
+ const handleTakeMeClick = () => {
280
+ if ( elementId ) {
281
+ selectElement( elementId );
282
+ }
283
+ };
284
+
285
+ return shouldRestrict && isVisible ? (
286
+ <Infotip
287
+ placement="right"
288
+ content={
289
+ <InfoTipCard
290
+ content={ INFOTIP_CONTENT[ reason ] }
291
+ svgIcon={ <AlertTriangleIcon /> }
292
+ learnMoreButton={ learnMoreButton }
293
+ ctaButton={ {
294
+ label: __( 'Take me there', 'elementor' ),
295
+ onClick: handleTakeMeClick,
296
+ } }
297
+ />
298
+ }
299
+ >
300
+ <Box>{ children }</Box>
301
+ </Infotip>
302
+ ) : (
303
+ <>{ children }</>
304
+ );
305
+ };
306
+
307
+ const INFOTIP_CONTENT = {
308
+ descendant: (
309
+ <>
310
+ { __( 'To add a link to this container,', 'elementor' ) }
311
+ <br />
312
+ { __( 'first remove the link from the elements inside of it.', 'elementor' ) }
313
+ </>
314
+ ),
315
+ ancestor: (
316
+ <>
317
+ { __( 'To add a link to this element,', 'elementor' ) }
318
+ <br />
319
+ { __( 'first remove the link from its parent container.', 'elementor' ) }
320
+ </>
321
+ ),
322
+ };