@elementor/editor-global-classes 0.3.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 ADDED
@@ -0,0 +1,11 @@
1
+ import { createError } from '@elementor/utils';
2
+
3
+ export const GlobalClassNotFoundError = createError< { styleId: string } >( {
4
+ code: 'global_class_not_found',
5
+ message: 'Global class not found.',
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,50 +1,76 @@
1
+ import { generateId } from '@elementor/editor-styles';
1
2
  import type { StylesProvider } from '@elementor/editor-styles-repository';
2
- import { __dispatch as dispatch, __getState as getState, __subscribe as subscribe } from '@elementor/store';
3
+ import {
4
+ __dispatch as dispatch,
5
+ __getState as getState,
6
+ __subscribeWithSelector as subscribeWithSelector,
7
+ } from '@elementor/store';
3
8
  import { __ } from '@wordpress/i18n';
4
9
 
5
- import { apiClient } from './api';
6
- import { 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';
7
18
 
8
- export const globalClassesStylesProvider: StylesProvider = {
19
+ export const globalClassesStylesProvider = {
9
20
  key: 'global-classes',
10
21
  priority: 30,
11
22
  actions: {
12
23
  get: () => selectOrderedGlobalClasses( getState() ),
13
- create: async ( style ) => {
14
- const res = await apiClient.post( style );
24
+ getById: ( id ) => selectClass( getState(), id ),
25
+ create: ( label ) => {
26
+ const classes = selectGlobalClasses( getState() );
15
27
 
16
- const { data, meta } = res.data;
28
+ const existingLabels = Object.values( classes ).map( ( style ) => style.label );
29
+
30
+ if ( existingLabels.includes( label ) ) {
31
+ throw new GlobalClassLabelAlreadyExistsError( { context: { label } } );
32
+ }
33
+
34
+ const existingIds = Object.keys( classes );
35
+ const id = generateId( 'g-', existingIds );
17
36
 
18
37
  dispatch(
19
38
  slice.actions.add( {
20
- style: data,
21
- order: meta.order,
39
+ id,
40
+ type: 'class',
41
+ label,
42
+ variants: [],
22
43
  } )
23
44
  );
24
45
 
25
- return data;
46
+ return id;
26
47
  },
27
- update: async ( payload ) => {
28
- const style = selectClass( getState(), payload.id );
29
- const mergedData = { ...style, ...payload };
30
-
31
- const res = await apiClient.put( payload.id, mergedData );
32
-
33
- const { data, meta } = res.data;
34
-
48
+ update: ( payload ) => {
35
49
  dispatch(
36
50
  slice.actions.update( {
37
- style: data,
38
- order: meta.order,
51
+ style: payload,
52
+ } )
53
+ );
54
+ },
55
+ delete: ( id ) => {
56
+ dispatch( slice.actions.delete( id ) );
57
+ },
58
+ setOrder: ( order ) => {
59
+ dispatch( slice.actions.setOrder( order ) );
60
+ },
61
+ updateProps: ( args ) => {
62
+ dispatch(
63
+ slice.actions.updateProps( {
64
+ id: args.id,
65
+ meta: args.meta,
66
+ props: args.props,
39
67
  } )
40
68
  );
41
-
42
- return data;
43
69
  },
44
70
  },
45
- subscribe: ( cb ) => subscribe( cb ),
71
+ subscribe: ( cb ) => subscribeWithSelector( ( state: StateWithGlobalClasses ) => state.globalClasses, cb ),
46
72
  labels: {
47
- singular: __( 'Global CSS Class', 'elementor' ),
73
+ singular: __( 'Global class', 'elementor' ),
48
74
  plural: __( 'Global CSS Classes', 'elementor' ),
49
75
  },
50
- };
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
@@ -1,50 +1,123 @@
1
- import { type StyleDefinition, type StyleDefinitionID } from '@elementor/editor-styles';
1
+ import { mergeProps, type Props } from '@elementor/editor-props';
2
+ import {
3
+ getVariantByMeta,
4
+ type StyleDefinition,
5
+ type StyleDefinitionID,
6
+ type StyleDefinitionVariant,
7
+ } from '@elementor/editor-styles';
8
+ import { type UpdateActionPayload } from '@elementor/editor-styles-repository';
2
9
  import {
3
10
  __createSelector as createSelector,
4
11
  __createSlice as createSlice,
12
+ __useSelector as useSelector,
5
13
  type PayloadAction,
6
14
  type SliceState,
7
15
  } from '@elementor/store';
8
16
 
9
- export type State = {
17
+ import { GlobalClassNotFoundError } from './errors';
18
+
19
+ export type GlobalClassesState = {
10
20
  items: Record< StyleDefinitionID, StyleDefinition >;
11
21
  order: StyleDefinitionID[];
22
+ isDirty: boolean;
12
23
  };
13
24
 
14
- const initialState: State = {
25
+ const initialState: GlobalClassesState = {
15
26
  items: {},
16
27
  order: [],
28
+ isDirty: false,
17
29
  };
18
30
 
31
+ export type StateWithGlobalClasses = SliceState< typeof slice >;
32
+
19
33
  // Slice
20
- export const SLICE_NAME = 'globalClasses';
34
+ const SLICE_NAME = 'globalClasses';
21
35
 
22
36
  export const slice = createSlice( {
23
37
  name: SLICE_NAME,
24
38
  initialState,
25
39
  reducers: {
26
- init( state, { payload }: PayloadAction< State > ) {
40
+ init( state, { payload }: PayloadAction< Pick< GlobalClassesState, 'items' | 'order' > > ) {
27
41
  state.items = payload.items;
28
42
  state.order = payload.order;
43
+
44
+ state.isDirty = false;
29
45
  },
30
- add( state, { payload }: PayloadAction< { style: StyleDefinition; order: State[ 'order' ] } > ) {
31
- state.items[ payload.style.id ] = payload.style;
32
- 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;
33
51
  },
34
- update( state, { payload }: PayloadAction< { style: StyleDefinition; order: State[ 'order' ] } > ) {
35
- state.items[ payload.style.id ] = payload.style;
36
- 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;
73
+ },
74
+ updateProps(
75
+ state,
76
+ {
77
+ payload,
78
+ }: PayloadAction< { id: StyleDefinitionID; meta: StyleDefinitionVariant[ 'meta' ]; props: Props } >
79
+ ) {
80
+ const style = state.items[ payload.id ];
81
+
82
+ if ( ! style ) {
83
+ throw new GlobalClassNotFoundError( { context: { styleId: payload.id } } );
84
+ }
85
+
86
+ const variant = getVariantByMeta( style, payload.meta );
87
+
88
+ if ( variant ) {
89
+ variant.props = mergeProps( variant.props, payload.props );
90
+ } else {
91
+ style.variants.push( { meta: payload.meta, props: payload.props } );
92
+ }
93
+
94
+ state.isDirty = true;
95
+ },
96
+
97
+ setPristine( state ) {
98
+ state.isDirty = false;
37
99
  },
38
100
  },
39
101
  } );
40
102
 
41
103
  // Selectors
42
- const selectItems = ( state: SliceState< typeof slice > ) => state[ SLICE_NAME ].items;
43
104
  const selectOrder = ( state: SliceState< typeof slice > ) => state[ SLICE_NAME ].order;
44
105
 
45
- 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 ) =>
46
109
  order.map( ( id ) => items[ id ] )
47
110
  );
48
111
 
49
112
  export const selectClass = ( state: SliceState< typeof slice >, id: StyleDefinitionID ) =>
50
- state[ SLICE_NAME ].items[ id ];
113
+ state[ SLICE_NAME ].items[ id ] ?? null;
114
+
115
+ export const selectIsDirty = ( state: SliceState< typeof slice > ) => state.globalClasses.isDirty;
116
+
117
+ export const useOrderedGlobalClasses = () => {
118
+ const items = useSelector( selectOrderedGlobalClasses );
119
+
120
+ return items;
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
+ }