@elementor/editor-controls 4.4.0-1084 → 4.4.0-1085

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elementor/editor-controls",
3
3
  "description": "This package contains the controls model and utils for the Elementor editor",
4
- "version": "4.4.0-1084",
4
+ "version": "4.4.0-1085",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -40,23 +40,23 @@
40
40
  "dev": "tsup --config=../../tsup.dev.ts"
41
41
  },
42
42
  "dependencies": {
43
- "@elementor/editor-current-user": "4.4.0-1084",
44
- "@elementor/editor-elements": "4.4.0-1084",
45
- "@elementor/editor-props": "4.4.0-1084",
46
- "@elementor/editor-responsive": "4.4.0-1084",
47
- "@elementor/editor-ui": "4.4.0-1084",
48
- "@elementor/editor-v1-adapters": "4.4.0-1084",
49
- "@elementor/env": "4.4.0-1084",
50
- "@elementor/events": "4.4.0-1084",
51
- "@elementor/http-client": "4.4.0-1084",
43
+ "@elementor/editor-current-user": "4.4.0-1085",
44
+ "@elementor/editor-elements": "4.4.0-1085",
45
+ "@elementor/editor-props": "4.4.0-1085",
46
+ "@elementor/editor-responsive": "4.4.0-1085",
47
+ "@elementor/editor-ui": "4.4.0-1085",
48
+ "@elementor/editor-v1-adapters": "4.4.0-1085",
49
+ "@elementor/env": "4.4.0-1085",
50
+ "@elementor/events": "4.4.0-1085",
51
+ "@elementor/http-client": "4.4.0-1085",
52
52
  "@elementor/icons": "~1.75.1",
53
- "@elementor/locations": "4.4.0-1084",
54
- "@elementor/query": "4.4.0-1084",
55
- "@elementor/schema": "4.4.0-1084",
56
- "@elementor/session": "4.4.0-1084",
53
+ "@elementor/locations": "4.4.0-1085",
54
+ "@elementor/query": "4.4.0-1085",
55
+ "@elementor/schema": "4.4.0-1085",
56
+ "@elementor/session": "4.4.0-1085",
57
57
  "@elementor/ui": "1.37.5",
58
- "@elementor/utils": "4.4.0-1084",
59
- "@elementor/wp-media": "4.4.0-1084",
58
+ "@elementor/utils": "4.4.0-1085",
59
+ "@elementor/wp-media": "4.4.0-1085",
60
60
  "@monaco-editor/react": "^4.7.0",
61
61
  "@tiptap/extension-bold": "^3.11.1",
62
62
  "@tiptap/extension-document": "^3.11.1",
@@ -8,10 +8,13 @@ import {
8
8
 
9
9
  export { FONT_AWESOME_7_LIBRARIES, getFontAwesome7EditorConfig } from './font-awesome-7-data';
10
10
 
11
+ export type FontAwesome7Library = ( typeof FONT_AWESOME_7_LIBRARIES )[ number ][ 'library' ];
12
+ export type FontAwesome7LibraryFilter = FontAwesome7Library[];
13
+
11
14
  export type FontAwesome7Icon = FontAwesome7IconDefinition & {
12
15
  id: string;
13
16
  label: string;
14
- library: string;
17
+ library: FontAwesome7Library;
15
18
  value: string;
16
19
  };
17
20
 
@@ -37,14 +40,20 @@ export async function loadFontAwesome7Catalog( signal?: AbortSignal ): Promise<
37
40
  return catalogs.flat();
38
41
  }
39
42
 
40
- export function filterFontAwesome7Icons( icons: FontAwesome7Icon[], searchValue?: string | null ): FontAwesome7Icon[] {
43
+ export function filterFontAwesome7Icons(
44
+ icons: FontAwesome7Icon[],
45
+ searchValue?: string | null,
46
+ libraries: FontAwesome7LibraryFilter = []
47
+ ): FontAwesome7Icon[] {
41
48
  const query = searchValue?.trim().toLowerCase() ?? '';
49
+ const libraryIcons =
50
+ libraries.length === 0 ? icons : icons.filter( ( icon ) => libraries.includes( icon.library ) );
42
51
 
43
52
  if ( query === '' ) {
44
- return icons;
53
+ return libraryIcons;
45
54
  }
46
55
 
47
- return icons.filter( ( icon ) => {
56
+ return libraryIcons.filter( ( icon ) => {
48
57
  if ( icon.name.includes( query ) || icon.label.toLowerCase().includes( query ) ) {
49
58
  return true;
50
59
  }
@@ -93,7 +102,7 @@ export function findFontAwesome7Icon(
93
102
  } );
94
103
  }
95
104
 
96
- function toCatalogIcon( icon: FontAwesome7IconDefinition, library: string ): FontAwesome7Icon {
105
+ function toCatalogIcon( icon: FontAwesome7IconDefinition, library: FontAwesome7Library ): FontAwesome7Icon {
97
106
  return {
98
107
  ...icon,
99
108
  id: `${ library }:${ icon.name }`,
@@ -0,0 +1,159 @@
1
+ import * as React from 'react';
2
+ import { useId } from 'react';
3
+ import { CheckIcon, FilterIcon, LibraryIcon, ListIcon, StarFilledIcon, StarIcon } from '@elementor/icons';
4
+ import {
5
+ bindMenu,
6
+ bindToggle,
7
+ Box,
8
+ Menu,
9
+ MenuItem,
10
+ Stack,
11
+ ToggleButton,
12
+ Tooltip,
13
+ Typography,
14
+ usePopupState,
15
+ } from '@elementor/ui';
16
+ import { __ } from '@wordpress/i18n';
17
+
18
+ import {
19
+ FONT_AWESOME_7_LIBRARIES,
20
+ type FontAwesome7Library,
21
+ type FontAwesome7LibraryFilter,
22
+ } from './font-awesome-7-catalog';
23
+
24
+ const FILTER_MENU_WIDTH = 280;
25
+ const FILTER_INDICATOR_SIZE = 6;
26
+ const FILTER_INDICATOR_OFFSET = 4;
27
+ const LIBRARY_FILTER_ORDER = {
28
+ regular: 0,
29
+ solid: 1,
30
+ brands: 2,
31
+ } as const;
32
+
33
+ const LIBRARY_FILTER_CONFIG: Record<
34
+ FontAwesome7Library,
35
+ { getLabel: () => string; Icon: typeof StarIcon; order: number }
36
+ > = {
37
+ 'fa-regular': {
38
+ getLabel: () => __( 'Font Awesome - Regular', 'elementor' ),
39
+ Icon: StarIcon,
40
+ order: LIBRARY_FILTER_ORDER.regular,
41
+ },
42
+ 'fa-solid': {
43
+ getLabel: () => __( 'Font Awesome - Solid', 'elementor' ),
44
+ Icon: StarFilledIcon,
45
+ order: LIBRARY_FILTER_ORDER.solid,
46
+ },
47
+ 'fa-brands': {
48
+ getLabel: () => __( 'Font Awesome - Brands', 'elementor' ),
49
+ Icon: LibraryIcon,
50
+ order: LIBRARY_FILTER_ORDER.brands,
51
+ },
52
+ };
53
+
54
+ type IconLibraryFilterProps = {
55
+ value: FontAwesome7LibraryFilter;
56
+ onChange: ( value: FontAwesome7LibraryFilter ) => void;
57
+ };
58
+
59
+ export const IconLibraryFilter = ( { value, onChange }: IconLibraryFilterProps ) => {
60
+ const popupId = useId();
61
+ const popupState = usePopupState( {
62
+ variant: 'popover',
63
+ popupId,
64
+ } );
65
+ const options = FONT_AWESOME_7_LIBRARIES.map( ( { library } ) => ( {
66
+ value: library,
67
+ label: LIBRARY_FILTER_CONFIG[ library ].getLabel(),
68
+ Icon: LIBRARY_FILTER_CONFIG[ library ].Icon,
69
+ order: LIBRARY_FILTER_CONFIG[ library ].order,
70
+ } ) ).sort( ( firstOption, secondOption ) => firstOption.order - secondOption.order );
71
+ const isFiltered = value.length > 0;
72
+ const filterButtonLabel = isFiltered
73
+ ? __( 'Filter by library, active', 'elementor' )
74
+ : __( 'Filter by library', 'elementor' );
75
+
76
+ const handleAllIconsClick = () => {
77
+ onChange( [] );
78
+ };
79
+
80
+ const handleLibraryClick = ( library: FontAwesome7Library ) => {
81
+ const nextValue = value.includes( library )
82
+ ? value.filter( ( selectedLibrary ) => selectedLibrary !== library )
83
+ : [ ...value, library ];
84
+
85
+ onChange( nextValue.length === FONT_AWESOME_7_LIBRARIES.length ? [] : nextValue );
86
+ };
87
+
88
+ return (
89
+ <>
90
+ <Tooltip title={ __( 'Filter by library', 'elementor' ) } placement="top">
91
+ <ToggleButton
92
+ aria-label={ filterButtonLabel }
93
+ value="filter"
94
+ size="tiny"
95
+ selected={ popupState.isOpen }
96
+ sx={ { position: 'relative', flexShrink: 0 } }
97
+ { ...bindToggle( popupState ) }
98
+ aria-expanded={ popupState.isOpen }
99
+ >
100
+ <FilterIcon fontSize="tiny" />
101
+ { isFiltered ? (
102
+ <Box
103
+ component="span"
104
+ aria-hidden="true"
105
+ sx={ {
106
+ position: 'absolute',
107
+ insetBlockStart: FILTER_INDICATOR_OFFSET,
108
+ insetInlineEnd: FILTER_INDICATOR_OFFSET,
109
+ width: FILTER_INDICATOR_SIZE,
110
+ height: FILTER_INDICATOR_SIZE,
111
+ borderRadius: '50%',
112
+ bgcolor: 'secondary.main',
113
+ } }
114
+ />
115
+ ) : null }
116
+ </ToggleButton>
117
+ </Tooltip>
118
+ <Menu
119
+ { ...bindMenu( popupState ) }
120
+ MenuListProps={ {
121
+ dense: true,
122
+ autoFocusItem: true,
123
+ 'aria-label': __( 'Filter by library', 'elementor' ),
124
+ } }
125
+ sx={ { '& .MuiPaper-root': { minWidth: FILTER_MENU_WIDTH } } }
126
+ >
127
+ <MenuItem
128
+ role="menuitemcheckbox"
129
+ aria-checked={ ! isFiltered }
130
+ selected={ ! isFiltered }
131
+ onClick={ handleAllIconsClick }
132
+ >
133
+ { renderFilterMenuItemContent( __( 'All icons', 'elementor' ), ListIcon, ! isFiltered ) }
134
+ </MenuItem>
135
+ { options.map( ( { value: library, label, Icon } ) => (
136
+ <MenuItem
137
+ key={ library }
138
+ role="menuitemcheckbox"
139
+ aria-checked={ value.includes( library ) }
140
+ selected={ value.includes( library ) }
141
+ onClick={ () => handleLibraryClick( library ) }
142
+ >
143
+ { renderFilterMenuItemContent( label, Icon, value.includes( library ) ) }
144
+ </MenuItem>
145
+ ) ) }
146
+ </Menu>
147
+ </>
148
+ );
149
+ };
150
+
151
+ const renderFilterMenuItemContent = ( label: string, Icon: typeof ListIcon, selected: boolean ) => (
152
+ <Stack direction="row" alignItems="center" gap={ 1 } width="100%">
153
+ <Icon fontSize="tiny" />
154
+ <Typography variant="caption" sx={ { flex: 1 } }>
155
+ { label }
156
+ </Typography>
157
+ { selected ? <CheckIcon fontSize="tiny" /> : null }
158
+ </Stack>
159
+ );
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { useMemo } from 'react';
2
+ import { useMemo, useState } from 'react';
3
3
  import {
4
4
  PopoverBody,
5
5
  PopoverHeader,
@@ -18,8 +18,10 @@ import {
18
18
  filterFontAwesome7Icons,
19
19
  findFontAwesome7Icon,
20
20
  type FontAwesome7Icon,
21
+ type FontAwesome7LibraryFilter,
21
22
  } from './font-awesome-7-catalog';
22
23
  import { FontAwesomeGlyph } from './font-awesome-glyph';
24
+ import { IconLibraryFilter } from './icon-library-filter';
23
25
  import { useFontAwesome7Catalog } from './use-font-awesome-7-catalog';
24
26
 
25
27
  export const ICON_LIBRARY_POPOVER_WIDTH = 300;
@@ -60,9 +62,13 @@ export const IconLibraryPopover = ( {
60
62
  handleChange: handleSearchChange,
61
63
  setImmediateValue: setSearchValue,
62
64
  } = useDebounceState( { delay: ICON_LIBRARY_SEARCH_DEBOUNCE_DELAY } );
65
+ const [ activeLibraries, setActiveLibraries ] = useState< FontAwesome7LibraryFilter >( [] );
63
66
  const { data: icons = [], isLoading } = useFontAwesome7Catalog( open );
64
67
 
65
- const items = useMemo( () => createIconLibraryItems( icons, searchValue ), [ icons, searchValue ] );
68
+ const items = useMemo(
69
+ () => createIconLibraryItems( icons, searchValue, activeLibraries ),
70
+ [ activeLibraries, icons, searchValue ]
71
+ );
66
72
  const selectedValue = useMemo(
67
73
  () => findFontAwesome7Icon( icons, selectedIconClass, selectedIconLibrary )?.id,
68
74
  [ icons, selectedIconClass, selectedIconLibrary ]
@@ -70,6 +76,7 @@ export const IconLibraryPopover = ( {
70
76
 
71
77
  const handleClose = () => {
72
78
  setSearchValue( '' );
79
+ setActiveLibraries( [] );
73
80
  onClose();
74
81
  };
75
82
 
@@ -98,13 +105,16 @@ export const IconLibraryPopover = ( {
98
105
  icon={ <ComponentsIcon fontSize="tiny" /> }
99
106
  sx={ { pl: ICON_LIBRARY_INLINE_SPACING, pr: 0.5 } }
100
107
  />
101
- <SearchField
102
- value={ searchInputValue }
103
- onSearch={ handleSearchChange }
104
- placeholder={ __( 'Search', 'elementor' ) }
105
- id="icon-library-search"
106
- sx={ { px: ICON_LIBRARY_INLINE_SPACING, pb: 1 } }
107
- />
108
+ <Stack direction="row" alignItems="center" gap={ 1 } sx={ { px: ICON_LIBRARY_INLINE_SPACING, pb: 1 } }>
109
+ <SearchField
110
+ value={ searchInputValue }
111
+ onSearch={ handleSearchChange }
112
+ placeholder={ __( 'Search', 'elementor' ) }
113
+ id="icon-library-search"
114
+ sx={ { flex: 1, px: 0, pb: 0 } }
115
+ />
116
+ <IconLibraryFilter value={ activeLibraries } onChange={ setActiveLibraries } />
117
+ </Stack>
108
118
  <Divider />
109
119
  <Box sx={ { flex: 1, overflow: 'auto', minHeight: 0 } }>
110
120
  <IconLibraryContent
@@ -112,6 +122,7 @@ export const IconLibraryPopover = ( {
112
122
  items={ items }
113
123
  selectedValue={ selectedValue }
114
124
  searchValue={ searchValue }
125
+ isCatalogAvailable={ icons.length > 0 }
115
126
  onSelect={ handleSelect }
116
127
  onClose={ handleClose }
117
128
  onClearSearch={ handleClearSearch }
@@ -126,6 +137,7 @@ type IconLibraryContentProps = {
126
137
  items: IconLibraryItem[];
127
138
  selectedValue: string | undefined;
128
139
  searchValue: string;
140
+ isCatalogAvailable: boolean;
129
141
  onSelect: ( id: string ) => void;
130
142
  onClose: () => void;
131
143
  onClearSearch: () => void;
@@ -136,6 +148,7 @@ const IconLibraryContent = ( {
136
148
  items,
137
149
  selectedValue,
138
150
  searchValue,
151
+ isCatalogAvailable,
139
152
  onSelect,
140
153
  onClose,
141
154
  onClearSearch,
@@ -153,7 +166,13 @@ const IconLibraryContent = ( {
153
166
  onClose={ onClose }
154
167
  itemHeight={ ICON_LIBRARY_ROW_HEIGHT }
155
168
  menuItemContentTemplate={ IconLibraryRow }
156
- noResultsComponent={ <IconLibraryEmptyState searchValue={ searchValue } onClear={ onClearSearch } /> }
169
+ noResultsComponent={
170
+ <IconLibraryEmptyState
171
+ searchValue={ searchValue }
172
+ isCatalogAvailable={ isCatalogAvailable }
173
+ onClear={ onClearSearch }
174
+ />
175
+ }
157
176
  data-testid="icon-library-list"
158
177
  />
159
178
  );
@@ -165,8 +184,16 @@ const IconLibraryLoadingState = () => (
165
184
  </Stack>
166
185
  );
167
186
 
168
- const IconLibraryEmptyState = ( { searchValue, onClear }: { searchValue: string; onClear: () => void } ) => {
169
- if ( searchValue.trim() === '' ) {
187
+ const IconLibraryEmptyState = ( {
188
+ searchValue,
189
+ isCatalogAvailable,
190
+ onClear,
191
+ }: {
192
+ searchValue: string;
193
+ isCatalogAvailable: boolean;
194
+ onClear: () => void;
195
+ } ) => {
196
+ if ( ! isCatalogAvailable ) {
170
197
  return <CatalogUnavailable />;
171
198
  }
172
199
 
@@ -218,17 +245,25 @@ const NoResults = ( { searchValue, onClear }: { searchValue: string; onClear: ()
218
245
  <Typography align="center" variant="subtitle2" color="text.secondary">
219
246
  { __( 'Sorry, nothing matched', 'elementor' ) }
220
247
  </Typography>
221
- <Typography align="center" variant="subtitle2" color="text.secondary" noWrap sx={ { maxWidth: '80%' } }>
222
- &ldquo;{ searchValue }&rdquo;.
223
- </Typography>
224
- <Link color="secondary" variant="caption" component="button" type="button" onClick={ onClear }>
225
- { __( 'Clear & try again', 'elementor' ) }
226
- </Link>
248
+ { searchValue ? (
249
+ <>
250
+ <Typography align="center" variant="subtitle2" color="text.secondary" noWrap sx={ { maxWidth: '80%' } }>
251
+ &ldquo;{ searchValue }&rdquo;.
252
+ </Typography>
253
+ <Link color="secondary" variant="caption" component="button" type="button" onClick={ onClear }>
254
+ { __( 'Clear & try again', 'elementor' ) }
255
+ </Link>
256
+ </>
257
+ ) : null }
227
258
  </Stack>
228
259
  );
229
260
 
230
- const createIconLibraryItems = ( icons: FontAwesome7Icon[], searchValue: string ): IconLibraryItem[] =>
231
- filterFontAwesome7Icons( icons, searchValue ).map( ( icon ) => ( {
261
+ const createIconLibraryItems = (
262
+ icons: FontAwesome7Icon[],
263
+ searchValue: string,
264
+ libraries: FontAwesome7LibraryFilter
265
+ ): IconLibraryItem[] =>
266
+ filterFontAwesome7Icons( icons, searchValue, libraries ).map( ( icon ) => ( {
232
267
  ...icon,
233
268
  type: 'item',
234
269
  value: icon.id,