@elementor/editor-controls 4.3.0-beta1 → 4.3.0-beta3
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 +25 -2
- package/dist/index.d.ts +25 -2
- package/dist/index.js +2108 -922
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1950 -751
- package/dist/index.mjs.map +1 -1
- package/package.json +17 -16
- package/src/controls/icon-library/font-awesome-7-catalog.ts +113 -0
- package/src/controls/icon-library/font-awesome-7-data.ts +260 -0
- package/src/controls/icon-library/font-awesome-glyph.tsx +30 -0
- package/src/controls/icon-library/get-icon-library-anchor.ts +29 -0
- package/src/controls/icon-library/icon-library-filter.tsx +166 -0
- package/src/controls/icon-library/icon-library-grid.tsx +344 -0
- package/src/controls/icon-library/icon-library-popover.tsx +305 -0
- package/src/controls/icon-library/icon-library-tooltip.ts +1 -0
- package/src/controls/icon-library/icon-library-view-toggle.tsx +104 -0
- package/src/controls/icon-library/use-font-awesome-7-catalog.ts +14 -0
- package/src/controls/notice-control.tsx +105 -0
- package/src/controls/svg-media-control.tsx +132 -159
- package/src/controls/svg-media-overlay.tsx +137 -0
- package/src/controls/svg-media-preview.tsx +68 -0
- package/src/controls/toggle-control.tsx +13 -2
- package/src/index.ts +7 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Box, Tooltip, useTheme } from '@elementor/ui';
|
|
4
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
5
|
+
import { __ } from '@wordpress/i18n';
|
|
6
|
+
|
|
7
|
+
import { type FontAwesome7Icon } from './font-awesome-7-catalog';
|
|
8
|
+
import { FontAwesomeGlyph } from './font-awesome-glyph';
|
|
9
|
+
|
|
10
|
+
export const ICON_LIBRARY_GRID_FALLBACK_COLUMN_COUNT = 4;
|
|
11
|
+
export const ICON_LIBRARY_GRID_MIN_CELL_SIZE = 52;
|
|
12
|
+
export const ICON_LIBRARY_GRID_TOOLTIP_ENTER_DELAY = 1000;
|
|
13
|
+
|
|
14
|
+
const ICON_GLYPH_SIZE = 20;
|
|
15
|
+
const GRID_OVERSCAN = 6;
|
|
16
|
+
const GRID_COLUMN_GAP = 1;
|
|
17
|
+
const GRID_HORIZONTAL_PADDING = 1;
|
|
18
|
+
const HOME_END_KEYS = new Set( [ 'Home', 'End' ] );
|
|
19
|
+
|
|
20
|
+
const getGridMetrics = ( containerWidth: number, columnGap: number, inlinePadding: number ) => {
|
|
21
|
+
const availableWidth = containerWidth - inlinePadding;
|
|
22
|
+
|
|
23
|
+
if ( availableWidth <= 0 ) {
|
|
24
|
+
return {
|
|
25
|
+
columnCount: ICON_LIBRARY_GRID_FALLBACK_COLUMN_COUNT,
|
|
26
|
+
cellSize: ICON_LIBRARY_GRID_MIN_CELL_SIZE,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const columnCount = Math.max(
|
|
31
|
+
1,
|
|
32
|
+
Math.floor( ( availableWidth + columnGap ) / ( ICON_LIBRARY_GRID_MIN_CELL_SIZE + columnGap ) )
|
|
33
|
+
);
|
|
34
|
+
const cellSize = Math.floor( ( availableWidth - columnGap * ( columnCount - 1 ) ) / columnCount );
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
columnCount,
|
|
38
|
+
cellSize: Math.max( cellSize, 1 ),
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type IconLibraryGridItem = FontAwesome7Icon & {
|
|
43
|
+
id: string;
|
|
44
|
+
value: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type IconLibraryGridProps = {
|
|
48
|
+
items: IconLibraryGridItem[];
|
|
49
|
+
selectedValue?: string;
|
|
50
|
+
onSelect: ( id: string ) => void;
|
|
51
|
+
onClose: () => void;
|
|
52
|
+
noResultsComponent?: React.ReactNode;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const IconLibraryGrid = ( {
|
|
56
|
+
items,
|
|
57
|
+
selectedValue,
|
|
58
|
+
onSelect,
|
|
59
|
+
onClose,
|
|
60
|
+
noResultsComponent,
|
|
61
|
+
}: IconLibraryGridProps ) => {
|
|
62
|
+
const theme = useTheme();
|
|
63
|
+
const containerRef = useRef< HTMLDivElement >( null );
|
|
64
|
+
const cellRefs = useRef( new Map< string, HTMLButtonElement >() );
|
|
65
|
+
const shouldRestoreFocusRef = useRef( false );
|
|
66
|
+
const selectedIndex = items.findIndex( ( item ) => item.id === selectedValue );
|
|
67
|
+
const [ focusedIndex, setFocusedIndex ] = useState( selectedIndex >= 0 ? selectedIndex : 0 );
|
|
68
|
+
const [ { columnCount, cellSize }, setGridMetrics ] = useState( () => getGridMetrics( 0, 0, 0 ) );
|
|
69
|
+
const rowCount = Math.ceil( items.length / columnCount );
|
|
70
|
+
const virtualizer = useVirtualizer( {
|
|
71
|
+
count: rowCount,
|
|
72
|
+
getScrollElement: () => containerRef.current,
|
|
73
|
+
estimateSize: () => cellSize,
|
|
74
|
+
overscan: GRID_OVERSCAN,
|
|
75
|
+
} );
|
|
76
|
+
const focusedItem = items[ focusedIndex ];
|
|
77
|
+
const visibleRowIndexes = virtualizer.getVirtualIndexes().join( ',' );
|
|
78
|
+
|
|
79
|
+
useLayoutEffect( () => {
|
|
80
|
+
setFocusedIndex( ( current ) => {
|
|
81
|
+
if ( items.length === 0 ) {
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if ( selectedIndex >= 0 ) {
|
|
86
|
+
return selectedIndex;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return Math.min( current, items.length - 1 );
|
|
90
|
+
} );
|
|
91
|
+
|
|
92
|
+
if ( selectedIndex >= 0 ) {
|
|
93
|
+
virtualizer.scrollToIndex( Math.floor( selectedIndex / columnCount ) );
|
|
94
|
+
}
|
|
95
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
96
|
+
}, [ columnCount, items, selectedIndex, selectedValue ] );
|
|
97
|
+
|
|
98
|
+
useLayoutEffect( () => {
|
|
99
|
+
if ( ! shouldRestoreFocusRef.current || ! focusedItem ) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const cell = cellRefs.current.get( focusedItem.id );
|
|
104
|
+
|
|
105
|
+
if ( ! cell ) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
cell.focus();
|
|
110
|
+
shouldRestoreFocusRef.current = false;
|
|
111
|
+
}, [ focusedItem, visibleRowIndexes ] );
|
|
112
|
+
|
|
113
|
+
useLayoutEffect( () => {
|
|
114
|
+
const container = containerRef.current;
|
|
115
|
+
|
|
116
|
+
if ( ! container ) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const measureGrid = () => {
|
|
121
|
+
const columnGap = Number.parseFloat( theme.spacing( GRID_COLUMN_GAP ) );
|
|
122
|
+
const inlinePadding = Number.parseFloat( theme.spacing( GRID_HORIZONTAL_PADDING ) ) * 2;
|
|
123
|
+
setGridMetrics( getGridMetrics( container.clientWidth, columnGap, inlinePadding ) );
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
measureGrid();
|
|
127
|
+
const resizeObserver = new ResizeObserver( measureGrid );
|
|
128
|
+
resizeObserver.observe( container );
|
|
129
|
+
|
|
130
|
+
return () => {
|
|
131
|
+
resizeObserver.disconnect();
|
|
132
|
+
};
|
|
133
|
+
}, [ theme, items.length ] );
|
|
134
|
+
|
|
135
|
+
useLayoutEffect( () => {
|
|
136
|
+
virtualizer.measure();
|
|
137
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
138
|
+
}, [ cellSize, columnCount ] );
|
|
139
|
+
|
|
140
|
+
const isRtl = theme.direction === 'rtl' || document.documentElement.dir === 'rtl';
|
|
141
|
+
const horizontalOffset = isRtl ? -1 : 1;
|
|
142
|
+
|
|
143
|
+
const moveFocus = ( nextIndex: number ) => {
|
|
144
|
+
if ( nextIndex < 0 || nextIndex >= items.length ) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
shouldRestoreFocusRef.current = true;
|
|
149
|
+
setFocusedIndex( nextIndex );
|
|
150
|
+
virtualizer.scrollToIndex( Math.floor( nextIndex / columnCount ) );
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const handleKeyDown = ( event: React.KeyboardEvent< HTMLButtonElement >, index: number ) => {
|
|
154
|
+
if ( event.key === 'Enter' || event.key === ' ' ) {
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
onSelect( items[ index ].id );
|
|
157
|
+
onClose();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if ( event.key === 'ArrowRight' ) {
|
|
162
|
+
event.preventDefault();
|
|
163
|
+
moveFocus( index + horizontalOffset );
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if ( event.key === 'ArrowLeft' ) {
|
|
168
|
+
event.preventDefault();
|
|
169
|
+
moveFocus( index - horizontalOffset );
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if ( event.key === 'ArrowDown' ) {
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
moveFocus( index + columnCount );
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if ( event.key === 'ArrowUp' ) {
|
|
180
|
+
event.preventDefault();
|
|
181
|
+
moveFocus( index - columnCount );
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if ( HOME_END_KEYS.has( event.key ) ) {
|
|
186
|
+
event.preventDefault();
|
|
187
|
+
|
|
188
|
+
if ( event.ctrlKey ) {
|
|
189
|
+
moveFocus( event.key === 'Home' ? 0 : items.length - 1 );
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const rowStart = Math.floor( index / columnCount ) * columnCount;
|
|
194
|
+
const rowEnd = Math.min( rowStart + columnCount - 1, items.length - 1 );
|
|
195
|
+
|
|
196
|
+
moveFocus( event.key === 'Home' ? rowStart : rowEnd );
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<Box
|
|
202
|
+
ref={ containerRef }
|
|
203
|
+
sx={ {
|
|
204
|
+
width: '100%',
|
|
205
|
+
height: '100%',
|
|
206
|
+
minWidth: 0,
|
|
207
|
+
overflowX: 'hidden',
|
|
208
|
+
overflowY: 'auto',
|
|
209
|
+
} }
|
|
210
|
+
>
|
|
211
|
+
{ items.length === 0 && noResultsComponent ? (
|
|
212
|
+
noResultsComponent
|
|
213
|
+
) : (
|
|
214
|
+
<Box
|
|
215
|
+
role="grid"
|
|
216
|
+
aria-label={ __( 'Icons', 'elementor' ) }
|
|
217
|
+
aria-rowcount={ rowCount }
|
|
218
|
+
aria-colcount={ columnCount }
|
|
219
|
+
data-testid="icon-library-grid"
|
|
220
|
+
sx={ {
|
|
221
|
+
width: '100%',
|
|
222
|
+
minWidth: 0,
|
|
223
|
+
height: virtualizer.getTotalSize(),
|
|
224
|
+
position: 'relative',
|
|
225
|
+
} }
|
|
226
|
+
>
|
|
227
|
+
{ virtualizer.getVirtualItems().map( ( virtualRow ) => {
|
|
228
|
+
const startIndex = virtualRow.index * columnCount;
|
|
229
|
+
const rowItems = items.slice( startIndex, startIndex + columnCount );
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<Box
|
|
233
|
+
key={ virtualRow.key }
|
|
234
|
+
role="row"
|
|
235
|
+
aria-rowindex={ virtualRow.index + 1 }
|
|
236
|
+
sx={ {
|
|
237
|
+
position: 'absolute',
|
|
238
|
+
top: 0,
|
|
239
|
+
left: 0,
|
|
240
|
+
width: '100%',
|
|
241
|
+
height: virtualRow.size,
|
|
242
|
+
transform: `translateY(${ virtualRow.start }px)`,
|
|
243
|
+
display: 'grid',
|
|
244
|
+
gridTemplateColumns: `repeat(${ columnCount }, minmax(0, 1fr))`,
|
|
245
|
+
gap: GRID_COLUMN_GAP,
|
|
246
|
+
px: GRID_HORIZONTAL_PADDING,
|
|
247
|
+
boxSizing: 'border-box',
|
|
248
|
+
} }
|
|
249
|
+
>
|
|
250
|
+
{ rowItems.map( ( item, columnIndex ) => {
|
|
251
|
+
const index = startIndex + columnIndex;
|
|
252
|
+
const isSelected = selectedValue === item.id;
|
|
253
|
+
const tabIndex = focusedIndex === index ? 0 : -1;
|
|
254
|
+
|
|
255
|
+
return (
|
|
256
|
+
<Box
|
|
257
|
+
key={ item.id }
|
|
258
|
+
role="presentation"
|
|
259
|
+
sx={ { minWidth: 0, minHeight: 0, width: '100%', height: '100%' } }
|
|
260
|
+
>
|
|
261
|
+
<Tooltip
|
|
262
|
+
title={ item.label }
|
|
263
|
+
placement="top"
|
|
264
|
+
enterDelay={ ICON_LIBRARY_GRID_TOOLTIP_ENTER_DELAY }
|
|
265
|
+
enterNextDelay={ ICON_LIBRARY_GRID_TOOLTIP_ENTER_DELAY }
|
|
266
|
+
disableInteractive
|
|
267
|
+
disableFocusListener
|
|
268
|
+
>
|
|
269
|
+
<Box
|
|
270
|
+
component="button"
|
|
271
|
+
type="button"
|
|
272
|
+
role="gridcell"
|
|
273
|
+
aria-colindex={ columnIndex + 1 }
|
|
274
|
+
aria-label={ item.label }
|
|
275
|
+
aria-selected={ isSelected }
|
|
276
|
+
tabIndex={ tabIndex }
|
|
277
|
+
ref={ ( node: HTMLButtonElement | null ) => {
|
|
278
|
+
if ( node ) {
|
|
279
|
+
cellRefs.current.set( item.id, node );
|
|
280
|
+
} else {
|
|
281
|
+
cellRefs.current.delete( item.id );
|
|
282
|
+
}
|
|
283
|
+
} }
|
|
284
|
+
onClick={ () => {
|
|
285
|
+
onSelect( item.id );
|
|
286
|
+
onClose();
|
|
287
|
+
} }
|
|
288
|
+
onFocus={ () => setFocusedIndex( index ) }
|
|
289
|
+
onKeyDown={ ( event: React.KeyboardEvent< HTMLButtonElement > ) =>
|
|
290
|
+
handleKeyDown( event, index )
|
|
291
|
+
}
|
|
292
|
+
sx={ {
|
|
293
|
+
boxSizing: 'border-box',
|
|
294
|
+
appearance: 'none',
|
|
295
|
+
m: 0,
|
|
296
|
+
width: '100%',
|
|
297
|
+
height: '100%',
|
|
298
|
+
minWidth: 0,
|
|
299
|
+
minHeight: 0,
|
|
300
|
+
display: 'flex',
|
|
301
|
+
alignItems: 'center',
|
|
302
|
+
justifyContent: 'center',
|
|
303
|
+
border: '1px solid',
|
|
304
|
+
borderColor: 'divider',
|
|
305
|
+
borderRadius: 1,
|
|
306
|
+
color: 'text.tertiary',
|
|
307
|
+
bgcolor: 'transparent',
|
|
308
|
+
p: 0,
|
|
309
|
+
cursor: 'pointer',
|
|
310
|
+
font: 'inherit',
|
|
311
|
+
lineHeight: 0,
|
|
312
|
+
overflow: 'hidden',
|
|
313
|
+
'&:hover, &:focus': {
|
|
314
|
+
bgcolor: 'action.hover',
|
|
315
|
+
},
|
|
316
|
+
'&[aria-selected="true"]': {
|
|
317
|
+
bgcolor: 'action.selected',
|
|
318
|
+
},
|
|
319
|
+
'&[aria-selected="true"]:hover, &[aria-selected="true"]:focus':
|
|
320
|
+
{
|
|
321
|
+
bgcolor: 'action.selected',
|
|
322
|
+
},
|
|
323
|
+
} }
|
|
324
|
+
>
|
|
325
|
+
{ item.paths.length > 0 ? (
|
|
326
|
+
<FontAwesomeGlyph
|
|
327
|
+
icon={ item }
|
|
328
|
+
size={ ICON_GLYPH_SIZE }
|
|
329
|
+
color="currentColor"
|
|
330
|
+
/>
|
|
331
|
+
) : null }
|
|
332
|
+
</Box>
|
|
333
|
+
</Tooltip>
|
|
334
|
+
</Box>
|
|
335
|
+
);
|
|
336
|
+
} ) }
|
|
337
|
+
</Box>
|
|
338
|
+
);
|
|
339
|
+
} ) }
|
|
340
|
+
</Box>
|
|
341
|
+
) }
|
|
342
|
+
</Box>
|
|
343
|
+
);
|
|
344
|
+
};
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useMemo, useState } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
PopoverBody,
|
|
5
|
+
PopoverHeader,
|
|
6
|
+
PopoverMenuList,
|
|
7
|
+
SearchField,
|
|
8
|
+
StyledMenuList,
|
|
9
|
+
type VirtualizedItem,
|
|
10
|
+
} from '@elementor/editor-ui';
|
|
11
|
+
import { ComponentsIcon } from '@elementor/icons';
|
|
12
|
+
import { useSessionStorage } from '@elementor/session';
|
|
13
|
+
import { Box, CircularProgress, Divider, Link, Stack, styled, Typography } from '@elementor/ui';
|
|
14
|
+
import { useDebounceState } from '@elementor/utils';
|
|
15
|
+
import { __ } from '@wordpress/i18n';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
createIconSelectionValue,
|
|
19
|
+
filterFontAwesome7Icons,
|
|
20
|
+
findFontAwesome7Icon,
|
|
21
|
+
type FontAwesome7Icon,
|
|
22
|
+
type FontAwesome7LibraryFilter,
|
|
23
|
+
} from './font-awesome-7-catalog';
|
|
24
|
+
import { FontAwesomeGlyph } from './font-awesome-glyph';
|
|
25
|
+
import { IconLibraryFilter } from './icon-library-filter';
|
|
26
|
+
import { IconLibraryGrid } from './icon-library-grid';
|
|
27
|
+
import { type IconLibraryView, IconLibraryViewToggle } from './icon-library-view-toggle';
|
|
28
|
+
import { useFontAwesome7Catalog } from './use-font-awesome-7-catalog';
|
|
29
|
+
|
|
30
|
+
export const ICON_LIBRARY_POPOVER_WIDTH = 300;
|
|
31
|
+
export const ICON_LIBRARY_ROW_HEIGHT = 48;
|
|
32
|
+
export const ICON_LIBRARY_SEARCH_DEBOUNCE_DELAY = 300;
|
|
33
|
+
export const ICON_LIBRARY_VIEW_STORAGE_KEY = 'icon-library-view';
|
|
34
|
+
export const ICON_LIBRARY_VIEW_STORAGE_PREFIX = 'editor-controls';
|
|
35
|
+
const DEFAULT_ICON_LIBRARY_VIEW: IconLibraryView = 'list';
|
|
36
|
+
|
|
37
|
+
const isIconLibraryView = ( value: unknown ): value is IconLibraryView => {
|
|
38
|
+
return value === 'grid' || value === 'list';
|
|
39
|
+
};
|
|
40
|
+
const ICON_TILE_SIZE = 40;
|
|
41
|
+
const ICON_GLYPH_SIZE = 20;
|
|
42
|
+
const ICON_LIBRARY_INLINE_SPACING = 1;
|
|
43
|
+
|
|
44
|
+
const CompactIconLibraryMenuList = styled( StyledMenuList )( ( { theme } ) => ( {
|
|
45
|
+
'& > [role="option"]': {
|
|
46
|
+
padding: theme.spacing( 0.75, ICON_LIBRARY_INLINE_SPACING ),
|
|
47
|
+
},
|
|
48
|
+
} ) );
|
|
49
|
+
|
|
50
|
+
type IconLibraryItem = VirtualizedItem< 'item', string > & Omit< FontAwesome7Icon, 'value' >;
|
|
51
|
+
|
|
52
|
+
type IconLibraryPopoverProps = {
|
|
53
|
+
open: boolean;
|
|
54
|
+
selectedIconClass: string | null;
|
|
55
|
+
selectedIconLibrary: string | null;
|
|
56
|
+
onSelect: ( icon: { value: string; library: string } ) => void;
|
|
57
|
+
onClose: () => void;
|
|
58
|
+
width?: number;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const IconLibraryPopover = ( {
|
|
62
|
+
open,
|
|
63
|
+
selectedIconClass,
|
|
64
|
+
selectedIconLibrary,
|
|
65
|
+
onSelect,
|
|
66
|
+
onClose,
|
|
67
|
+
width = ICON_LIBRARY_POPOVER_WIDTH,
|
|
68
|
+
}: IconLibraryPopoverProps ) => {
|
|
69
|
+
const {
|
|
70
|
+
debouncedValue: searchValue,
|
|
71
|
+
inputValue: searchInputValue,
|
|
72
|
+
handleChange: handleSearchChange,
|
|
73
|
+
setImmediateValue: setSearchValue,
|
|
74
|
+
} = useDebounceState( { delay: ICON_LIBRARY_SEARCH_DEBOUNCE_DELAY } );
|
|
75
|
+
const [ activeLibraries, setActiveLibraries ] = useState< FontAwesome7LibraryFilter >( [] );
|
|
76
|
+
const [ storedView, setStoredView ] = useSessionStorage< IconLibraryView >(
|
|
77
|
+
ICON_LIBRARY_VIEW_STORAGE_KEY,
|
|
78
|
+
ICON_LIBRARY_VIEW_STORAGE_PREFIX
|
|
79
|
+
);
|
|
80
|
+
const view = isIconLibraryView( storedView ) ? storedView : DEFAULT_ICON_LIBRARY_VIEW;
|
|
81
|
+
const { data: icons = [], isLoading } = useFontAwesome7Catalog( open );
|
|
82
|
+
|
|
83
|
+
const items = useMemo(
|
|
84
|
+
() => createIconLibraryItems( icons, searchValue, activeLibraries ),
|
|
85
|
+
[ activeLibraries, icons, searchValue ]
|
|
86
|
+
);
|
|
87
|
+
const selectedValue = useMemo(
|
|
88
|
+
() => findFontAwesome7Icon( icons, selectedIconClass, selectedIconLibrary )?.id,
|
|
89
|
+
[ icons, selectedIconClass, selectedIconLibrary ]
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const handleClose = () => {
|
|
93
|
+
setSearchValue( '' );
|
|
94
|
+
setActiveLibraries( [] );
|
|
95
|
+
onClose();
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const handleSelect = ( id: string ) => {
|
|
99
|
+
const icon = items.find( ( item ) => item.id === id );
|
|
100
|
+
|
|
101
|
+
if ( ! icon ) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
onSelect( {
|
|
106
|
+
value: createIconSelectionValue( icon.library, icon.name ),
|
|
107
|
+
library: icon.library,
|
|
108
|
+
} );
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const handleClearSearch = () => {
|
|
112
|
+
setSearchValue( '' );
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<PopoverBody width={ width } fillWidth id="icon-library">
|
|
117
|
+
<PopoverHeader
|
|
118
|
+
title={ __( 'Icon library', 'elementor' ) }
|
|
119
|
+
onClose={ handleClose }
|
|
120
|
+
icon={ <ComponentsIcon fontSize="tiny" /> }
|
|
121
|
+
actions={ [ <IconLibraryViewToggle key="view" value={ view } onChange={ setStoredView } /> ] }
|
|
122
|
+
sx={ { pl: ICON_LIBRARY_INLINE_SPACING, pr: 0.5 } }
|
|
123
|
+
/>
|
|
124
|
+
<Stack direction="row" alignItems="center" gap={ 1 } sx={ { px: ICON_LIBRARY_INLINE_SPACING, pb: 1 } }>
|
|
125
|
+
<SearchField
|
|
126
|
+
value={ searchInputValue }
|
|
127
|
+
onSearch={ handleSearchChange }
|
|
128
|
+
placeholder={ __( 'Search', 'elementor' ) }
|
|
129
|
+
id="icon-library-search"
|
|
130
|
+
sx={ { flex: 1, px: 0, pb: 0 } }
|
|
131
|
+
/>
|
|
132
|
+
<IconLibraryFilter value={ activeLibraries } onChange={ setActiveLibraries } />
|
|
133
|
+
</Stack>
|
|
134
|
+
<Divider />
|
|
135
|
+
<Box sx={ { flex: 1, overflow: 'hidden', minHeight: 0, minWidth: 0 } }>
|
|
136
|
+
<IconLibraryContent
|
|
137
|
+
isLoading={ isLoading }
|
|
138
|
+
items={ items }
|
|
139
|
+
selectedValue={ selectedValue }
|
|
140
|
+
searchValue={ searchValue }
|
|
141
|
+
isCatalogAvailable={ icons.length > 0 }
|
|
142
|
+
view={ view }
|
|
143
|
+
onSelect={ handleSelect }
|
|
144
|
+
onClose={ handleClose }
|
|
145
|
+
onClearSearch={ handleClearSearch }
|
|
146
|
+
/>
|
|
147
|
+
</Box>
|
|
148
|
+
</PopoverBody>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
type IconLibraryContentProps = {
|
|
153
|
+
isLoading: boolean;
|
|
154
|
+
items: IconLibraryItem[];
|
|
155
|
+
selectedValue: string | undefined;
|
|
156
|
+
searchValue: string;
|
|
157
|
+
isCatalogAvailable: boolean;
|
|
158
|
+
view: IconLibraryView;
|
|
159
|
+
onSelect: ( id: string ) => void;
|
|
160
|
+
onClose: () => void;
|
|
161
|
+
onClearSearch: () => void;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const IconLibraryContent = ( {
|
|
165
|
+
isLoading,
|
|
166
|
+
items,
|
|
167
|
+
selectedValue,
|
|
168
|
+
searchValue,
|
|
169
|
+
isCatalogAvailable,
|
|
170
|
+
view,
|
|
171
|
+
onSelect,
|
|
172
|
+
onClose,
|
|
173
|
+
onClearSearch,
|
|
174
|
+
}: IconLibraryContentProps ) => {
|
|
175
|
+
if ( isLoading ) {
|
|
176
|
+
return <IconLibraryLoadingState />;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const emptyState = (
|
|
180
|
+
<IconLibraryEmptyState
|
|
181
|
+
searchValue={ searchValue }
|
|
182
|
+
isCatalogAvailable={ isCatalogAvailable }
|
|
183
|
+
onClear={ onClearSearch }
|
|
184
|
+
/>
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
if ( view === 'grid' ) {
|
|
188
|
+
return (
|
|
189
|
+
<Box sx={ { width: '100%', height: '100%', minWidth: 0, minHeight: 0 } }>
|
|
190
|
+
<IconLibraryGrid
|
|
191
|
+
items={ items }
|
|
192
|
+
selectedValue={ selectedValue }
|
|
193
|
+
onSelect={ onSelect }
|
|
194
|
+
onClose={ onClose }
|
|
195
|
+
noResultsComponent={ emptyState }
|
|
196
|
+
/>
|
|
197
|
+
</Box>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<PopoverMenuList
|
|
203
|
+
items={ items }
|
|
204
|
+
selectedValue={ selectedValue }
|
|
205
|
+
menuListTemplate={ CompactIconLibraryMenuList }
|
|
206
|
+
onSelect={ onSelect }
|
|
207
|
+
onClose={ onClose }
|
|
208
|
+
itemHeight={ ICON_LIBRARY_ROW_HEIGHT }
|
|
209
|
+
menuItemContentTemplate={ IconLibraryRow }
|
|
210
|
+
noResultsComponent={ emptyState }
|
|
211
|
+
data-testid="icon-library-list"
|
|
212
|
+
/>
|
|
213
|
+
);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const IconLibraryLoadingState = () => (
|
|
217
|
+
<Stack alignItems="center" justifyContent="center" height="100%">
|
|
218
|
+
<CircularProgress role="progressbar" size={ 24 } />
|
|
219
|
+
</Stack>
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const IconLibraryEmptyState = ( {
|
|
223
|
+
searchValue,
|
|
224
|
+
isCatalogAvailable,
|
|
225
|
+
onClear,
|
|
226
|
+
}: {
|
|
227
|
+
searchValue: string;
|
|
228
|
+
isCatalogAvailable: boolean;
|
|
229
|
+
onClear: () => void;
|
|
230
|
+
} ) => {
|
|
231
|
+
if ( ! isCatalogAvailable ) {
|
|
232
|
+
return <CatalogUnavailable />;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return <NoResults searchValue={ searchValue } onClear={ onClear } />;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const IconLibraryRow = ( item: VirtualizedItem< string, string > ) => {
|
|
239
|
+
const icon = item as IconLibraryItem;
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<Stack direction="row" alignItems="center" gap={ 1 } sx={ { width: '100%' } }>
|
|
243
|
+
<Box
|
|
244
|
+
sx={ {
|
|
245
|
+
width: ICON_TILE_SIZE,
|
|
246
|
+
height: ICON_TILE_SIZE,
|
|
247
|
+
display: 'flex',
|
|
248
|
+
alignItems: 'center',
|
|
249
|
+
justifyContent: 'center',
|
|
250
|
+
border: 1,
|
|
251
|
+
borderColor: 'divider',
|
|
252
|
+
borderRadius: 1,
|
|
253
|
+
color: 'text.tertiary',
|
|
254
|
+
flexShrink: 0,
|
|
255
|
+
} }
|
|
256
|
+
>
|
|
257
|
+
{ icon.paths.length > 0 ? (
|
|
258
|
+
<FontAwesomeGlyph icon={ icon } size={ ICON_GLYPH_SIZE } color="currentColor" />
|
|
259
|
+
) : null }
|
|
260
|
+
</Box>
|
|
261
|
+
<Typography variant="caption" color="text.primary" noWrap>
|
|
262
|
+
{ icon.label }
|
|
263
|
+
</Typography>
|
|
264
|
+
</Stack>
|
|
265
|
+
);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const CatalogUnavailable = () => (
|
|
269
|
+
<Stack alignItems="center" justifyContent="center" height="100%" p={ 2.5 } gap={ 1.5 }>
|
|
270
|
+
<ComponentsIcon fontSize="large" />
|
|
271
|
+
<Typography align="center" variant="subtitle2" color="text.secondary">
|
|
272
|
+
{ __( "Icons couldn't be loaded.", 'elementor' ) }
|
|
273
|
+
</Typography>
|
|
274
|
+
</Stack>
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const NoResults = ( { searchValue, onClear }: { searchValue: string; onClear: () => void } ) => (
|
|
278
|
+
<Stack alignItems="center" justifyContent="center" height="100%" p={ 2.5 } gap={ 1.5 }>
|
|
279
|
+
<ComponentsIcon fontSize="large" />
|
|
280
|
+
<Typography align="center" variant="subtitle2" color="text.secondary">
|
|
281
|
+
{ __( 'Sorry, nothing matched', 'elementor' ) }
|
|
282
|
+
</Typography>
|
|
283
|
+
{ searchValue ? (
|
|
284
|
+
<>
|
|
285
|
+
<Typography align="center" variant="subtitle2" color="text.secondary" noWrap sx={ { maxWidth: '80%' } }>
|
|
286
|
+
“{ searchValue }”.
|
|
287
|
+
</Typography>
|
|
288
|
+
<Link color="secondary" variant="caption" component="button" type="button" onClick={ onClear }>
|
|
289
|
+
{ __( 'Clear & try again', 'elementor' ) }
|
|
290
|
+
</Link>
|
|
291
|
+
</>
|
|
292
|
+
) : null }
|
|
293
|
+
</Stack>
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const createIconLibraryItems = (
|
|
297
|
+
icons: FontAwesome7Icon[],
|
|
298
|
+
searchValue: string,
|
|
299
|
+
libraries: FontAwesome7LibraryFilter
|
|
300
|
+
): IconLibraryItem[] =>
|
|
301
|
+
filterFontAwesome7Icons( icons, searchValue, libraries ).map( ( icon ) => ( {
|
|
302
|
+
...icon,
|
|
303
|
+
type: 'item',
|
|
304
|
+
value: icon.id,
|
|
305
|
+
} ) );
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ICON_LIBRARY_ACTION_TOOLTIP_ENTER_DELAY = 0;
|