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

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.
@@ -11,7 +11,12 @@ import {
11
11
  } from '@elementor/ui';
12
12
 
13
13
  export const SortableProvider = < T extends string >( props: UnstableSortableProviderProps< T > ) => (
14
- <UnstableSortableProvider restrictAxis variant="static" dragPlaceholderStyle={ { opacity: '1' } } { ...props } />
14
+ <UnstableSortableProvider
15
+ restrictAxis
16
+ variant="static"
17
+ dragPlaceholderStyle={ { visibility: 'hidden' } }
18
+ { ...props }
19
+ />
15
20
  );
16
21
 
17
22
  export type SortableTriggerProps = React.HTMLAttributes< HTMLDivElement >;
@@ -24,10 +29,11 @@ export const SortableTrigger = ( props: SortableTriggerProps ) => (
24
29
 
25
30
  type SortableItemProps = {
26
31
  id: UnstableSortableItemProps[ 'id' ];
32
+ style?: React.CSSProperties;
27
33
  children: ( props: Partial< UnstableSortableItemRenderProps > ) => React.ReactNode;
28
34
  };
29
35
 
30
- export const SortableItem = ( { children, id, ...props }: SortableItemProps ) => {
36
+ export const SortableItem = ( { children, id, style, ...props }: SortableItemProps ) => {
31
37
  return (
32
38
  <UnstableSortableItem
33
39
  { ...props }
@@ -46,7 +52,7 @@ export const SortableItem = ( { children, id, ...props }: SortableItemProps ) =>
46
52
  return (
47
53
  <Box
48
54
  { ...itemProps }
49
- style={ itemStyle }
55
+ style={ { ...itemStyle, ...( ! isDragOverlay ? style : null ) } }
50
56
  component={ 'li' }
51
57
  role="listitem"
52
58
  sx={ {
@@ -1,59 +1,32 @@
1
1
  import { useEffect } from 'react';
2
+ import { GLOBAL_STYLES_IMPORTED_EVENT, type ImportedGlobalStylesPayload } from '@elementor/editor-canvas';
2
3
  import { __useDispatch as useDispatch } from '@elementor/store';
3
4
 
4
- import { apiClient } from '../api';
5
5
  import { slice } from '../store';
6
6
 
7
7
  export function GlobalStylesImportListener() {
8
8
  const dispatch = useDispatch();
9
9
 
10
10
  useEffect( () => {
11
- const handleGlobalStylesImported = ( event: CustomEvent ) => {
12
- const importedClasses = event.detail?.global_classes;
11
+ const handleGlobalStylesImported = ( event: CustomEvent< ImportedGlobalStylesPayload > ) => {
12
+ const importedClasses = event.detail?.global_classes as ImportedGlobalStylesPayload[ 'global_classes' ];
13
13
 
14
14
  if ( importedClasses?.items && importedClasses?.order ) {
15
+ const { items } = importedClasses;
16
+
15
17
  dispatch(
16
- slice.actions.load( {
17
- preview: {
18
- items: importedClasses.items,
19
- order: importedClasses.order,
20
- },
21
- frontend: {
22
- items: importedClasses.items,
23
- order: importedClasses.order,
24
- },
18
+ slice.actions.mergeExistingClasses( {
19
+ preview: items,
20
+ frontend: items,
25
21
  } )
26
22
  );
27
23
  }
28
-
29
- Promise.all( [ apiClient.all( 'preview' ), apiClient.all( 'frontend' ) ] )
30
- .then( ( [ previewRes, frontendRes ] ) => {
31
- const { data: previewData } = previewRes;
32
- const { data: frontendData } = frontendRes;
33
-
34
- dispatch(
35
- slice.actions.load( {
36
- preview: {
37
- items: previewData.data,
38
- order: previewData.meta.order,
39
- },
40
- frontend: {
41
- items: frontendData.data,
42
- order: frontendData.meta.order,
43
- },
44
- } )
45
- );
46
- } )
47
- .catch( () => {} );
48
24
  };
49
25
 
50
- window.addEventListener( 'elementor/global-styles/imported', handleGlobalStylesImported as EventListener );
26
+ window.addEventListener( GLOBAL_STYLES_IMPORTED_EVENT, handleGlobalStylesImported as EventListener );
51
27
 
52
28
  return () => {
53
- window.removeEventListener(
54
- 'elementor/global-styles/imported',
55
- handleGlobalStylesImported as EventListener
56
- );
29
+ window.removeEventListener( GLOBAL_STYLES_IMPORTED_EVENT, handleGlobalStylesImported as EventListener );
57
30
  };
58
31
  }, [ dispatch ] );
59
32
 
@@ -1,33 +1,19 @@
1
1
  import { useEffect } from 'react';
2
- import { __useDispatch as useDispatch } from '@elementor/store';
2
+ import { registerDataHook } from '@elementor/editor-v1-adapters';
3
3
 
4
- import { apiClient } from '../api';
5
- import { slice } from '../store';
4
+ import { loadCurrentDocumentClasses } from '../load-document-classes';
6
5
 
7
6
  export function PopulateStore() {
8
- const dispatch = useDispatch();
9
-
10
7
  useEffect( () => {
11
- Promise.all( [ apiClient.all( 'preview' ), apiClient.all( 'frontend' ) ] ).then(
12
- ( [ previewRes, frontendRes ] ) => {
13
- const { data: previewData } = previewRes;
14
- const { data: frontendData } = frontendRes;
8
+ // TODO - we run it early to have the labels mapping prior to the canvas rendering
9
+ // but in fact we need a way to re-render any dependant twig-templated widgets/elements once we get the initial data
10
+ // in case the canvas rendering has occurred prior to the resolving of this fetch
11
+ loadCurrentDocumentClasses();
15
12
 
16
- dispatch(
17
- slice.actions.load( {
18
- preview: {
19
- items: previewData.data,
20
- order: previewData.meta.order,
21
- },
22
- frontend: {
23
- items: frontendData.data,
24
- order: frontendData.meta.order,
25
- },
26
- } )
27
- );
28
- }
29
- );
30
- }, [ dispatch ] );
13
+ registerDataHook( 'after', 'editor/documents/attach-preview', async () => {
14
+ await loadCurrentDocumentClasses();
15
+ } );
16
+ }, [] );
31
17
 
32
18
  return null;
33
19
  }
@@ -1,4 +1,9 @@
1
- import { generateId, type StyleDefinition, type StyleDefinitionVariant } from '@elementor/editor-styles';
1
+ import {
2
+ generateId,
3
+ type StyleDefinition,
4
+ type StyleDefinitionID,
5
+ type StyleDefinitionVariant,
6
+ } from '@elementor/editor-styles';
2
7
  import { createStylesProvider } from '@elementor/editor-styles-repository';
3
8
  import {
4
9
  __dispatch as dispatch,
@@ -9,20 +14,23 @@ import { __ } from '@wordpress/i18n';
9
14
 
10
15
  import { getCapabilities } from './capabilities';
11
16
  import { GlobalClassLabelAlreadyExistsError, GlobalClassTrackingError } from './errors';
17
+ import { loadExistingClasses } from './load-existing-classes';
12
18
  import {
19
+ placeholderDefinition,
13
20
  selectClass,
21
+ selectClassLabels,
14
22
  selectData,
15
- selectGlobalClasses,
23
+ selectIsClassFetched,
16
24
  selectOrderedClasses,
17
25
  slice,
18
26
  type StateWithGlobalClasses,
19
27
  } from './store';
20
28
  import { trackGlobalClasses, type TrackingEvent } from './utils/tracking';
21
29
 
22
- const MAX_CLASSES = 100;
30
+ const MAX_CLASSES = 5000;
23
31
 
24
32
  export const GLOBAL_CLASSES_PROVIDER_KEY = 'global-classes';
25
- const PREGENERATED_LINK_PATTERN = /^global-(preview|frontend)-[a-zA-Z_-]+-css$/;
33
+ const PREGENERATED_LINK_PATTERN = /^global-([0-9]+-)?(preview|frontend)-[a-zA-Z_-]+-css$/;
26
34
 
27
35
  export const globalClassesStylesProvider = createStylesProvider( {
28
36
  key: GLOBAL_CLASSES_PROVIDER_KEY,
@@ -37,21 +45,45 @@ export const globalClassesStylesProvider = createStylesProvider( {
37
45
  capabilities: getCapabilities(),
38
46
  actions: {
39
47
  all: () => selectOrderedClasses( getState() ),
40
- get: ( id ) => selectClass( getState(), id ),
48
+ get: ( id ) => {
49
+ const state = getState();
50
+
51
+ const isFetched = selectIsClassFetched( state, id );
52
+ const style = selectClass( state, id );
53
+
54
+ // the isFetched flag is based on the existence of the style in the initial data
55
+ // so if the style is created during the same session - it won't be stored as part of the initial data
56
+ if ( isFetched || style ) {
57
+ return style;
58
+ }
59
+
60
+ loadExistingClasses( [ id ] );
61
+
62
+ const label = selectClassLabels( state )[ id ] ?? id;
63
+ return placeholderDefinition( id, label );
64
+ },
41
65
  resolveCssName: ( id: string ) => {
42
- return selectClass( getState(), id )?.label ?? id;
66
+ const state = getState();
67
+ const loaded = selectClass( state, id );
68
+ if ( loaded ) {
69
+ return loaded.label;
70
+ }
71
+ const fromIndex = selectClassLabels( state )[ id ];
72
+ return fromIndex ?? id;
43
73
  },
44
- create: ( label, variants: StyleDefinitionVariant[] = [] ) => {
45
- const classes = selectGlobalClasses( getState() );
46
-
47
- const existingLabels = Object.values( classes ).map( ( style ) => style.label );
74
+ create: ( label, variants: StyleDefinitionVariant[] = [], id?: StyleDefinitionID ) => {
75
+ const existingClasses = Object.entries( selectClassLabels( getState() ) );
76
+ const existingLabels = existingClasses.map( ( [ , classLabel ] ) => classLabel );
48
77
 
49
78
  if ( existingLabels.includes( label ) ) {
50
79
  throw new GlobalClassLabelAlreadyExistsError( { context: { label } } );
51
80
  }
52
81
 
53
- const existingIds = Object.keys( classes );
54
- const id = generateId( 'g-', existingIds );
82
+ const existingIds = existingClasses.map( ( [ existingId ] ) => existingId );
83
+
84
+ if ( ! id ) {
85
+ id = generateId( 'g-', existingIds );
86
+ }
55
87
 
56
88
  dispatch(
57
89
  slice.actions.add( {
@@ -80,6 +112,7 @@ export const globalClassesStylesProvider = createStylesProvider( {
80
112
  id: args.id,
81
113
  meta: args.meta,
82
114
  props: args.props,
115
+ mode: args.mode,
83
116
  } )
84
117
  );
85
118
  },
@@ -107,10 +140,10 @@ const subscribeWithStates = (
107
140
  let previousState = selectData( getState() );
108
141
 
109
142
  return subscribeWithSelector(
110
- ( state: StateWithGlobalClasses ) => state.globalClasses,
143
+ ( state: StateWithGlobalClasses ) => selectData( state ),
111
144
  ( currentState ) => {
112
- cb( previousState.items, currentState.data.items );
113
- previousState = currentState.data;
145
+ cb( previousState.items, currentState.items );
146
+ previousState = currentState;
114
147
  }
115
148
  );
116
149
  };
package/src/index.ts CHANGED
@@ -1,3 +1,12 @@
1
+ export {
2
+ ClassManagerPanelEmbedded,
3
+ type ClassManagerPanelEmbeddedProps,
4
+ } from './components/class-manager/class-manager-panel';
1
5
  export { GLOBAL_CLASSES_URI } from './mcp-integration/classes-resource';
2
6
 
3
7
  export { init } from './init';
8
+
9
+ export { loadExistingClasses } from './load-existing-classes';
10
+ export { addDocumentClasses } from './load-document-classes';
11
+ export type { GlobalClassIndexEntry } from './api';
12
+ export { createLabelsForClasses } from './utils/create-labels-for-classes';
package/src/init.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  import { getMCPByDomain } from '@elementor/editor-mcp';
8
8
  import { __registerPanel as registerPanel } from '@elementor/editor-panels';
9
9
  import { stylesRepository } from '@elementor/editor-styles-repository';
10
+ import { isExperimentActive } from '@elementor/editor-v1-adapters';
10
11
  import { __registerSlice as registerSlice } from '@elementor/store';
11
12
 
12
13
  import { ClassManagerButton } from './components/class-manager/class-manager-button';
@@ -23,7 +24,10 @@ import { SyncWithDocumentSave } from './sync-with-document';
23
24
 
24
25
  export function init() {
25
26
  registerSlice( slice );
26
- registerPanel( panel );
27
+
28
+ if ( ! isExperimentActive( 'e_editor_design_system_panel' ) ) {
29
+ registerPanel( panel );
30
+ }
27
31
 
28
32
  stylesRepository.register( globalClassesStylesProvider );
29
33
 
@@ -47,10 +51,12 @@ export function init() {
47
51
  component: PrefetchCssClassUsage,
48
52
  } );
49
53
 
50
- injectIntoLogic( {
51
- id: 'global-classes-open-panel-from-url',
52
- component: OpenPanelFromUrl,
53
- } );
54
+ if ( ! isExperimentActive( 'e_editor_design_system_panel' ) ) {
55
+ injectIntoLogic( {
56
+ id: 'global-classes-open-panel-from-url',
57
+ component: OpenPanelFromUrl,
58
+ } );
59
+ }
54
60
 
55
61
  injectIntoCssClassConvert( {
56
62
  id: 'global-classes-convert-from-local-class',
@@ -0,0 +1,76 @@
1
+ import { getCurrentDocument } from '@elementor/editor-documents';
2
+ import { type StyleDefinition, type StyleDefinitionID } from '@elementor/editor-styles';
3
+ import { __dispatch as dispatch } from '@elementor/store';
4
+
5
+ import { apiClient, type StyleDefinitionsNullableMap } from './api';
6
+ import { slice } from './store';
7
+ import { createLabelsForClasses } from './utils/create-labels-for-classes';
8
+
9
+ export function styleDefinitionsMapWithoutNull(
10
+ map: StyleDefinitionsNullableMap
11
+ ): Record< StyleDefinitionID, StyleDefinition > {
12
+ return Object.fromEntries(
13
+ Object.entries( map ).filter(
14
+ ( entry ): entry is [ StyleDefinitionID, StyleDefinition ] => entry[ 1 ] !== null
15
+ )
16
+ );
17
+ }
18
+
19
+ function resetGlobalClassesState( globalOrder: StyleDefinitionID[], classLabels: Record< StyleDefinitionID, string > ) {
20
+ dispatch(
21
+ slice.actions.load( {
22
+ preview: { items: {}, order: globalOrder },
23
+ frontend: { items: {}, order: globalOrder },
24
+ classLabels,
25
+ } )
26
+ );
27
+ }
28
+
29
+ export async function loadCurrentDocumentClasses() {
30
+ const previewIndexRes = await apiClient.all( 'preview' );
31
+ const previewIndex = previewIndexRes.data.data;
32
+ const classLabels = createLabelsForClasses( previewIndex );
33
+ const globalOrder = previewIndex.map( ( e ) => e.id );
34
+
35
+ // This is intended to establish the baseline with current labels and order
36
+ // without it we won't be able to properly resolve the styles' class names
37
+ resetGlobalClassesState( globalOrder, classLabels );
38
+
39
+ const postId = getCurrentDocument()?.id;
40
+ if ( ! postId ) {
41
+ return;
42
+ }
43
+
44
+ const [ previewPostRes, frontendPostRes ] = await Promise.all( [
45
+ apiClient.getStylesForPost( postId, 'preview' ),
46
+ apiClient.getStylesForPost( postId, 'frontend' ),
47
+ ] );
48
+
49
+ const previewItems = styleDefinitionsMapWithoutNull( previewPostRes.data.data );
50
+ const frontendItems = styleDefinitionsMapWithoutNull( frontendPostRes.data.data );
51
+
52
+ dispatch(
53
+ slice.actions.load( {
54
+ preview: { items: previewItems, order: globalOrder },
55
+ frontend: { items: frontendItems, order: globalOrder },
56
+ classLabels,
57
+ } )
58
+ );
59
+ }
60
+
61
+ export async function addDocumentClasses( documentId: number ) {
62
+ const [ previewPostRes, frontendPostRes ] = await Promise.all( [
63
+ apiClient.getStylesForPost( documentId, 'preview' ),
64
+ apiClient.getStylesForPost( documentId, 'frontend' ),
65
+ ] );
66
+
67
+ const previewItems = styleDefinitionsMapWithoutNull( previewPostRes.data.data );
68
+ const frontendItems = styleDefinitionsMapWithoutNull( frontendPostRes.data.data );
69
+
70
+ dispatch(
71
+ slice.actions.mergeExistingClasses( {
72
+ preview: previewItems,
73
+ frontend: frontendItems,
74
+ } )
75
+ );
76
+ }
@@ -0,0 +1,49 @@
1
+ import { type StyleDefinitionID } from '@elementor/editor-styles';
2
+ import { __dispatch as dispatch, __getState as getState } from '@elementor/store';
3
+
4
+ import { apiClient } from './api';
5
+ import { styleDefinitionsMapWithoutNull } from './load-document-classes';
6
+ import { selectGlobalClasses, slice } from './store';
7
+
8
+ let pendingLoad: Promise< void > | null = null;
9
+ const pendingIds = new Set< StyleDefinitionID >();
10
+
11
+ export async function loadExistingClasses( classIds: StyleDefinitionID[] ): Promise< void > {
12
+ const existingClasses = selectGlobalClasses( getState() );
13
+ const missingIds = classIds.filter( ( id ) => ! ( id in existingClasses ) );
14
+
15
+ if ( missingIds.length === 0 ) {
16
+ return;
17
+ }
18
+
19
+ missingIds.forEach( ( id ) => pendingIds.add( id ) );
20
+
21
+ if ( pendingLoad ) {
22
+ await pendingLoad;
23
+ return loadExistingClasses( classIds );
24
+ }
25
+
26
+ pendingLoad = fetchAndMergeClasses();
27
+
28
+ try {
29
+ await pendingLoad;
30
+ } finally {
31
+ pendingLoad = null;
32
+ }
33
+ }
34
+
35
+ async function fetchAndMergeClasses(): Promise< void > {
36
+ const idsToFetch = Array.from( pendingIds );
37
+ pendingIds.clear();
38
+
39
+ if ( idsToFetch.length === 0 ) {
40
+ return;
41
+ }
42
+
43
+ const previewResponse = await apiClient.getStylesByIds( idsToFetch, 'preview' );
44
+ const frontendResponse = await apiClient.getStylesByIds( idsToFetch, 'frontend' );
45
+ const previewItems = styleDefinitionsMapWithoutNull( previewResponse.data.data );
46
+ const frontendItems = styleDefinitionsMapWithoutNull( frontendResponse.data.data );
47
+
48
+ dispatch( slice.actions.mergeExistingClasses( { preview: previewItems, frontend: frontendItems } ) );
49
+ }
@@ -1,13 +1,15 @@
1
1
  import { type MCPRegistryEntry } from '@elementor/editor-mcp';
2
+ import { __getState as getState } from '@elementor/store';
2
3
 
3
4
  import { globalClassesStylesProvider } from '../global-classes-styles-provider';
5
+ import { selectOrderedClasses } from '../store';
4
6
 
5
7
  export const GLOBAL_CLASSES_URI = 'elementor://global-classes';
6
8
 
7
9
  const STORAGE_KEY = 'elementor-global-classes';
8
10
 
9
11
  const updateLocalStorageCache = () => {
10
- const classes = globalClassesStylesProvider.actions.all();
12
+ const classes = selectOrderedClasses( getState() );
11
13
 
12
14
  localStorage.setItem( STORAGE_KEY, JSON.stringify( classes ) );
13
15
  };
@@ -79,7 +79,7 @@ export default function initMcpApplyUnapplyGlobalClasses( server: MCPRegistryEnt
79
79
  - Make sure you have the correct class ID that you want to unapply.
80
80
 
81
81
  <note>
82
- If the user want to unapply a class by it's name and not ID, retreive the id from the list, available at uri elementor://global-classes
82
+ If the user want to unapply a class by it's name and not ID, retrieve the id from the list, available at uri elementor://global-classes
83
83
  </note>
84
84
  `,
85
85
  handler: async ( params ) => {
@@ -12,7 +12,7 @@ export default function initMcpApplyGetGlobalClassUsages( reg: MCPRegistryEntry
12
12
  classId: z
13
13
  .string()
14
14
  .describe(
15
- 'The ID of the class, not visible to the user. To retreive the name of the class, use the "list-global-classes" tool'
15
+ 'The ID of the class, not visible to the user. To retrieve the name of the class, use the "list-global-classes" tool'
16
16
  ),
17
17
  usages: z.array(
18
18
  z.object( {
@@ -32,13 +32,13 @@ export default function initMcpApplyGetGlobalClassUsages( reg: MCPRegistryEntry
32
32
  intelligencePriority: 0.6,
33
33
  speedPriority: 0.8,
34
34
  },
35
- description: `Retreive the usages of global-classes ACCROSS PAGES designed by Elementor editor.
35
+ description: `Retrieve the usages of global-classes ACROSS PAGES designed by Elementor editor.
36
36
 
37
- ## Prequisites: CRITICAL
37
+ ## Prerequisites: CRITICAL
38
38
  - The list of global classes and their applid values is available at resource uri elementor://global-classes
39
39
 
40
40
  ## When to use this tool:
41
- - When a user requests to see where a specific global class is being used accross the site.
41
+ - When a user requests to see where a specific global class is being used across the site.
42
42
  - When you need to manage or clean up unused global classes.
43
43
  - Before deleting a global class, to ensure it is not in use in any other pages.
44
44
 
@@ -17,10 +17,17 @@ export async function saveGlobalClasses( { context, onApprove }: Options ) {
17
17
  const state = selectData( getState() );
18
18
  const apiAction = context === 'preview' ? apiClient.saveDraft : apiClient.publish;
19
19
  const currentContext = context === 'preview' ? selectPreviewInitialData : selectFrontendInitialData;
20
+ const changes = calculateChanges( state, currentContext( getState() ) );
21
+
22
+ const touchedIds = [ ...changes.added, ...changes.modified ];
23
+ const touchedItems = Object.fromEntries(
24
+ touchedIds.map( ( id ) => [ id, state.items[ id ] ] ).filter( ( [ , v ] ) => v )
25
+ );
26
+
20
27
  const response = await apiAction( {
21
- items: state.items,
28
+ items: touchedItems,
22
29
  order: state.order,
23
- changes: calculateChanges( state, currentContext( getState() ) ),
30
+ changes,
24
31
  } );
25
32
 
26
33
  dispatch( slice.actions.reset( { context } ) );
@@ -29,10 +36,12 @@ export async function saveGlobalClasses( { context, onApprove }: Options ) {
29
36
 
30
37
  if ( response?.data?.data?.code === API_ERROR_CODES.DUPLICATED_LABEL ) {
31
38
  dispatch( slice.actions.updateMultiple( response.data.data.modifiedLabels ) );
39
+
32
40
  trackGlobalClasses( {
33
41
  event: 'classPublishConflict',
34
42
  numOfConflicts: Object.keys( response.data.data.modifiedLabels ).length,
35
43
  } );
44
+
36
45
  openDialog( {
37
46
  component: (
38
47
  <DuplicateLabelDialog
@@ -48,11 +57,17 @@ function calculateChanges( state: GlobalClasses, initialData: GlobalClasses ) {
48
57
  const stateIds = Object.keys( state.items );
49
58
  const initialDataIds = Object.keys( initialData.items );
50
59
 
60
+ const { order: stateOrder } = state;
61
+ const { order: initialDataOrder } = initialData;
62
+
63
+ const order = stateOrder.join( ';' ) !== initialDataOrder.join( ';' );
64
+
51
65
  return {
52
66
  added: stateIds.filter( ( id ) => ! initialDataIds.includes( id ) ),
53
67
  deleted: initialDataIds.filter( ( id ) => ! stateIds.includes( id ) ),
54
68
  modified: stateIds.filter( ( id ) => {
55
69
  return id in initialData.items && hash( state.items[ id ] ) !== hash( initialData.items[ id ] );
56
70
  } ),
71
+ order,
57
72
  };
58
73
  }