@elementor/editor-global-classes 0.20.5 → 0.21.1

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,21 +11,37 @@ 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
- import { Alert, Box, Button, ErrorBoundary, IconButton, type IconButtonProps, Stack } from '@elementor/ui';
17
+ import { __dispatch as dispatch } from '@elementor/store';
18
+ import {
19
+ Alert,
20
+ Box,
21
+ Button,
22
+ DialogHeader,
23
+ Divider,
24
+ ErrorBoundary,
25
+ IconButton,
26
+ type IconButtonProps,
27
+ Stack,
28
+ } from '@elementor/ui';
29
+ import { useDebounceState } from '@elementor/utils';
17
30
  import { __ } from '@wordpress/i18n';
18
31
 
19
32
  import { useDirtyState } from '../../hooks/use-dirty-state';
20
33
  import { saveGlobalClasses } from '../../save-global-classes';
34
+ import { slice } from '../../store';
21
35
  import { ClassManagerIntroduction } from './class-manager-introduction';
36
+ import { ClassManagerSearch } from './class-manager-search';
22
37
  import { hasDeletedItems, onDelete } from './delete-class';
23
38
  import { FlippedColorSwatchIcon } from './flipped-color-swatch-icon';
24
39
  import { GlobalClassesList } from './global-classes-list';
25
40
  import { blockPanelInteractions, unblockPanelInteractions } from './panel-interactions';
26
41
  import { SaveChangesDialog, useDialog } from './save-changes-dialog';
27
42
 
43
+ const isVersion311IsActive = isExperimentActive( EXPERIMENTAL_FEATURES.V_3_31 );
44
+
28
45
  const id = 'global-classes-manager';
29
46
 
30
47
  // We need to disable the app-bar buttons, and the elements overlays when opening the classes manager panel.
@@ -48,6 +65,11 @@ export const { panel, usePanelActions } = createPanel( {
48
65
  } );
49
66
 
50
67
  export function ClassManagerPanel() {
68
+ const { debouncedValue, inputValue, handleChange } = useDebounceState( {
69
+ delay: 300,
70
+ initialValue: '',
71
+ } );
72
+
51
73
  const isDirty = useDirtyState();
52
74
 
53
75
  const { close: closePanel } = usePanelActions();
@@ -55,6 +77,11 @@ export function ClassManagerPanel() {
55
77
 
56
78
  const { mutateAsync: publish, isPending: isPublishing } = usePublish();
57
79
 
80
+ const resetAndClosePanel = () => {
81
+ dispatch( slice.actions.resetToInitialState( { context: 'frontend' } ) );
82
+ closeSaveChangesDialog();
83
+ };
84
+
58
85
  usePreventUnload();
59
86
 
60
87
  return (
@@ -81,9 +108,39 @@ export function ClassManagerPanel() {
81
108
  />
82
109
  </Stack>
83
110
  </PanelHeader>
84
- <PanelBody px={ 2 }>
85
- <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>
86
142
  </PanelBody>
143
+
87
144
  <PanelFooter>
88
145
  <Button
89
146
  fullWidth
@@ -102,7 +159,11 @@ export function ClassManagerPanel() {
102
159
  <ClassManagerIntroduction />
103
160
  { isSaveChangesDialogOpen && (
104
161
  <SaveChangesDialog>
105
- <SaveChangesDialog.Title>{ __( 'You have unsaved changes', 'elementor' ) }</SaveChangesDialog.Title>
162
+ <DialogHeader onClose={ closeSaveChangesDialog } logo={ false }>
163
+ <SaveChangesDialog.Title>
164
+ { __( 'You have unsaved changes', 'elementor' ) }
165
+ </SaveChangesDialog.Title>
166
+ </DialogHeader>
106
167
  <SaveChangesDialog.Content>
107
168
  <SaveChangesDialog.ContentText>
108
169
  { __( 'You have unsaved changes in the Class Manager.', 'elementor' ) }
@@ -113,9 +174,11 @@ export function ClassManagerPanel() {
113
174
  </SaveChangesDialog.Content>
114
175
  <SaveChangesDialog.Actions
115
176
  actions={ {
116
- cancel: {
117
- label: __( 'Cancel', 'elementor' ),
118
- action: closeSaveChangesDialog,
177
+ discard: {
178
+ label: __( 'Discard', 'elementor' ),
179
+ action: () => {
180
+ resetAndClosePanel();
181
+ },
119
182
  },
120
183
  confirm: {
121
184
  label: __( 'Save & Continue', 'elementor' ),
@@ -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
+ );