@elementor/editor-global-classes 0.21.0 → 0.22.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.
@@ -0,0 +1,238 @@
1
+ import * as React from 'react';
2
+ import { useRef } from 'react';
3
+ import { EXPERIMENTAL_FEATURES } from '@elementor/editor-editing-panel';
4
+ import { validateStyleLabel } from '@elementor/editor-styles-repository';
5
+ import { EditableField, EllipsisWithTooltip, MenuListItem, useEditable, WarningInfotip } from '@elementor/editor-ui';
6
+ import { isExperimentActive } from '@elementor/editor-v1-adapters';
7
+ import { DotsVerticalIcon } from '@elementor/icons';
8
+ import {
9
+ bindMenu,
10
+ bindTrigger,
11
+ Box,
12
+ IconButton,
13
+ ListItemButton,
14
+ type ListItemButtonProps,
15
+ Menu,
16
+ Stack,
17
+ styled,
18
+ type Theme,
19
+ Tooltip,
20
+ Typography,
21
+ usePopupState,
22
+ } from '@elementor/ui';
23
+ import { __ } from '@wordpress/i18n';
24
+
25
+ import { useDeleteConfirmation } from './delete-confirmation-dialog';
26
+ import { SortableTrigger, type SortableTriggerProps } from './sortable';
27
+
28
+ const isVersion311IsActive = isExperimentActive( EXPERIMENTAL_FEATURES.V_3_31 );
29
+
30
+ type ClassItemProps = React.PropsWithChildren< {
31
+ id: string;
32
+ label: string;
33
+ renameClass: ( newLabel: string ) => void;
34
+ selected?: boolean;
35
+ disabled?: boolean;
36
+ sortableTriggerProps: SortableTriggerProps;
37
+ isSearchActive: boolean;
38
+ } >;
39
+
40
+ export const ClassItem = ( {
41
+ id,
42
+ label,
43
+ renameClass,
44
+ selected,
45
+ disabled,
46
+ sortableTriggerProps,
47
+ isSearchActive,
48
+ }: ClassItemProps ) => {
49
+ const itemRef = useRef< HTMLElement >( null );
50
+
51
+ const {
52
+ ref: editableRef,
53
+ openEditMode,
54
+ isEditing,
55
+ error,
56
+ getProps: getEditableProps,
57
+ } = useEditable( {
58
+ value: label,
59
+ onSubmit: renameClass,
60
+ validation: validateLabel,
61
+ } );
62
+
63
+ const { openDialog } = useDeleteConfirmation();
64
+
65
+ const popupState = usePopupState( {
66
+ variant: 'popover',
67
+ disableAutoFocus: true,
68
+ } );
69
+
70
+ const isSelected = ( selected || popupState.isOpen ) && ! disabled;
71
+ return (
72
+ <>
73
+ <Stack p={ 0 }>
74
+ <WarningInfotip
75
+ open={ Boolean( error ) }
76
+ text={ error ?? '' }
77
+ placement="bottom"
78
+ width={ itemRef.current?.getBoundingClientRect().width }
79
+ offset={ [ 0, -15 ] }
80
+ >
81
+ <StyledListItemButton
82
+ ref={ itemRef }
83
+ dense
84
+ disableGutters
85
+ showSortIndicator={ isSearchActive }
86
+ showActions={ isSelected || isEditing }
87
+ shape="rounded"
88
+ onDoubleClick={ openEditMode }
89
+ selected={ isSelected }
90
+ disabled={ disabled }
91
+ focusVisibleClassName="visible-class-item"
92
+ >
93
+ <SortableTrigger { ...sortableTriggerProps } />
94
+ <Indicator isActive={ isEditing } isError={ !! error }>
95
+ { isEditing ? (
96
+ <EditableField
97
+ ref={ editableRef }
98
+ as={ Typography }
99
+ variant="caption"
100
+ { ...getEditableProps() }
101
+ />
102
+ ) : (
103
+ <EllipsisWithTooltip title={ label } as={ Typography } variant="caption" />
104
+ ) }
105
+ </Indicator>
106
+ <Tooltip
107
+ placement="top"
108
+ className={ 'class-item-more-actions' }
109
+ title={ __( 'More actions', 'elementor' ) }
110
+ >
111
+ <IconButton size="tiny" { ...bindTrigger( popupState ) } aria-label="More actions">
112
+ <DotsVerticalIcon fontSize="tiny" />
113
+ </IconButton>
114
+ </Tooltip>
115
+ </StyledListItemButton>
116
+ </WarningInfotip>
117
+ </Stack>
118
+ <Menu
119
+ { ...bindMenu( popupState ) }
120
+ anchorOrigin={ {
121
+ vertical: 'bottom',
122
+ horizontal: 'right',
123
+ } }
124
+ transformOrigin={ {
125
+ vertical: 'top',
126
+ horizontal: 'right',
127
+ } }
128
+ >
129
+ <MenuListItem
130
+ sx={ { minWidth: '160px' } }
131
+ onClick={ () => {
132
+ popupState.close();
133
+ openEditMode();
134
+ } }
135
+ >
136
+ <Typography variant="caption" sx={ { color: 'text.primary' } }>
137
+ { __( 'Rename', 'elementor' ) }
138
+ </Typography>
139
+ </MenuListItem>
140
+ <MenuListItem
141
+ onClick={ () => {
142
+ popupState.close();
143
+ openDialog( { id, label } );
144
+ } }
145
+ >
146
+ <Typography variant="caption" sx={ { color: 'error.light' } }>
147
+ { __( 'Delete', 'elementor' ) }
148
+ </Typography>
149
+ </MenuListItem>
150
+ </Menu>
151
+ </>
152
+ );
153
+ };
154
+
155
+ // Custom styles for sortable list item, until the component is available in the UI package.
156
+
157
+ // Experimental start
158
+
159
+ const StyledListItemButtonV2 = styled( ListItemButton, {
160
+ shouldForwardProp: ( prop: string ) => ! [ 'showActions', 'showSortIndicator' ].includes( prop ),
161
+ } )< ListItemButtonProps & { showActions: boolean; showSortIndicator: boolean } >(
162
+ ( { showActions, showSortIndicator } ) =>
163
+ `
164
+ min-height: 36px;
165
+
166
+ &.visible-class-item {
167
+ box-shadow: none !important;
168
+ }
169
+ .class-item-sortable-trigger {
170
+ visibility: ${ showSortIndicator && showActions ? 'visible' : 'hidden' };
171
+ }
172
+ &:hover&:not(:disabled) {
173
+ .class-item-sortable-trigger {
174
+ visibility: ${ showSortIndicator ? 'visible' : 'hidden' };
175
+ }
176
+ }
177
+ `
178
+ );
179
+
180
+ const StyledListItemButtonV1 = styled( ListItemButton, {
181
+ shouldForwardProp: ( prop: string ) => ! [ 'showActions', 'showSortIndicator' ].includes( prop ),
182
+ } )< ListItemButtonProps & { showActions: boolean; showSortIndicator: boolean } >(
183
+ ( { showActions } ) => `
184
+ min-height: 36px;
185
+ &.visible-class-item {
186
+ box-shadow: none !important;
187
+ }
188
+ .class-item-more-actions, .class-item-sortable-trigger {
189
+ visibility: ${ showActions ? 'visible' : 'hidden' };
190
+ }
191
+ .class-item-sortable-trigger {
192
+ visibility: ${ showActions ? 'visible' : 'hidden' };
193
+ }
194
+ &:hover&:not(:disabled) {
195
+ .class-item-more-actions, .class-item-sortable-trigger {
196
+ visibility: visible;
197
+ }
198
+ }
199
+ `
200
+ );
201
+ // Experimental start
202
+
203
+ const StyledListItemButton = isVersion311IsActive ? StyledListItemButtonV2 : StyledListItemButtonV1;
204
+
205
+ const Indicator = styled( Box, {
206
+ shouldForwardProp: ( prop: string ) => ! [ 'isActive', 'isError' ].includes( prop ),
207
+ } )< { isActive: boolean; isError: boolean } >( ( { theme, isActive, isError } ) => ( {
208
+ display: 'flex',
209
+ width: '100%',
210
+ flexGrow: 1,
211
+ borderRadius: theme.spacing( 0.5 ),
212
+ border: getIndicatorBorder( { isActive, isError, theme } ),
213
+ padding: `0 ${ theme.spacing( 1 ) }`,
214
+ marginLeft: isActive ? theme.spacing( 1 ) : 0,
215
+ minWidth: 0,
216
+ } ) );
217
+
218
+ const getIndicatorBorder = ( { isActive, isError, theme }: { isActive: boolean; isError: boolean; theme: Theme } ) => {
219
+ if ( isError ) {
220
+ return `2px solid ${ theme.palette.error.main }`;
221
+ }
222
+
223
+ if ( isActive ) {
224
+ return `2px solid ${ theme.palette.secondary.main }`;
225
+ }
226
+
227
+ return 'none';
228
+ };
229
+
230
+ const validateLabel = ( newLabel: string ) => {
231
+ const result = validateStyleLabel( newLabel, 'rename' );
232
+
233
+ if ( result.isValid ) {
234
+ return null;
235
+ }
236
+
237
+ return result.errorMessage;
238
+ };
@@ -0,0 +1,56 @@
1
+ import * as React from 'react';
2
+ import { Box, Link, Stack, Typography } from '@elementor/ui';
3
+ import { __ } from '@wordpress/i18n';
4
+
5
+ import { FlippedColorSwatchIcon } from './flipped-color-swatch-icon';
6
+
7
+ type CssClassNotFoundedProps = {
8
+ searchValue: string;
9
+ onClear: () => void;
10
+ };
11
+
12
+ export const CssClassNotFound = ( { onClear, searchValue }: CssClassNotFoundedProps ) => (
13
+ <Stack
14
+ color={ 'text.secondary' }
15
+ pt={ 5 }
16
+ alignItems="center"
17
+ gap={ 1 }
18
+ overflow={ 'hidden' }
19
+ maxWidth={ '170px' }
20
+ justifySelf={ 'center' }
21
+ >
22
+ <FlippedColorSwatchIcon color={ 'inherit' } fontSize="large" />
23
+ <Box>
24
+ <Typography align="center" variant="subtitle2" color="inherit">
25
+ { __( 'Sorry, nothing matched', 'elementor' ) }
26
+ </Typography>
27
+ <Typography
28
+ variant="subtitle2"
29
+ color="inherit"
30
+ sx={ {
31
+ display: 'flex',
32
+ width: '100%',
33
+ justifyContent: 'center',
34
+ } }
35
+ >
36
+ <span>&ldquo;</span>
37
+ <span
38
+ style={ {
39
+ maxWidth: '80%',
40
+ overflow: 'hidden',
41
+ textOverflow: 'ellipsis',
42
+ } }
43
+ >
44
+ { searchValue }
45
+ </span>
46
+ <span>&rdquo;.</span>
47
+ </Typography>
48
+ </Box>
49
+ <Typography align="center" variant="caption" color="inherit">
50
+ { __( 'Try something else.', 'elementor' ) }
51
+ <Link color="secondary" variant="caption" component="button" onClick={ onClear }>
52
+ { __( 'Clear & try again', 'elementor' ) }
53
+ </Link>
54
+ </Typography>
55
+ </Stack>
56
+ );
@@ -1,6 +1,7 @@
1
- import * as React from 'react';
2
1
  import { useEffect } from 'react';
2
+ import * as React from 'react';
3
3
  import { setDocumentModifiedStatus } from '@elementor/editor-documents';
4
+ import { EXPERIMENTAL_FEATURES } from '@elementor/editor-editing-panel';
4
5
  import {
5
6
  __createPanel as createPanel,
6
7
  Panel,
@@ -10,7 +11,7 @@ import {
10
11
  PanelHeaderTitle,
11
12
  } from '@elementor/editor-panels';
12
13
  import { ThemeProvider } from '@elementor/editor-ui';
13
- import { changeEditMode } from '@elementor/editor-v1-adapters';
14
+ import { changeEditMode, isExperimentActive } from '@elementor/editor-v1-adapters';
14
15
  import { XIcon } from '@elementor/icons';
15
16
  import { useMutation } from '@elementor/query';
16
17
  import { __dispatch as dispatch } from '@elementor/store';
@@ -19,23 +20,28 @@ import {
19
20
  Box,
20
21
  Button,
21
22
  DialogHeader,
23
+ Divider,
22
24
  ErrorBoundary,
23
25
  IconButton,
24
26
  type IconButtonProps,
25
27
  Stack,
26
28
  } from '@elementor/ui';
29
+ import { useDebounceState } from '@elementor/utils';
27
30
  import { __ } from '@wordpress/i18n';
28
31
 
29
32
  import { useDirtyState } from '../../hooks/use-dirty-state';
30
33
  import { saveGlobalClasses } from '../../save-global-classes';
31
34
  import { slice } from '../../store';
32
35
  import { ClassManagerIntroduction } from './class-manager-introduction';
36
+ import { ClassManagerSearch } from './class-manager-search';
33
37
  import { hasDeletedItems, onDelete } from './delete-class';
34
38
  import { FlippedColorSwatchIcon } from './flipped-color-swatch-icon';
35
39
  import { GlobalClassesList } from './global-classes-list';
36
40
  import { blockPanelInteractions, unblockPanelInteractions } from './panel-interactions';
37
41
  import { SaveChangesDialog, useDialog } from './save-changes-dialog';
38
42
 
43
+ const isVersion311IsActive = isExperimentActive( EXPERIMENTAL_FEATURES.V_3_31 );
44
+
39
45
  const id = 'global-classes-manager';
40
46
 
41
47
  // We need to disable the app-bar buttons, and the elements overlays when opening the classes manager panel.
@@ -59,6 +65,11 @@ export const { panel, usePanelActions } = createPanel( {
59
65
  } );
60
66
 
61
67
  export function ClassManagerPanel() {
68
+ const { debouncedValue, inputValue, handleChange } = useDebounceState( {
69
+ delay: 300,
70
+ initialValue: '',
71
+ } );
72
+
62
73
  const isDirty = useDirtyState();
63
74
 
64
75
  const { close: closePanel } = usePanelActions();
@@ -97,9 +108,39 @@ export function ClassManagerPanel() {
97
108
  />
98
109
  </Stack>
99
110
  </PanelHeader>
100
- <PanelBody px={ 2 }>
101
- <GlobalClassesList disabled={ isPublishing } />
111
+ <PanelBody
112
+ sx={ {
113
+ display: 'flex',
114
+ flexDirection: 'column',
115
+ height: '100%',
116
+ } }
117
+ >
118
+ { isVersion311IsActive && (
119
+ <>
120
+ <ClassManagerSearch searchValue={ inputValue } onChange={ handleChange } />
121
+ <Divider
122
+ sx={ {
123
+ borderWidth: '1px 0 0 0',
124
+ } }
125
+ />
126
+ </>
127
+ ) }
128
+
129
+ <Box
130
+ px={ 2 }
131
+ sx={ {
132
+ flexGrow: 1,
133
+ overflowY: 'auto',
134
+ } }
135
+ >
136
+ <GlobalClassesList
137
+ disabled={ isPublishing }
138
+ searchValue={ debouncedValue }
139
+ onSearch={ handleChange }
140
+ />
141
+ </Box>
102
142
  </PanelBody>
143
+
103
144
  <PanelFooter>
104
145
  <Button
105
146
  fullWidth
@@ -0,0 +1,33 @@
1
+ import * as React from 'react';
2
+ import { SearchIcon } from '@elementor/icons';
3
+ import { Box, Grid, InputAdornment, Stack, TextField } from '@elementor/ui';
4
+ import { __ } from '@wordpress/i18n';
5
+
6
+ type ClassMangerSearchProps = {
7
+ searchValue: string;
8
+ onChange: ( value: string ) => void;
9
+ };
10
+
11
+ export const ClassManagerSearch = ( { searchValue, onChange }: ClassMangerSearchProps ) => (
12
+ <Grid item xs={ 6 } px={ 2 } pb={ 1 }>
13
+ <Stack direction="row" gap={ 0.5 } sx={ { width: '100%' } }>
14
+ <Box sx={ { flexGrow: 1 } }>
15
+ <TextField
16
+ role={ 'search' }
17
+ fullWidth
18
+ size={ 'tiny' }
19
+ value={ searchValue }
20
+ placeholder={ __( 'Search', 'elementor' ) }
21
+ onChange={ ( e: React.ChangeEvent< HTMLInputElement > ) => onChange( e.target.value ) }
22
+ InputProps={ {
23
+ startAdornment: (
24
+ <InputAdornment position="start">
25
+ <SearchIcon fontSize={ 'tiny' } />
26
+ </InputAdornment>
27
+ ),
28
+ } }
29
+ />
30
+ </Box>
31
+ </Stack>
32
+ </Grid>
33
+ );