@elementor/editor-controls 4.3.3 → 4.3.5

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 (30) hide show
  1. package/dist/index.d.mts +10 -3
  2. package/dist/index.d.ts +10 -3
  3. package/dist/index.js +2431 -1698
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +2093 -1362
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +18 -18
  8. package/src/controls/color-control.tsx +5 -0
  9. package/src/controls/email-form-action-control/fields.tsx +27 -2
  10. package/src/controls/icon-library/custom-icon-libraries.ts +556 -0
  11. package/src/controls/icon-library/font-awesome-7-catalog.ts +16 -7
  12. package/src/controls/icon-library/font-awesome-7-data.ts +2 -0
  13. package/src/controls/icon-library/font-awesome-glyph.tsx +62 -17
  14. package/src/controls/icon-library/fontello-svg-font.ts +227 -0
  15. package/src/controls/icon-library/icomoon-selection.ts +104 -0
  16. package/src/controls/icon-library/icon-library-filter.tsx +142 -68
  17. package/src/controls/icon-library/icon-library-grid.tsx +19 -15
  18. package/src/controls/icon-library/icon-library-popover.tsx +31 -15
  19. package/src/controls/icon-library/icon-library-view-toggle.tsx +22 -82
  20. package/src/controls/icon-library/sanitize-icon-svg-markup.ts +53 -0
  21. package/src/controls/icon-library/svg-element-icon.tsx +15 -0
  22. package/src/controls/image-control.tsx +3 -3
  23. package/src/controls/image-media-control.tsx +15 -33
  24. package/src/controls/media-select-actions.tsx +105 -0
  25. package/src/controls/notice-control.tsx +11 -1
  26. package/src/controls/svg-media-control.tsx +4 -1
  27. package/src/controls/svg-media-overlay.tsx +24 -76
  28. package/src/controls/svg-media-preview.tsx +7 -2
  29. package/src/controls/video-media-control.tsx +13 -30
  30. package/src/index.ts +1 -0
@@ -1,10 +1,20 @@
1
1
  import * as React from 'react';
2
2
  import { useId } from 'react';
3
- import { CheckIcon, FilterIcon, LibraryIcon, ListIcon, StarFilledIcon, StarIcon } from '@elementor/icons';
3
+ import {
4
+ CheckIcon,
5
+ FilterIcon,
6
+ LibraryIcon,
7
+ MenuIcon,
8
+ RosetteDiscountCheckIcon,
9
+ StarFilledIcon,
10
+ StarIcon,
11
+ } from '@elementor/icons';
4
12
  import {
5
13
  bindMenu,
6
14
  bindToggle,
7
15
  Box,
16
+ Divider,
17
+ ListSubheader,
8
18
  Menu,
9
19
  MenuItem,
10
20
  Stack,
@@ -15,43 +25,36 @@ import {
15
25
  } from '@elementor/ui';
16
26
  import { __ } from '@wordpress/i18n';
17
27
 
18
- import {
19
- FONT_AWESOME_7_LIBRARIES,
20
- type FontAwesome7Library,
21
- type FontAwesome7LibraryFilter,
22
- } from './font-awesome-7-catalog';
28
+ import { type FontAwesome7LibraryFilter } from './font-awesome-7-catalog';
23
29
  import { ICON_LIBRARY_ACTION_TOOLTIP_ENTER_DELAY } from './icon-library-tooltip';
24
30
 
25
31
  const FILTER_MENU_WIDTH = 280;
26
32
  const FILTER_INDICATOR_SIZE = 6;
27
- const FILTER_INDICATOR_OFFSET = 4;
28
- const LIBRARY_FILTER_ORDER = {
29
- regular: 0,
30
- solid: 1,
31
- brands: 2,
32
- } as const;
33
-
34
- const LIBRARY_FILTER_CONFIG: Record<
35
- FontAwesome7Library,
36
- { getLabel: () => string; Icon: typeof StarIcon; order: number }
37
- > = {
38
- 'fa-regular': {
39
- getLabel: () => __( 'Font Awesome - Regular', 'elementor' ),
40
- Icon: StarIcon,
41
- order: LIBRARY_FILTER_ORDER.regular,
42
- },
43
- 'fa-solid': {
44
- getLabel: () => __( 'Font Awesome - Solid', 'elementor' ),
45
- Icon: StarFilledIcon,
46
- order: LIBRARY_FILTER_ORDER.solid,
47
- },
48
- 'fa-brands': {
49
- getLabel: () => __( 'Font Awesome - Brands', 'elementor' ),
50
- Icon: LibraryIcon,
51
- order: LIBRARY_FILTER_ORDER.brands,
52
- },
33
+ const FILTER_INDICATOR_CORNER_OFFSET = -( FILTER_INDICATOR_SIZE / 2 );
34
+ const FILTER_TYPE_ALL = 'all';
35
+ const FILTER_TYPE_GROUP = 'group';
36
+ const FILTER_TYPE_ITEM = 'item';
37
+
38
+ const FILTER_ICONS: Record< string, typeof MenuIcon > = {
39
+ list: MenuIcon,
40
+ star: StarIcon,
41
+ 'star-filled': StarFilledIcon,
42
+ library: LibraryIcon,
43
+ rosette: RosetteDiscountCheckIcon,
53
44
  };
54
45
 
46
+ const DEFAULT_FILTER = [
47
+ { type: FILTER_TYPE_ALL, label: 'All icons', icon: 'list' },
48
+ { type: FILTER_TYPE_ITEM, value: 'fa-regular', label: 'Font Awesome - Regular', icon: 'star' },
49
+ { type: FILTER_TYPE_ITEM, value: 'fa-solid', label: 'Font Awesome - Solid', icon: 'star-filled' },
50
+ { type: FILTER_TYPE_ITEM, value: 'fa-brands', label: 'Font Awesome - Brands', icon: 'rosette' },
51
+ ] as const;
52
+
53
+ type FilterEntry =
54
+ | { type: typeof FILTER_TYPE_ALL; label: string; icon?: string }
55
+ | { type: typeof FILTER_TYPE_GROUP; label: string }
56
+ | { type: typeof FILTER_TYPE_ITEM; value: string; label: string; icon?: string };
57
+
55
58
  type IconLibraryFilterProps = {
56
59
  value: FontAwesome7LibraryFilter;
57
60
  onChange: ( value: FontAwesome7LibraryFilter ) => void;
@@ -63,27 +66,19 @@ export const IconLibraryFilter = ( { value, onChange }: IconLibraryFilterProps )
63
66
  variant: 'popover',
64
67
  popupId,
65
68
  } );
66
- const options = FONT_AWESOME_7_LIBRARIES.map( ( { library } ) => ( {
67
- value: library,
68
- label: LIBRARY_FILTER_CONFIG[ library ].getLabel(),
69
- Icon: LIBRARY_FILTER_CONFIG[ library ].Icon,
70
- order: LIBRARY_FILTER_CONFIG[ library ].order,
71
- } ) ).sort( ( firstOption, secondOption ) => firstOption.order - secondOption.order );
69
+ const entries = getFilterEntries();
70
+ const selectableValues = entries.flatMap( ( entry ) => ( entry.type === FILTER_TYPE_ITEM ? [ entry.value ] : [] ) );
72
71
  const isFiltered = value.length > 0;
73
72
  const filterButtonLabel = isFiltered
74
73
  ? __( 'Filter by library, active', 'elementor' )
75
74
  : __( 'Filter by library', 'elementor' );
76
75
 
77
- const handleAllIconsClick = () => {
78
- onChange( [] );
79
- };
80
-
81
- const handleLibraryClick = ( library: FontAwesome7Library ) => {
76
+ const handleLibraryClick = ( library: string ) => {
82
77
  const nextValue = value.includes( library )
83
78
  ? value.filter( ( selectedLibrary ) => selectedLibrary !== library )
84
79
  : [ ...value, library ];
85
80
 
86
- onChange( nextValue.length === FONT_AWESOME_7_LIBRARIES.length ? [] : nextValue );
81
+ onChange( nextValue.length === selectableValues.length ? [] : nextValue );
87
82
  };
88
83
 
89
84
  return (
@@ -100,7 +95,7 @@ export const IconLibraryFilter = ( { value, onChange }: IconLibraryFilterProps )
100
95
  value="filter"
101
96
  size="tiny"
102
97
  selected={ popupState.isOpen }
103
- sx={ { position: 'relative', flexShrink: 0 } }
98
+ sx={ { position: 'relative', flexShrink: 0, overflow: 'visible' } }
104
99
  { ...bindToggle( popupState ) }
105
100
  aria-expanded={ popupState.isOpen }
106
101
  >
@@ -111,12 +106,12 @@ export const IconLibraryFilter = ( { value, onChange }: IconLibraryFilterProps )
111
106
  aria-hidden="true"
112
107
  sx={ {
113
108
  position: 'absolute',
114
- insetBlockStart: FILTER_INDICATOR_OFFSET,
115
- insetInlineEnd: FILTER_INDICATOR_OFFSET,
109
+ insetBlockStart: FILTER_INDICATOR_CORNER_OFFSET,
110
+ insetInlineEnd: FILTER_INDICATOR_CORNER_OFFSET,
116
111
  width: FILTER_INDICATOR_SIZE,
117
112
  height: FILTER_INDICATOR_SIZE,
118
113
  borderRadius: '50%',
119
- bgcolor: 'secondary.main',
114
+ bgcolor: 'text.primary',
120
115
  } }
121
116
  />
122
117
  ) : null }
@@ -131,31 +126,63 @@ export const IconLibraryFilter = ( { value, onChange }: IconLibraryFilterProps )
131
126
  } }
132
127
  sx={ { '& .MuiPaper-root': { minWidth: FILTER_MENU_WIDTH } } }
133
128
  >
134
- <MenuItem
135
- role="menuitemcheckbox"
136
- aria-checked={ ! isFiltered }
137
- selected={ ! isFiltered }
138
- onClick={ handleAllIconsClick }
139
- >
140
- { renderFilterMenuItemContent( __( 'All icons', 'elementor' ), ListIcon, ! isFiltered ) }
141
- </MenuItem>
142
- { options.map( ( { value: library, label, Icon } ) => (
143
- <MenuItem
144
- key={ library }
145
- role="menuitemcheckbox"
146
- aria-checked={ value.includes( library ) }
147
- selected={ value.includes( library ) }
148
- onClick={ () => handleLibraryClick( library ) }
149
- >
150
- { renderFilterMenuItemContent( label, Icon, value.includes( library ) ) }
151
- </MenuItem>
152
- ) ) }
129
+ { entries.flatMap( ( entry, index ) =>
130
+ renderFilterEntry( entry, index, value, isFiltered, () => onChange( [] ), handleLibraryClick )
131
+ ) }
153
132
  </Menu>
154
133
  </>
155
134
  );
156
135
  };
157
136
 
158
- const renderFilterMenuItemContent = ( label: string, Icon: typeof ListIcon, selected: boolean ) => (
137
+ const renderFilterEntry = (
138
+ entry: FilterEntry,
139
+ index: number,
140
+ selectedLibraries: FontAwesome7LibraryFilter,
141
+ isFiltered: boolean,
142
+ onAllIconsClick: () => void,
143
+ onLibraryClick: ( library: string ) => void
144
+ ): React.ReactNode[] => {
145
+ if ( entry.type === FILTER_TYPE_GROUP ) {
146
+ return [
147
+ <Divider key={ `divider-${ index }` } />,
148
+ <ListSubheader key={ `group-${ index }` } disableSticky>
149
+ { entry.label }
150
+ </ListSubheader>,
151
+ ];
152
+ }
153
+
154
+ if ( entry.type === FILTER_TYPE_ALL ) {
155
+ return [
156
+ <MenuItem
157
+ key={ `all-${ index }` }
158
+ role="menuitemcheckbox"
159
+ aria-checked={ ! isFiltered }
160
+ selected={ ! isFiltered }
161
+ onClick={ onAllIconsClick }
162
+ >
163
+ { renderFilterMenuItemContent( entry.label, getFilterIcon( entry.icon ), ! isFiltered ) }
164
+ </MenuItem>,
165
+ ];
166
+ }
167
+
168
+ const isSelected = selectedLibraries.includes( entry.value );
169
+
170
+ return [
171
+ <MenuItem
172
+ key={ entry.value }
173
+ role="menuitemcheckbox"
174
+ aria-checked={ isSelected }
175
+ selected={ isSelected }
176
+ onClick={ () => onLibraryClick( entry.value ) }
177
+ >
178
+ { renderFilterMenuItemContent( entry.label, getFilterIcon( entry.icon ), isSelected ) }
179
+ </MenuItem>,
180
+ ];
181
+ };
182
+
183
+ const getFilterIcon = ( icon?: string ) => FILTER_ICONS[ icon ?? '' ] ?? LibraryIcon;
184
+
185
+ const renderFilterMenuItemContent = ( label: string, Icon: typeof MenuIcon, selected: boolean ) => (
159
186
  <Stack direction="row" alignItems="center" gap={ 1 } width="100%">
160
187
  <Icon fontSize="tiny" />
161
188
  <Typography variant="caption" sx={ { flex: 1 } }>
@@ -164,3 +191,50 @@ const renderFilterMenuItemContent = ( label: string, Icon: typeof ListIcon, sele
164
191
  { selected ? <CheckIcon fontSize="tiny" aria-hidden="true" /> : null }
165
192
  </Stack>
166
193
  );
194
+
195
+ function getFilterEntries(): FilterEntry[] {
196
+ const filter = window.elementorCommon?.config?.fontAwesome?.v7?.filter;
197
+
198
+ if ( ! Array.isArray( filter ) ) {
199
+ return [ ...DEFAULT_FILTER ];
200
+ }
201
+
202
+ const entries = filter.map( parseFilterEntry ).filter( ( entry ): entry is FilterEntry => entry !== null );
203
+
204
+ return entries.length > 0 ? entries : [ ...DEFAULT_FILTER ];
205
+ }
206
+
207
+ function parseFilterEntry( value: unknown ): FilterEntry | null {
208
+ if ( ! value || typeof value !== 'object' ) {
209
+ return null;
210
+ }
211
+
212
+ const entry = value as { type?: unknown; label?: unknown; value?: unknown; icon?: unknown };
213
+
214
+ if ( typeof entry.label !== 'string' || entry.label === '' ) {
215
+ return null;
216
+ }
217
+
218
+ if ( entry.type === FILTER_TYPE_GROUP ) {
219
+ return { type: FILTER_TYPE_GROUP, label: entry.label };
220
+ }
221
+
222
+ if ( entry.type === FILTER_TYPE_ALL ) {
223
+ return {
224
+ type: FILTER_TYPE_ALL,
225
+ label: entry.label,
226
+ icon: typeof entry.icon === 'string' ? entry.icon : undefined,
227
+ };
228
+ }
229
+
230
+ if ( entry.type === FILTER_TYPE_ITEM && typeof entry.value === 'string' && entry.value !== '' ) {
231
+ return {
232
+ type: FILTER_TYPE_ITEM,
233
+ value: entry.value,
234
+ label: entry.label,
235
+ icon: typeof entry.icon === 'string' ? entry.icon : undefined,
236
+ };
237
+ }
238
+
239
+ return null;
240
+ }
@@ -13,8 +13,9 @@ export const ICON_LIBRARY_GRID_TOOLTIP_ENTER_DELAY = 1000;
13
13
 
14
14
  const ICON_GLYPH_SIZE = 20;
15
15
  const GRID_OVERSCAN = 6;
16
- const GRID_COLUMN_GAP = 1;
17
- const GRID_HORIZONTAL_PADDING = 1;
16
+ const GRID_CELL_GAP = 1;
17
+ const GRID_HORIZONTAL_PADDING = 2;
18
+ const GRID_VERTICAL_PADDING = 1.5;
18
19
  const HOME_END_KEYS = new Set( [ 'Home', 'End' ] );
19
20
 
20
21
  const getGridMetrics = ( containerWidth: number, columnGap: number, inlinePadding: number ) => {
@@ -67,10 +68,15 @@ export const IconLibraryGrid = ( {
67
68
  const [ focusedIndex, setFocusedIndex ] = useState( selectedIndex >= 0 ? selectedIndex : 0 );
68
69
  const [ { columnCount, cellSize }, setGridMetrics ] = useState( () => getGridMetrics( 0, 0, 0 ) );
69
70
  const rowCount = Math.ceil( items.length / columnCount );
71
+ const cellGap = Number.parseFloat( theme.spacing( GRID_CELL_GAP ) );
72
+ const verticalPadding = Number.parseFloat( theme.spacing( GRID_VERTICAL_PADDING ) );
70
73
  const virtualizer = useVirtualizer( {
71
74
  count: rowCount,
72
75
  getScrollElement: () => containerRef.current,
73
76
  estimateSize: () => cellSize,
77
+ gap: cellGap,
78
+ paddingStart: verticalPadding,
79
+ paddingEnd: verticalPadding,
74
80
  overscan: GRID_OVERSCAN,
75
81
  } );
76
82
  const focusedItem = items[ focusedIndex ];
@@ -90,10 +96,11 @@ export const IconLibraryGrid = ( {
90
96
  } );
91
97
 
92
98
  if ( selectedIndex >= 0 ) {
93
- virtualizer.scrollToIndex( Math.floor( selectedIndex / columnCount ) );
99
+ virtualizer.measure();
100
+ virtualizer.scrollToIndex( Math.floor( selectedIndex / columnCount ), { align: 'center' } );
94
101
  }
95
102
  // eslint-disable-next-line react-hooks/exhaustive-deps
96
- }, [ columnCount, items, selectedIndex, selectedValue ] );
103
+ }, [ cellSize, columnCount, items, selectedIndex, selectedValue ] );
97
104
 
98
105
  useLayoutEffect( () => {
99
106
  if ( ! shouldRestoreFocusRef.current || ! focusedItem ) {
@@ -118,9 +125,8 @@ export const IconLibraryGrid = ( {
118
125
  }
119
126
 
120
127
  const measureGrid = () => {
121
- const columnGap = Number.parseFloat( theme.spacing( GRID_COLUMN_GAP ) );
122
128
  const inlinePadding = Number.parseFloat( theme.spacing( GRID_HORIZONTAL_PADDING ) ) * 2;
123
- setGridMetrics( getGridMetrics( container.clientWidth, columnGap, inlinePadding ) );
129
+ setGridMetrics( getGridMetrics( container.clientWidth, cellGap, inlinePadding ) );
124
130
  };
125
131
 
126
132
  measureGrid();
@@ -130,7 +136,7 @@ export const IconLibraryGrid = ( {
130
136
  return () => {
131
137
  resizeObserver.disconnect();
132
138
  };
133
- }, [ theme, items.length ] );
139
+ }, [ theme, cellGap, items.length ] );
134
140
 
135
141
  useLayoutEffect( () => {
136
142
  virtualizer.measure();
@@ -242,7 +248,7 @@ export const IconLibraryGrid = ( {
242
248
  transform: `translateY(${ virtualRow.start }px)`,
243
249
  display: 'grid',
244
250
  gridTemplateColumns: `repeat(${ columnCount }, minmax(0, 1fr))`,
245
- gap: GRID_COLUMN_GAP,
251
+ gap: GRID_CELL_GAP,
246
252
  px: GRID_HORIZONTAL_PADDING,
247
253
  boxSizing: 'border-box',
248
254
  } }
@@ -322,13 +328,11 @@ export const IconLibraryGrid = ( {
322
328
  },
323
329
  } }
324
330
  >
325
- { item.paths.length > 0 ? (
326
- <FontAwesomeGlyph
327
- icon={ item }
328
- size={ ICON_GLYPH_SIZE }
329
- color="currentColor"
330
- />
331
- ) : null }
331
+ <FontAwesomeGlyph
332
+ icon={ item }
333
+ size={ ICON_GLYPH_SIZE }
334
+ color="currentColor"
335
+ />
332
336
  </Box>
333
337
  </Tooltip>
334
338
  </Box>
@@ -8,12 +8,13 @@ import {
8
8
  StyledMenuList,
9
9
  type VirtualizedItem,
10
10
  } from '@elementor/editor-ui';
11
- import { ComponentsIcon } from '@elementor/icons';
11
+ import { LibraryIcon } from '@elementor/icons';
12
12
  import { useSessionStorage } from '@elementor/session';
13
13
  import { Box, CircularProgress, Divider, Link, Stack, styled, Typography } from '@elementor/ui';
14
14
  import { useDebounceState } from '@elementor/utils';
15
15
  import { __ } from '@wordpress/i18n';
16
16
 
17
+ import { useCustomIconLibraries } from './custom-icon-libraries';
17
18
  import {
18
19
  createIconSelectionValue,
19
20
  filterFontAwesome7Icons,
@@ -25,25 +26,30 @@ import { FontAwesomeGlyph } from './font-awesome-glyph';
25
26
  import { IconLibraryFilter } from './icon-library-filter';
26
27
  import { IconLibraryGrid } from './icon-library-grid';
27
28
  import { type IconLibraryView, IconLibraryViewToggle } from './icon-library-view-toggle';
29
+ import { SvgElementIcon } from './svg-element-icon';
28
30
  import { useFontAwesome7Catalog } from './use-font-awesome-7-catalog';
29
31
 
30
32
  export const ICON_LIBRARY_POPOVER_WIDTH = 300;
31
- export const ICON_LIBRARY_ROW_HEIGHT = 48;
32
33
  export const ICON_LIBRARY_SEARCH_DEBOUNCE_DELAY = 300;
33
34
  export const ICON_LIBRARY_VIEW_STORAGE_KEY = 'icon-library-view';
34
35
  export const ICON_LIBRARY_VIEW_STORAGE_PREFIX = 'editor-controls';
35
- const DEFAULT_ICON_LIBRARY_VIEW: IconLibraryView = 'list';
36
+ const DEFAULT_ICON_LIBRARY_VIEW: IconLibraryView = 'grid';
36
37
 
37
38
  const isIconLibraryView = ( value: unknown ): value is IconLibraryView => {
38
39
  return value === 'grid' || value === 'list';
39
40
  };
40
41
  const ICON_TILE_SIZE = 40;
42
+ const ICON_LIBRARY_ROW_VERTICAL_PADDING = 0.5;
43
+ const ICON_LIBRARY_ROW_VERTICAL_PADDING_PX = 4;
44
+ export const ICON_LIBRARY_ROW_HEIGHT = ICON_TILE_SIZE + ICON_LIBRARY_ROW_VERTICAL_PADDING_PX * 2;
41
45
  const ICON_GLYPH_SIZE = 20;
42
- const ICON_LIBRARY_INLINE_SPACING = 1;
46
+ const ICON_LIBRARY_INLINE_SPACING = 2;
47
+ const ICON_LIBRARY_SEARCH_PADDING_BOTTOM = 2;
48
+ const ICON_LIBRARY_TITLE_SIZE = '14px';
43
49
 
44
50
  const CompactIconLibraryMenuList = styled( StyledMenuList )( ( { theme } ) => ( {
45
51
  '& > [role="option"]': {
46
- padding: theme.spacing( 0.75, ICON_LIBRARY_INLINE_SPACING ),
52
+ padding: theme.spacing( ICON_LIBRARY_ROW_VERTICAL_PADDING, ICON_LIBRARY_INLINE_SPACING ),
47
53
  },
48
54
  } ) );
49
55
 
@@ -78,7 +84,10 @@ export const IconLibraryPopover = ( {
78
84
  ICON_LIBRARY_VIEW_STORAGE_PREFIX
79
85
  );
80
86
  const view = isIconLibraryView( storedView ) ? storedView : DEFAULT_ICON_LIBRARY_VIEW;
81
- const { data: icons = [], isLoading } = useFontAwesome7Catalog( open );
87
+ const { data: fontAwesomeIcons = [], isLoading: isFontAwesomeLoading } = useFontAwesome7Catalog( open );
88
+ const { data: customIcons = [], isLoading: isCustomLoading } = useCustomIconLibraries( open );
89
+ const isLoading = isFontAwesomeLoading || isCustomLoading;
90
+ const icons = useMemo( () => [ ...fontAwesomeIcons, ...customIcons ], [ customIcons, fontAwesomeIcons ] );
82
91
 
83
92
  const items = useMemo(
84
93
  () => createIconLibraryItems( icons, searchValue, activeLibraries ),
@@ -103,7 +112,7 @@ export const IconLibraryPopover = ( {
103
112
  }
104
113
 
105
114
  onSelect( {
106
- value: createIconSelectionValue( icon.library, icon.name ),
115
+ value: icon.glyphClass ?? createIconSelectionValue( icon.library, icon.name ),
107
116
  library: icon.library,
108
117
  } );
109
118
  };
@@ -117,11 +126,20 @@ export const IconLibraryPopover = ( {
117
126
  <PopoverHeader
118
127
  title={ __( 'Icon library', 'elementor' ) }
119
128
  onClose={ handleClose }
120
- icon={ <ComponentsIcon fontSize="tiny" /> }
129
+ icon={ <LibraryIcon fontSize="tiny" /> }
121
130
  actions={ [ <IconLibraryViewToggle key="view" value={ view } onChange={ setStoredView } /> ] }
122
- sx={ { pl: ICON_LIBRARY_INLINE_SPACING, pr: 0.5 } }
131
+ sx={ {
132
+ pl: ICON_LIBRARY_INLINE_SPACING,
133
+ pr: 1,
134
+ '& .MuiTypography-subtitle2': { fontSize: ICON_LIBRARY_TITLE_SIZE },
135
+ } }
123
136
  />
124
- <Stack direction="row" alignItems="center" gap={ 1 } sx={ { px: ICON_LIBRARY_INLINE_SPACING, pb: 1 } }>
137
+ <Stack
138
+ direction="row"
139
+ alignItems="center"
140
+ gap={ 1 }
141
+ sx={ { px: ICON_LIBRARY_INLINE_SPACING, pb: ICON_LIBRARY_SEARCH_PADDING_BOTTOM } }
142
+ >
125
143
  <SearchField
126
144
  value={ searchInputValue }
127
145
  onSearch={ handleSearchChange }
@@ -254,9 +272,7 @@ const IconLibraryRow = ( item: VirtualizedItem< string, string > ) => {
254
272
  flexShrink: 0,
255
273
  } }
256
274
  >
257
- { icon.paths.length > 0 ? (
258
- <FontAwesomeGlyph icon={ icon } size={ ICON_GLYPH_SIZE } color="currentColor" />
259
- ) : null }
275
+ <FontAwesomeGlyph icon={ icon } size={ ICON_GLYPH_SIZE } color="currentColor" />
260
276
  </Box>
261
277
  <Typography variant="caption" color="text.primary" noWrap>
262
278
  { icon.label }
@@ -267,7 +283,7 @@ const IconLibraryRow = ( item: VirtualizedItem< string, string > ) => {
267
283
 
268
284
  const CatalogUnavailable = () => (
269
285
  <Stack alignItems="center" justifyContent="center" height="100%" p={ 2.5 } gap={ 1.5 }>
270
- <ComponentsIcon fontSize="large" />
286
+ <SvgElementIcon fontSize="large" />
271
287
  <Typography align="center" variant="subtitle2" color="text.secondary">
272
288
  { __( "Icons couldn't be loaded.", 'elementor' ) }
273
289
  </Typography>
@@ -276,7 +292,7 @@ const CatalogUnavailable = () => (
276
292
 
277
293
  const NoResults = ( { searchValue, onClear }: { searchValue: string; onClear: () => void } ) => (
278
294
  <Stack alignItems="center" justifyContent="center" height="100%" p={ 2.5 } gap={ 1.5 }>
279
- <ComponentsIcon fontSize="large" />
295
+ <SvgElementIcon fontSize="large" />
280
296
  <Typography align="center" variant="subtitle2" color="text.secondary">
281
297
  { __( 'Sorry, nothing matched', 'elementor' ) }
282
298
  </Typography>
@@ -1,24 +1,13 @@
1
1
  import * as React from 'react';
2
- import { useId } from 'react';
3
- import { CheckIcon, ListIcon, WidgetsIcon } from '@elementor/icons';
4
- import {
5
- bindMenu,
6
- bindToggle,
7
- Menu,
8
- MenuItem,
9
- Stack,
10
- ToggleButton,
11
- Tooltip,
12
- Typography,
13
- usePopupState,
14
- } from '@elementor/ui';
2
+ import { ListIcon, WidgetsIcon } from '@elementor/icons';
3
+ import { IconButton, Tooltip } from '@elementor/ui';
15
4
  import { __ } from '@wordpress/i18n';
16
5
 
17
6
  import { ICON_LIBRARY_ACTION_TOOLTIP_ENTER_DELAY } from './icon-library-tooltip';
18
7
 
19
8
  export type IconLibraryView = 'grid' | 'list';
20
9
 
21
- const VIEW_MENU_WIDTH = 122;
10
+ const HEADER_ACTION_GAP = 0.5;
22
11
 
23
12
  type IconLibraryViewToggleProps = {
24
13
  value: IconLibraryView;
@@ -26,79 +15,30 @@ type IconLibraryViewToggleProps = {
26
15
  };
27
16
 
28
17
  export const IconLibraryViewToggle = ( { value, onChange }: IconLibraryViewToggleProps ) => {
29
- const popupId = useId();
30
- const popupState = usePopupState( {
31
- variant: 'popover',
32
- popupId,
33
- } );
34
18
  const isGrid = value === 'grid';
19
+ const nextView: IconLibraryView = isGrid ? 'list' : 'grid';
35
20
  const ViewIcon = isGrid ? WidgetsIcon : ListIcon;
36
21
  const viewButtonLabel = isGrid ? __( 'Grid view', 'elementor' ) : __( 'List view', 'elementor' );
22
+ const viewButtonAction = isGrid
23
+ ? __( 'Switch to list view', 'elementor' )
24
+ : __( 'Switch to grid view', 'elementor' );
37
25
 
38
26
  return (
39
- <>
40
- <Tooltip
41
- title={ viewButtonLabel }
42
- placement="top"
43
- enterDelay={ ICON_LIBRARY_ACTION_TOOLTIP_ENTER_DELAY }
44
- enterNextDelay={ ICON_LIBRARY_ACTION_TOOLTIP_ENTER_DELAY }
45
- disableInteractive
27
+ <Tooltip
28
+ title={ viewButtonLabel }
29
+ placement="top"
30
+ enterDelay={ ICON_LIBRARY_ACTION_TOOLTIP_ENTER_DELAY }
31
+ enterNextDelay={ ICON_LIBRARY_ACTION_TOOLTIP_ENTER_DELAY }
32
+ disableInteractive
33
+ >
34
+ <IconButton
35
+ aria-label={ viewButtonAction }
36
+ size="tiny"
37
+ onClick={ () => onChange( nextView ) }
38
+ sx={ { flexShrink: 0, mr: HEADER_ACTION_GAP } }
46
39
  >
47
- <ToggleButton
48
- aria-label={ viewButtonLabel }
49
- value="view"
50
- size="tiny"
51
- selected={ popupState.isOpen }
52
- sx={ { flexShrink: 0 } }
53
- { ...bindToggle( popupState ) }
54
- aria-expanded={ popupState.isOpen }
55
- >
56
- <ViewIcon fontSize="tiny" />
57
- </ToggleButton>
58
- </Tooltip>
59
- <Menu
60
- { ...bindMenu( popupState ) }
61
- MenuListProps={ {
62
- dense: true,
63
- autoFocusItem: true,
64
- 'aria-label': __( 'View', 'elementor' ),
65
- } }
66
- sx={ { '& .MuiPaper-root': { minWidth: VIEW_MENU_WIDTH } } }
67
- >
68
- <ViewMenuItem
69
- label={ __( 'List', 'elementor' ) }
70
- selected={ value === 'list' }
71
- onClick={ () => {
72
- onChange( 'list' );
73
- popupState.close();
74
- } }
75
- />
76
- <ViewMenuItem
77
- label={ __( 'Grid', 'elementor' ) }
78
- selected={ value === 'grid' }
79
- onClick={ () => {
80
- onChange( 'grid' );
81
- popupState.close();
82
- } }
83
- />
84
- </Menu>
85
- </>
40
+ <ViewIcon fontSize="tiny" />
41
+ </IconButton>
42
+ </Tooltip>
86
43
  );
87
44
  };
88
-
89
- type ViewMenuItemProps = {
90
- label: string;
91
- selected: boolean;
92
- onClick: () => void;
93
- };
94
-
95
- const ViewMenuItem = ( { label, selected, onClick }: ViewMenuItemProps ) => (
96
- <MenuItem role="menuitemradio" aria-checked={ selected } selected={ selected } onClick={ onClick }>
97
- <Stack direction="row" alignItems="center" gap={ 1 } width="100%">
98
- <Typography variant="caption" sx={ { flex: 1 } }>
99
- { label }
100
- </Typography>
101
- { selected ? <CheckIcon fontSize="tiny" aria-hidden="true" /> : null }
102
- </Stack>
103
- </MenuItem>
104
- );
@@ -0,0 +1,53 @@
1
+ const ALLOWED_SVG_TAGS = new Set( [ 'svg', 'g', 'path', 'title', 'desc' ] );
2
+ const ALLOWED_SVG_ATTRS = new Set( [
3
+ 'd',
4
+ 'fill',
5
+ 'viewbox',
6
+ 'xmlns',
7
+ 'width',
8
+ 'height',
9
+ 'transform',
10
+ 'overflow',
11
+ 'aria-hidden',
12
+ 'aria-label',
13
+ 'role',
14
+ ] );
15
+
16
+ export function sanitizeIconSvgMarkup( markup: unknown ): string {
17
+ if ( typeof markup !== 'string' || markup === '' ) {
18
+ return '';
19
+ }
20
+
21
+ const doc = new DOMParser().parseFromString( markup, 'image/svg+xml' );
22
+
23
+ if ( doc.querySelector( 'parsererror' ) ) {
24
+ return '';
25
+ }
26
+
27
+ const svg = doc.querySelector( 'svg' );
28
+
29
+ if ( ! svg ) {
30
+ return '';
31
+ }
32
+
33
+ pruneNode( svg );
34
+
35
+ return svg.tagName.toLowerCase() === 'svg' ? svg.outerHTML : '';
36
+ }
37
+
38
+ function pruneNode( node: Element ): void {
39
+ [ ...node.children ].forEach( ( child ) => {
40
+ if ( ! ALLOWED_SVG_TAGS.has( child.tagName.toLowerCase() ) ) {
41
+ child.remove();
42
+ return;
43
+ }
44
+
45
+ pruneNode( child );
46
+ } );
47
+
48
+ [ ...node.attributes ].forEach( ( attribute ) => {
49
+ if ( ! ALLOWED_SVG_ATTRS.has( attribute.name.toLowerCase() ) ) {
50
+ node.removeAttribute( attribute.name );
51
+ }
52
+ } );
53
+ }
@@ -0,0 +1,15 @@
1
+ import * as React from 'react';
2
+ import { SvgIcon } from '@elementor/ui';
3
+
4
+ const SVG_ELEMENT_ICON_PATH =
5
+ 'M502 772c10 0 19-6 24-14l201-351c4-9 4-19 0-27-5-9-14-14-24-14h-401c-10 0-19 5-24 14-4 8-5 18 0 27l201 351c5 8 14 14 23 14z m0-82l-154-270h309l-154 270z m-323-471c-6 0-12-2-16-7-4-4-7-10-7-16v-201c0-6 3-12 7-16 4-5 10-7 16-7h201c6 0 12 2 16 7 5 4 7 10 7 16v201c0 6-2 12-7 16-4 5-10 7-16 7h-201z m-54 32c14 14 34 22 54 22h201c20 0 40-8 55-22 14-15 22-35 22-55v-201c0-20-8-40-22-54-15-15-35-23-55-23h-201c-20 0-40 8-54 23-15 14-23 34-23 54v201c0 20 8 40 23 55z m480-30c34 33 79 52 126 52s92-19 125-52c34-33 52-78 52-125s-18-93-52-126c-33-33-78-52-125-52s-92 19-126 52c-33 33-52 78-52 126s19 92 52 125z m126-2c-33 0-64-13-87-36-24-23-37-55-37-87s13-65 37-88c23-23 54-36 87-36s64 13 87 36c23 23 36 55 36 88s-13 64-36 87c-23 23-54 36-87 36z';
6
+
7
+ export const SvgElementIcon = React.forwardRef< SVGSVGElement, React.ComponentProps< typeof SvgIcon > >(
8
+ ( props, ref ) => (
9
+ <SvgIcon viewBox="0 0 1000 1000" { ...props } ref={ ref }>
10
+ <g transform="translate(0, 850) scale(1, -1)">
11
+ <path d={ SVG_ELEMENT_ICON_PATH } />
12
+ </g>
13
+ </SvgIcon>
14
+ )
15
+ );