@elementor/editor-controls 4.3.4 → 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.
- package/dist/index.d.mts +10 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.js +2431 -1698
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2093 -1362
- package/dist/index.mjs.map +1 -1
- package/package.json +18 -18
- package/src/controls/color-control.tsx +5 -0
- package/src/controls/email-form-action-control/fields.tsx +27 -2
- package/src/controls/icon-library/custom-icon-libraries.ts +556 -0
- package/src/controls/icon-library/font-awesome-7-catalog.ts +16 -7
- package/src/controls/icon-library/font-awesome-7-data.ts +2 -0
- package/src/controls/icon-library/font-awesome-glyph.tsx +62 -17
- package/src/controls/icon-library/fontello-svg-font.ts +227 -0
- package/src/controls/icon-library/icomoon-selection.ts +104 -0
- package/src/controls/icon-library/icon-library-filter.tsx +142 -68
- package/src/controls/icon-library/icon-library-grid.tsx +19 -15
- package/src/controls/icon-library/icon-library-popover.tsx +31 -15
- package/src/controls/icon-library/icon-library-view-toggle.tsx +22 -82
- package/src/controls/icon-library/sanitize-icon-svg-markup.ts +53 -0
- package/src/controls/icon-library/svg-element-icon.tsx +15 -0
- package/src/controls/image-control.tsx +3 -3
- package/src/controls/image-media-control.tsx +15 -33
- package/src/controls/media-select-actions.tsx +105 -0
- package/src/controls/notice-control.tsx +11 -1
- package/src/controls/svg-media-control.tsx +4 -1
- package/src/controls/svg-media-overlay.tsx +24 -76
- package/src/controls/svg-media-preview.tsx +7 -2
- package/src/controls/video-media-control.tsx +13 -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 {
|
|
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
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
67
|
-
|
|
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
|
|
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 ===
|
|
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:
|
|
115
|
-
insetInlineEnd:
|
|
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: '
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
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
|
|
17
|
-
const GRID_HORIZONTAL_PADDING =
|
|
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.
|
|
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,
|
|
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:
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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 {
|
|
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 = '
|
|
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 =
|
|
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(
|
|
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:
|
|
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={ <
|
|
129
|
+
icon={ <LibraryIcon fontSize="tiny" /> }
|
|
121
130
|
actions={ [ <IconLibraryViewToggle key="view" value={ view } onChange={ setStoredView } /> ] }
|
|
122
|
-
sx={ {
|
|
131
|
+
sx={ {
|
|
132
|
+
pl: ICON_LIBRARY_INLINE_SPACING,
|
|
133
|
+
pr: 1,
|
|
134
|
+
'& .MuiTypography-subtitle2': { fontSize: ICON_LIBRARY_TITLE_SIZE },
|
|
135
|
+
} }
|
|
123
136
|
/>
|
|
124
|
-
<Stack
|
|
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
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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 {
|
|
3
|
-
import {
|
|
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
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
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
|
+
);
|