@elementor/editor-controls 0.15.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.
@@ -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>
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { useEffect, useRef, useState } from 'react';
3
3
  import { stringPropTypeUtil } from '@elementor/editor-props';
4
- import { ChevronDownIcon, EditIcon, PhotoIcon, SearchIcon, XIcon } from '@elementor/icons';
4
+ import { ChevronDownIcon, SearchIcon, TextIcon, XIcon } from '@elementor/icons';
5
5
  import {
6
6
  bindPopover,
7
7
  bindTrigger,
@@ -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 ) => {
@@ -72,7 +77,7 @@ export const FontFamilyControl = createControl( ( { fontFamilies }: FontFamilyCo
72
77
  >
73
78
  <Stack>
74
79
  <Stack direction="row" alignItems="center" pl={ 1.5 } pr={ 0.5 } py={ 1.5 }>
75
- <EditIcon fontSize={ SIZE } sx={ { mr: 0.5 } } />
80
+ <TextIcon fontSize={ SIZE } sx={ { mr: 0.5 } } />
76
81
  <Typography variant="subtitle2">{ __( 'Font Family', 'elementor' ) }</Typography>
77
82
  <IconButton size={ SIZE } sx={ { ml: 'auto' } } onClick={ handleClose }>
78
83
  <XIcon fontSize={ SIZE } />
@@ -105,24 +110,40 @@ export const FontFamilyControl = createControl( ( { fontFamilies }: FontFamilyCo
105
110
  />
106
111
  ) : (
107
112
  <Box sx={ { overflowY: 'auto', height: 260, width: 220 } }>
108
- <Stack alignItems="center" p={ 2.5 } gap={ 1.5 }>
109
- <PhotoIcon fontSize="large" />
110
- <Typography align="center" variant="caption" color="text.secondary">
111
- { __( 'Sorry, nothing matched', 'elementor' ) }
112
- <br />
113
- &ldquo;{ searchValue }&rdquo;.
114
- </Typography>
113
+ <Stack alignItems="center" p={ 2.5 } gap={ 1.5 } overflow={ 'hidden' }>
114
+ <TextIcon fontSize="large" />
115
+ <Box sx={ { maxWidth: 160, overflow: 'hidden' } }>
116
+ <Typography align="center" variant="subtitle2" color="text.secondary">
117
+ { __( 'Sorry, nothing matched', 'elementor' ) }
118
+ </Typography>
119
+ <Typography
120
+ variant="subtitle2"
121
+ color="text.secondary"
122
+ sx={ {
123
+ display: 'flex',
124
+ width: '100%',
125
+ justifyContent: 'center',
126
+ } }
127
+ >
128
+ <span>&ldquo;</span>
129
+ <span
130
+ style={ { maxWidth: '80%', overflow: 'hidden', textOverflow: 'ellipsis' } }
131
+ >
132
+ { searchValue }
133
+ </span>
134
+ <span>&rdquo;.</span>
135
+ </Typography>
136
+ </Box>
115
137
  <Typography align="center" variant="caption" color="text.secondary">
138
+ { __( 'Try something else.', 'elementor' ) }
116
139
  <Link
117
140
  color="secondary"
118
141
  variant="caption"
119
142
  component="button"
120
143
  onClick={ () => setSearchValue( '' ) }
121
144
  >
122
- { __( 'Clear the filters', 'elementor' ) }
145
+ { __( 'Clear & try again', 'elementor' ) }
123
146
  </Link>
124
- &nbsp;
125
- { __( 'and try again.', 'elementor' ) }
126
147
  </Typography>
127
148
  </Stack>
128
149
  </Box>
@@ -262,7 +283,7 @@ const StyledMenuList = styled( MenuList )( ( { theme } ) => ( {
262
283
  '& > [role="option"]': {
263
284
  ...theme.typography.caption,
264
285
  lineHeight: 'inherit',
265
- padding: theme.spacing( 0.75, 2 ),
286
+ padding: theme.spacing( 0.75, 2, 0.75, 4 ),
266
287
  '&:hover, &:focus': {
267
288
  backgroundColor: theme.palette.action.hover,
268
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>
@@ -6,28 +6,54 @@ import { useBoundProp } from '../bound-prop-context';
6
6
  import { ControlToggleButtonGroup, type ToggleButtonGroupItem } from '../components/control-toggle-button-group';
7
7
  import { createControl } from '../create-control';
8
8
 
9
- type ToggleControlProps< T extends PropValue > = {
10
- options: ToggleButtonGroupItem< T >[];
9
+ export type ToggleControlProps< T extends PropValue > = {
10
+ options: Array< ToggleButtonGroupItem< T > & { exclusive?: boolean } >;
11
11
  fullWidth?: boolean;
12
12
  size?: ToggleButtonProps[ 'size' ];
13
+ exclusive?: boolean;
13
14
  };
14
15
 
15
16
  export const ToggleControl = createControl(
16
- ( { options, fullWidth = false, size = 'tiny' }: ToggleControlProps< StringPropValue[ 'value' ] > ) => {
17
+ ( {
18
+ options,
19
+ fullWidth = false,
20
+ size = 'tiny',
21
+ exclusive = true,
22
+ }: ToggleControlProps< StringPropValue[ 'value' ] > ) => {
17
23
  const { value, setValue } = useBoundProp( stringPropTypeUtil );
18
24
 
19
- const handleToggle = ( option: StringPropValue[ 'value' ] | null ) => {
20
- setValue( option );
25
+ const exclusiveValues = options.filter( ( option ) => option.exclusive ).map( ( option ) => option.value );
26
+
27
+ const handleNonExclusiveToggle = ( selectedValues: StringPropValue[ 'value' ][] ) => {
28
+ const newSelectedValue = selectedValues[ selectedValues.length - 1 ];
29
+ const isNewSelectedValueExclusive = exclusiveValues.includes( newSelectedValue );
30
+
31
+ const updatedValues = isNewSelectedValueExclusive
32
+ ? [ newSelectedValue ]
33
+ : selectedValues?.filter( ( val ) => ! exclusiveValues.includes( val ) );
34
+
35
+ setValue( updatedValues?.join( ' ' ) || null );
21
36
  };
22
37
 
23
- return (
38
+ const toggleButtonGroupProps = {
39
+ items: options,
40
+ fullWidth,
41
+ size,
42
+ };
43
+
44
+ return exclusive ? (
24
45
  <ControlToggleButtonGroup
25
- items={ options }
46
+ { ...toggleButtonGroupProps }
26
47
  value={ value ?? null }
27
- onChange={ handleToggle }
48
+ onChange={ setValue }
28
49
  exclusive={ true }
29
- fullWidth={ fullWidth }
30
- size={ size }
50
+ />
51
+ ) : (
52
+ <ControlToggleButtonGroup
53
+ { ...toggleButtonGroupProps }
54
+ value={ value?.split( ' ' ) ?? [] }
55
+ onChange={ handleNonExclusiveToggle }
56
+ exclusive={ false }
31
57
  />
32
58
  );
33
59
  }
@@ -1,25 +1,24 @@
1
+ import { type FontCategory } from '@elementor/editor-controls';
2
+
1
3
  export type FontListItem = {
2
4
  type: 'font' | 'category';
3
5
  value: string;
4
6
  };
5
7
 
6
- export const useFilteredFontFamilies = ( fontFamilies: Record< string, string[] >, searchValue: string ) => {
7
- const filteredFontFamilies = Object.entries( fontFamilies ).reduce< FontListItem[] >(
8
- ( acc, [ category, fonts ] ) => {
9
- const filteredFonts = fonts.filter( ( font ) => font.toLowerCase().includes( searchValue.toLowerCase() ) );
10
-
11
- if ( filteredFonts.length ) {
12
- acc.push( { type: 'category', value: category } );
8
+ export const useFilteredFontFamilies = ( fontFamilies: FontCategory[], searchValue: string ) => {
9
+ return fontFamilies.reduce< FontListItem[] >( ( acc, category ) => {
10
+ const filteredFonts = category.fonts.filter( ( font ) =>
11
+ font.toLowerCase().includes( searchValue.toLowerCase() )
12
+ );
13
13
 
14
- filteredFonts.forEach( ( font ) => {
15
- acc.push( { type: 'font', value: font } );
16
- } );
17
- }
14
+ if ( filteredFonts.length ) {
15
+ acc.push( { type: 'category', value: category.label } );
18
16
 
19
- return acc;
20
- },
21
- []
22
- );
17
+ filteredFonts.forEach( ( font ) => {
18
+ acc.push( { type: 'font', value: font } );
19
+ } );
20
+ }
23
21
 
24
- return [ ...filteredFontFamilies ];
22
+ return acc;
23
+ }, [] );
25
24
  };
package/src/index.ts CHANGED
@@ -30,6 +30,8 @@ export type { ControlActionsItems } from './control-actions/control-actions-cont
30
30
  export type { PropProviderProps } from './bound-prop-context';
31
31
  export type { SetValue } from './bound-prop-context/prop-context';
32
32
  export type { ExtendedValue } from './controls/size-control';
33
+ export type { ToggleControlProps } from './controls/toggle-control';
34
+ export type { FontCategory } from './controls/font-family-control/font-family-control';
33
35
 
34
36
  // providers
35
37
  export { createControlReplacement, ControlReplacementProvider } from './create-control-replacement';
@@ -0,0 +1,47 @@
1
+ import { getContainer } from '@elementor/editor-elements';
2
+
3
+ type LinkRestriction =
4
+ | {
5
+ shouldRestrict: true;
6
+ restrictReason: 'ancestor' | 'descendant';
7
+ }
8
+ | {
9
+ shouldRestrict: false;
10
+ restrictReason?: never;
11
+ };
12
+
13
+ export function getLinkRestriction( elementId: string ): LinkRestriction {
14
+ if ( getAncestorAnchor( elementId ) ) {
15
+ return {
16
+ shouldRestrict: true,
17
+ restrictReason: 'ancestor',
18
+ };
19
+ }
20
+
21
+ if ( getDescendantAnchor( elementId ) ) {
22
+ return {
23
+ shouldRestrict: true,
24
+ restrictReason: 'descendant',
25
+ };
26
+ }
27
+
28
+ return { shouldRestrict: false };
29
+ }
30
+
31
+ function getAncestorAnchor( elementId: string ) {
32
+ const element = getElementView( elementId );
33
+
34
+ return element?.closest( 'a' ) || null;
35
+ }
36
+
37
+ function getDescendantAnchor( elementId: string ) {
38
+ const element = getElementView( elementId );
39
+
40
+ return element?.querySelector( 'a' ) || null;
41
+ }
42
+
43
+ function getElementView( id: string ) {
44
+ const elementContainer = getContainer( id );
45
+
46
+ return elementContainer?.view?.el || null;
47
+ }
@@ -0,0 +1,5 @@
1
+ export type ControlProps< TControlProps = unknown > = TControlProps & {
2
+ context: {
3
+ elementId: string;
4
+ };
5
+ };