@elementor/editor-controls 4.4.0-1094 → 4.4.0-1096
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.js +1235 -825
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1120 -700
- package/dist/index.mjs.map +1 -1
- package/package.json +17 -16
- package/src/controls/icon-library/icon-library-filter.tsx +9 -2
- package/src/controls/icon-library/icon-library-grid.tsx +344 -0
- package/src/controls/icon-library/icon-library-popover.tsx +43 -8
- package/src/controls/icon-library/icon-library-tooltip.ts +1 -0
- package/src/controls/icon-library/icon-library-view-toggle.tsx +104 -0
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-
|
|
4
|
+
"version": "4.4.0-1096",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Elementor Team",
|
|
7
7
|
"homepage": "https://elementor.com/",
|
|
@@ -40,24 +40,25 @@
|
|
|
40
40
|
"dev": "tsup --config=../../tsup.dev.ts"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@elementor/editor-current-user": "4.4.0-
|
|
44
|
-
"@elementor/editor-elements": "4.4.0-
|
|
45
|
-
"@elementor/editor-props": "4.4.0-
|
|
46
|
-
"@elementor/editor-responsive": "4.4.0-
|
|
47
|
-
"@elementor/editor-ui": "4.4.0-
|
|
48
|
-
"@elementor/editor-v1-adapters": "4.4.0-
|
|
49
|
-
"@elementor/env": "4.4.0-
|
|
50
|
-
"@elementor/events": "4.4.0-
|
|
51
|
-
"@elementor/http-client": "4.4.0-
|
|
43
|
+
"@elementor/editor-current-user": "4.4.0-1096",
|
|
44
|
+
"@elementor/editor-elements": "4.4.0-1096",
|
|
45
|
+
"@elementor/editor-props": "4.4.0-1096",
|
|
46
|
+
"@elementor/editor-responsive": "4.4.0-1096",
|
|
47
|
+
"@elementor/editor-ui": "4.4.0-1096",
|
|
48
|
+
"@elementor/editor-v1-adapters": "4.4.0-1096",
|
|
49
|
+
"@elementor/env": "4.4.0-1096",
|
|
50
|
+
"@elementor/events": "4.4.0-1096",
|
|
51
|
+
"@elementor/http-client": "4.4.0-1096",
|
|
52
52
|
"@elementor/icons": "~1.75.1",
|
|
53
|
-
"@elementor/locations": "4.4.0-
|
|
54
|
-
"@elementor/query": "4.4.0-
|
|
55
|
-
"@elementor/schema": "4.4.0-
|
|
56
|
-
"@elementor/session": "4.4.0-
|
|
53
|
+
"@elementor/locations": "4.4.0-1096",
|
|
54
|
+
"@elementor/query": "4.4.0-1096",
|
|
55
|
+
"@elementor/schema": "4.4.0-1096",
|
|
56
|
+
"@elementor/session": "4.4.0-1096",
|
|
57
57
|
"@elementor/ui": "1.37.5",
|
|
58
|
-
"@elementor/utils": "4.4.0-
|
|
59
|
-
"@elementor/wp-media": "4.4.0-
|
|
58
|
+
"@elementor/utils": "4.4.0-1096",
|
|
59
|
+
"@elementor/wp-media": "4.4.0-1096",
|
|
60
60
|
"@monaco-editor/react": "^4.7.0",
|
|
61
|
+
"@tanstack/react-virtual": "^3.13.3",
|
|
61
62
|
"@tiptap/extension-bold": "^3.11.1",
|
|
62
63
|
"@tiptap/extension-document": "^3.11.1",
|
|
63
64
|
"@tiptap/extension-hard-break": "^3.11.1",
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
type FontAwesome7Library,
|
|
21
21
|
type FontAwesome7LibraryFilter,
|
|
22
22
|
} from './font-awesome-7-catalog';
|
|
23
|
+
import { ICON_LIBRARY_ACTION_TOOLTIP_ENTER_DELAY } from './icon-library-tooltip';
|
|
23
24
|
|
|
24
25
|
const FILTER_MENU_WIDTH = 280;
|
|
25
26
|
const FILTER_INDICATOR_SIZE = 6;
|
|
@@ -87,7 +88,13 @@ export const IconLibraryFilter = ( { value, onChange }: IconLibraryFilterProps )
|
|
|
87
88
|
|
|
88
89
|
return (
|
|
89
90
|
<>
|
|
90
|
-
<Tooltip
|
|
91
|
+
<Tooltip
|
|
92
|
+
title={ __( 'Filter by library', 'elementor' ) }
|
|
93
|
+
placement="top"
|
|
94
|
+
enterDelay={ ICON_LIBRARY_ACTION_TOOLTIP_ENTER_DELAY }
|
|
95
|
+
enterNextDelay={ ICON_LIBRARY_ACTION_TOOLTIP_ENTER_DELAY }
|
|
96
|
+
disableInteractive
|
|
97
|
+
>
|
|
91
98
|
<ToggleButton
|
|
92
99
|
aria-label={ filterButtonLabel }
|
|
93
100
|
value="filter"
|
|
@@ -154,6 +161,6 @@ const renderFilterMenuItemContent = ( label: string, Icon: typeof ListIcon, sele
|
|
|
154
161
|
<Typography variant="caption" sx={ { flex: 1 } }>
|
|
155
162
|
{ label }
|
|
156
163
|
</Typography>
|
|
157
|
-
{ selected ? <CheckIcon fontSize="tiny" /> : null }
|
|
164
|
+
{ selected ? <CheckIcon fontSize="tiny" aria-hidden="true" /> : null }
|
|
158
165
|
</Stack>
|
|
159
166
|
);
|
|
@@ -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
|
+
};
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type VirtualizedItem,
|
|
10
10
|
} from '@elementor/editor-ui';
|
|
11
11
|
import { ComponentsIcon } from '@elementor/icons';
|
|
12
|
+
import { useSessionStorage } from '@elementor/session';
|
|
12
13
|
import { Box, CircularProgress, Divider, Link, Stack, styled, Typography } from '@elementor/ui';
|
|
13
14
|
import { useDebounceState } from '@elementor/utils';
|
|
14
15
|
import { __ } from '@wordpress/i18n';
|
|
@@ -22,11 +23,20 @@ import {
|
|
|
22
23
|
} from './font-awesome-7-catalog';
|
|
23
24
|
import { FontAwesomeGlyph } from './font-awesome-glyph';
|
|
24
25
|
import { IconLibraryFilter } from './icon-library-filter';
|
|
26
|
+
import { IconLibraryGrid } from './icon-library-grid';
|
|
27
|
+
import { type IconLibraryView, IconLibraryViewToggle } from './icon-library-view-toggle';
|
|
25
28
|
import { useFontAwesome7Catalog } from './use-font-awesome-7-catalog';
|
|
26
29
|
|
|
27
30
|
export const ICON_LIBRARY_POPOVER_WIDTH = 300;
|
|
28
31
|
export const ICON_LIBRARY_ROW_HEIGHT = 48;
|
|
29
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
|
+
};
|
|
30
40
|
const ICON_TILE_SIZE = 40;
|
|
31
41
|
const ICON_GLYPH_SIZE = 20;
|
|
32
42
|
const ICON_LIBRARY_INLINE_SPACING = 1;
|
|
@@ -63,6 +73,11 @@ export const IconLibraryPopover = ( {
|
|
|
63
73
|
setImmediateValue: setSearchValue,
|
|
64
74
|
} = useDebounceState( { delay: ICON_LIBRARY_SEARCH_DEBOUNCE_DELAY } );
|
|
65
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;
|
|
66
81
|
const { data: icons = [], isLoading } = useFontAwesome7Catalog( open );
|
|
67
82
|
|
|
68
83
|
const items = useMemo(
|
|
@@ -103,6 +118,7 @@ export const IconLibraryPopover = ( {
|
|
|
103
118
|
title={ __( 'Icon library', 'elementor' ) }
|
|
104
119
|
onClose={ handleClose }
|
|
105
120
|
icon={ <ComponentsIcon fontSize="tiny" /> }
|
|
121
|
+
actions={ [ <IconLibraryViewToggle key="view" value={ view } onChange={ setStoredView } /> ] }
|
|
106
122
|
sx={ { pl: ICON_LIBRARY_INLINE_SPACING, pr: 0.5 } }
|
|
107
123
|
/>
|
|
108
124
|
<Stack direction="row" alignItems="center" gap={ 1 } sx={ { px: ICON_LIBRARY_INLINE_SPACING, pb: 1 } }>
|
|
@@ -116,13 +132,14 @@ export const IconLibraryPopover = ( {
|
|
|
116
132
|
<IconLibraryFilter value={ activeLibraries } onChange={ setActiveLibraries } />
|
|
117
133
|
</Stack>
|
|
118
134
|
<Divider />
|
|
119
|
-
<Box sx={ { flex: 1, overflow: '
|
|
135
|
+
<Box sx={ { flex: 1, overflow: 'hidden', minHeight: 0, minWidth: 0 } }>
|
|
120
136
|
<IconLibraryContent
|
|
121
137
|
isLoading={ isLoading }
|
|
122
138
|
items={ items }
|
|
123
139
|
selectedValue={ selectedValue }
|
|
124
140
|
searchValue={ searchValue }
|
|
125
141
|
isCatalogAvailable={ icons.length > 0 }
|
|
142
|
+
view={ view }
|
|
126
143
|
onSelect={ handleSelect }
|
|
127
144
|
onClose={ handleClose }
|
|
128
145
|
onClearSearch={ handleClearSearch }
|
|
@@ -138,6 +155,7 @@ type IconLibraryContentProps = {
|
|
|
138
155
|
selectedValue: string | undefined;
|
|
139
156
|
searchValue: string;
|
|
140
157
|
isCatalogAvailable: boolean;
|
|
158
|
+
view: IconLibraryView;
|
|
141
159
|
onSelect: ( id: string ) => void;
|
|
142
160
|
onClose: () => void;
|
|
143
161
|
onClearSearch: () => void;
|
|
@@ -149,6 +167,7 @@ const IconLibraryContent = ( {
|
|
|
149
167
|
selectedValue,
|
|
150
168
|
searchValue,
|
|
151
169
|
isCatalogAvailable,
|
|
170
|
+
view,
|
|
152
171
|
onSelect,
|
|
153
172
|
onClose,
|
|
154
173
|
onClearSearch,
|
|
@@ -157,6 +176,28 @@ const IconLibraryContent = ( {
|
|
|
157
176
|
return <IconLibraryLoadingState />;
|
|
158
177
|
}
|
|
159
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
|
+
|
|
160
201
|
return (
|
|
161
202
|
<PopoverMenuList
|
|
162
203
|
items={ items }
|
|
@@ -166,13 +207,7 @@ const IconLibraryContent = ( {
|
|
|
166
207
|
onClose={ onClose }
|
|
167
208
|
itemHeight={ ICON_LIBRARY_ROW_HEIGHT }
|
|
168
209
|
menuItemContentTemplate={ IconLibraryRow }
|
|
169
|
-
noResultsComponent={
|
|
170
|
-
<IconLibraryEmptyState
|
|
171
|
-
searchValue={ searchValue }
|
|
172
|
-
isCatalogAvailable={ isCatalogAvailable }
|
|
173
|
-
onClear={ onClearSearch }
|
|
174
|
-
/>
|
|
175
|
-
}
|
|
210
|
+
noResultsComponent={ emptyState }
|
|
176
211
|
data-testid="icon-library-list"
|
|
177
212
|
/>
|
|
178
213
|
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ICON_LIBRARY_ACTION_TOOLTIP_ENTER_DELAY = 0;
|
|
@@ -0,0 +1,104 @@
|
|
|
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';
|
|
15
|
+
import { __ } from '@wordpress/i18n';
|
|
16
|
+
|
|
17
|
+
import { ICON_LIBRARY_ACTION_TOOLTIP_ENTER_DELAY } from './icon-library-tooltip';
|
|
18
|
+
|
|
19
|
+
export type IconLibraryView = 'grid' | 'list';
|
|
20
|
+
|
|
21
|
+
const VIEW_MENU_WIDTH = 122;
|
|
22
|
+
|
|
23
|
+
type IconLibraryViewToggleProps = {
|
|
24
|
+
value: IconLibraryView;
|
|
25
|
+
onChange: ( value: IconLibraryView ) => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const IconLibraryViewToggle = ( { value, onChange }: IconLibraryViewToggleProps ) => {
|
|
29
|
+
const popupId = useId();
|
|
30
|
+
const popupState = usePopupState( {
|
|
31
|
+
variant: 'popover',
|
|
32
|
+
popupId,
|
|
33
|
+
} );
|
|
34
|
+
const isGrid = value === 'grid';
|
|
35
|
+
const ViewIcon = isGrid ? WidgetsIcon : ListIcon;
|
|
36
|
+
const viewButtonLabel = isGrid ? __( 'Grid view', 'elementor' ) : __( 'List view', 'elementor' );
|
|
37
|
+
|
|
38
|
+
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
|
|
46
|
+
>
|
|
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
|
+
</>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
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
|
+
);
|