@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.
@@ -1,42 +1,48 @@
1
1
  import * as React from 'react';
2
- import { useEffect, useRef } from 'react';
2
+ import { useEffect, useMemo } from 'react';
3
3
  import { type StyleDefinitionID } from '@elementor/editor-styles';
4
- import { validateStyleLabel } from '@elementor/editor-styles-repository';
5
- import { EditableField, EllipsisWithTooltip, MenuListItem, useEditable, WarningInfotip } from '@elementor/editor-ui';
6
- import { DotsVerticalIcon } from '@elementor/icons';
7
4
  import { __useDispatch as useDispatch } from '@elementor/store';
8
- import {
9
- bindMenu,
10
- bindTrigger,
11
- Box,
12
- IconButton,
13
- List,
14
- ListItemButton,
15
- type ListItemButtonProps,
16
- Menu,
17
- Stack,
18
- styled,
19
- type Theme,
20
- Tooltip,
21
- Typography,
22
- type TypographyProps,
23
- usePopupState,
24
- } from '@elementor/ui';
5
+ import { List, Stack, styled, Typography, type TypographyProps } from '@elementor/ui';
25
6
  import { __ } from '@wordpress/i18n';
26
7
 
27
8
  import { useClassesOrder } from '../../hooks/use-classes-order';
28
9
  import { useOrderedClasses } from '../../hooks/use-ordered-classes';
29
10
  import { slice } from '../../store';
30
- import { DeleteConfirmationProvider, useDeleteConfirmation } from './delete-confirmation-dialog';
11
+ import { ClassItem } from './class-item';
12
+ import { CssClassNotFound } from './class-manager-class-not-found';
13
+ import { DeleteConfirmationProvider } from './delete-confirmation-dialog';
31
14
  import { FlippedColorSwatchIcon } from './flipped-color-swatch-icon';
32
- import { SortableItem, SortableProvider, SortableTrigger, type SortableTriggerProps } from './sortable';
15
+ import { SortableItem, SortableProvider } from './sortable';
33
16
 
34
- export const GlobalClassesList = ( { disabled }: { disabled?: boolean } ) => {
17
+ type GlobalClassesListProps = {
18
+ disabled?: boolean;
19
+ searchValue: string;
20
+ onSearch: ( searchValue: string ) => void;
21
+ };
22
+
23
+ export const GlobalClassesList = ( { disabled, searchValue, onSearch }: GlobalClassesListProps ) => {
35
24
  const cssClasses = useOrderedClasses();
36
25
  const dispatch = useDispatch();
37
26
 
38
27
  const [ classesOrder, reorderClasses ] = useReorder();
39
28
 
29
+ const lowercaseLabels = useMemo(
30
+ () =>
31
+ cssClasses.map( ( cssClass ) => ( {
32
+ ...cssClass,
33
+ lowerLabel: cssClass.label.toLowerCase(),
34
+ } ) ),
35
+ [ cssClasses ]
36
+ );
37
+
38
+ const filteredClasses = useMemo( () => {
39
+ return searchValue.length > 1
40
+ ? lowercaseLabels.filter( ( cssClass ) =>
41
+ cssClass.lowerLabel.toLowerCase().includes( searchValue.toLowerCase() )
42
+ )
43
+ : cssClasses;
44
+ }, [ searchValue, cssClasses, lowercaseLabels ] );
45
+
40
46
  useEffect( () => {
41
47
  const handler = ( event: KeyboardEvent ) => {
42
48
  if ( event.key === 'z' && ( event.ctrlKey || event.metaKey ) ) {
@@ -61,195 +67,44 @@ export const GlobalClassesList = ( { disabled }: { disabled?: boolean } ) => {
61
67
 
62
68
  return (
63
69
  <DeleteConfirmationProvider>
64
- <List sx={ { display: 'flex', flexDirection: 'column', gap: 0.5 } }>
65
- <SortableProvider value={ classesOrder } onChange={ reorderClasses }>
66
- { cssClasses?.map( ( { id, label } ) => {
67
- const renameClass = ( newLabel: string ) => {
68
- dispatch(
69
- slice.actions.update( {
70
- style: {
71
- id,
72
- label: newLabel,
73
- },
74
- } )
70
+ { filteredClasses.length <= 0 && searchValue.length > 1 ? (
71
+ <CssClassNotFound onClear={ () => onSearch( '' ) } searchValue={ searchValue } />
72
+ ) : (
73
+ <List sx={ { display: 'flex', flexDirection: 'column', gap: 0.5 } }>
74
+ <SortableProvider value={ classesOrder } onChange={ reorderClasses }>
75
+ { filteredClasses?.map( ( { id, label } ) => {
76
+ return (
77
+ <SortableItem key={ id } id={ id }>
78
+ { ( { isDragged, isDragPlaceholder, triggerProps, triggerStyle } ) => (
79
+ <ClassItem
80
+ isSearchActive={ searchValue.length < 2 }
81
+ id={ id }
82
+ label={ label }
83
+ renameClass={ ( newLabel: string ) => {
84
+ dispatch(
85
+ slice.actions.update( {
86
+ style: {
87
+ id,
88
+ label: newLabel,
89
+ },
90
+ } )
91
+ );
92
+ } }
93
+ selected={ isDragged }
94
+ disabled={ disabled || isDragPlaceholder }
95
+ sortableTriggerProps={ { ...triggerProps, style: triggerStyle } }
96
+ />
97
+ ) }
98
+ </SortableItem>
75
99
  );
76
- };
77
-
78
- return (
79
- <SortableItem key={ id } id={ id }>
80
- { ( { isDragged, isDragPlaceholder, triggerProps, triggerStyle } ) => (
81
- <ClassItem
82
- id={ id }
83
- label={ label }
84
- renameClass={ renameClass }
85
- selected={ isDragged }
86
- disabled={ disabled || isDragPlaceholder }
87
- sortableTriggerProps={ { ...triggerProps, style: triggerStyle } }
88
- />
89
- ) }
90
- </SortableItem>
91
- );
92
- } ) }
93
- </SortableProvider>
94
- </List>
100
+ } ) }
101
+ </SortableProvider>
102
+ </List>
103
+ ) }
95
104
  </DeleteConfirmationProvider>
96
105
  );
97
106
  };
98
107
 
99
- const useReorder = () => {
100
- const dispatch = useDispatch();
101
- const order = useClassesOrder();
102
-
103
- const reorder = ( newIds: StyleDefinitionID[] ) => {
104
- dispatch( slice.actions.setOrder( newIds ) );
105
- };
106
-
107
- return [ order, reorder ] as const;
108
- };
109
-
110
- type ClassItemProps = React.PropsWithChildren< {
111
- id: string;
112
- label: string;
113
- renameClass: ( newLabel: string ) => void;
114
- selected?: boolean;
115
- disabled?: boolean;
116
- sortableTriggerProps: SortableTriggerProps;
117
- } >;
118
-
119
- const ClassItem = ( { id, label, renameClass, selected, disabled, sortableTriggerProps }: ClassItemProps ) => {
120
- const itemRef = useRef< HTMLElement >( null );
121
-
122
- const {
123
- ref: editableRef,
124
- openEditMode,
125
- isEditing,
126
- error,
127
- getProps: getEditableProps,
128
- } = useEditable( {
129
- value: label,
130
- onSubmit: renameClass,
131
- validation: validateLabel,
132
- } );
133
-
134
- const { openDialog } = useDeleteConfirmation();
135
-
136
- const popupState = usePopupState( {
137
- variant: 'popover',
138
- disableAutoFocus: true,
139
- } );
140
-
141
- const isSelected = ( selected || popupState.isOpen ) && ! disabled;
142
-
143
- return (
144
- <>
145
- <Stack p={ 0 }>
146
- <WarningInfotip
147
- open={ Boolean( error ) }
148
- text={ error ?? '' }
149
- placement="bottom"
150
- width={ itemRef.current?.getBoundingClientRect().width }
151
- offset={ [ 0, -15 ] }
152
- >
153
- <StyledListItemButton
154
- ref={ itemRef }
155
- dense
156
- disableGutters
157
- showActions={ isSelected || isEditing }
158
- shape="rounded"
159
- onDoubleClick={ openEditMode }
160
- selected={ isSelected }
161
- disabled={ disabled }
162
- focusVisibleClassName="visible-class-item"
163
- >
164
- <SortableTrigger { ...sortableTriggerProps } />
165
- <Indicator isActive={ isEditing } isError={ !! error }>
166
- { isEditing ? (
167
- <EditableField
168
- ref={ editableRef }
169
- as={ Typography }
170
- variant="caption"
171
- { ...getEditableProps() }
172
- />
173
- ) : (
174
- <EllipsisWithTooltip title={ label } as={ Typography } variant="caption" />
175
- ) }
176
- </Indicator>
177
- <Tooltip
178
- placement="top"
179
- className={ 'class-item-more-actions' }
180
- title={ __( 'More actions', 'elementor' ) }
181
- >
182
- <IconButton size="tiny" { ...bindTrigger( popupState ) } aria-label="More actions">
183
- <DotsVerticalIcon fontSize="tiny" />
184
- </IconButton>
185
- </Tooltip>
186
- </StyledListItemButton>
187
- </WarningInfotip>
188
- </Stack>
189
- <Menu
190
- { ...bindMenu( popupState ) }
191
- anchorOrigin={ {
192
- vertical: 'bottom',
193
- horizontal: 'right',
194
- } }
195
- transformOrigin={ {
196
- vertical: 'top',
197
- horizontal: 'right',
198
- } }
199
- >
200
- <MenuListItem
201
- sx={ { minWidth: '160px' } }
202
- onClick={ () => {
203
- popupState.close();
204
- openEditMode();
205
- } }
206
- >
207
- <Typography variant="caption" sx={ { color: 'text.primary' } }>
208
- { __( 'Rename', 'elementor' ) }
209
- </Typography>
210
- </MenuListItem>
211
- <MenuListItem
212
- onClick={ () => {
213
- popupState.close();
214
- openDialog( { id, label } );
215
- } }
216
- >
217
- <Typography variant="caption" sx={ { color: 'error.light' } }>
218
- { __( 'Delete', 'elementor' ) }
219
- </Typography>
220
- </MenuListItem>
221
- </Menu>
222
- </>
223
- );
224
- };
225
-
226
- // Custom styles for sortable list item, until the component is available in the UI package.
227
- const StyledListItemButton = styled( ListItemButton, {
228
- shouldForwardProp: ( prop: string ) => ! [ 'showActions' ].includes( prop ),
229
- } )< ListItemButtonProps & { showActions: boolean } >(
230
- ( { showActions } ) => `
231
- min-height: 36px;
232
-
233
- &.visible-class-item {
234
- box-shadow: none !important;
235
- }
236
-
237
- .class-item-more-actions, .class-item-sortable-trigger {
238
- visibility: ${ showActions ? 'visible' : 'hidden' };
239
- }
240
-
241
- .class-item-sortable-trigger {
242
- visibility: ${ showActions ? 'visible' : 'hidden' };
243
- }
244
-
245
- &:hover&:not(:disabled) {
246
- .class-item-more-actions, .class-item-sortable-trigger {
247
- visibility: visible;
248
- }
249
- }
250
- `
251
- );
252
-
253
108
  const EmptyState = () => (
254
109
  <Stack alignItems="center" gap={ 1.5 } pt={ 10 } px={ 0.5 } maxWidth="260px" margin="auto">
255
110
  <FlippedColorSwatchIcon fontSize="large" />
@@ -272,37 +127,13 @@ const StyledHeader = styled( Typography )< TypographyProps >( ( { theme, variant
272
127
  },
273
128
  } ) );
274
129
 
275
- const Indicator = styled( Box, {
276
- shouldForwardProp: ( prop: string ) => ! [ 'isActive', 'isError' ].includes( prop ),
277
- } )< { isActive: boolean; isError: boolean } >( ( { theme, isActive, isError } ) => ( {
278
- display: 'flex',
279
- width: '100%',
280
- flexGrow: 1,
281
- borderRadius: theme.spacing( 0.5 ),
282
- border: getIndicatorBorder( { isActive, isError, theme } ),
283
- padding: `0 ${ theme.spacing( 1 ) }`,
284
- marginLeft: isActive ? theme.spacing( 1 ) : 0,
285
- minWidth: 0,
286
- } ) );
287
-
288
- const getIndicatorBorder = ( { isActive, isError, theme }: { isActive: boolean; isError: boolean; theme: Theme } ) => {
289
- if ( isError ) {
290
- return `2px solid ${ theme.palette.error.main }`;
291
- }
292
-
293
- if ( isActive ) {
294
- return `2px solid ${ theme.palette.secondary.main }`;
295
- }
296
-
297
- return 'none';
298
- };
299
-
300
- const validateLabel = ( newLabel: string ) => {
301
- const result = validateStyleLabel( newLabel, 'rename' );
130
+ const useReorder = () => {
131
+ const dispatch = useDispatch();
132
+ const order = useClassesOrder();
302
133
 
303
- if ( result.isValid ) {
304
- return null;
305
- }
134
+ const reorder = ( newIds: StyleDefinitionID[] ) => {
135
+ dispatch( slice.actions.setOrder( newIds ) );
136
+ };
306
137
 
307
- return result.errorMessage;
138
+ return [ order, reorder ] as const;
308
139
  };