@elementor/editor-global-classes 0.22.2 → 3.32.0-21

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +0 -21
  2. package/dist/index.js +1069 -327
  3. package/dist/index.js.map +1 -1
  4. package/dist/index.mjs +1049 -279
  5. package/dist/index.mjs.map +1 -1
  6. package/package.json +18 -18
  7. package/src/api.ts +14 -2
  8. package/src/components/class-manager/class-item.tsx +40 -65
  9. package/src/components/class-manager/class-manager-button.tsx +4 -0
  10. package/src/components/class-manager/class-manager-panel.tsx +82 -74
  11. package/src/components/class-manager/delete-confirmation-dialog.tsx +26 -6
  12. package/src/components/class-manager/global-classes-list.tsx +76 -57
  13. package/src/components/class-manager/not-found.tsx +138 -0
  14. package/src/components/convert-local-class-to-global-class.tsx +62 -0
  15. package/src/components/css-class-usage/components/css-class-usage-popover.tsx +169 -0
  16. package/src/components/css-class-usage/components/css-class-usage-trigger.tsx +116 -0
  17. package/src/components/css-class-usage/components/index.ts +2 -0
  18. package/src/components/css-class-usage/types.ts +19 -0
  19. package/src/components/css-class-usage/utils.ts +10 -0
  20. package/src/components/search-and-filter/components/filter/active-filters.tsx +54 -0
  21. package/src/components/search-and-filter/components/filter/clear-icon-button.tsx +31 -0
  22. package/src/components/search-and-filter/components/filter/css-class-filter.tsx +74 -0
  23. package/src/components/search-and-filter/components/filter/filter-list.tsx +77 -0
  24. package/src/components/{class-manager → search-and-filter/components/search}/class-manager-search.tsx +12 -11
  25. package/src/components/search-and-filter/context.tsx +78 -0
  26. package/src/global-classes-styles-provider.ts +13 -8
  27. package/src/hooks/use-css-class-usage-by-id.ts +15 -0
  28. package/src/hooks/use-css-class-usage.ts +13 -0
  29. package/src/hooks/use-empty-css-class.ts +12 -0
  30. package/src/hooks/use-filtered-css-class-usage.tsx +67 -0
  31. package/src/hooks/use-filters.ts +30 -0
  32. package/src/hooks/use-prefetch-css-class-usage.ts +16 -0
  33. package/src/init.ts +11 -1
  34. package/src/store.ts +23 -6
  35. package/src/components/class-manager/class-manager-class-not-found.tsx +0 -56
@@ -0,0 +1,54 @@
1
+ import * as React from 'react';
2
+ import { Chip, Stack } from '@elementor/ui';
3
+ import { __ } from '@wordpress/i18n';
4
+
5
+ import type { FilterKey } from '../../../../hooks/use-filtered-css-class-usage';
6
+ import { useSearchAndFilters } from '../../context';
7
+ import { ClearIconButton } from './clear-icon-button';
8
+ import { filterConfig } from './filter-list';
9
+
10
+ export const ActiveFilters = () => {
11
+ const {
12
+ filters: { filters, setFilters },
13
+ } = useSearchAndFilters();
14
+
15
+ const handleRemove = ( key: FilterKey ) => {
16
+ setFilters( ( prev ) => ( { ...prev, [ key ]: false } ) );
17
+ };
18
+
19
+ const activeKeys = Object.keys( filters ).filter( ( key ): key is FilterKey => filters[ key as FilterKey ] );
20
+
21
+ const showClearIcon = activeKeys.length > 0;
22
+
23
+ return (
24
+ <Stack direction="row" alignItems="center" justifyContent="space-between">
25
+ <Stack direction="row" gap={ 0.5 } alignItems="center" flexWrap="wrap">
26
+ { activeKeys.map( ( key ) => (
27
+ <Chip
28
+ key={ key }
29
+ label={ filterConfig[ key ] }
30
+ onDelete={ () => handleRemove( key ) }
31
+ sx={ chipSx }
32
+ size="tiny"
33
+ />
34
+ ) ) }
35
+ </Stack>
36
+ { showClearIcon && (
37
+ <ClearIconButton
38
+ tooltipText={ __( 'Clear Filters', 'elementor' ) }
39
+ sx={ { margin: '0 0 auto auto' } }
40
+ />
41
+ ) }
42
+ </Stack>
43
+ );
44
+ };
45
+
46
+ const chipSx = {
47
+ '& .MuiChip-deleteIcon': {
48
+ display: 'none',
49
+ transition: 'opacity 0.2s',
50
+ },
51
+ '&:hover .MuiChip-deleteIcon': {
52
+ display: 'block',
53
+ },
54
+ };
@@ -0,0 +1,31 @@
1
+ import * as React from 'react';
2
+ import { BrushBigIcon } from '@elementor/icons';
3
+ import { Box, IconButton, styled, type SxProps, type Theme, Tooltip } from '@elementor/ui';
4
+
5
+ import { useSearchAndFilters } from '../../context';
6
+
7
+ type ClearIconButtonProps = { tooltipText: React.ReactNode; sx?: SxProps< Theme > };
8
+
9
+ export const ClearIconButton = ( { tooltipText, sx }: ClearIconButtonProps ) => {
10
+ const {
11
+ filters: { onClearFilter },
12
+ } = useSearchAndFilters();
13
+
14
+ return (
15
+ <Tooltip title={ tooltipText } placement="top" disableInteractive>
16
+ <Box>
17
+ <CustomIconButton aria-label={ tooltipText } size="tiny" onClick={ onClearFilter } sx={ sx }>
18
+ <BrushBigIcon fontSize="tiny" />
19
+ </CustomIconButton>
20
+ </Box>
21
+ </Tooltip>
22
+ );
23
+ };
24
+ const CustomIconButton = styled( IconButton )( ( { theme } ) => ( {
25
+ '&.Mui-disabled': {
26
+ pointerEvents: 'auto',
27
+ '&:hover': {
28
+ color: theme.palette.action.disabled,
29
+ },
30
+ },
31
+ } ) );
@@ -0,0 +1,74 @@
1
+ import * as React from 'react';
2
+ import { PopoverBody, PopoverHeader } from '@elementor/editor-ui';
3
+ import { FilterIcon } from '@elementor/icons';
4
+ import { bindPopover, bindToggle, Divider, Popover, ToggleButton, Tooltip, usePopupState } from '@elementor/ui';
5
+ import { __ } from '@wordpress/i18n';
6
+
7
+ import { useSearchAndFilters } from '../../context';
8
+ import { ClearIconButton } from './clear-icon-button';
9
+ import { FilterList } from './filter-list';
10
+
11
+ export const CssClassFilter = () => {
12
+ const {
13
+ filters: { filters },
14
+ } = useSearchAndFilters();
15
+ const popupState = usePopupState( {
16
+ variant: 'popover',
17
+ disableAutoFocus: true,
18
+ } );
19
+
20
+ const showCleanIcon = Object.values( filters ).some( ( value ) => value );
21
+
22
+ return (
23
+ <>
24
+ <Tooltip title={ __( 'Filters', 'elementor' ) } placement="top">
25
+ <ToggleButton
26
+ value="filter"
27
+ size={ 'tiny' }
28
+ selected={ popupState.isOpen }
29
+ { ...bindToggle( popupState ) }
30
+ >
31
+ <FilterIcon fontSize="tiny" />
32
+ </ToggleButton>
33
+ </Tooltip>
34
+ <Popover
35
+ sx={ {
36
+ maxWidth: '344px',
37
+ } }
38
+ anchorOrigin={ {
39
+ vertical: 'top',
40
+ horizontal: 'right',
41
+ } }
42
+ transformOrigin={ {
43
+ vertical: 'top',
44
+ horizontal: -21,
45
+ } }
46
+ { ...bindPopover( popupState ) }
47
+ >
48
+ <PopoverHeader
49
+ actions={
50
+ showCleanIcon
51
+ ? [
52
+ <ClearIconButton
53
+ key="clear-all-button"
54
+ tooltipText={ __( 'Clear all', 'elementor' ) }
55
+ />,
56
+ ]
57
+ : []
58
+ }
59
+ onClose={ popupState.close }
60
+ title={ __( 'Filters', 'elementor' ) }
61
+ icon={ <FilterIcon fontSize={ 'tiny' } /> }
62
+ />
63
+ <Divider
64
+ sx={ {
65
+ borderWidth: '1px 0 0 0',
66
+ } }
67
+ />
68
+ <PopoverBody width={ 344 } height={ 125 }>
69
+ <FilterList />
70
+ </PopoverBody>
71
+ </Popover>
72
+ </>
73
+ );
74
+ };
@@ -0,0 +1,77 @@
1
+ import * as React from 'react';
2
+ import { Checkbox, Chip, MenuItem, MenuList, Stack, Typography } from '@elementor/ui';
3
+ import { __ } from '@wordpress/i18n';
4
+
5
+ import { type FilterKey, useFilteredCssClassUsage } from '../../../../hooks/use-filtered-css-class-usage';
6
+ import { useSearchAndFilters } from '../../context';
7
+
8
+ export const filterConfig: Record< FilterKey, string > = {
9
+ unused: __( 'Unused', 'elementor' ),
10
+ empty: __( 'Empty', 'elementor' ),
11
+ onThisPage: __( 'On this page', 'elementor' ),
12
+ };
13
+
14
+ export const FilterList = () => {
15
+ const {
16
+ filters: { filters, setFilters },
17
+ } = useSearchAndFilters();
18
+ const filteredCssClass = useFilteredCssClassUsage();
19
+
20
+ const handleOnClick = ( value: FilterKey ) => {
21
+ setFilters( ( prev ) => ( { ...prev, [ value ]: ! prev[ value ] } ) );
22
+ };
23
+
24
+ return (
25
+ <MenuList>
26
+ <MenuItem onClick={ () => handleOnClick( 'unused' ) }>
27
+ <LabeledCheckbox
28
+ label={ filterConfig.unused }
29
+ checked={ filters.unused }
30
+ suffix={ <Chip size={ 'tiny' } sx={ { ml: 'auto' } } label={ filteredCssClass.unused.length } /> }
31
+ />
32
+ </MenuItem>
33
+ <MenuItem onClick={ () => handleOnClick( 'empty' ) }>
34
+ <LabeledCheckbox
35
+ label={ filterConfig.empty }
36
+ checked={ filters.empty }
37
+ suffix={ <Chip size={ 'tiny' } sx={ { ml: 'auto' } } label={ filteredCssClass.empty.length } /> }
38
+ />
39
+ </MenuItem>
40
+ <MenuItem onClick={ () => handleOnClick( 'onThisPage' ) }>
41
+ <LabeledCheckbox
42
+ label={ filterConfig.onThisPage }
43
+ checked={ filters.onThisPage }
44
+ suffix={
45
+ <Chip size={ 'tiny' } sx={ { ml: 'auto' } } label={ filteredCssClass.onThisPage.length } />
46
+ }
47
+ />
48
+ </MenuItem>
49
+ </MenuList>
50
+ );
51
+ };
52
+
53
+ type LabeledCheckboxProps = {
54
+ label: string;
55
+ suffix?: React.ReactNode;
56
+ checked: boolean;
57
+ };
58
+
59
+ const LabeledCheckbox = ( { label, suffix, checked }: LabeledCheckboxProps ) => (
60
+ <Stack direction="row" alignItems="center" gap={ 0.5 } flex={ 1 }>
61
+ <Checkbox
62
+ size={ 'small' }
63
+ checked={ checked }
64
+ sx={ {
65
+ padding: 0,
66
+ color: 'text.tertiary',
67
+ '&.Mui-checked': {
68
+ color: 'text.tertiary',
69
+ },
70
+ } }
71
+ />
72
+ <Typography variant="caption" sx={ { color: 'text.secondary' } }>
73
+ { label }
74
+ </Typography>
75
+ { suffix }
76
+ </Stack>
77
+ );
@@ -1,24 +1,25 @@
1
1
  import * as React from 'react';
2
2
  import { SearchIcon } from '@elementor/icons';
3
- import { Box, Grid, InputAdornment, Stack, TextField } from '@elementor/ui';
3
+ import { Box, InputAdornment, Stack, TextField } from '@elementor/ui';
4
4
  import { __ } from '@wordpress/i18n';
5
5
 
6
- type ClassMangerSearchProps = {
7
- searchValue: string;
8
- onChange: ( value: string ) => void;
9
- };
6
+ import { useSearchAndFilters } from '../../context';
7
+
8
+ export const ClassManagerSearch = () => {
9
+ const {
10
+ search: { inputValue, handleChange },
11
+ } = useSearchAndFilters();
10
12
 
11
- export const ClassManagerSearch = ( { searchValue, onChange }: ClassMangerSearchProps ) => (
12
- <Grid item xs={ 6 } px={ 2 } pb={ 1 }>
13
+ return (
13
14
  <Stack direction="row" gap={ 0.5 } sx={ { width: '100%' } }>
14
15
  <Box sx={ { flexGrow: 1 } }>
15
16
  <TextField
16
17
  role={ 'search' }
17
18
  fullWidth
18
19
  size={ 'tiny' }
19
- value={ searchValue }
20
+ value={ inputValue }
20
21
  placeholder={ __( 'Search', 'elementor' ) }
21
- onChange={ ( e: React.ChangeEvent< HTMLInputElement > ) => onChange( e.target.value ) }
22
+ onChange={ ( e: React.ChangeEvent< HTMLInputElement > ) => handleChange( e.target.value ) }
22
23
  InputProps={ {
23
24
  startAdornment: (
24
25
  <InputAdornment position="start">
@@ -29,5 +30,5 @@ export const ClassManagerSearch = ( { searchValue, onChange }: ClassMangerSearch
29
30
  />
30
31
  </Box>
31
32
  </Stack>
32
- </Grid>
33
- );
33
+ );
34
+ };
@@ -0,0 +1,78 @@
1
+ import * as React from 'react';
2
+ import { createContext, useContext } from 'react';
3
+ import { useDebounceState } from '@elementor/utils';
4
+
5
+ export type CheckedFilters = {
6
+ empty: boolean;
7
+ onThisPage: boolean;
8
+ unused: boolean;
9
+ };
10
+
11
+ type SearchContextType = {
12
+ debouncedValue: string;
13
+ inputValue: string;
14
+ handleChange: ( value: string ) => void;
15
+ onClearSearch: () => void;
16
+ };
17
+ type FilterAndSortContextType = {
18
+ filters: CheckedFilters;
19
+ setFilters: React.Dispatch< React.SetStateAction< CheckedFilters > >;
20
+ onClearFilter: () => void;
21
+ };
22
+
23
+ export type SearchAndFilterContextType = {
24
+ search: SearchContextType;
25
+ filters: FilterAndSortContextType;
26
+ };
27
+
28
+ const SearchAndFilterContext = createContext< SearchAndFilterContextType | undefined >( undefined );
29
+
30
+ const INIT_CHECKED_FILTERS: CheckedFilters = {
31
+ empty: false,
32
+ onThisPage: false,
33
+ unused: false,
34
+ };
35
+
36
+ export const SearchAndFilterProvider = ( { children }: React.PropsWithChildren ) => {
37
+ const [ filters, setFilters ] = React.useState< CheckedFilters >( INIT_CHECKED_FILTERS );
38
+ const { debouncedValue, inputValue, handleChange } = useDebounceState( {
39
+ delay: 300,
40
+ initialValue: '',
41
+ } );
42
+
43
+ const onClearSearch = () => {
44
+ handleChange( '' );
45
+ };
46
+
47
+ const onClearFilter = () => {
48
+ setFilters( INIT_CHECKED_FILTERS );
49
+ };
50
+
51
+ return (
52
+ <SearchAndFilterContext.Provider
53
+ value={ {
54
+ search: {
55
+ debouncedValue,
56
+ inputValue,
57
+ handleChange,
58
+ onClearSearch,
59
+ },
60
+ filters: {
61
+ filters,
62
+ setFilters,
63
+ onClearFilter,
64
+ },
65
+ } }
66
+ >
67
+ { children }
68
+ </SearchAndFilterContext.Provider>
69
+ );
70
+ };
71
+
72
+ export const useSearchAndFilters = () => {
73
+ const context = useContext( SearchAndFilterContext );
74
+ if ( ! context ) {
75
+ throw new Error( 'useSearchContext must be used within a SearchContextProvider' );
76
+ }
77
+ return context;
78
+ };
@@ -1,6 +1,5 @@
1
- import { generateId } from '@elementor/editor-styles';
1
+ import { generateId, type StyleDefinitionVariant } from '@elementor/editor-styles';
2
2
  import { createStylesProvider } from '@elementor/editor-styles-repository';
3
- import { isExperimentActive } from '@elementor/editor-v1-adapters';
4
3
  import {
5
4
  __dispatch as dispatch,
6
5
  __getState as getState,
@@ -30,13 +29,9 @@ export const globalClassesStylesProvider = createStylesProvider( {
30
29
  all: () => selectOrderedClasses( getState() ),
31
30
  get: ( id ) => selectClass( getState(), id ),
32
31
  resolveCssName: ( id: string ) => {
33
- if ( ! isExperimentActive( 'e_v_3_30' ) ) {
34
- return id;
35
- }
36
-
37
32
  return selectClass( getState(), id )?.label ?? id;
38
33
  },
39
- create: ( label ) => {
34
+ create: ( label, variants: StyleDefinitionVariant[] = [] ) => {
40
35
  const classes = selectGlobalClasses( getState() );
41
36
 
42
37
  const existingLabels = Object.values( classes ).map( ( style ) => style.label );
@@ -53,7 +48,7 @@ export const globalClassesStylesProvider = createStylesProvider( {
53
48
  id,
54
49
  type: 'class',
55
50
  label,
56
- variants: [],
51
+ variants,
57
52
  } )
58
53
  );
59
54
 
@@ -78,5 +73,15 @@ export const globalClassesStylesProvider = createStylesProvider( {
78
73
  } )
79
74
  );
80
75
  },
76
+ updateCustomCss: ( args ) => {
77
+ dispatch(
78
+ slice.actions.updateProps( {
79
+ id: args.id,
80
+ meta: args.meta,
81
+ custom_css: args.custom_css,
82
+ props: {},
83
+ } )
84
+ );
85
+ },
81
86
  },
82
87
  } );
@@ -0,0 +1,15 @@
1
+ import { type EnhancedCssClassUsageContent } from '../components/css-class-usage/types';
2
+ import { useCssClassUsage } from './use-css-class-usage';
3
+
4
+ const EMPTY_CLASS_USAGE: EnhancedCssClassUsageContent = {
5
+ total: 0,
6
+ content: [],
7
+ };
8
+
9
+ export const useCssClassUsageByID = (
10
+ id: string
11
+ ): { data: EnhancedCssClassUsageContent; isLoading: boolean; isSuccess?: boolean } => {
12
+ const { data, ...rest } = useCssClassUsage();
13
+ const classData = data?.[ id ] ?? EMPTY_CLASS_USAGE;
14
+ return { ...rest, data: classData };
15
+ };
@@ -0,0 +1,13 @@
1
+ import { useQuery, type UseQueryResult } from '@elementor/query';
2
+
3
+ import { fetchCssClassUsage } from '../../service/css-class-usage-service';
4
+ import { type EnhancedCssClassUsage, QUERY_KEY } from '../components/css-class-usage/types';
5
+
6
+ export const useCssClassUsage = (): UseQueryResult< EnhancedCssClassUsage > => {
7
+ return useQuery( {
8
+ queryKey: [ QUERY_KEY ],
9
+ queryFn: fetchCssClassUsage,
10
+ refetchOnMount: false,
11
+ refetchOnWindowFocus: true,
12
+ } );
13
+ };
@@ -0,0 +1,12 @@
1
+ import { __useSelector } from '@elementor/store';
2
+
3
+ import { selectEmptyCssClass, selectGlobalClasses } from '../store';
4
+
5
+ export const useEmptyCssClass = () => {
6
+ return __useSelector( selectEmptyCssClass );
7
+ };
8
+
9
+ export const useAllCssClassesIDs = () => {
10
+ const cssClasses = __useSelector( selectGlobalClasses );
11
+ return Object.keys( cssClasses );
12
+ };
@@ -0,0 +1,67 @@
1
+ import { useMemo } from 'react';
2
+ import { __useActiveDocument as useActiveDocument } from '@elementor/editor-documents';
3
+
4
+ import { type CssClassUsageContent, type EnhancedCssClassUsage } from '../components/css-class-usage/types';
5
+ import { useCssClassUsage } from './use-css-class-usage';
6
+ import { useAllCssClassesIDs, useEmptyCssClass } from './use-empty-css-class';
7
+
8
+ export type FilterKey = 'empty' | 'onThisPage' | 'unused';
9
+
10
+ type FilteredCssClassUsage = Record< FilterKey, string[] >;
11
+
12
+ const findCssClassKeysByPageID = ( data: EnhancedCssClassUsage, pageId: number ) => {
13
+ const result: string[] = [];
14
+ for ( const key in data ) {
15
+ data[ key ].content.forEach( ( content: CssClassUsageContent ) => {
16
+ if ( +content.pageId === pageId ) {
17
+ result.push( key );
18
+ }
19
+ } );
20
+ }
21
+ return result;
22
+ };
23
+
24
+ const getUnusedClasses = ( usedCssClass: string[], potentialUnused: string[] ): string[] => {
25
+ const set = new Set( usedCssClass );
26
+ return potentialUnused.filter( ( cssClass: string ) => ! set.has( cssClass ) );
27
+ };
28
+
29
+ const EMPTY_FILTERED_CSS_CLASS_RESPONSE: FilteredCssClassUsage = {
30
+ empty: [],
31
+ onThisPage: [],
32
+ unused: [],
33
+ };
34
+
35
+ export const useFilteredCssClassUsage = (): FilteredCssClassUsage => {
36
+ const document = useActiveDocument();
37
+ const emptyCssClasses = useEmptyCssClass();
38
+ const { data, isLoading } = useCssClassUsage();
39
+ const listOfCssClasses = useAllCssClassesIDs();
40
+
41
+ const emptyCssClassesIDs = useMemo( () => emptyCssClasses.map( ( { id } ) => id ), [ emptyCssClasses ] );
42
+
43
+ const onThisPage = useMemo( () => {
44
+ if ( ! data || ! document ) {
45
+ return [];
46
+ }
47
+ return findCssClassKeysByPageID( data, document.id );
48
+ }, [ data, document ] );
49
+
50
+ const unused = useMemo( () => {
51
+ if ( ! data ) {
52
+ return [];
53
+ }
54
+
55
+ return getUnusedClasses( Object.keys( data ), listOfCssClasses );
56
+ }, [ data, listOfCssClasses ] );
57
+
58
+ if ( isLoading || ! data || ! document ) {
59
+ return EMPTY_FILTERED_CSS_CLASS_RESPONSE;
60
+ }
61
+
62
+ return {
63
+ onThisPage,
64
+ unused,
65
+ empty: emptyCssClassesIDs,
66
+ };
67
+ };
@@ -0,0 +1,30 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import { useSearchAndFilters } from '../components/search-and-filter/context';
4
+ import { type FilterKey, useFilteredCssClassUsage } from './use-filtered-css-class-usage';
5
+
6
+ export const useFilters = () => {
7
+ const {
8
+ filters: { filters },
9
+ } = useSearchAndFilters();
10
+ const allFilters = useFilteredCssClassUsage();
11
+
12
+ return useMemo( () => {
13
+ const activeEntries = Object.entries( filters ).filter( ( [ , isActive ] ) => isActive ) as [
14
+ FilterKey,
15
+ true,
16
+ ][];
17
+
18
+ if ( activeEntries.length === 0 ) {
19
+ return null;
20
+ }
21
+
22
+ return activeEntries.reduce< string[] >( ( acc, [ key ], index ) => {
23
+ const current = allFilters[ key ] || [];
24
+ if ( index === 0 ) {
25
+ return current;
26
+ }
27
+ return acc.filter( ( val ) => current.includes( val ) );
28
+ }, [] );
29
+ }, [ filters, allFilters ] );
30
+ };
@@ -0,0 +1,16 @@
1
+ import { useQueryClient } from '@elementor/query';
2
+
3
+ import { fetchCssClassUsage } from '../../service/css-class-usage-service';
4
+ import { QUERY_KEY } from '../components/css-class-usage/types';
5
+
6
+ export function usePrefetchCssClassUsage() {
7
+ const queryClient = useQueryClient();
8
+
9
+ const prefetchClassesUsage = () =>
10
+ queryClient.prefetchQuery( {
11
+ queryKey: [ QUERY_KEY ],
12
+ queryFn: fetchCssClassUsage,
13
+ } );
14
+
15
+ return { prefetchClassesUsage };
16
+ }
package/src/init.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import { injectIntoLogic } from '@elementor/editor';
2
- import { injectIntoClassSelectorActions, registerStyleProviderToColors } from '@elementor/editor-editing-panel';
2
+ import {
3
+ injectIntoClassSelectorActions,
4
+ injectIntoCssClassConvert,
5
+ registerStyleProviderToColors,
6
+ } from '@elementor/editor-editing-panel';
3
7
  import { __registerPanel as registerPanel } from '@elementor/editor-panels';
4
8
  import { stylesRepository } from '@elementor/editor-styles-repository';
5
9
  import { __privateListenTo as listenTo, v1ReadyEvent } from '@elementor/editor-v1-adapters';
@@ -7,6 +11,7 @@ import { __registerSlice as registerSlice } from '@elementor/store';
7
11
 
8
12
  import { ClassManagerButton } from './components/class-manager/class-manager-button';
9
13
  import { panel } from './components/class-manager/class-manager-panel';
14
+ import { ConvertLocalClassToGlobalClass } from './components/convert-local-class-to-global-class';
10
15
  import { PopulateStore } from './components/populate-store';
11
16
  import { GLOBAL_CLASSES_PROVIDER_KEY, globalClassesStylesProvider } from './global-classes-styles-provider';
12
17
  import { slice } from './store';
@@ -23,6 +28,11 @@ export function init() {
23
28
  component: PopulateStore,
24
29
  } );
25
30
 
31
+ injectIntoCssClassConvert( {
32
+ id: 'global-classes-convert-from-local-class',
33
+ component: ConvertLocalClassToGlobalClass,
34
+ } );
35
+
26
36
  injectIntoClassSelectorActions( {
27
37
  id: 'global-classes-manager-button',
28
38
  component: ClassManagerButton,
package/src/store.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { mergeProps, type Props } from '@elementor/editor-props';
2
2
  import {
3
+ type CustomCss,
3
4
  getVariantByMeta,
4
5
  type StyleDefinition,
5
6
  type StyleDefinitionID,
@@ -111,26 +112,32 @@ export const slice = createSlice( {
111
112
  state,
112
113
  {
113
114
  payload,
114
- }: PayloadAction< { id: StyleDefinitionID; meta: StyleDefinitionVariant[ 'meta' ]; props: Props } >
115
+ }: PayloadAction< {
116
+ id: StyleDefinitionID;
117
+ meta: StyleDefinitionVariant[ 'meta' ];
118
+ props: Props;
119
+ custom_css?: CustomCss | null;
120
+ } >
115
121
  ) {
116
122
  const style = state.data.items[ payload.id ];
117
123
 
118
124
  if ( ! style ) {
119
125
  throw new GlobalClassNotFoundError( { context: { styleId: payload.id } } );
120
126
  }
127
+
121
128
  localHistory.next( state.data );
122
129
 
123
130
  const variant = getVariantByMeta( style, payload.meta );
131
+ let customCss = ( 'custom_css' in payload ? payload.custom_css : variant?.custom_css ) ?? null;
132
+ customCss = customCss?.raw ? customCss : null;
124
133
 
125
134
  if ( variant ) {
126
135
  variant.props = mergeProps( variant.props, payload.props );
136
+ variant.custom_css = customCss;
127
137
 
128
- if ( Object.keys( variant.props ).length === 0 ) {
129
- // If the props object is empty after merging, we remove the variant.
130
- style.variants = style.variants.filter( ( v ) => v !== variant );
131
- }
138
+ style.variants = getNonEmptyVariants( style );
132
139
  } else {
133
- style.variants.push( { meta: payload.meta, props: payload.props } );
140
+ style.variants.push( { meta: payload.meta, props: payload.props, custom_css: customCss } );
134
141
  }
135
142
 
136
143
  state.isDirty = true;
@@ -179,6 +186,12 @@ export const slice = createSlice( {
179
186
  },
180
187
  } );
181
188
 
189
+ const getNonEmptyVariants = ( style: StyleDefinition ) => {
190
+ return style.variants.filter(
191
+ ( { props, custom_css: customCss }: StyleDefinitionVariant ) => Object.keys( props ).length || customCss?.raw
192
+ );
193
+ };
194
+
182
195
  // Selectors
183
196
  export const selectData = ( state: SliceState< typeof slice > ) => state[ SLICE_NAME ].data;
184
197
 
@@ -200,3 +213,7 @@ export const selectOrderedClasses = createSelector( selectGlobalClasses, selectO
200
213
 
201
214
  export const selectClass = ( state: SliceState< typeof slice >, id: StyleDefinitionID ) =>
202
215
  state[ SLICE_NAME ].data.items[ id ] ?? null;
216
+
217
+ export const selectEmptyCssClass = createSelector( selectData, ( { items } ) =>
218
+ Object.values( items ).filter( ( cssClass ) => cssClass.variants.length === 0 )
219
+ );