@elementor/editor-global-classes 0.22.2 → 3.32.0-20
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 +0 -21
- package/dist/index.js +1069 -327
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1049 -279
- package/dist/index.mjs.map +1 -1
- package/package.json +18 -18
- package/src/api.ts +14 -2
- package/src/components/class-manager/class-item.tsx +40 -65
- package/src/components/class-manager/class-manager-button.tsx +4 -0
- package/src/components/class-manager/class-manager-panel.tsx +82 -74
- package/src/components/class-manager/delete-confirmation-dialog.tsx +26 -6
- package/src/components/class-manager/global-classes-list.tsx +76 -57
- package/src/components/class-manager/not-found.tsx +138 -0
- package/src/components/convert-local-class-to-global-class.tsx +62 -0
- package/src/components/css-class-usage/components/css-class-usage-popover.tsx +169 -0
- package/src/components/css-class-usage/components/css-class-usage-trigger.tsx +116 -0
- package/src/components/css-class-usage/components/index.ts +2 -0
- package/src/components/css-class-usage/types.ts +19 -0
- package/src/components/css-class-usage/utils.ts +10 -0
- package/src/components/search-and-filter/components/filter/active-filters.tsx +54 -0
- package/src/components/search-and-filter/components/filter/clear-icon-button.tsx +31 -0
- package/src/components/search-and-filter/components/filter/css-class-filter.tsx +74 -0
- package/src/components/search-and-filter/components/filter/filter-list.tsx +77 -0
- package/src/components/{class-manager → search-and-filter/components/search}/class-manager-search.tsx +12 -11
- package/src/components/search-and-filter/context.tsx +78 -0
- package/src/global-classes-styles-provider.ts +13 -8
- package/src/hooks/use-css-class-usage-by-id.ts +15 -0
- package/src/hooks/use-css-class-usage.ts +13 -0
- package/src/hooks/use-empty-css-class.ts +12 -0
- package/src/hooks/use-filtered-css-class-usage.tsx +67 -0
- package/src/hooks/use-filters.ts +30 -0
- package/src/hooks/use-prefetch-css-class-usage.ts +16 -0
- package/src/init.ts +11 -1
- package/src/store.ts +23 -6
- 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,
|
|
3
|
+
import { Box, InputAdornment, Stack, TextField } from '@elementor/ui';
|
|
4
4
|
import { __ } from '@wordpress/i18n';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
import { useSearchAndFilters } from '../../context';
|
|
7
|
+
|
|
8
|
+
export const ClassManagerSearch = () => {
|
|
9
|
+
const {
|
|
10
|
+
search: { inputValue, handleChange },
|
|
11
|
+
} = useSearchAndFilters();
|
|
10
12
|
|
|
11
|
-
|
|
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={
|
|
20
|
+
value={ inputValue }
|
|
20
21
|
placeholder={ __( 'Search', 'elementor' ) }
|
|
21
|
-
onChange={ ( e: React.ChangeEvent< HTMLInputElement > ) =>
|
|
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
|
-
|
|
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 {
|
|
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< {
|
|
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
|
-
|
|
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
|
+
);
|