@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
|
@@ -1,47 +1,34 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { useEffect, useMemo } from 'react';
|
|
3
|
-
import { type StyleDefinitionID } from '@elementor/editor-styles';
|
|
3
|
+
import { type StyleDefinition, type StyleDefinitionID } from '@elementor/editor-styles';
|
|
4
4
|
import { __useDispatch as useDispatch } from '@elementor/store';
|
|
5
5
|
import { List, Stack, styled, Typography, type TypographyProps } from '@elementor/ui';
|
|
6
6
|
import { __ } from '@wordpress/i18n';
|
|
7
7
|
|
|
8
8
|
import { useClassesOrder } from '../../hooks/use-classes-order';
|
|
9
|
+
import { useFilters } from '../../hooks/use-filters';
|
|
9
10
|
import { useOrderedClasses } from '../../hooks/use-ordered-classes';
|
|
10
11
|
import { slice } from '../../store';
|
|
12
|
+
import { useSearchAndFilters } from '../search-and-filter/context';
|
|
11
13
|
import { ClassItem } from './class-item';
|
|
12
|
-
import { CssClassNotFound } from './class-manager-class-not-found';
|
|
13
14
|
import { DeleteConfirmationProvider } from './delete-confirmation-dialog';
|
|
14
15
|
import { FlippedColorSwatchIcon } from './flipped-color-swatch-icon';
|
|
16
|
+
import { getNotFoundType, NotFound } from './not-found';
|
|
15
17
|
import { SortableItem, SortableProvider } from './sortable';
|
|
16
18
|
|
|
17
19
|
type GlobalClassesListProps = {
|
|
18
20
|
disabled?: boolean;
|
|
19
|
-
searchValue: string;
|
|
20
|
-
onSearch: ( searchValue: string ) => void;
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
export const GlobalClassesList = ( { disabled
|
|
23
|
+
export const GlobalClassesList = ( { disabled }: GlobalClassesListProps ) => {
|
|
24
|
+
const {
|
|
25
|
+
search: { debouncedValue: searchValue },
|
|
26
|
+
} = useSearchAndFilters();
|
|
24
27
|
const cssClasses = useOrderedClasses();
|
|
25
28
|
const dispatch = useDispatch();
|
|
26
|
-
|
|
29
|
+
const filters = useFilters();
|
|
27
30
|
const [ classesOrder, reorderClasses ] = useReorder();
|
|
28
|
-
|
|
29
|
-
const lowercaseLabels = useMemo(
|
|
30
|
-
() =>
|
|
31
|
-
cssClasses.map( ( cssClass ) => ( {
|
|
32
|
-
...cssClass,
|
|
33
|
-
lowerLabel: cssClass.label.toLowerCase(),
|
|
34
|
-
} ) ),
|
|
35
|
-
[ cssClasses ]
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
const filteredClasses = useMemo( () => {
|
|
39
|
-
return searchValue.length > 1
|
|
40
|
-
? lowercaseLabels.filter( ( cssClass ) =>
|
|
41
|
-
cssClass.lowerLabel.toLowerCase().includes( searchValue.toLowerCase() )
|
|
42
|
-
)
|
|
43
|
-
: cssClasses;
|
|
44
|
-
}, [ searchValue, cssClasses, lowercaseLabels ] );
|
|
31
|
+
const filteredCssClasses = useFilteredCssClasses();
|
|
45
32
|
|
|
46
33
|
useEffect( () => {
|
|
47
34
|
const handler = ( event: KeyboardEvent ) => {
|
|
@@ -65,42 +52,43 @@ export const GlobalClassesList = ( { disabled, searchValue, onSearch }: GlobalCl
|
|
|
65
52
|
return <EmptyState />;
|
|
66
53
|
}
|
|
67
54
|
|
|
55
|
+
const notFoundType = getNotFoundType( searchValue, filters, filteredCssClasses );
|
|
56
|
+
|
|
57
|
+
if ( notFoundType ) {
|
|
58
|
+
return <NotFound notFoundType={ notFoundType } />;
|
|
59
|
+
}
|
|
60
|
+
|
|
68
61
|
return (
|
|
69
62
|
<DeleteConfirmationProvider>
|
|
70
|
-
{
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
);
|
|
100
|
-
} ) }
|
|
101
|
-
</SortableProvider>
|
|
102
|
-
</List>
|
|
103
|
-
) }
|
|
63
|
+
<List sx={ { display: 'flex', flexDirection: 'column', gap: 0.5 } }>
|
|
64
|
+
<SortableProvider value={ classesOrder } onChange={ reorderClasses }>
|
|
65
|
+
{ filteredCssClasses?.map( ( { id, label } ) => {
|
|
66
|
+
return (
|
|
67
|
+
<SortableItem key={ id } id={ id }>
|
|
68
|
+
{ ( { isDragged, isDragPlaceholder, triggerProps, triggerStyle } ) => (
|
|
69
|
+
<ClassItem
|
|
70
|
+
id={ id }
|
|
71
|
+
label={ label }
|
|
72
|
+
renameClass={ ( newLabel: string ) => {
|
|
73
|
+
dispatch(
|
|
74
|
+
slice.actions.update( {
|
|
75
|
+
style: {
|
|
76
|
+
id,
|
|
77
|
+
label: newLabel,
|
|
78
|
+
},
|
|
79
|
+
} )
|
|
80
|
+
);
|
|
81
|
+
} }
|
|
82
|
+
selected={ isDragged }
|
|
83
|
+
disabled={ disabled || isDragPlaceholder }
|
|
84
|
+
sortableTriggerProps={ { ...triggerProps, style: triggerStyle } }
|
|
85
|
+
/>
|
|
86
|
+
) }
|
|
87
|
+
</SortableItem>
|
|
88
|
+
);
|
|
89
|
+
} ) }
|
|
90
|
+
</SortableProvider>
|
|
91
|
+
</List>
|
|
104
92
|
</DeleteConfirmationProvider>
|
|
105
93
|
);
|
|
106
94
|
};
|
|
@@ -137,3 +125,34 @@ const useReorder = () => {
|
|
|
137
125
|
|
|
138
126
|
return [ order, reorder ] as const;
|
|
139
127
|
};
|
|
128
|
+
|
|
129
|
+
const useFilteredCssClasses = (): StyleDefinition[] => {
|
|
130
|
+
const cssClasses = useOrderedClasses();
|
|
131
|
+
const {
|
|
132
|
+
search: { debouncedValue: searchValue },
|
|
133
|
+
} = useSearchAndFilters();
|
|
134
|
+
const filters = useFilters();
|
|
135
|
+
|
|
136
|
+
const lowercaseLabels = useMemo(
|
|
137
|
+
() =>
|
|
138
|
+
cssClasses.map( ( cssClass ) => ( {
|
|
139
|
+
...cssClass,
|
|
140
|
+
lowerLabel: cssClass.label.toLowerCase(),
|
|
141
|
+
} ) ),
|
|
142
|
+
[ cssClasses ]
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const filteredClasses = useMemo( () => {
|
|
146
|
+
if ( searchValue.length > 1 ) {
|
|
147
|
+
return lowercaseLabels.filter( ( cssClass ) => cssClass.lowerLabel.includes( searchValue.toLowerCase() ) );
|
|
148
|
+
}
|
|
149
|
+
return cssClasses;
|
|
150
|
+
}, [ searchValue, cssClasses, lowercaseLabels ] );
|
|
151
|
+
|
|
152
|
+
return useMemo( () => {
|
|
153
|
+
if ( filters && filters.length > 0 ) {
|
|
154
|
+
return filteredClasses.filter( ( cssClass ) => filters.includes( cssClass.id ) );
|
|
155
|
+
}
|
|
156
|
+
return filteredClasses;
|
|
157
|
+
}, [ filteredClasses, filters ] );
|
|
158
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type FC } from 'react';
|
|
3
|
+
import type { StyleDefinition } from '@elementor/editor-styles';
|
|
4
|
+
import { ColorSwatchIcon, PhotoIcon } from '@elementor/icons';
|
|
5
|
+
import { Box, Link, Stack, Typography } from '@elementor/ui';
|
|
6
|
+
import { __ } from '@wordpress/i18n';
|
|
7
|
+
|
|
8
|
+
import { useSearchAndFilters } from '../search-and-filter/context';
|
|
9
|
+
|
|
10
|
+
export const getNotFoundType = (
|
|
11
|
+
searchValue: string,
|
|
12
|
+
filters: string[] | null | undefined,
|
|
13
|
+
filteredClasses: StyleDefinition[]
|
|
14
|
+
): NotFoundType | undefined => {
|
|
15
|
+
const searchNotFound = filteredClasses.length <= 0 && searchValue.length > 1;
|
|
16
|
+
const filterNotFound = filters && filters.length === 0;
|
|
17
|
+
const filterAndSearchNotFound = searchNotFound && filterNotFound;
|
|
18
|
+
|
|
19
|
+
if ( filterAndSearchNotFound ) {
|
|
20
|
+
return 'filterAndSearch';
|
|
21
|
+
}
|
|
22
|
+
if ( searchNotFound ) {
|
|
23
|
+
return 'search';
|
|
24
|
+
}
|
|
25
|
+
if ( filterNotFound ) {
|
|
26
|
+
return 'filter';
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type NotFoundType = 'filter' | 'search' | 'filterAndSearch';
|
|
32
|
+
|
|
33
|
+
const notFound = {
|
|
34
|
+
filterAndSearch: {
|
|
35
|
+
mainText: __( 'Sorry, nothing matched.', 'elementor' ),
|
|
36
|
+
sceneryText: __( 'Try something else.', 'elementor' ),
|
|
37
|
+
icon: <PhotoIcon color="inherit" fontSize="large" />,
|
|
38
|
+
},
|
|
39
|
+
search: {
|
|
40
|
+
mainText: __( 'Sorry, nothing matched', 'elementor' ),
|
|
41
|
+
sceneryText: __( 'Clear your input and try something else.', 'elementor' ),
|
|
42
|
+
icon: <PhotoIcon color="inherit" fontSize="large" />,
|
|
43
|
+
},
|
|
44
|
+
filter: {
|
|
45
|
+
mainText: __( 'Sorry, nothing matched that search.', 'elementor' ),
|
|
46
|
+
sceneryText: __( 'Clear the filters and try something else.', 'elementor' ),
|
|
47
|
+
icon: <ColorSwatchIcon color="inherit" fontSize="large" />,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type GetNotFoundConfigProps = {
|
|
52
|
+
notFoundType: NotFoundType;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const NotFound = ( { notFoundType }: GetNotFoundConfigProps ): React.ReactElement => {
|
|
56
|
+
const {
|
|
57
|
+
search: { onClearSearch, inputValue },
|
|
58
|
+
filters: { onClearFilter },
|
|
59
|
+
} = useSearchAndFilters();
|
|
60
|
+
|
|
61
|
+
switch ( notFoundType ) {
|
|
62
|
+
case 'filter':
|
|
63
|
+
return <NotFoundLayout { ...notFound.filter } onClear={ onClearFilter } />;
|
|
64
|
+
case 'search':
|
|
65
|
+
return <NotFoundLayout { ...notFound.search } searchValue={ inputValue } onClear={ onClearSearch } />;
|
|
66
|
+
case 'filterAndSearch':
|
|
67
|
+
return (
|
|
68
|
+
<NotFoundLayout
|
|
69
|
+
{ ...notFound.filterAndSearch }
|
|
70
|
+
onClear={ () => {
|
|
71
|
+
onClearFilter();
|
|
72
|
+
onClearSearch();
|
|
73
|
+
} }
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type NotFoundLayoutProps = {
|
|
80
|
+
searchValue?: string;
|
|
81
|
+
onClear: () => void;
|
|
82
|
+
mainText: string;
|
|
83
|
+
sceneryText: string;
|
|
84
|
+
icon: React.ReactElement;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const NotFoundLayout: FC< NotFoundLayoutProps > = ( { onClear, searchValue, mainText, sceneryText, icon } ) => (
|
|
88
|
+
<Stack
|
|
89
|
+
color={ 'text.secondary' }
|
|
90
|
+
pt={ 5 }
|
|
91
|
+
alignItems="center"
|
|
92
|
+
gap={ 1 }
|
|
93
|
+
overflow={ 'hidden' }
|
|
94
|
+
justifySelf={ 'center' }
|
|
95
|
+
>
|
|
96
|
+
{ icon }
|
|
97
|
+
<Box
|
|
98
|
+
sx={ {
|
|
99
|
+
width: '100%',
|
|
100
|
+
} }
|
|
101
|
+
>
|
|
102
|
+
<Typography align="center" variant="subtitle2" color="inherit">
|
|
103
|
+
{ mainText }
|
|
104
|
+
</Typography>
|
|
105
|
+
{ searchValue && (
|
|
106
|
+
<Typography
|
|
107
|
+
variant="subtitle2"
|
|
108
|
+
color="inherit"
|
|
109
|
+
sx={ {
|
|
110
|
+
display: 'flex',
|
|
111
|
+
width: '100%',
|
|
112
|
+
justifyContent: 'center',
|
|
113
|
+
} }
|
|
114
|
+
>
|
|
115
|
+
<span>“</span>
|
|
116
|
+
<span
|
|
117
|
+
style={ {
|
|
118
|
+
maxWidth: '80%',
|
|
119
|
+
overflow: 'hidden',
|
|
120
|
+
textOverflow: 'ellipsis',
|
|
121
|
+
} }
|
|
122
|
+
>
|
|
123
|
+
{ searchValue }
|
|
124
|
+
</span>
|
|
125
|
+
<span>”.</span>
|
|
126
|
+
</Typography>
|
|
127
|
+
) }
|
|
128
|
+
</Box>
|
|
129
|
+
<Typography align="center" variant="caption" color="inherit">
|
|
130
|
+
{ sceneryText }
|
|
131
|
+
</Typography>
|
|
132
|
+
<Typography align="center" variant="caption" color="inherit">
|
|
133
|
+
<Link color="secondary" variant="caption" component="button" onClick={ onClear }>
|
|
134
|
+
{ __( 'Clear & try again', 'elementor' ) }
|
|
135
|
+
</Link>
|
|
136
|
+
</Typography>
|
|
137
|
+
</Stack>
|
|
138
|
+
);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type StyleDefinition } from '@elementor/editor-styles';
|
|
3
|
+
import { validateStyleLabel } from '@elementor/editor-styles-repository';
|
|
4
|
+
import { MenuListItem } from '@elementor/editor-ui';
|
|
5
|
+
import { Divider } from '@elementor/ui';
|
|
6
|
+
import { __ } from '@wordpress/i18n';
|
|
7
|
+
|
|
8
|
+
import { globalClassesStylesProvider } from '../global-classes-styles-provider';
|
|
9
|
+
|
|
10
|
+
type OwnProps = {
|
|
11
|
+
successCallback: ( _: string ) => void;
|
|
12
|
+
styleDef: StyleDefinition | null;
|
|
13
|
+
canConvert: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const ConvertLocalClassToGlobalClass = ( props: OwnProps ) => {
|
|
17
|
+
const localStyleData = props.styleDef;
|
|
18
|
+
|
|
19
|
+
const handleConversion = () => {
|
|
20
|
+
const newClassName = createClassName( `converted-class-` );
|
|
21
|
+
|
|
22
|
+
if ( ! localStyleData ) {
|
|
23
|
+
throw new Error( 'Style definition is required for converting local class to global class.' );
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const newId = globalClassesStylesProvider.actions.create?.( newClassName, localStyleData.variants );
|
|
27
|
+
if ( newId ) {
|
|
28
|
+
props.successCallback( newId );
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
<MenuListItem
|
|
35
|
+
disabled={ ! props.canConvert }
|
|
36
|
+
onClick={ handleConversion }
|
|
37
|
+
dense
|
|
38
|
+
sx={ {
|
|
39
|
+
'&.Mui-focusVisible': {
|
|
40
|
+
border: 'none',
|
|
41
|
+
boxShadow: 'none !important',
|
|
42
|
+
backgroundColor: 'transparent',
|
|
43
|
+
},
|
|
44
|
+
} }
|
|
45
|
+
>
|
|
46
|
+
{ __( 'Convert to global class', 'elementor' ) }
|
|
47
|
+
</MenuListItem>
|
|
48
|
+
<Divider />
|
|
49
|
+
</>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function createClassName( prefix: string ): string {
|
|
54
|
+
let i = 1;
|
|
55
|
+
let newClassName = `${ prefix }${ i }`;
|
|
56
|
+
|
|
57
|
+
while ( ! validateStyleLabel( newClassName, 'create' ).isValid ) {
|
|
58
|
+
newClassName = `${ prefix }${ ++i }`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return newClassName;
|
|
62
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { __useOpenDocumentInNewTab as useOpenDocumentInNewTab } from '@elementor/editor-documents';
|
|
3
|
+
import {
|
|
4
|
+
EllipsisWithTooltip,
|
|
5
|
+
PopoverBody,
|
|
6
|
+
PopoverHeader,
|
|
7
|
+
PopoverMenuList,
|
|
8
|
+
type VirtualizedItem,
|
|
9
|
+
} from '@elementor/editor-ui';
|
|
10
|
+
import {
|
|
11
|
+
CurrentLocationIcon,
|
|
12
|
+
ExternalLinkIcon,
|
|
13
|
+
FooterTemplateIcon,
|
|
14
|
+
HeaderTemplateIcon,
|
|
15
|
+
PagesIcon,
|
|
16
|
+
PopupTemplateIcon,
|
|
17
|
+
PostTypeIcon,
|
|
18
|
+
} from '@elementor/icons';
|
|
19
|
+
import { Box, Chip, Divider, Icon, MenuList, Stack, styled, Tooltip, Typography } from '@elementor/ui';
|
|
20
|
+
import { __ } from '@wordpress/i18n';
|
|
21
|
+
|
|
22
|
+
import { useCssClassUsageByID } from '../../../hooks/use-css-class-usage-by-id';
|
|
23
|
+
import { type ContentType } from '../types';
|
|
24
|
+
|
|
25
|
+
type CssClassUsageRecord = VirtualizedItem< 'item', string > & { docType: ContentType };
|
|
26
|
+
|
|
27
|
+
const iconMapper: Record< ContentType, { label: string; icon: React.ReactElement } > = {
|
|
28
|
+
'wp-post': {
|
|
29
|
+
label: __( 'Post', 'elementor' ),
|
|
30
|
+
icon: <PostTypeIcon fontSize={ 'inherit' } />,
|
|
31
|
+
},
|
|
32
|
+
'wp-page': {
|
|
33
|
+
label: __( 'Page', 'elementor' ),
|
|
34
|
+
icon: <PagesIcon fontSize={ 'inherit' } />,
|
|
35
|
+
},
|
|
36
|
+
popup: {
|
|
37
|
+
label: __( 'Popup', 'elementor' ),
|
|
38
|
+
icon: <PopupTemplateIcon fontSize={ 'inherit' } />,
|
|
39
|
+
},
|
|
40
|
+
header: {
|
|
41
|
+
label: __( 'Header', 'elementor' ),
|
|
42
|
+
icon: <HeaderTemplateIcon fontSize={ 'inherit' } />,
|
|
43
|
+
},
|
|
44
|
+
footer: {
|
|
45
|
+
label: __( 'Footer', 'elementor' ),
|
|
46
|
+
icon: <FooterTemplateIcon fontSize={ 'inherit' } />,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const CssClassUsagePopover = ( {
|
|
51
|
+
cssClassID,
|
|
52
|
+
onClose,
|
|
53
|
+
}: {
|
|
54
|
+
onClose: React.ComponentProps< typeof PopoverHeader >[ 'onClose' ];
|
|
55
|
+
cssClassID: string;
|
|
56
|
+
} ) => {
|
|
57
|
+
const { data: classUsage } = useCssClassUsageByID( cssClassID );
|
|
58
|
+
const onNavigate = useOpenDocumentInNewTab();
|
|
59
|
+
|
|
60
|
+
const cssClassUsageRecords: CssClassUsageRecord[] =
|
|
61
|
+
classUsage?.content.map(
|
|
62
|
+
( { title, elements, pageId, type } ) =>
|
|
63
|
+
( {
|
|
64
|
+
type: 'item',
|
|
65
|
+
value: pageId,
|
|
66
|
+
label: title,
|
|
67
|
+
secondaryText: elements.length.toString(),
|
|
68
|
+
docType: type,
|
|
69
|
+
} ) as CssClassUsageRecord
|
|
70
|
+
) ?? [];
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<>
|
|
74
|
+
<PopoverHeader
|
|
75
|
+
icon={ <CurrentLocationIcon fontSize={ 'tiny' } /> }
|
|
76
|
+
title={
|
|
77
|
+
<Stack flexDirection={ 'row' } gap={ 1 } alignItems={ 'center' }>
|
|
78
|
+
<Box aria-label={ 'header-title' }>{ __( 'Locator', 'elementor' ) }</Box>
|
|
79
|
+
<Box>
|
|
80
|
+
<Chip sx={ { lineHeight: 1 } } size={ 'tiny' } label={ classUsage.total } />
|
|
81
|
+
</Box>
|
|
82
|
+
</Stack>
|
|
83
|
+
}
|
|
84
|
+
onClose={ onClose }
|
|
85
|
+
/>
|
|
86
|
+
<Divider />
|
|
87
|
+
<PopoverBody width={ 300 }>
|
|
88
|
+
<PopoverMenuList
|
|
89
|
+
onSelect={ ( value ) => onNavigate( +value ) }
|
|
90
|
+
items={ cssClassUsageRecords }
|
|
91
|
+
onClose={ () => {} }
|
|
92
|
+
menuListTemplate={ StyledCssClassUsageItem }
|
|
93
|
+
menuItemContentTemplate={ ( cssClassUsageRecord ) => (
|
|
94
|
+
<Stack flexDirection={ 'row' } flex={ 1 } alignItems={ 'center' }>
|
|
95
|
+
<Box display={ 'flex' } sx={ { pr: 1 } }>
|
|
96
|
+
<Tooltip
|
|
97
|
+
disableInteractive
|
|
98
|
+
title={
|
|
99
|
+
iconMapper?.[ cssClassUsageRecord.docType as ContentType ]?.label ??
|
|
100
|
+
cssClassUsageRecord.docType
|
|
101
|
+
}
|
|
102
|
+
placement="top"
|
|
103
|
+
>
|
|
104
|
+
<Icon fontSize={ 'small' }>
|
|
105
|
+
{ iconMapper?.[ cssClassUsageRecord.docType as ContentType ]?.icon || (
|
|
106
|
+
<PagesIcon fontSize={ 'inherit' } />
|
|
107
|
+
) }
|
|
108
|
+
</Icon>
|
|
109
|
+
</Tooltip>
|
|
110
|
+
</Box>
|
|
111
|
+
<Box sx={ { pr: 0.5, maxWidth: '173px' } } display={ 'flex' }>
|
|
112
|
+
<EllipsisWithTooltip
|
|
113
|
+
title={ cssClassUsageRecord.label }
|
|
114
|
+
as={ Typography }
|
|
115
|
+
variant="caption"
|
|
116
|
+
maxWidth="173px"
|
|
117
|
+
sx={ {
|
|
118
|
+
lineHeight: 1,
|
|
119
|
+
} }
|
|
120
|
+
/>
|
|
121
|
+
</Box>
|
|
122
|
+
<ExternalLinkIcon className={ 'hover-only-icon' } fontSize={ 'tiny' } />
|
|
123
|
+
<Chip
|
|
124
|
+
sx={ {
|
|
125
|
+
ml: 'auto',
|
|
126
|
+
} }
|
|
127
|
+
size={ 'tiny' }
|
|
128
|
+
label={ cssClassUsageRecord.secondaryText }
|
|
129
|
+
/>
|
|
130
|
+
</Stack>
|
|
131
|
+
) }
|
|
132
|
+
/>
|
|
133
|
+
</PopoverBody>
|
|
134
|
+
</>
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const StyledCssClassUsageItem = styled( MenuList )( ( { theme } ) => ( {
|
|
139
|
+
'& > li': {
|
|
140
|
+
display: 'flex',
|
|
141
|
+
cursor: 'pointer',
|
|
142
|
+
height: 32,
|
|
143
|
+
width: '100%',
|
|
144
|
+
},
|
|
145
|
+
'& > [role="option"]': {
|
|
146
|
+
...theme.typography.caption,
|
|
147
|
+
lineHeight: 'inherit',
|
|
148
|
+
padding: theme.spacing( 0.5, 1, 0.5, 2 ),
|
|
149
|
+
textOverflow: 'ellipsis',
|
|
150
|
+
position: 'absolute',
|
|
151
|
+
top: 0,
|
|
152
|
+
left: 0,
|
|
153
|
+
opacity: 1,
|
|
154
|
+
'.hover-only-icon': {
|
|
155
|
+
color: theme.palette.text.disabled,
|
|
156
|
+
opacity: 0,
|
|
157
|
+
},
|
|
158
|
+
'&:hover': {
|
|
159
|
+
borderRadius: theme.spacing( 0.5 ),
|
|
160
|
+
backgroundColor: theme.palette.action.hover,
|
|
161
|
+
'.hover-only-icon': {
|
|
162
|
+
color: theme.palette.text.disabled,
|
|
163
|
+
opacity: 1,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
width: '100%',
|
|
168
|
+
position: 'relative',
|
|
169
|
+
} ) );
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type MouseEvent, type PropsWithChildren } from 'react';
|
|
3
|
+
import { InfoAlert } from '@elementor/editor-ui';
|
|
4
|
+
import { CurrentLocationIcon } from '@elementor/icons';
|
|
5
|
+
import {
|
|
6
|
+
bindPopover,
|
|
7
|
+
bindTrigger,
|
|
8
|
+
Box,
|
|
9
|
+
IconButton,
|
|
10
|
+
Infotip,
|
|
11
|
+
Popover,
|
|
12
|
+
styled,
|
|
13
|
+
Tooltip,
|
|
14
|
+
usePopupState,
|
|
15
|
+
} from '@elementor/ui';
|
|
16
|
+
import { __ } from '@wordpress/i18n';
|
|
17
|
+
|
|
18
|
+
import { useCssClassUsageByID } from '../../../hooks/use-css-class-usage-by-id';
|
|
19
|
+
import { type CssClassID } from '../types';
|
|
20
|
+
import { CssClassUsagePopover } from './css-class-usage-popover';
|
|
21
|
+
|
|
22
|
+
export const CssClassUsageTrigger = ( { id, onClick }: { id: CssClassID; onClick: ( id: CssClassID ) => void } ) => {
|
|
23
|
+
const {
|
|
24
|
+
data: { total },
|
|
25
|
+
isLoading,
|
|
26
|
+
} = useCssClassUsageByID( id );
|
|
27
|
+
const cssClassUsagePopover = usePopupState( { variant: 'popover', popupId: 'css-class-usage-popover' } );
|
|
28
|
+
|
|
29
|
+
if ( isLoading ) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const WrapperComponent = total !== 0 ? TooltipWrapper : InfoAlertMessage;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<>
|
|
37
|
+
<Box position={ 'relative' }>
|
|
38
|
+
<WrapperComponent total={ total }>
|
|
39
|
+
<CustomIconButton
|
|
40
|
+
disabled={ total === 0 }
|
|
41
|
+
size={ 'tiny' }
|
|
42
|
+
{ ...bindTrigger( cssClassUsagePopover ) }
|
|
43
|
+
onClick={ ( e: MouseEvent ) => {
|
|
44
|
+
if ( total !== 0 ) {
|
|
45
|
+
bindTrigger( cssClassUsagePopover ).onClick( e );
|
|
46
|
+
onClick( id );
|
|
47
|
+
}
|
|
48
|
+
} }
|
|
49
|
+
>
|
|
50
|
+
<CurrentLocationIcon fontSize={ 'tiny' } />
|
|
51
|
+
</CustomIconButton>
|
|
52
|
+
</WrapperComponent>
|
|
53
|
+
</Box>
|
|
54
|
+
<Box>
|
|
55
|
+
<Popover
|
|
56
|
+
anchorOrigin={ {
|
|
57
|
+
vertical: 'center',
|
|
58
|
+
horizontal: 'right',
|
|
59
|
+
} }
|
|
60
|
+
transformOrigin={ {
|
|
61
|
+
vertical: 15,
|
|
62
|
+
horizontal: -50,
|
|
63
|
+
} }
|
|
64
|
+
{ ...bindPopover( cssClassUsagePopover ) }
|
|
65
|
+
onClose={ () => {
|
|
66
|
+
bindPopover( cssClassUsagePopover ).onClose();
|
|
67
|
+
onClick( '' );
|
|
68
|
+
} }
|
|
69
|
+
>
|
|
70
|
+
<CssClassUsagePopover
|
|
71
|
+
onClose={ cssClassUsagePopover.close }
|
|
72
|
+
aria-label="css-class-usage-popover"
|
|
73
|
+
cssClassID={ id }
|
|
74
|
+
/>
|
|
75
|
+
</Popover>
|
|
76
|
+
</Box>
|
|
77
|
+
</>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const CustomIconButton = styled( IconButton )( ( { theme } ) => ( {
|
|
82
|
+
'&.Mui-disabled': {
|
|
83
|
+
pointerEvents: 'auto', // Enable hover
|
|
84
|
+
'&:hover': {
|
|
85
|
+
color: theme.palette.action.disabled, // optional
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
height: '22px',
|
|
89
|
+
width: '22px',
|
|
90
|
+
} ) );
|
|
91
|
+
|
|
92
|
+
const TooltipWrapper = ( { children, total }: PropsWithChildren< { total: number } > ) => (
|
|
93
|
+
<Tooltip
|
|
94
|
+
disableInteractive
|
|
95
|
+
placement={ 'top' }
|
|
96
|
+
title={ `${ __( 'Show {{number}} {{locations}}', 'elementor' )
|
|
97
|
+
.replace( '{{number}}', total.toString() )
|
|
98
|
+
.replace(
|
|
99
|
+
'{{locations}}',
|
|
100
|
+
total === 1 ? __( 'location', 'elementor' ) : __( 'locations', 'elementor' )
|
|
101
|
+
) }` }
|
|
102
|
+
>
|
|
103
|
+
<span>{ children }</span>
|
|
104
|
+
</Tooltip>
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const InfoAlertMessage = ( { children }: PropsWithChildren ) => (
|
|
108
|
+
<Infotip
|
|
109
|
+
disableInteractive
|
|
110
|
+
placement={ 'top' }
|
|
111
|
+
color={ 'secondary' }
|
|
112
|
+
content={ <InfoAlert sx={ { mt: 1 } }>{ __( 'This class isn’t being used yet.', 'elementor' ) }</InfoAlert> }
|
|
113
|
+
>
|
|
114
|
+
<span>{ children }</span>
|
|
115
|
+
</Infotip>
|
|
116
|
+
);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const QUERY_KEY = 'css-classes-usage';
|
|
2
|
+
|
|
3
|
+
export type CssClassID = string;
|
|
4
|
+
|
|
5
|
+
export type ContentType = 'header' | 'footer' | 'wp-page' | 'wp-post' | 'popup';
|
|
6
|
+
|
|
7
|
+
export type CssClassUsageContent = {
|
|
8
|
+
elements: string[];
|
|
9
|
+
pageId: string;
|
|
10
|
+
total: number;
|
|
11
|
+
title: string;
|
|
12
|
+
type: ContentType;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type CssClassUsage = Record< CssClassID, Array< CssClassUsageContent > >;
|
|
16
|
+
|
|
17
|
+
export type EnhancedCssClassUsageContent = { content: Array< CssClassUsageContent >; total: number };
|
|
18
|
+
|
|
19
|
+
export type EnhancedCssClassUsage = Record< CssClassID, EnhancedCssClassUsageContent >;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CssClassUsage, EnhancedCssClassUsage } from './types';
|
|
2
|
+
|
|
3
|
+
export const transformData = ( data: CssClassUsage ): EnhancedCssClassUsage =>
|
|
4
|
+
Object.entries( data ).reduce< EnhancedCssClassUsage >( ( acc, [ key, value ] ) => {
|
|
5
|
+
acc[ key ] = {
|
|
6
|
+
content: value || [],
|
|
7
|
+
total: value.reduce( ( total, val ) => total + ( val?.total || 0 ), 0 ),
|
|
8
|
+
};
|
|
9
|
+
return acc;
|
|
10
|
+
}, {} );
|