@elementor/editor-global-classes 0.4.0 → 0.6.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,25 +1,261 @@
1
1
  import * as React from 'react';
2
- import { EllipsisWithTooltip } from '@elementor/editor-ui';
3
- import { List, ListItemButton, Stack, Typography } from '@elementor/ui';
2
+ import { type StyleDefinitionID } from '@elementor/editor-styles';
3
+ import { stylesRepository } from '@elementor/editor-styles-repository';
4
+ import { EditableField, EllipsisWithTooltip, useEditable } from '@elementor/editor-ui';
5
+ import { DotsVerticalIcon, PhotoIcon } from '@elementor/icons';
6
+ import {
7
+ bindMenu,
8
+ bindTrigger,
9
+ Box,
10
+ IconButton,
11
+ List,
12
+ ListItem,
13
+ ListItemButton,
14
+ ListItemText,
15
+ Menu,
16
+ MenuItem,
17
+ Stack,
18
+ styled,
19
+ type Theme,
20
+ Tooltip,
21
+ Typography,
22
+ type TypographyProps,
23
+ usePopupState,
24
+ } from '@elementor/ui';
25
+ import { __ } from '@wordpress/i18n';
4
26
 
5
- import { useOrderedGlobalClasses } from '../../store';
27
+ import { globalClassesStylesProvider } from '../../global-classes-styles-provider';
28
+ import { useGlobalClassesOrder, useOrderedGlobalClasses } from '../../store';
29
+ import { DeleteConfirmationProvider, useDeleteConfirmation } from './delete-confirmation-dialog';
30
+ import { SortableItem, SortableItemIndicator, SortableProvider } from './sortable';
6
31
 
7
32
  export const GlobalClassesList = () => {
8
33
  const cssClasses = useOrderedGlobalClasses();
9
34
 
35
+ const [ classesOrder, reorderClasses ] = useClassesOrder();
36
+
37
+ if ( ! cssClasses?.length ) {
38
+ return <EmptyState />;
39
+ }
40
+
10
41
  return (
11
- <Stack>
12
- <List role="list">
13
- { cssClasses?.map( ( { id, label } ) => {
14
- return (
15
- <Stack key={ id } direction="row" alignItems="center" gap={ 1 } role="listitem">
16
- <ListItemButton sx={ { borderRadius: 1 } }>
17
- <EllipsisWithTooltip title={ label } as={ Typography } variant="caption" />
18
- </ListItemButton>
19
- </Stack>
20
- );
21
- } ) }
42
+ <DeleteConfirmationProvider>
43
+ <List sx={ { display: 'flex', flexDirection: 'column', gap: 0.5 } }>
44
+ <SortableProvider value={ classesOrder } onChange={ reorderClasses }>
45
+ { cssClasses?.map( ( { id, label } ) => {
46
+ const renameClass = ( newLabel: string ) => {
47
+ globalClassesStylesProvider.actions.update( { label: newLabel, id } );
48
+ };
49
+
50
+ return (
51
+ <SortableItem key={ id } id={ id }>
52
+ { ( { isDragged, showDropIndication, dropIndicationStyle } ) => (
53
+ <ClassItem
54
+ id={ id }
55
+ label={ label }
56
+ renameClass={ renameClass }
57
+ selected={ isDragged }
58
+ >
59
+ { showDropIndication && (
60
+ <SortableItemIndicator style={ dropIndicationStyle } />
61
+ ) }
62
+ </ClassItem>
63
+ ) }
64
+ </SortableItem>
65
+ );
66
+ } ) }
67
+ </SortableProvider>
22
68
  </List>
69
+ </DeleteConfirmationProvider>
70
+ );
71
+ };
72
+
73
+ const useClassesOrder = () => {
74
+ const order = useGlobalClassesOrder();
75
+
76
+ const reorder = ( newIds: StyleDefinitionID[] ) => {
77
+ globalClassesStylesProvider.actions.setOrder( newIds );
78
+ };
79
+
80
+ return [ order, reorder ] as const;
81
+ };
82
+
83
+ const ClassItem = ( {
84
+ id,
85
+ label,
86
+ renameClass,
87
+ selected,
88
+ children,
89
+ }: React.PropsWithChildren< {
90
+ id: string;
91
+ label: string;
92
+ renameClass: ( newLabel: string ) => void;
93
+ selected: boolean;
94
+ } > ) => {
95
+ const {
96
+ ref: editableRef,
97
+ openEditMode,
98
+ isEditing,
99
+ error,
100
+ getProps: getEditableProps,
101
+ } = useEditable( {
102
+ value: label,
103
+ onSubmit: renameClass,
104
+ validation: validateLabel,
105
+ } );
106
+
107
+ const { openDialog } = useDeleteConfirmation();
108
+
109
+ const popupState = usePopupState( {
110
+ variant: 'popover',
111
+ disableAutoFocus: true,
112
+ } );
113
+
114
+ return (
115
+ <Stack direction="row" alignItems="center" gap={ 1 } flexGrow={ 1 } flexShrink={ 0 }>
116
+ <StyledListItem
117
+ component={ 'div' }
118
+ disablePadding
119
+ disableGutters
120
+ secondaryAction={
121
+ <Tooltip
122
+ placement="top"
123
+ className="class-item-more-actions"
124
+ title={ __( 'More actions', 'elementor' ) }
125
+ >
126
+ <IconButton size="tiny" { ...bindTrigger( popupState ) } aria-label="More actions">
127
+ <DotsVerticalIcon fontSize="tiny" />
128
+ </IconButton>
129
+ </Tooltip>
130
+ }
131
+ >
132
+ <ListItemButton
133
+ dense
134
+ disableGutters
135
+ shape="rounded"
136
+ onDoubleClick={ openEditMode }
137
+ selected={ selected || popupState.isOpen }
138
+ focusVisibleClassName="visible-class-item"
139
+ sx={ {
140
+ minHeight: '36px',
141
+ display: 'flex',
142
+ '&.visible-class-item': {
143
+ boxShadow: 'none !important',
144
+ },
145
+ } }
146
+ >
147
+ <Indicator isActive={ isEditing } isError={ !! error }>
148
+ { isEditing ? (
149
+ <EditableField
150
+ ref={ editableRef }
151
+ error={ error }
152
+ as={ Typography }
153
+ variant="caption"
154
+ { ...getEditableProps() }
155
+ />
156
+ ) : (
157
+ <EllipsisWithTooltip title={ label } as={ Typography } variant="caption" />
158
+ ) }
159
+ </Indicator>
160
+ </ListItemButton>
161
+ { children }
162
+ <Menu
163
+ { ...bindMenu( popupState ) }
164
+ anchorOrigin={ {
165
+ vertical: 'bottom',
166
+ horizontal: 'right',
167
+ } }
168
+ transformOrigin={ {
169
+ vertical: 'top',
170
+ horizontal: 'right',
171
+ } }
172
+ >
173
+ <MenuItem
174
+ sx={ { minWidth: '160px' } }
175
+ onClick={ () => {
176
+ popupState.close();
177
+ openEditMode();
178
+ } }
179
+ >
180
+ <ListItemText primary={ __( 'Rename', 'elementor' ) } />
181
+ </MenuItem>
182
+ <MenuItem
183
+ onClick={ () => {
184
+ popupState.close();
185
+ openDialog( { id, label } );
186
+ } }
187
+ >
188
+ <ListItemText primary={ __( 'Delete', 'elementor' ) } sx={ { color: 'error.light' } } />
189
+ </MenuItem>
190
+ </Menu>
191
+ </StyledListItem>
23
192
  </Stack>
24
193
  );
25
194
  };
195
+
196
+ const StyledListItem = styled( ListItem )`
197
+ .class-item-more-actions {
198
+ visibility: hidden;
199
+ }
200
+ &:hover {
201
+ .class-item-more-actions {
202
+ visibility: visible;
203
+ }
204
+ }
205
+ `;
206
+
207
+ const EmptyState = () => (
208
+ <Stack alignItems="center" gap={ 3 } pt={ 4 } px={ 0.5 }>
209
+ <PhotoIcon fontSize="large" />
210
+ <StyledHeader variant="subtitle2" component="h2" color="text.secondary">
211
+ { __( 'No CSS classes created yet', 'elementor' ) }
212
+ </StyledHeader>
213
+ <Typography align="center" variant="caption" color="text.secondary">
214
+ { __(
215
+ 'CSS classes created in the editor panel will appear here. Once they are available, you can arrange their hierarchy, rename them, or delete them as needed.',
216
+ 'elementor'
217
+ ) }
218
+ </Typography>
219
+ </Stack>
220
+ );
221
+
222
+ // Override panel reset styles.
223
+ const StyledHeader = styled( Typography )< TypographyProps >( ( { theme, variant } ) => ( {
224
+ '&.MuiTypography-root': {
225
+ ...( theme.typography[ variant as keyof typeof theme.typography ] as React.CSSProperties ),
226
+ },
227
+ } ) );
228
+
229
+ const Indicator = styled( Box, {
230
+ shouldForwardProp: ( prop: string ) => ! [ 'isActive', 'isError' ].includes( prop ),
231
+ } )< { isActive: boolean; isError: boolean } >( ( { theme, isActive, isError } ) => ( {
232
+ display: 'flex',
233
+ width: '100%',
234
+ flexGrow: 1,
235
+ borderRadius: theme.spacing( 0.5 ),
236
+ border: getIndicatorBorder( { isActive, isError, theme } ),
237
+ padding: `0 ${ theme.spacing( 1 ) }`,
238
+ marginLeft: isActive ? theme.spacing( 1 ) : 0,
239
+ } ) );
240
+
241
+ const getIndicatorBorder = ( { isActive, isError, theme }: { isActive: boolean; isError: boolean; theme: Theme } ) => {
242
+ if ( isError ) {
243
+ return `2px solid ${ theme.palette.error.main }`;
244
+ }
245
+
246
+ if ( isActive ) {
247
+ return `2px solid ${ theme.palette.secondary.main }`;
248
+ }
249
+
250
+ return 'none';
251
+ };
252
+
253
+ const validateLabel = ( newLabel: string ) => {
254
+ if ( ! stylesRepository.isLabelValid( newLabel ) ) {
255
+ return __( 'Format is not valid', 'elementor' );
256
+ }
257
+
258
+ if ( stylesRepository.isLabelExist( newLabel ) ) {
259
+ return __( 'Existing name', 'elementor' );
260
+ }
261
+ };
@@ -0,0 +1,90 @@
1
+ import * as React from 'react';
2
+ import { GripVerticalIcon } from '@elementor/icons';
3
+ import {
4
+ Box,
5
+ Paper,
6
+ styled,
7
+ UnstableSortableItem,
8
+ type UnstableSortableItemProps,
9
+ type UnstableSortableItemRenderProps,
10
+ UnstableSortableProvider,
11
+ type UnstableSortableProviderProps,
12
+ } from '@elementor/ui';
13
+
14
+ export const SortableProvider = < T extends string >( props: UnstableSortableProviderProps< T > ) => (
15
+ <UnstableSortableProvider restrictAxis variant="static" dragPlaceholderStyle={ { opacity: '1' } } { ...props } />
16
+ );
17
+
18
+ const SortableTrigger = ( props: React.HTMLAttributes< HTMLDivElement > ) => (
19
+ <div { ...props } role="button" className="class-item-sortable-trigger">
20
+ <GripVerticalIcon fontSize="tiny" />
21
+ </div>
22
+ );
23
+
24
+ type ItemRenderProps = Record< string, unknown > & {
25
+ isDragged: boolean;
26
+ showDropIndication: boolean;
27
+ dropIndicationStyle: React.CSSProperties;
28
+ };
29
+
30
+ type SortableItemProps = {
31
+ id: UnstableSortableItemProps[ 'id' ];
32
+ children: ( props: ItemRenderProps ) => React.ReactNode;
33
+ };
34
+
35
+ export const SortableItem = ( { children, id }: SortableItemProps ) => {
36
+ return (
37
+ <UnstableSortableItem
38
+ id={ id }
39
+ render={ ( {
40
+ itemProps,
41
+ isDragged,
42
+ triggerProps,
43
+ itemStyle,
44
+ triggerStyle,
45
+ dropIndicationStyle,
46
+ showDropIndication,
47
+ }: UnstableSortableItemRenderProps ) => {
48
+ return (
49
+ <StyledSortableItem { ...itemProps } elevation={ 0 } sx={ itemStyle } role="listitem">
50
+ <SortableTrigger { ...triggerProps } style={ triggerStyle } />
51
+ { children( {
52
+ itemProps,
53
+ isDragged,
54
+ triggerProps,
55
+ itemStyle,
56
+ triggerStyle,
57
+ dropIndicationStyle,
58
+ showDropIndication,
59
+ } ) }
60
+ </StyledSortableItem>
61
+ );
62
+ } }
63
+ />
64
+ );
65
+ };
66
+
67
+ const StyledSortableItem = styled( Paper )`
68
+ position: relative;
69
+
70
+ &:hover {
71
+ & .class-item-sortable-trigger {
72
+ visibility: visible;
73
+ }
74
+ }
75
+
76
+ & .class-item-sortable-trigger {
77
+ visibility: hidden;
78
+ position: absolute;
79
+ left: 0;
80
+ top: 50%;
81
+ transform: translate( -75%, -50% );
82
+ }
83
+ `;
84
+
85
+ export const SortableItemIndicator = styled( Box )`
86
+ width: 100%;
87
+ height: 3px;
88
+ border-radius: ${ ( { theme } ) => theme.spacing( 0.5 ) };
89
+ background-color: ${ ( { theme } ) => theme.palette.text.primary };
90
+ `;
@@ -4,7 +4,7 @@ import { __useDispatch as useDispatch } from '@elementor/store';
4
4
  import { apiClient } from '../api';
5
5
  import { slice } from '../store';
6
6
 
7
- export function LogicHooks() {
7
+ export function PopulateStore() {
8
8
  const dispatch = useDispatch();
9
9
 
10
10
  useEffect( () => {
package/src/errors.ts CHANGED
@@ -4,3 +4,8 @@ export const GlobalClassNotFoundError = createError< { styleId: string } >( {
4
4
  code: 'global_class_not_found',
5
5
  message: 'Global class not found.',
6
6
  } );
7
+
8
+ export const GlobalClassLabelAlreadyExistsError = createError< { label: string } >( {
9
+ code: 'global_class_label_already_exists',
10
+ message: 'Class with this name already exists.',
11
+ } );
@@ -1,3 +1,4 @@
1
+ import { generateId } from '@elementor/editor-styles';
1
2
  import type { StylesProvider } from '@elementor/editor-styles-repository';
2
3
  import {
3
4
  __dispatch as dispatch,
@@ -6,8 +7,14 @@ import {
6
7
  } from '@elementor/store';
7
8
  import { __ } from '@wordpress/i18n';
8
9
 
9
- import { apiClient } from './api';
10
- import { type GlobalClassesState, selectClass, selectOrderedGlobalClasses, slice } from './store';
10
+ import { GlobalClassLabelAlreadyExistsError } from './errors';
11
+ import {
12
+ selectClass,
13
+ selectGlobalClasses,
14
+ selectOrderedGlobalClasses,
15
+ slice,
16
+ type StateWithGlobalClasses,
17
+ } from './store';
11
18
 
12
19
  export const globalClassesStylesProvider = {
13
20
  key: 'global-classes',
@@ -15,38 +22,42 @@ export const globalClassesStylesProvider = {
15
22
  actions: {
16
23
  get: () => selectOrderedGlobalClasses( getState() ),
17
24
  getById: ( id ) => selectClass( getState(), id ),
18
- create: async ( style ) => {
19
- const res = await apiClient.post( style );
25
+ create: ( label ) => {
26
+ const classes = selectGlobalClasses( getState() );
27
+
28
+ const existingLabels = Object.values( classes ).map( ( style ) => style.label );
29
+
30
+ if ( existingLabels.includes( label ) ) {
31
+ throw new GlobalClassLabelAlreadyExistsError( { context: { label } } );
32
+ }
20
33
 
21
- const { data, meta } = res.data;
34
+ const existingIds = Object.keys( classes );
35
+ const id = generateId( 'g-', existingIds );
22
36
 
23
37
  dispatch(
24
38
  slice.actions.add( {
25
- style: data,
26
- order: meta.order,
39
+ id,
40
+ type: 'class',
41
+ label,
42
+ variants: [],
27
43
  } )
28
44
  );
29
45
 
30
- return data;
46
+ return id;
31
47
  },
32
- update: async ( payload ) => {
33
- const style = selectClass( getState(), payload.id );
34
- const mergedData = { ...style, ...payload };
35
-
36
- const res = await apiClient.put( payload.id, mergedData );
37
-
38
- const { data, meta } = res.data;
39
-
48
+ update: ( payload ) => {
40
49
  dispatch(
41
50
  slice.actions.update( {
42
- style: data,
43
- order: meta.order,
51
+ style: payload,
44
52
  } )
45
53
  );
46
-
47
- return data;
48
54
  },
49
-
55
+ delete: ( id ) => {
56
+ dispatch( slice.actions.delete( id ) );
57
+ },
58
+ setOrder: ( order ) => {
59
+ dispatch( slice.actions.setOrder( order ) );
60
+ },
50
61
  updateProps: ( args ) => {
51
62
  dispatch(
52
63
  slice.actions.updateProps( {
@@ -57,9 +68,9 @@ export const globalClassesStylesProvider = {
57
68
  );
58
69
  },
59
70
  },
60
- subscribe: ( cb ) => subscribeWithSelector( ( state: GlobalClassesState ) => state.globalClasses, cb ),
71
+ subscribe: ( cb ) => subscribeWithSelector( ( state: StateWithGlobalClasses ) => state.globalClasses, cb ),
61
72
  labels: {
62
- singular: __( 'Global CSS Class', 'elementor' ),
73
+ singular: __( 'Global class', 'elementor' ),
63
74
  plural: __( 'Global CSS Classes', 'elementor' ),
64
75
  },
65
76
  } satisfies StylesProvider;
package/src/init.ts CHANGED
@@ -2,13 +2,15 @@ import { injectIntoLogic } from '@elementor/editor';
2
2
  import { injectIntoClassSelectorActions } from '@elementor/editor-editing-panel';
3
3
  import { __registerPanel as registerPanel } from '@elementor/editor-panels';
4
4
  import { stylesRepository } from '@elementor/editor-styles-repository';
5
+ import { __privateListenTo as listenTo, v1ReadyEvent } from '@elementor/editor-v1-adapters';
5
6
  import { __registerSlice as registerSlice } from '@elementor/store';
6
7
 
7
8
  import { ClassManagerButton } from './components/class-manager/class-manager-button';
8
9
  import { panel } from './components/class-manager/class-manager-panel';
9
- import { LogicHooks } from './components/logic-hooks';
10
+ import { PopulateStore } from './components/populate-store';
10
11
  import { globalClassesStylesProvider } from './global-classes-styles-provider';
11
12
  import { slice } from './store';
13
+ import { syncWithDocumentSave } from './sync-with-document-save';
12
14
 
13
15
  export function init() {
14
16
  registerSlice( slice );
@@ -17,12 +19,16 @@ export function init() {
17
19
  stylesRepository.register( globalClassesStylesProvider );
18
20
 
19
21
  injectIntoLogic( {
20
- id: 'global-classes-hooks',
21
- component: LogicHooks,
22
+ id: 'global-classes-populate-store',
23
+ component: PopulateStore,
22
24
  } );
23
25
 
24
26
  injectIntoClassSelectorActions( {
25
- id: 'global-classes',
27
+ id: 'global-classes-manager-button',
26
28
  component: ClassManagerButton,
27
29
  } );
30
+
31
+ listenTo( v1ReadyEvent(), () => {
32
+ syncWithDocumentSave();
33
+ } );
28
34
  }
package/src/store.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  type StyleDefinitionID,
6
6
  type StyleDefinitionVariant,
7
7
  } from '@elementor/editor-styles';
8
+ import { type UpdateActionPayload } from '@elementor/editor-styles-repository';
8
9
  import {
9
10
  __createSelector as createSelector,
10
11
  __createSlice as createSlice,
@@ -15,36 +16,60 @@ import {
15
16
 
16
17
  import { GlobalClassNotFoundError } from './errors';
17
18
 
18
- export type State = {
19
+ export type GlobalClassesState = {
19
20
  items: Record< StyleDefinitionID, StyleDefinition >;
20
21
  order: StyleDefinitionID[];
22
+ isDirty: boolean;
21
23
  };
22
24
 
23
- const initialState: State = {
25
+ const initialState: GlobalClassesState = {
24
26
  items: {},
25
27
  order: [],
28
+ isDirty: false,
26
29
  };
27
30
 
28
- export type GlobalClassesState = SliceState< typeof slice >;
31
+ export type StateWithGlobalClasses = SliceState< typeof slice >;
29
32
 
30
33
  // Slice
31
- export const SLICE_NAME = 'globalClasses';
34
+ const SLICE_NAME = 'globalClasses';
32
35
 
33
36
  export const slice = createSlice( {
34
37
  name: SLICE_NAME,
35
38
  initialState,
36
39
  reducers: {
37
- init( state, { payload }: PayloadAction< State > ) {
40
+ init( state, { payload }: PayloadAction< Pick< GlobalClassesState, 'items' | 'order' > > ) {
38
41
  state.items = payload.items;
39
42
  state.order = payload.order;
43
+
44
+ state.isDirty = false;
40
45
  },
41
- add( state, { payload }: PayloadAction< { style: StyleDefinition; order: State[ 'order' ] } > ) {
42
- state.items[ payload.style.id ] = payload.style;
43
- state.order = payload.order;
46
+ add( state, { payload }: PayloadAction< StyleDefinition > ) {
47
+ state.items[ payload.id ] = payload;
48
+ state.order.unshift( payload.id );
49
+
50
+ state.isDirty = true;
44
51
  },
45
- update( state, { payload }: PayloadAction< { style: StyleDefinition; order: State[ 'order' ] } > ) {
46
- state.items[ payload.style.id ] = payload.style;
47
- state.order = payload.order;
52
+ delete( state, { payload }: PayloadAction< StyleDefinitionID > ) {
53
+ state.items = Object.fromEntries( Object.entries( state.items ).filter( ( [ id ] ) => id !== payload ) );
54
+
55
+ state.order = state.order.filter( ( id ) => id !== payload );
56
+
57
+ state.isDirty = true;
58
+ },
59
+ setOrder( state, { payload }: PayloadAction< StyleDefinitionID[] > ) {
60
+ state.order = payload;
61
+ },
62
+ update( state, { payload }: PayloadAction< { style: UpdateActionPayload } > ) {
63
+ const style = state.items[ payload.style.id ];
64
+
65
+ const mergedData = {
66
+ ...style,
67
+ ...payload.style,
68
+ };
69
+
70
+ state.items[ payload.style.id ] = mergedData;
71
+
72
+ state.isDirty = true;
48
73
  },
49
74
  updateProps(
50
75
  state,
@@ -65,23 +90,34 @@ export const slice = createSlice( {
65
90
  } else {
66
91
  style.variants.push( { meta: payload.meta, props: payload.props } );
67
92
  }
93
+
94
+ state.isDirty = true;
95
+ },
96
+
97
+ setPristine( state ) {
98
+ state.isDirty = false;
68
99
  },
69
100
  },
70
101
  } );
71
102
 
72
103
  // Selectors
73
- const selectItems = ( state: SliceState< typeof slice > ) => state[ SLICE_NAME ].items;
74
104
  const selectOrder = ( state: SliceState< typeof slice > ) => state[ SLICE_NAME ].order;
75
105
 
76
- export const selectOrderedGlobalClasses = createSelector( selectItems, selectOrder, ( items, order ) =>
106
+ export const selectGlobalClasses = ( state: SliceState< typeof slice > ) => state[ SLICE_NAME ].items;
107
+
108
+ export const selectOrderedGlobalClasses = createSelector( selectGlobalClasses, selectOrder, ( items, order ) =>
77
109
  order.map( ( id ) => items[ id ] )
78
110
  );
79
111
 
80
112
  export const selectClass = ( state: SliceState< typeof slice >, id: StyleDefinitionID ) =>
81
113
  state[ SLICE_NAME ].items[ id ] ?? null;
82
114
 
115
+ export const selectIsDirty = ( state: SliceState< typeof slice > ) => state.globalClasses.isDirty;
116
+
83
117
  export const useOrderedGlobalClasses = () => {
84
118
  const items = useSelector( selectOrderedGlobalClasses );
85
119
 
86
120
  return items;
87
121
  };
122
+
123
+ export const useGlobalClassesOrder = () => useSelector( selectOrder );
@@ -0,0 +1,44 @@
1
+ import { __privateRunCommandSync as runCommandSync, registerDataHook } from '@elementor/editor-v1-adapters';
2
+ import { __dispatch, __getState as getState, __subscribeWithSelector as subscribeWithSelector } from '@elementor/store';
3
+
4
+ import { apiClient } from './api';
5
+ import { type GlobalClassesState, selectIsDirty, slice } from './store';
6
+
7
+ export function syncWithDocumentSave() {
8
+ const unsubscribe = syncDirtyState();
9
+
10
+ bindSaveAction();
11
+
12
+ return unsubscribe;
13
+ }
14
+
15
+ function syncDirtyState() {
16
+ return subscribeWithSelector( selectIsDirty, () => {
17
+ if ( ! isDirty() ) {
18
+ return;
19
+ }
20
+
21
+ runCommandSync( 'document/save/set-is-modified', { status: true }, { internal: true } );
22
+ } );
23
+ }
24
+
25
+ function bindSaveAction() {
26
+ registerDataHook( 'after', 'document/save/save', async () => {
27
+ if ( ! isDirty() ) {
28
+ return;
29
+ }
30
+
31
+ const state: GlobalClassesState = getState().globalClasses;
32
+
33
+ await apiClient.update( {
34
+ items: state.items,
35
+ order: state.order,
36
+ } );
37
+
38
+ __dispatch( slice.actions.setPristine() );
39
+ } );
40
+ }
41
+
42
+ function isDirty() {
43
+ return selectIsDirty( getState() );
44
+ }