@elementor/editor-global-classes 0.4.0 → 0.5.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,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.push( 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
+ }