@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.
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
@@ -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, searchValue, onSearch }: GlobalClassesListProps ) => {
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
- { filteredClasses.length <= 0 && searchValue.length > 1 ? (
71
- <CssClassNotFound onClear={ () => onSearch( '' ) } searchValue={ searchValue } />
72
- ) : (
73
- <List sx={ { display: 'flex', flexDirection: 'column', gap: 0.5 } }>
74
- <SortableProvider value={ classesOrder } onChange={ reorderClasses }>
75
- { filteredClasses?.map( ( { id, label } ) => {
76
- return (
77
- <SortableItem key={ id } id={ id }>
78
- { ( { isDragged, isDragPlaceholder, triggerProps, triggerStyle } ) => (
79
- <ClassItem
80
- isSearchActive={ searchValue.length < 2 }
81
- id={ id }
82
- label={ label }
83
- renameClass={ ( newLabel: string ) => {
84
- dispatch(
85
- slice.actions.update( {
86
- style: {
87
- id,
88
- label: newLabel,
89
- },
90
- } )
91
- );
92
- } }
93
- selected={ isDragged }
94
- disabled={ disabled || isDragPlaceholder }
95
- sortableTriggerProps={ { ...triggerProps, style: triggerStyle } }
96
- />
97
- ) }
98
- </SortableItem>
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>&ldquo;</span>
116
+ <span
117
+ style={ {
118
+ maxWidth: '80%',
119
+ overflow: 'hidden',
120
+ textOverflow: 'ellipsis',
121
+ } }
122
+ >
123
+ { searchValue }
124
+ </span>
125
+ <span>&rdquo;.</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,2 @@
1
+ export { CssClassUsagePopover } from './css-class-usage-popover';
2
+ export { CssClassUsageTrigger } from './css-class-usage-trigger';
@@ -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
+ }, {} );