@elementor/editor-controls 0.14.0 → 0.15.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,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { dimensionsPropTypeUtil, type PropKey, sizePropTypeUtil } from '@elementor/editor-props';
3
3
  import { DetachIcon, LinkIcon, SideBottomIcon, SideLeftIcon, SideRightIcon, SideTopIcon } from '@elementor/icons';
4
- import { Grid, Stack, ToggleButton } from '@elementor/ui';
4
+ import { Grid, Stack, ToggleButton, Tooltip } from '@elementor/ui';
5
5
  import { __ } from '@wordpress/i18n';
6
6
 
7
7
  import { PropKeyProvider, PropProvider, useBoundProp } from '../bound-prop-context';
@@ -10,7 +10,15 @@ import { createControl } from '../create-control';
10
10
  import { type ExtendedValue, SizeControl } from './size-control';
11
11
 
12
12
  export const LinkedDimensionsControl = createControl(
13
- ( { label, extendedValues }: { label: string; extendedValues?: ExtendedValue[] } ) => {
13
+ ( {
14
+ label,
15
+ isSiteRtl = false,
16
+ extendedValues,
17
+ }: {
18
+ label: string;
19
+ isSiteRtl?: boolean;
20
+ extendedValues?: ExtendedValue[];
21
+ } ) => {
14
22
  const {
15
23
  value: dimensionsValue,
16
24
  setValue: setDimensionsValue,
@@ -22,36 +30,44 @@ export const LinkedDimensionsControl = createControl(
22
30
 
23
31
  const onLinkToggle = () => {
24
32
  if ( ! isLinked ) {
25
- setSizeValue( dimensionsValue?.top?.value );
33
+ setSizeValue( dimensionsValue[ 'block-start' ]?.value );
26
34
  return;
27
35
  }
28
36
 
29
37
  const value = sizeValue ? sizePropTypeUtil.create( sizeValue ) : null;
30
38
 
31
39
  setDimensionsValue( {
32
- top: value,
33
- right: value,
34
- bottom: value,
35
- left: value,
40
+ 'block-start': value,
41
+ 'block-end': value,
42
+ 'inline-start': value,
43
+ 'inline-end': value,
36
44
  } );
37
45
  };
38
46
 
47
+ const tooltipLabel = label.toLowerCase();
48
+
39
49
  const LinkedIcon = isLinked ? LinkIcon : DetachIcon;
50
+ // translators: %s: Tooltip title.
51
+ const linkedLabel = __( 'Link %s', 'elementor' ).replace( '%s', tooltipLabel );
52
+ // translators: %s: Tooltip title.
53
+ const unlinkedLabel = __( 'Unlink %s', 'elementor' ).replace( '%s', tooltipLabel );
40
54
 
41
55
  return (
42
56
  <PropProvider propType={ propType } value={ dimensionsValue } setValue={ setDimensionsValue }>
43
57
  <Stack direction="row" gap={ 2 } flexWrap="nowrap">
44
58
  <ControlLabel>{ label }</ControlLabel>
45
- <ToggleButton
46
- aria-label={ __( 'Link Inputs', 'elementor' ) }
47
- size={ 'tiny' }
48
- value={ 'check' }
49
- selected={ isLinked }
50
- sx={ { marginLeft: 'auto' } }
51
- onChange={ onLinkToggle }
52
- >
53
- <LinkedIcon fontSize={ 'tiny' } />
54
- </ToggleButton>
59
+ <Tooltip title={ isLinked ? unlinkedLabel : linkedLabel } placement="top">
60
+ <ToggleButton
61
+ aria-label={ isLinked ? unlinkedLabel : linkedLabel }
62
+ size={ 'tiny' }
63
+ value={ 'check' }
64
+ selected={ isLinked }
65
+ sx={ { marginLeft: 'auto' } }
66
+ onChange={ onLinkToggle }
67
+ >
68
+ <LinkedIcon fontSize={ 'tiny' } />
69
+ </ToggleButton>
70
+ </Tooltip>
55
71
  </Stack>
56
72
  <Stack direction="row" gap={ 2 } flexWrap="nowrap">
57
73
  <Grid container gap={ 1 } alignItems="center">
@@ -60,7 +76,7 @@ export const LinkedDimensionsControl = createControl(
60
76
  </Grid>
61
77
  <Grid item xs={ 12 }>
62
78
  <Control
63
- bind={ 'top' }
79
+ bind={ 'block-start' }
64
80
  startIcon={ <SideTopIcon fontSize={ 'tiny' } /> }
65
81
  isLinked={ isLinked }
66
82
  extendedValues={ extendedValues }
@@ -69,12 +85,20 @@ export const LinkedDimensionsControl = createControl(
69
85
  </Grid>
70
86
  <Grid container gap={ 1 } alignItems="center">
71
87
  <Grid item xs={ 12 }>
72
- <ControlLabel>{ __( 'Right', 'elementor' ) }</ControlLabel>
88
+ <ControlLabel>
89
+ { isSiteRtl ? __( 'Left', 'elementor' ) : __( 'Right', 'elementor' ) }
90
+ </ControlLabel>
73
91
  </Grid>
74
92
  <Grid item xs={ 12 }>
75
93
  <Control
76
- bind={ 'right' }
77
- startIcon={ <SideRightIcon fontSize={ 'tiny' } /> }
94
+ bind={ 'inline-end' }
95
+ startIcon={
96
+ isSiteRtl ? (
97
+ <SideLeftIcon fontSize={ 'tiny' } />
98
+ ) : (
99
+ <SideRightIcon fontSize={ 'tiny' } />
100
+ )
101
+ }
78
102
  isLinked={ isLinked }
79
103
  extendedValues={ extendedValues }
80
104
  />
@@ -88,7 +112,7 @@ export const LinkedDimensionsControl = createControl(
88
112
  </Grid>
89
113
  <Grid item xs={ 12 }>
90
114
  <Control
91
- bind={ 'bottom' }
115
+ bind={ 'block-end' }
92
116
  startIcon={ <SideBottomIcon fontSize={ 'tiny' } /> }
93
117
  isLinked={ isLinked }
94
118
  extendedValues={ extendedValues }
@@ -97,12 +121,20 @@ export const LinkedDimensionsControl = createControl(
97
121
  </Grid>
98
122
  <Grid container gap={ 1 } alignItems="center">
99
123
  <Grid item xs={ 12 }>
100
- <ControlLabel>{ __( 'Left', 'elementor' ) }</ControlLabel>
124
+ <ControlLabel>
125
+ { isSiteRtl ? __( 'Right', 'elementor' ) : __( 'Left', 'elementor' ) }
126
+ </ControlLabel>
101
127
  </Grid>
102
128
  <Grid item xs={ 12 }>
103
129
  <Control
104
- bind={ 'left' }
105
- startIcon={ <SideLeftIcon fontSize={ 'tiny' } /> }
130
+ bind={ 'inline-start' }
131
+ startIcon={
132
+ isSiteRtl ? (
133
+ <SideRightIcon fontSize={ 'tiny' } />
134
+ ) : (
135
+ <SideLeftIcon fontSize={ 'tiny' } />
136
+ )
137
+ }
106
138
  isLinked={ isLinked }
107
139
  extendedValues={ extendedValues }
108
140
  />
@@ -2,13 +2,14 @@ import * as React from 'react';
2
2
  import { imageSrcPropTypeUtil } from '@elementor/editor-props';
3
3
  import { UploadIcon } from '@elementor/icons';
4
4
  import { Button, Card, CardMedia, CardOverlay, CircularProgress, Stack, styled } from '@elementor/ui';
5
- import { useWpMediaAttachment, useWpMediaFrame } from '@elementor/wp-media';
5
+ import { type OpenOptions, useWpMediaAttachment, useWpMediaFrame } from '@elementor/wp-media';
6
6
  import { __ } from '@wordpress/i18n';
7
7
 
8
8
  import { useBoundProp } from '../bound-prop-context';
9
9
  import { ControlLabel } from '../components/control-label';
10
10
  import ControlActions from '../control-actions/control-actions';
11
11
  import { createControl } from '../create-control';
12
+ import { useUnfilteredFilesUpload } from '../hooks/use-unfiltered-files-upload';
12
13
 
13
14
  const TILE_SIZE = 8;
14
15
  const TILE_WHITE = 'transparent';
@@ -40,9 +41,10 @@ export const SvgMediaControl = createControl( () => {
40
41
  const { id, url } = value ?? {};
41
42
  const { data: attachment, isFetching } = useWpMediaAttachment( id?.value || null );
42
43
  const src = attachment?.url ?? url?.value ?? null;
44
+ const { data: allowSvgUpload } = useUnfilteredFilesUpload();
45
+
43
46
  const { open } = useWpMediaFrame( {
44
- types: [ 'image/svg+xml' ],
45
- allowedExtensions: [ 'svg' ],
47
+ mediaTypes: [ 'svg' ],
46
48
  multiple: false,
47
49
  selected: id?.value || null,
48
50
  onSelect: ( selectedAttachment ) => {
@@ -56,6 +58,14 @@ export const SvgMediaControl = createControl( () => {
56
58
  },
57
59
  } );
58
60
 
61
+ const handleClick = ( openOptions?: OpenOptions ) => {
62
+ if ( allowSvgUpload ) {
63
+ open( openOptions );
64
+ } else {
65
+ // TODO open upload SVG confirmation modal
66
+ }
67
+ };
68
+
59
69
  return (
60
70
  <Stack gap={ 1 }>
61
71
  <ControlLabel> { __( 'SVG', 'elementor' ) } </ControlLabel>
@@ -85,7 +95,7 @@ export const SvgMediaControl = createControl( () => {
85
95
  size="tiny"
86
96
  color="inherit"
87
97
  variant="outlined"
88
- onClick={ () => open( { mode: 'browse' } ) }
98
+ onClick={ () => handleClick( { mode: 'browse' } ) }
89
99
  >
90
100
  { __( 'Select SVG', 'elementor' ) }
91
101
  </Button>
@@ -94,7 +104,7 @@ export const SvgMediaControl = createControl( () => {
94
104
  variant="text"
95
105
  color="inherit"
96
106
  startIcon={ <UploadIcon /> }
97
- onClick={ () => open( { mode: 'upload' } ) }
107
+ onClick={ () => handleClick( { mode: 'upload' } ) }
98
108
  >
99
109
  { __( 'Upload', 'elementor' ) }
100
110
  </Button>
@@ -1,37 +1,24 @@
1
- import { __ } from '@wordpress/i18n';
2
-
3
- export type SupportedFonts = 'system' | 'googlefonts' | 'customfonts';
4
-
5
- const supportedCategories: Record< SupportedFonts, string > = {
6
- system: __( 'System', 'elementor' ),
7
- googlefonts: __( 'Google Fonts', 'elementor' ),
8
- customfonts: __( 'Custom Fonts', 'elementor' ),
1
+ export type FontListItem = {
2
+ type: 'font' | 'category';
3
+ value: string;
9
4
  };
10
5
 
11
- export const useFilteredFontFamilies = ( fontFamilies: Record< string, SupportedFonts >, searchValue: string ) => {
12
- const filteredFontFamilies = Object.entries( fontFamilies ).reduce< Map< string, string[] > >(
13
- ( acc, [ font, category ] ) => {
14
- const isMatch = font.toLowerCase().includes( searchValue.trim().toLowerCase() );
15
-
16
- if ( ! isMatch ) {
17
- return acc;
18
- }
19
-
20
- const categoryLabel = supportedCategories[ category as SupportedFonts ];
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() ) );
21
10
 
22
- if ( categoryLabel ) {
23
- const existingCategory = acc.get( categoryLabel );
11
+ if ( filteredFonts.length ) {
12
+ acc.push( { type: 'category', value: category } );
24
13
 
25
- if ( existingCategory ) {
26
- existingCategory.push( font );
27
- } else {
28
- acc.set( categoryLabel, [ font ] );
29
- }
14
+ filteredFonts.forEach( ( font ) => {
15
+ acc.push( { type: 'font', value: font } );
16
+ } );
30
17
  }
31
18
 
32
19
  return acc;
33
20
  },
34
- new Map()
21
+ []
35
22
  );
36
23
 
37
24
  return [ ...filteredFontFamilies ];
@@ -0,0 +1,40 @@
1
+ import { useMutation, useQuery, useQueryClient } from '@elementor/query';
2
+
3
+ import { apiClient } from '../api';
4
+
5
+ export const UNFILTERED_FILES_UPLOAD_KEY = 'elementor_unfiltered_files_upload';
6
+
7
+ const unfilteredFilesQueryKey = {
8
+ queryKey: [ UNFILTERED_FILES_UPLOAD_KEY ],
9
+ };
10
+
11
+ type Value = '0' | '1';
12
+
13
+ export const useUnfilteredFilesUpload = () =>
14
+ useQuery( {
15
+ ...unfilteredFilesQueryKey,
16
+ queryFn: (): Promise< boolean > =>
17
+ apiClient.getElementorSetting< Value >( UNFILTERED_FILES_UPLOAD_KEY ).then( ( res ) => {
18
+ return formatResponse( res );
19
+ } ),
20
+ staleTime: Infinity,
21
+ } );
22
+
23
+ export function useUpdateUnfilteredFilesUpload() {
24
+ const queryClient = useQueryClient();
25
+
26
+ const mutate = useMutation( {
27
+ mutationFn: ( { allowUnfilteredFilesUpload }: { allowUnfilteredFilesUpload: boolean } ) =>
28
+ apiClient.updateElementorSetting< Value >(
29
+ UNFILTERED_FILES_UPLOAD_KEY,
30
+ allowUnfilteredFilesUpload ? '1' : '0'
31
+ ),
32
+ onSuccess: () => queryClient.invalidateQueries( unfilteredFilesQueryKey ),
33
+ } );
34
+
35
+ return mutate;
36
+ }
37
+
38
+ const formatResponse = ( response: Value ): boolean => {
39
+ return Boolean( response === '1' );
40
+ };
package/src/index.ts CHANGED
@@ -11,7 +11,7 @@ export { ToggleControl } from './controls/toggle-control';
11
11
  export { NumberControl } from './controls/number-control';
12
12
  export { EqualUnequalSizesControl } from './controls/equal-unequal-sizes-control';
13
13
  export { LinkedDimensionsControl } from './controls/linked-dimensions-control';
14
- export { FontFamilyControl } from './controls/font-family-control';
14
+ export { FontFamilyControl } from './controls/font-family-control/font-family-control';
15
15
  export { UrlControl } from './controls/url-control';
16
16
  export { LinkControl } from './controls/link-control';
17
17
  export { GapControl } from './controls/gap-control';
@@ -1,157 +0,0 @@
1
- import { Fragment, useId, useState } from 'react';
2
- import * as React from 'react';
3
- import { stringPropTypeUtil } from '@elementor/editor-props';
4
- import { ChevronDownIcon, EditIcon, PhotoIcon, SearchIcon, XIcon } from '@elementor/icons';
5
- import {
6
- bindPopover,
7
- bindTrigger,
8
- Box,
9
- Divider,
10
- IconButton,
11
- InputAdornment,
12
- Link,
13
- ListSubheader,
14
- MenuItem,
15
- MenuList,
16
- Popover,
17
- Stack,
18
- TextField,
19
- Typography,
20
- UnstableTag,
21
- usePopupState,
22
- } from '@elementor/ui';
23
- import { __ } from '@wordpress/i18n';
24
-
25
- import { useBoundProp } from '../bound-prop-context';
26
- import { createControl } from '../create-control';
27
- import { useFilteredFontFamilies } from '../hooks/use-filtered-font-families';
28
-
29
- const SIZE = 'tiny';
30
-
31
- export const FontFamilyControl = createControl( ( { fontFamilies } ) => {
32
- const [ searchValue, setSearchValue ] = useState( '' );
33
- const { value: fontFamily, setValue: setFontFamily } = useBoundProp( stringPropTypeUtil );
34
-
35
- const popupId = useId();
36
- const popoverState = usePopupState( { variant: 'popover', popupId } );
37
-
38
- const filteredFontFamilies = useFilteredFontFamilies( fontFamilies, searchValue );
39
-
40
- if ( ! filteredFontFamilies ) {
41
- return null;
42
- }
43
-
44
- const handleSearch = ( event: React.ChangeEvent< HTMLInputElement > ) => {
45
- setSearchValue( event.target.value );
46
- };
47
-
48
- const handleClose = () => {
49
- setSearchValue( '' );
50
-
51
- popoverState.close();
52
- };
53
-
54
- return (
55
- <>
56
- <UnstableTag
57
- variant="outlined"
58
- label={ fontFamily }
59
- endIcon={ <ChevronDownIcon fontSize="tiny" /> }
60
- { ...bindTrigger( popoverState ) }
61
- fullWidth
62
- />
63
-
64
- <Popover
65
- disablePortal
66
- disableScrollLock
67
- anchorOrigin={ { vertical: 'bottom', horizontal: 'left' } }
68
- { ...bindPopover( popoverState ) }
69
- onClose={ handleClose }
70
- >
71
- <Stack>
72
- <Stack direction="row" alignItems="center" pl={ 1.5 } pr={ 0.5 } py={ 1.5 }>
73
- <EditIcon fontSize={ SIZE } sx={ { mr: 0.5 } } />
74
- <Typography variant="subtitle2">{ __( 'Font family', 'elementor' ) }</Typography>
75
- <IconButton size={ SIZE } sx={ { ml: 'auto' } } onClick={ handleClose }>
76
- <XIcon fontSize={ SIZE } />
77
- </IconButton>
78
- </Stack>
79
-
80
- <Box px={ 1.5 } pb={ 1 }>
81
- <TextField
82
- fullWidth
83
- size={ SIZE }
84
- value={ searchValue }
85
- placeholder={ __( 'Search fonts…', 'elementor' ) }
86
- onChange={ handleSearch }
87
- InputProps={ {
88
- startAdornment: (
89
- <InputAdornment position="start">
90
- <SearchIcon fontSize={ SIZE } />
91
- </InputAdornment>
92
- ),
93
- } }
94
- />
95
- </Box>
96
- <Divider />
97
- <Box sx={ { overflowY: 'auto', height: 260, width: 220 } }>
98
- { filteredFontFamilies.length > 0 ? (
99
- <MenuList role="listbox" tabIndex={ 0 }>
100
- { filteredFontFamilies.map( ( [ category, items ], index ) => (
101
- <Fragment key={ index }>
102
- <ListSubheader
103
- sx={ { px: 1.5, typography: 'caption', color: 'text.tertiary' } }
104
- >
105
- { category }
106
- </ListSubheader>
107
- { items.map( ( item ) => {
108
- const isSelected = item === fontFamily;
109
-
110
- return (
111
- <MenuItem
112
- key={ item }
113
- selected={ isSelected }
114
- // eslint-disable-next-line jsx-a11y/no-autofocus
115
- autoFocus={ isSelected }
116
- onClick={ () => {
117
- setFontFamily( item );
118
- handleClose();
119
- } }
120
- sx={ { px: 1.5, typography: 'caption' } }
121
- style={ { fontFamily: item } }
122
- >
123
- { item }
124
- </MenuItem>
125
- );
126
- } ) }
127
- </Fragment>
128
- ) ) }
129
- </MenuList>
130
- ) : (
131
- <Stack alignItems="center" p={ 2.5 } gap={ 1.5 }>
132
- <PhotoIcon fontSize="large" />
133
- <Typography align="center" variant="caption" color="text.secondary">
134
- { __( 'Sorry, nothing matched', 'elementor' ) }
135
- <br />
136
- &ldquo;{ searchValue }&rdquo;.
137
- </Typography>
138
- <Typography align="center" variant="caption" color="text.secondary">
139
- <Link
140
- color="secondary"
141
- variant="caption"
142
- component="button"
143
- onClick={ () => setSearchValue( '' ) }
144
- >
145
- { __( 'Clear the filters', 'elementor' ) }
146
- </Link>
147
- &nbsp;
148
- { __( 'and try again.', 'elementor' ) }
149
- </Typography>
150
- </Stack>
151
- ) }
152
- </Box>
153
- </Stack>
154
- </Popover>
155
- </>
156
- );
157
- } );