@elementor/editor-global-classes 4.1.0-manual → 4.2.0-840

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/src/store.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  getVariantByMeta,
5
5
  type StyleDefinition,
6
6
  type StyleDefinitionID,
7
+ type StyleDefinitionsMap,
7
8
  type StyleDefinitionVariant,
8
9
  } from '@elementor/editor-styles';
9
10
  import { type UpdateActionPayload } from '@elementor/editor-styles-repository';
@@ -19,12 +20,13 @@ import { GlobalClassNotFoundError } from './errors';
19
20
  import { SnapshotHistory } from './utils/snapshot-history';
20
21
 
21
22
  export type GlobalClasses = {
22
- items: Record< StyleDefinitionID, StyleDefinition >;
23
+ items: StyleDefinitionsMap;
23
24
  order: StyleDefinitionID[];
24
25
  };
25
26
 
26
27
  type GlobalClassesState = {
27
28
  data: GlobalClasses;
29
+ classLabels: Record< StyleDefinitionID, string >;
28
30
  initialData: {
29
31
  frontend: GlobalClasses;
30
32
  preview: GlobalClasses;
@@ -43,6 +45,7 @@ const localHistory = SnapshotHistory.get< GlobalClasses >( 'global-classes' );
43
45
 
44
46
  const initialState: GlobalClassesState = {
45
47
  data: { items: {}, order: [] },
48
+ classLabels: {},
46
49
  initialData: {
47
50
  frontend: { items: {}, order: [] },
48
51
  preview: { items: {}, order: [] },
@@ -62,15 +65,17 @@ export const slice = createSlice( {
62
65
  load(
63
66
  state,
64
67
  {
65
- payload: { frontend, preview },
68
+ payload: { frontend, preview, classLabels },
66
69
  }: PayloadAction< {
67
70
  frontend: GlobalClasses;
68
71
  preview: GlobalClasses;
72
+ classLabels: Record< StyleDefinitionID, string >;
69
73
  } >
70
74
  ) {
71
75
  state.initialData.frontend = frontend;
72
76
  state.initialData.preview = preview;
73
77
  state.data = preview;
78
+ state.classLabels = classLabels;
74
79
 
75
80
  state.isDirty = false;
76
81
  },
@@ -79,6 +84,7 @@ export const slice = createSlice( {
79
84
  localHistory.next( state.data );
80
85
  state.data.items[ payload.id ] = payload;
81
86
  state.data.order.unshift( payload.id );
87
+ state.classLabels[ payload.id ] = payload.label;
82
88
 
83
89
  state.isDirty = true;
84
90
  },
@@ -90,6 +96,8 @@ export const slice = createSlice( {
90
96
  );
91
97
 
92
98
  state.data.order = state.data.order.filter( ( id ) => id !== payload );
99
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
100
+ delete state.classLabels[ payload ];
93
101
 
94
102
  state.isDirty = true;
95
103
  },
@@ -119,6 +127,7 @@ export const slice = createSlice( {
119
127
  localHistory.next( state.data );
120
128
  Object.entries( payload ).forEach( ( [ id, { modified } ] ) => {
121
129
  state.data.items[ id ].label = modified;
130
+ state.classLabels[ id ] = modified;
122
131
  } );
123
132
 
124
133
  state.isDirty = false;
@@ -132,6 +141,7 @@ export const slice = createSlice( {
132
141
  meta: StyleDefinitionVariant[ 'meta' ];
133
142
  props: Props;
134
143
  custom_css?: CustomCss | null;
144
+ mode?: 'merge' | 'replace';
135
145
  } >
136
146
  ) {
137
147
  const style = state.data.items[ payload.id ];
@@ -147,10 +157,16 @@ export const slice = createSlice( {
147
157
  customCss = customCss?.raw ? customCss : null;
148
158
 
149
159
  if ( variant ) {
150
- // mergeProps fails with Proxy objects from store, manually re-create clones
151
- const variantProps = JSON.parse( JSON.stringify( variant.props ) ) as Props;
152
160
  const payloadProps = JSON.parse( JSON.stringify( payload.props ) ) as Props;
153
- variant.props = mergeProps( variantProps, payloadProps );
161
+ const mode = payload.mode ?? 'merge';
162
+
163
+ if ( mode === 'replace' ) {
164
+ variant.props = payloadProps;
165
+ } else {
166
+ const variantProps = JSON.parse( JSON.stringify( variant.props ) ) as Props;
167
+ variant.props = mergeProps( variantProps, payloadProps );
168
+ }
169
+
154
170
  variant.custom_css = customCss;
155
171
 
156
172
  style.variants = getNonEmptyVariants( style );
@@ -201,6 +217,33 @@ export const slice = createSlice( {
201
217
  state.isDirty = true;
202
218
  }
203
219
  },
220
+
221
+ mergeExistingClasses(
222
+ state,
223
+ {
224
+ payload: { preview, frontend },
225
+ }: PayloadAction< { preview: GlobalClasses[ 'items' ]; frontend: GlobalClasses[ 'items' ] } >
226
+ ) {
227
+ Object.entries( preview ).forEach( ( [ id, previewClassData ] ) => {
228
+ const frontendClassData = frontend[ id ];
229
+
230
+ if ( previewClassData === null || previewClassData === undefined ) {
231
+ return;
232
+ }
233
+ if ( ! ( id in state.data.items ) ) {
234
+ state.data.items[ id ] = previewClassData;
235
+ }
236
+ if ( ! ( id in state.initialData.frontend.items ) ) {
237
+ state.initialData.frontend.items[ id ] = frontendClassData;
238
+ }
239
+ if ( ! ( id in state.initialData.preview.items ) ) {
240
+ state.initialData.preview.items[ id ] = previewClassData;
241
+ }
242
+ if ( ! ( id in state.classLabels ) ) {
243
+ state.classLabels[ id ] = previewClassData.label;
244
+ }
245
+ } );
246
+ },
204
247
  },
205
248
  } );
206
249
 
@@ -226,9 +269,18 @@ const getNonEmptyVariants = ( style: StyleDefinition ) => {
226
269
  );
227
270
  };
228
271
 
272
+ export const placeholderDefinition = ( id: StyleDefinitionID, label: string ): StyleDefinition => ( {
273
+ id,
274
+ type: 'class',
275
+ label,
276
+ variants: [],
277
+ } );
278
+
229
279
  // Selectors
230
280
  export const selectData = ( state: SliceState< typeof slice > ) => state[ SLICE_NAME ].data;
231
281
 
282
+ export const selectClassLabels = ( state: SliceState< typeof slice > ) => state[ SLICE_NAME ].classLabels;
283
+
232
284
  export const selectFrontendInitialData = ( state: SliceState< typeof slice > ) =>
233
285
  state[ SLICE_NAME ].initialData.frontend;
234
286
 
@@ -241,8 +293,17 @@ export const selectGlobalClasses = createSelector( selectData, ( { items } ) =>
241
293
 
242
294
  export const selectIsDirty = ( state: SliceState< typeof slice > ) => state[ SLICE_NAME ].isDirty;
243
295
 
244
- export const selectOrderedClasses = createSelector( selectGlobalClasses, selectOrder, ( items, order ) =>
245
- order.map( ( id ) => items[ id ] )
296
+ export const selectOrderedClasses = createSelector( selectData, selectClassLabels, ( { items, order }, classLabels ) =>
297
+ order
298
+ .map( ( id ) => {
299
+ const loaded = items[ id ];
300
+ if ( loaded ) {
301
+ return loaded;
302
+ }
303
+ const label = classLabels[ id ];
304
+ return label !== undefined ? placeholderDefinition( id, label ) : null;
305
+ } )
306
+ .filter( ( s ): s is StyleDefinition => s !== null )
246
307
  );
247
308
 
248
309
  export const selectClass = ( state: SliceState< typeof slice >, id: StyleDefinitionID ) =>
@@ -251,3 +312,8 @@ export const selectClass = ( state: SliceState< typeof slice >, id: StyleDefinit
251
312
  export const selectEmptyCssClass = createSelector( selectData, ( { items } ) =>
252
313
  Object.values( items ).filter( ( cssClass ) => cssClass.variants.length === 0 )
253
314
  );
315
+
316
+ export const selectIsClassFetched = ( state: SliceState< typeof slice >, id: StyleDefinitionID ) =>
317
+ !! state[ SLICE_NAME ].initialData.preview.items[ id ] ||
318
+ !! state[ SLICE_NAME ].initialData.frontend.items[ id ] ||
319
+ false;
@@ -1,18 +1,26 @@
1
1
  import { useEffect } from 'react';
2
- import { __privateListenTo as listenTo, v1ReadyEvent } from '@elementor/editor-v1-adapters';
2
+ import { __privateListenTo as listenTo, isExperimentActive, v1ReadyEvent } from '@elementor/editor-v1-adapters';
3
3
 
4
4
  import { usePanelActions } from './components/class-manager/class-manager-panel';
5
5
  import { syncWithDocumentSave } from './sync-with-document-save';
6
6
 
7
7
  export function SyncWithDocumentSave() {
8
- const panelActions = usePanelActions();
8
+ const { open: openClassPanel } = usePanelActions();
9
9
 
10
10
  useEffect( () => {
11
- listenTo( v1ReadyEvent(), () => {
12
- syncWithDocumentSave( panelActions );
11
+ const unsubscribe = listenTo( v1ReadyEvent(), () => {
12
+ const open = isExperimentActive( 'e_editor_design_system_panel' )
13
+ ? () => {
14
+ window.dispatchEvent( new CustomEvent( 'elementor/open-global-classes-manager' ) );
15
+ }
16
+ : openClassPanel;
17
+
18
+ syncWithDocumentSave( { open } );
13
19
  } );
14
20
 
15
- // eslint-disable-next-line react-hooks/exhaustive-deps
21
+ return unsubscribe;
22
+
23
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- bind once at v1 ready; openClassPanel from createPanel is stable
16
24
  }, [] );
17
25
 
18
26
  return null;
@@ -0,0 +1,7 @@
1
+ import { type StyleDefinitionID } from '@elementor/editor-styles';
2
+
3
+ import { type GlobalClassIndexEntry } from '../api';
4
+
5
+ export function createLabelsForClasses( entries: GlobalClassIndexEntry[] ): Record< StyleDefinitionID, string > {
6
+ return Object.fromEntries( entries.map( ( e ) => [ e.id, e.label ] ) );
7
+ }
@@ -5,7 +5,7 @@ import { __getState as getState } from '@elementor/store';
5
5
  import { fetchCssClassUsage } from '../../service/css-class-usage-service';
6
6
  import { GlobalClassTrackingError } from '../errors';
7
7
  import { type FilterKey } from '../hooks/use-filtered-css-class-usage';
8
- import { selectClass } from '../store';
8
+ import { placeholderDefinition, selectClass, selectClassLabels } from '../store';
9
9
 
10
10
  type EventMap = {
11
11
  classCreated: {
@@ -220,16 +220,24 @@ const extractCssClassData = ( classId: StyleDefinitionID ) => {
220
220
  };
221
221
 
222
222
  const getCssClass = ( classId: StyleDefinitionID ) => {
223
- const cssClass = selectClass( getState(), classId );
224
- if ( ! cssClass ) {
225
- throw new Error( `CSS class with ID ${ classId } not found` );
223
+ const state = getState();
224
+ const cssClass = selectClass( state, classId );
225
+
226
+ if ( cssClass ) {
227
+ return cssClass;
228
+ }
229
+
230
+ const label = selectClassLabels( state )[ classId ];
231
+ if ( label !== undefined ) {
232
+ return placeholderDefinition( classId, label );
226
233
  }
227
- return cssClass;
234
+
235
+ throw new Error( `CSS class with ID ${ classId } not found` );
228
236
  };
229
237
 
230
238
  const trackDeleteClass = async ( classId: StyleDefinitionID ) => {
231
- const totalInstances = await getTotalInstancesByCssClassID( classId );
232
239
  const classTitle = getCssClass( classId ).label;
240
+ const totalInstances = await getTotalInstancesByCssClassID( classId );
233
241
  return { totalInstances, classTitle };
234
242
  };
235
243