@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.
- package/CHANGELOG.md +60 -0
- package/dist/index.js +436 -100
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +467 -101
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -11
- package/src/api.ts +6 -12
- package/src/components/class-manager/class-manager-panel.tsx +2 -2
- package/src/components/class-manager/delete-confirmation-dialog.tsx +96 -0
- package/src/components/class-manager/global-classes-list.tsx +250 -14
- package/src/components/class-manager/sortable.tsx +90 -0
- package/src/components/{logic-hooks.tsx → populate-store.tsx} +1 -1
- package/src/errors.ts +5 -0
- package/src/global-classes-styles-provider.ts +34 -23
- package/src/init.ts +10 -4
- package/src/store.ts +49 -13
- package/src/sync-with-document-save.ts +44 -0
|
@@ -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
|
|
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 {
|
|
10
|
-
import {
|
|
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:
|
|
19
|
-
const
|
|
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
|
|
34
|
+
const existingIds = Object.keys( classes );
|
|
35
|
+
const id = generateId( 'g-', existingIds );
|
|
22
36
|
|
|
23
37
|
dispatch(
|
|
24
38
|
slice.actions.add( {
|
|
25
|
-
|
|
26
|
-
|
|
39
|
+
id,
|
|
40
|
+
type: 'class',
|
|
41
|
+
label,
|
|
42
|
+
variants: [],
|
|
27
43
|
} )
|
|
28
44
|
);
|
|
29
45
|
|
|
30
|
-
return
|
|
46
|
+
return id;
|
|
31
47
|
},
|
|
32
|
-
update:
|
|
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:
|
|
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:
|
|
71
|
+
subscribe: ( cb ) => subscribeWithSelector( ( state: StateWithGlobalClasses ) => state.globalClasses, cb ),
|
|
61
72
|
labels: {
|
|
62
|
-
singular: __( 'Global
|
|
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 {
|
|
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-
|
|
21
|
-
component:
|
|
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
|
|
19
|
+
export type GlobalClassesState = {
|
|
19
20
|
items: Record< StyleDefinitionID, StyleDefinition >;
|
|
20
21
|
order: StyleDefinitionID[];
|
|
22
|
+
isDirty: boolean;
|
|
21
23
|
};
|
|
22
24
|
|
|
23
|
-
const initialState:
|
|
25
|
+
const initialState: GlobalClassesState = {
|
|
24
26
|
items: {},
|
|
25
27
|
order: [],
|
|
28
|
+
isDirty: false,
|
|
26
29
|
};
|
|
27
30
|
|
|
28
|
-
export type
|
|
31
|
+
export type StateWithGlobalClasses = SliceState< typeof slice >;
|
|
29
32
|
|
|
30
33
|
// Slice
|
|
31
|
-
|
|
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<
|
|
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<
|
|
42
|
-
state.items[ payload.
|
|
43
|
-
state.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
|
-
|
|
46
|
-
state.items
|
|
47
|
-
|
|
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
|
|
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
|
+
}
|