@elementor/editor-controls 4.4.0-1077 → 4.4.0-1078

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elementor/editor-controls",
3
3
  "description": "This package contains the controls model and utils for the Elementor editor",
4
- "version": "4.4.0-1077",
4
+ "version": "4.4.0-1078",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -40,23 +40,23 @@
40
40
  "dev": "tsup --config=../../tsup.dev.ts"
41
41
  },
42
42
  "dependencies": {
43
- "@elementor/editor-current-user": "4.4.0-1077",
44
- "@elementor/editor-elements": "4.4.0-1077",
45
- "@elementor/editor-props": "4.4.0-1077",
46
- "@elementor/editor-responsive": "4.4.0-1077",
47
- "@elementor/editor-ui": "4.4.0-1077",
48
- "@elementor/editor-v1-adapters": "4.4.0-1077",
49
- "@elementor/env": "4.4.0-1077",
50
- "@elementor/events": "4.4.0-1077",
51
- "@elementor/http-client": "4.4.0-1077",
43
+ "@elementor/editor-current-user": "4.4.0-1078",
44
+ "@elementor/editor-elements": "4.4.0-1078",
45
+ "@elementor/editor-props": "4.4.0-1078",
46
+ "@elementor/editor-responsive": "4.4.0-1078",
47
+ "@elementor/editor-ui": "4.4.0-1078",
48
+ "@elementor/editor-v1-adapters": "4.4.0-1078",
49
+ "@elementor/env": "4.4.0-1078",
50
+ "@elementor/events": "4.4.0-1078",
51
+ "@elementor/http-client": "4.4.0-1078",
52
52
  "@elementor/icons": "~1.75.1",
53
- "@elementor/locations": "4.4.0-1077",
54
- "@elementor/query": "4.4.0-1077",
55
- "@elementor/schema": "4.4.0-1077",
56
- "@elementor/session": "4.4.0-1077",
53
+ "@elementor/locations": "4.4.0-1078",
54
+ "@elementor/query": "4.4.0-1078",
55
+ "@elementor/schema": "4.4.0-1078",
56
+ "@elementor/session": "4.4.0-1078",
57
57
  "@elementor/ui": "1.37.5",
58
- "@elementor/utils": "4.4.0-1077",
59
- "@elementor/wp-media": "4.4.0-1077",
58
+ "@elementor/utils": "4.4.0-1078",
59
+ "@elementor/wp-media": "4.4.0-1078",
60
60
  "@monaco-editor/react": "^4.7.0",
61
61
  "@tiptap/extension-bold": "^3.11.1",
62
62
  "@tiptap/extension-document": "^3.11.1",
@@ -0,0 +1,104 @@
1
+ import {
2
+ FONT_AWESOME_7_LIBRARIES,
3
+ type FontAwesome7IconDefinition,
4
+ getFontAwesome7EditorConfig,
5
+ getFontAwesome7IconName,
6
+ loadFontAwesome7Library,
7
+ } from './font-awesome-7-data';
8
+
9
+ export { FONT_AWESOME_7_LIBRARIES, getFontAwesome7EditorConfig } from './font-awesome-7-data';
10
+
11
+ export type FontAwesome7Icon = FontAwesome7IconDefinition & {
12
+ id: string;
13
+ label: string;
14
+ library: string;
15
+ value: string;
16
+ };
17
+
18
+ export async function loadFontAwesome7Catalog( signal?: AbortSignal ): Promise< FontAwesome7Icon[] > {
19
+ const config = getFontAwesome7EditorConfig();
20
+
21
+ if ( ! config ) {
22
+ return [];
23
+ }
24
+
25
+ const catalogs = await Promise.all(
26
+ FONT_AWESOME_7_LIBRARIES.map( async ( { file, library } ) => {
27
+ if ( ! config.jsonFiles.includes( file ) ) {
28
+ return [];
29
+ }
30
+
31
+ const icons = await loadFontAwesome7Library( file, signal );
32
+
33
+ return icons.map( ( icon ) => toCatalogIcon( icon, library ) );
34
+ } )
35
+ );
36
+
37
+ return catalogs.flat();
38
+ }
39
+
40
+ export function filterFontAwesome7Icons( icons: FontAwesome7Icon[], searchValue?: string | null ): FontAwesome7Icon[] {
41
+ const query = searchValue?.trim().toLowerCase() ?? '';
42
+
43
+ if ( query === '' ) {
44
+ return icons;
45
+ }
46
+
47
+ return icons.filter( ( icon ) => {
48
+ if ( icon.name.includes( query ) || icon.label.toLowerCase().includes( query ) ) {
49
+ return true;
50
+ }
51
+
52
+ return icon.aliases.some( ( alias ) => alias.toLowerCase().includes( query ) );
53
+ } );
54
+ }
55
+
56
+ export function createIconSelectionValue( library: string, name: string ): string {
57
+ return `${ library } fa-${ name }`;
58
+ }
59
+
60
+ export function getSelectedIconId( iconClass: string | null, library: string | null ): string | undefined {
61
+ if ( ! iconClass || ! library ) {
62
+ return undefined;
63
+ }
64
+
65
+ const name = getFontAwesome7IconName( iconClass );
66
+
67
+ if ( ! name ) {
68
+ return undefined;
69
+ }
70
+
71
+ return `${ library }:${ name }`;
72
+ }
73
+
74
+ export function findFontAwesome7Icon(
75
+ icons: FontAwesome7Icon[],
76
+ iconClass: string | null,
77
+ library: string | null
78
+ ): FontAwesome7Icon | undefined {
79
+ const selectedId = getSelectedIconId( iconClass, library );
80
+
81
+ if ( ! selectedId || ! library ) {
82
+ return undefined;
83
+ }
84
+
85
+ const selectedName = selectedId.slice( `${ library }:`.length );
86
+
87
+ return icons.find( ( icon ) => {
88
+ if ( icon.library !== library ) {
89
+ return false;
90
+ }
91
+
92
+ return icon.id === selectedId || icon.name === selectedName || icon.aliases.includes( selectedName );
93
+ } );
94
+ }
95
+
96
+ function toCatalogIcon( icon: FontAwesome7IconDefinition, library: string ): FontAwesome7Icon {
97
+ return {
98
+ ...icon,
99
+ id: `${ library }:${ icon.name }`,
100
+ label: icon.name.replace( /-/g, ' ' ),
101
+ library,
102
+ value: createIconSelectionValue( library, icon.name ),
103
+ };
104
+ }
@@ -0,0 +1,260 @@
1
+ export const FONT_AWESOME_7_LIBRARIES = [
2
+ { file: 'solid', library: 'fa-solid' },
3
+ { file: 'regular', library: 'fa-regular' },
4
+ { file: 'brands', library: 'fa-brands' },
5
+ ] as const;
6
+
7
+ const FONT_AWESOME_JSON = {
8
+ width: 0,
9
+ height: 1,
10
+ aliases: 2,
11
+ unicode: 3,
12
+ path: 4,
13
+ } as const;
14
+
15
+ type FontAwesomeIconJson = [ number, number, unknown[], unknown, string | string[] ];
16
+
17
+ export type FontAwesome7EditorConfig = {
18
+ jsonFiles: string[];
19
+ jsonBaseUrl: string;
20
+ };
21
+
22
+ export type FontAwesome7IconDefinition = {
23
+ name: string;
24
+ aliases: string[];
25
+ width: number;
26
+ height: number;
27
+ paths: string[];
28
+ };
29
+
30
+ type CachedLibrary = {
31
+ icons: FontAwesome7IconDefinition[];
32
+ lookup: Record< string, FontAwesome7IconDefinition >;
33
+ };
34
+
35
+ const libraryCache = new Map< string, CachedLibrary >();
36
+
37
+ export function getFontAwesome7EditorConfig(): FontAwesome7EditorConfig | null {
38
+ const config = window.elementorCommon?.config?.fontAwesome?.v7;
39
+
40
+ if ( ! config || ! Array.isArray( config.jsonFiles ) ) {
41
+ return null;
42
+ }
43
+
44
+ const jsonBaseUrl = getAllowedJsonBaseUrl( config.jsonBaseUrl );
45
+
46
+ if ( ! jsonBaseUrl ) {
47
+ return null;
48
+ }
49
+
50
+ return {
51
+ jsonFiles: config.jsonFiles,
52
+ jsonBaseUrl,
53
+ };
54
+ }
55
+
56
+ const FONT_AWESOME_ICON_NAME_PATTERN = /^fa\S*\s+fa-([^\s]+)/;
57
+
58
+ export function getFontAwesome7IconName( iconValue: string ): string | null {
59
+ return iconValue.match( FONT_AWESOME_ICON_NAME_PATTERN )?.[ 1 ] ?? null;
60
+ }
61
+
62
+ export function resetFontAwesome7IconsCache() {
63
+ libraryCache.clear();
64
+ }
65
+
66
+ export async function loadFontAwesome7Library(
67
+ file: string,
68
+ signal?: AbortSignal
69
+ ): Promise< FontAwesome7IconDefinition[] > {
70
+ const cached = await getCachedLibrary( file, signal );
71
+
72
+ return cached?.icons ?? [];
73
+ }
74
+
75
+ export async function resolveFontAwesome7Icon(
76
+ library: string,
77
+ iconName: string,
78
+ signal?: AbortSignal
79
+ ): Promise< FontAwesome7IconDefinition | null > {
80
+ const file = getLibraryFileName( library );
81
+
82
+ if ( ! file ) {
83
+ return null;
84
+ }
85
+
86
+ const cached = await getCachedLibrary( file, signal );
87
+
88
+ return cached?.lookup[ iconName ] ?? null;
89
+ }
90
+
91
+ async function getCachedLibrary( file: string, signal?: AbortSignal ): Promise< CachedLibrary | null > {
92
+ const cached = libraryCache.get( file );
93
+
94
+ if ( cached ) {
95
+ return cached;
96
+ }
97
+
98
+ const loaded = await fetchLibrary( file, signal );
99
+
100
+ if ( loaded ) {
101
+ libraryCache.set( file, loaded );
102
+ }
103
+
104
+ return loaded;
105
+ }
106
+
107
+ function getLibraryFileName( library: string ): string | null {
108
+ const config = getFontAwesome7EditorConfig();
109
+ const match = FONT_AWESOME_7_LIBRARIES.find( ( item ) => item.library === library );
110
+
111
+ if ( ! config || ! match || ! config.jsonFiles.includes( match.file ) ) {
112
+ return null;
113
+ }
114
+
115
+ return match.file;
116
+ }
117
+
118
+ async function fetchLibrary( file: string, signal?: AbortSignal ): Promise< CachedLibrary | null > {
119
+ const config = getFontAwesome7EditorConfig();
120
+
121
+ if ( ! config?.jsonFiles.includes( file ) ) {
122
+ return null;
123
+ }
124
+
125
+ const catalogUrl = getCatalogFileUrl( config.jsonBaseUrl, file );
126
+
127
+ if ( ! catalogUrl ) {
128
+ return null;
129
+ }
130
+
131
+ try {
132
+ const response = await fetch( catalogUrl, { signal } );
133
+
134
+ if ( ! response.ok ) {
135
+ return null;
136
+ }
137
+
138
+ const data = ( await response.json() ) as { icons?: Record< string, FontAwesomeIconJson > };
139
+ const icons = data.icons;
140
+
141
+ if ( ! icons || typeof icons !== 'object' ) {
142
+ return null;
143
+ }
144
+
145
+ return indexLibrary( icons );
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+
151
+ function indexLibrary( icons: Record< string, FontAwesomeIconJson > ): CachedLibrary {
152
+ const definitions: FontAwesome7IconDefinition[] = [];
153
+ const lookup: Record< string, FontAwesome7IconDefinition > = Object.create( null );
154
+
155
+ for ( const [ name, iconData ] of Object.entries( icons ) ) {
156
+ const definition = toIconDefinition( name, iconData );
157
+
158
+ if ( ! definition ) {
159
+ continue;
160
+ }
161
+
162
+ definitions.push( definition );
163
+ lookup[ name ] = definition;
164
+
165
+ for ( const alias of definition.aliases ) {
166
+ if ( ! lookup[ alias ] ) {
167
+ lookup[ alias ] = definition;
168
+ }
169
+ }
170
+ }
171
+
172
+ return { icons: definitions, lookup };
173
+ }
174
+
175
+ function toIconDefinition( name: string, iconData: unknown ): FontAwesome7IconDefinition | null {
176
+ if ( ! isValidIconTuple( iconData ) ) {
177
+ return null;
178
+ }
179
+
180
+ const paths = normalizePaths( iconData[ FONT_AWESOME_JSON.path ] );
181
+
182
+ if ( paths.length === 0 ) {
183
+ return null;
184
+ }
185
+
186
+ const aliases = iconData[ FONT_AWESOME_JSON.aliases ].filter(
187
+ ( alias ): alias is string => typeof alias === 'string' && alias !== ''
188
+ );
189
+
190
+ return {
191
+ name,
192
+ aliases,
193
+ width: iconData[ FONT_AWESOME_JSON.width ],
194
+ height: iconData[ FONT_AWESOME_JSON.height ],
195
+ paths,
196
+ };
197
+ }
198
+
199
+ function isValidIconTuple( iconData: unknown ): iconData is FontAwesomeIconJson {
200
+ return (
201
+ Array.isArray( iconData ) &&
202
+ iconData.length > FONT_AWESOME_JSON.path &&
203
+ typeof iconData[ FONT_AWESOME_JSON.width ] === 'number' &&
204
+ typeof iconData[ FONT_AWESOME_JSON.height ] === 'number' &&
205
+ Array.isArray( iconData[ FONT_AWESOME_JSON.aliases ] )
206
+ );
207
+ }
208
+
209
+ function normalizePaths( pathData: string | string[] ): string[] {
210
+ if ( typeof pathData === 'string' && isSafeSvgPath( pathData ) ) {
211
+ return [ pathData ];
212
+ }
213
+
214
+ if ( ! Array.isArray( pathData ) ) {
215
+ return [];
216
+ }
217
+
218
+ return pathData.filter( ( path ): path is string => typeof path === 'string' && isSafeSvgPath( path ) );
219
+ }
220
+
221
+ function isSafeSvgPath( path: string ): boolean {
222
+ return path !== '' && ! /[<>"'`]/.test( path );
223
+ }
224
+
225
+ function getAllowedJsonBaseUrl( jsonBaseUrl: unknown ): string | null {
226
+ if ( typeof jsonBaseUrl !== 'string' || jsonBaseUrl === '' ) {
227
+ return null;
228
+ }
229
+
230
+ try {
231
+ const url = new URL( jsonBaseUrl );
232
+
233
+ if ( url.protocol !== 'http:' && url.protocol !== 'https:' ) {
234
+ return null;
235
+ }
236
+
237
+ return url.href;
238
+ } catch {
239
+ return null;
240
+ }
241
+ }
242
+
243
+ function getCatalogFileUrl( jsonBaseUrl: string, file: string ): string | null {
244
+ try {
245
+ const baseUrl = new URL( jsonBaseUrl );
246
+ const fileUrl = new URL( `${ file }.json`, jsonBaseUrl );
247
+
248
+ if ( fileUrl.origin !== baseUrl.origin || ! fileUrl.pathname.startsWith( baseUrl.pathname ) ) {
249
+ return null;
250
+ }
251
+
252
+ if ( fileUrl.protocol !== 'http:' && fileUrl.protocol !== 'https:' ) {
253
+ return null;
254
+ }
255
+
256
+ return fileUrl.href;
257
+ } catch {
258
+ return null;
259
+ }
260
+ }
@@ -0,0 +1,30 @@
1
+ import * as React from 'react';
2
+
3
+ import { type FontAwesome7Icon } from './font-awesome-7-catalog';
4
+
5
+ type FontAwesomeGlyphProps = {
6
+ icon: FontAwesome7Icon;
7
+ size: number;
8
+ color: string;
9
+ label?: string;
10
+ };
11
+
12
+ export const FontAwesomeGlyph = ( { icon, size, color, label }: FontAwesomeGlyphProps ) => {
13
+ return (
14
+ <svg
15
+ xmlns="http://www.w3.org/2000/svg"
16
+ viewBox={ `0 0 ${ icon.width } ${ icon.height }` }
17
+ width={ size }
18
+ height={ size }
19
+ fill={ color }
20
+ overflow="visible"
21
+ aria-hidden={ label ? undefined : true }
22
+ aria-label={ label }
23
+ role={ label ? 'img' : undefined }
24
+ >
25
+ { icon.paths.map( ( path, index ) => (
26
+ <path key={ `${ index }-${ path }` } d={ path } />
27
+ ) ) }
28
+ </svg>
29
+ );
30
+ };
@@ -0,0 +1,29 @@
1
+ export type IconLibraryAnchor = {
2
+ top: number;
3
+ left: number;
4
+ width: number;
5
+ };
6
+
7
+ type AnchorRect = Pick< DOMRect, 'top' | 'left' | 'width' >;
8
+
9
+ export function getIconLibraryAnchor(
10
+ containerRect: AnchorRect | null | undefined,
11
+ buttonGroupRect: Pick< DOMRect, 'top' > | null | undefined
12
+ ): IconLibraryAnchor | null {
13
+ if ( ! containerRect || containerRect.width <= 0 ) {
14
+ return null;
15
+ }
16
+
17
+ return {
18
+ top: buttonGroupRect?.top ?? containerRect.top,
19
+ left: containerRect.left,
20
+ width: containerRect.width,
21
+ };
22
+ }
23
+
24
+ export function measureIconLibraryAnchor(
25
+ container: HTMLElement | null,
26
+ buttonGroup: HTMLElement | null
27
+ ): IconLibraryAnchor | null {
28
+ return getIconLibraryAnchor( container?.getBoundingClientRect(), buttonGroup?.getBoundingClientRect() );
29
+ }
@@ -0,0 +1,228 @@
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 { Box, CircularProgress, Divider, Link, Stack, styled, Typography } from '@elementor/ui';
13
+ import { __ } from '@wordpress/i18n';
14
+
15
+ import {
16
+ createIconSelectionValue,
17
+ filterFontAwesome7Icons,
18
+ findFontAwesome7Icon,
19
+ type FontAwesome7Icon,
20
+ } from './font-awesome-7-catalog';
21
+ import { FontAwesomeGlyph } from './font-awesome-glyph';
22
+ import { useFontAwesome7Catalog } from './use-font-awesome-7-catalog';
23
+
24
+ export const ICON_LIBRARY_POPOVER_WIDTH = 300;
25
+ export const ICON_LIBRARY_ROW_HEIGHT = 48;
26
+ const ICON_TILE_SIZE = 40;
27
+ const ICON_GLYPH_SIZE = 20;
28
+ const ICON_LIBRARY_INLINE_SPACING = 1;
29
+
30
+ const CompactIconLibraryMenuList = styled( StyledMenuList )( ( { theme } ) => ( {
31
+ '& > [role="option"]': {
32
+ padding: theme.spacing( 0.75, ICON_LIBRARY_INLINE_SPACING ),
33
+ },
34
+ } ) );
35
+
36
+ type IconLibraryItem = VirtualizedItem< 'item', string > & Omit< FontAwesome7Icon, 'value' >;
37
+
38
+ type IconLibraryPopoverProps = {
39
+ open: boolean;
40
+ selectedIconClass: string | null;
41
+ selectedIconLibrary: string | null;
42
+ onSelect: ( icon: { value: string; library: string } ) => void;
43
+ onClose: () => void;
44
+ width?: number;
45
+ };
46
+
47
+ export const IconLibraryPopover = ( {
48
+ open,
49
+ selectedIconClass,
50
+ selectedIconLibrary,
51
+ onSelect,
52
+ onClose,
53
+ width = ICON_LIBRARY_POPOVER_WIDTH,
54
+ }: IconLibraryPopoverProps ) => {
55
+ const [ searchValue, setSearchValue ] = useState( '' );
56
+ const { data: icons = [], isLoading } = useFontAwesome7Catalog( open );
57
+
58
+ const items = useMemo( () => createIconLibraryItems( icons, searchValue ), [ icons, searchValue ] );
59
+ const selectedValue = useMemo(
60
+ () => findFontAwesome7Icon( icons, selectedIconClass, selectedIconLibrary )?.id,
61
+ [ icons, selectedIconClass, selectedIconLibrary ]
62
+ );
63
+
64
+ const handleClose = () => {
65
+ setSearchValue( '' );
66
+ onClose();
67
+ };
68
+
69
+ const handleSelect = ( id: string ) => {
70
+ const icon = items.find( ( item ) => item.id === id );
71
+
72
+ if ( ! icon ) {
73
+ return;
74
+ }
75
+
76
+ onSelect( {
77
+ value: createIconSelectionValue( icon.library, icon.name ),
78
+ library: icon.library,
79
+ } );
80
+ };
81
+
82
+ const handleClearSearch = () => {
83
+ setSearchValue( '' );
84
+ };
85
+
86
+ return (
87
+ <PopoverBody width={ width } fillWidth id="icon-library">
88
+ <PopoverHeader
89
+ title={ __( 'Icon library', 'elementor' ) }
90
+ onClose={ handleClose }
91
+ icon={ <ComponentsIcon fontSize="tiny" /> }
92
+ sx={ { pl: ICON_LIBRARY_INLINE_SPACING, pr: 0.5 } }
93
+ />
94
+ <SearchField
95
+ value={ searchValue }
96
+ onSearch={ setSearchValue }
97
+ placeholder={ __( 'Search', 'elementor' ) }
98
+ id="icon-library-search"
99
+ sx={ { px: ICON_LIBRARY_INLINE_SPACING, pb: 1 } }
100
+ />
101
+ <Divider />
102
+ <Box sx={ { flex: 1, overflow: 'auto', minHeight: 0 } }>
103
+ <IconLibraryContent
104
+ isLoading={ isLoading }
105
+ items={ items }
106
+ selectedValue={ selectedValue }
107
+ searchValue={ searchValue }
108
+ onSelect={ handleSelect }
109
+ onClose={ handleClose }
110
+ onClearSearch={ handleClearSearch }
111
+ />
112
+ </Box>
113
+ </PopoverBody>
114
+ );
115
+ };
116
+
117
+ type IconLibraryContentProps = {
118
+ isLoading: boolean;
119
+ items: IconLibraryItem[];
120
+ selectedValue: string | undefined;
121
+ searchValue: string;
122
+ onSelect: ( id: string ) => void;
123
+ onClose: () => void;
124
+ onClearSearch: () => void;
125
+ };
126
+
127
+ const IconLibraryContent = ( {
128
+ isLoading,
129
+ items,
130
+ selectedValue,
131
+ searchValue,
132
+ onSelect,
133
+ onClose,
134
+ onClearSearch,
135
+ }: IconLibraryContentProps ) => {
136
+ if ( isLoading ) {
137
+ return <IconLibraryLoadingState />;
138
+ }
139
+
140
+ return (
141
+ <PopoverMenuList
142
+ items={ items }
143
+ selectedValue={ selectedValue }
144
+ menuListTemplate={ CompactIconLibraryMenuList }
145
+ onSelect={ onSelect }
146
+ onClose={ onClose }
147
+ itemHeight={ ICON_LIBRARY_ROW_HEIGHT }
148
+ menuItemContentTemplate={ IconLibraryRow }
149
+ noResultsComponent={ <IconLibraryEmptyState searchValue={ searchValue } onClear={ onClearSearch } /> }
150
+ data-testid="icon-library-list"
151
+ />
152
+ );
153
+ };
154
+
155
+ const IconLibraryLoadingState = () => (
156
+ <Stack alignItems="center" justifyContent="center" height="100%">
157
+ <CircularProgress role="progressbar" size={ 24 } />
158
+ </Stack>
159
+ );
160
+
161
+ const IconLibraryEmptyState = ( { searchValue, onClear }: { searchValue: string; onClear: () => void } ) => {
162
+ if ( searchValue.trim() === '' ) {
163
+ return <CatalogUnavailable />;
164
+ }
165
+
166
+ return <NoResults searchValue={ searchValue } onClear={ onClear } />;
167
+ };
168
+
169
+ const IconLibraryRow = ( item: VirtualizedItem< string, string > ) => {
170
+ const icon = item as IconLibraryItem;
171
+
172
+ return (
173
+ <Stack direction="row" alignItems="center" gap={ 1 } sx={ { width: '100%' } }>
174
+ <Box
175
+ sx={ {
176
+ width: ICON_TILE_SIZE,
177
+ height: ICON_TILE_SIZE,
178
+ display: 'flex',
179
+ alignItems: 'center',
180
+ justifyContent: 'center',
181
+ border: 1,
182
+ borderColor: 'divider',
183
+ borderRadius: 1,
184
+ color: 'text.tertiary',
185
+ flexShrink: 0,
186
+ } }
187
+ >
188
+ { icon.paths.length > 0 ? (
189
+ <FontAwesomeGlyph icon={ icon } size={ ICON_GLYPH_SIZE } color="currentColor" />
190
+ ) : null }
191
+ </Box>
192
+ <Typography variant="caption" color="text.primary" noWrap>
193
+ { icon.label }
194
+ </Typography>
195
+ </Stack>
196
+ );
197
+ };
198
+
199
+ const CatalogUnavailable = () => (
200
+ <Stack alignItems="center" justifyContent="center" height="100%" p={ 2.5 } gap={ 1.5 }>
201
+ <ComponentsIcon fontSize="large" />
202
+ <Typography align="center" variant="subtitle2" color="text.secondary">
203
+ { __( "Icons couldn't be loaded.", 'elementor' ) }
204
+ </Typography>
205
+ </Stack>
206
+ );
207
+
208
+ const NoResults = ( { searchValue, onClear }: { searchValue: string; onClear: () => void } ) => (
209
+ <Stack alignItems="center" justifyContent="center" height="100%" p={ 2.5 } gap={ 1.5 }>
210
+ <ComponentsIcon fontSize="large" />
211
+ <Typography align="center" variant="subtitle2" color="text.secondary">
212
+ { __( 'Sorry, nothing matched', 'elementor' ) }
213
+ </Typography>
214
+ <Typography align="center" variant="subtitle2" color="text.secondary" noWrap sx={ { maxWidth: '80%' } }>
215
+ &ldquo;{ searchValue }&rdquo;.
216
+ </Typography>
217
+ <Link color="secondary" variant="caption" component="button" type="button" onClick={ onClear }>
218
+ { __( 'Clear & try again', 'elementor' ) }
219
+ </Link>
220
+ </Stack>
221
+ );
222
+
223
+ const createIconLibraryItems = ( icons: FontAwesome7Icon[], searchValue: string ): IconLibraryItem[] =>
224
+ filterFontAwesome7Icons( icons, searchValue ).map( ( icon ) => ( {
225
+ ...icon,
226
+ type: 'item',
227
+ value: icon.id,
228
+ } ) );