@elementor/editor-controls 4.4.0-1077 → 4.4.0-1079
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 +12 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.js +1416 -846
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1289 -726
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -16
- package/src/controls/icon-library/font-awesome-7-catalog.ts +104 -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-popover.tsx +235 -0
- package/src/controls/icon-library/use-font-awesome-7-catalog.ts +14 -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/index.ts +6 -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-1079",
|
|
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-
|
|
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-1079",
|
|
44
|
+
"@elementor/editor-elements": "4.4.0-1079",
|
|
45
|
+
"@elementor/editor-props": "4.4.0-1079",
|
|
46
|
+
"@elementor/editor-responsive": "4.4.0-1079",
|
|
47
|
+
"@elementor/editor-ui": "4.4.0-1079",
|
|
48
|
+
"@elementor/editor-v1-adapters": "4.4.0-1079",
|
|
49
|
+
"@elementor/env": "4.4.0-1079",
|
|
50
|
+
"@elementor/events": "4.4.0-1079",
|
|
51
|
+
"@elementor/http-client": "4.4.0-1079",
|
|
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-1079",
|
|
54
|
+
"@elementor/query": "4.4.0-1079",
|
|
55
|
+
"@elementor/schema": "4.4.0-1079",
|
|
56
|
+
"@elementor/session": "4.4.0-1079",
|
|
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-1079",
|
|
59
|
+
"@elementor/wp-media": "4.4.0-1079",
|
|
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,235 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useMemo } 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 { useDebounceState } from '@elementor/utils';
|
|
14
|
+
import { __ } from '@wordpress/i18n';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
createIconSelectionValue,
|
|
18
|
+
filterFontAwesome7Icons,
|
|
19
|
+
findFontAwesome7Icon,
|
|
20
|
+
type FontAwesome7Icon,
|
|
21
|
+
} from './font-awesome-7-catalog';
|
|
22
|
+
import { FontAwesomeGlyph } from './font-awesome-glyph';
|
|
23
|
+
import { useFontAwesome7Catalog } from './use-font-awesome-7-catalog';
|
|
24
|
+
|
|
25
|
+
export const ICON_LIBRARY_POPOVER_WIDTH = 300;
|
|
26
|
+
export const ICON_LIBRARY_ROW_HEIGHT = 48;
|
|
27
|
+
export const ICON_LIBRARY_SEARCH_DEBOUNCE_DELAY = 300;
|
|
28
|
+
const ICON_TILE_SIZE = 40;
|
|
29
|
+
const ICON_GLYPH_SIZE = 20;
|
|
30
|
+
const ICON_LIBRARY_INLINE_SPACING = 1;
|
|
31
|
+
|
|
32
|
+
const CompactIconLibraryMenuList = styled( StyledMenuList )( ( { theme } ) => ( {
|
|
33
|
+
'& > [role="option"]': {
|
|
34
|
+
padding: theme.spacing( 0.75, ICON_LIBRARY_INLINE_SPACING ),
|
|
35
|
+
},
|
|
36
|
+
} ) );
|
|
37
|
+
|
|
38
|
+
type IconLibraryItem = VirtualizedItem< 'item', string > & Omit< FontAwesome7Icon, 'value' >;
|
|
39
|
+
|
|
40
|
+
type IconLibraryPopoverProps = {
|
|
41
|
+
open: boolean;
|
|
42
|
+
selectedIconClass: string | null;
|
|
43
|
+
selectedIconLibrary: string | null;
|
|
44
|
+
onSelect: ( icon: { value: string; library: string } ) => void;
|
|
45
|
+
onClose: () => void;
|
|
46
|
+
width?: number;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const IconLibraryPopover = ( {
|
|
50
|
+
open,
|
|
51
|
+
selectedIconClass,
|
|
52
|
+
selectedIconLibrary,
|
|
53
|
+
onSelect,
|
|
54
|
+
onClose,
|
|
55
|
+
width = ICON_LIBRARY_POPOVER_WIDTH,
|
|
56
|
+
}: IconLibraryPopoverProps ) => {
|
|
57
|
+
const {
|
|
58
|
+
debouncedValue: searchValue,
|
|
59
|
+
inputValue: searchInputValue,
|
|
60
|
+
handleChange: handleSearchChange,
|
|
61
|
+
setImmediateValue: setSearchValue,
|
|
62
|
+
} = useDebounceState( { delay: ICON_LIBRARY_SEARCH_DEBOUNCE_DELAY } );
|
|
63
|
+
const { data: icons = [], isLoading } = useFontAwesome7Catalog( open );
|
|
64
|
+
|
|
65
|
+
const items = useMemo( () => createIconLibraryItems( icons, searchValue ), [ icons, searchValue ] );
|
|
66
|
+
const selectedValue = useMemo(
|
|
67
|
+
() => findFontAwesome7Icon( icons, selectedIconClass, selectedIconLibrary )?.id,
|
|
68
|
+
[ icons, selectedIconClass, selectedIconLibrary ]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const handleClose = () => {
|
|
72
|
+
setSearchValue( '' );
|
|
73
|
+
onClose();
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const handleSelect = ( id: string ) => {
|
|
77
|
+
const icon = items.find( ( item ) => item.id === id );
|
|
78
|
+
|
|
79
|
+
if ( ! icon ) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
onSelect( {
|
|
84
|
+
value: createIconSelectionValue( icon.library, icon.name ),
|
|
85
|
+
library: icon.library,
|
|
86
|
+
} );
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const handleClearSearch = () => {
|
|
90
|
+
setSearchValue( '' );
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<PopoverBody width={ width } fillWidth id="icon-library">
|
|
95
|
+
<PopoverHeader
|
|
96
|
+
title={ __( 'Icon library', 'elementor' ) }
|
|
97
|
+
onClose={ handleClose }
|
|
98
|
+
icon={ <ComponentsIcon fontSize="tiny" /> }
|
|
99
|
+
sx={ { pl: ICON_LIBRARY_INLINE_SPACING, pr: 0.5 } }
|
|
100
|
+
/>
|
|
101
|
+
<SearchField
|
|
102
|
+
value={ searchInputValue }
|
|
103
|
+
onSearch={ handleSearchChange }
|
|
104
|
+
placeholder={ __( 'Search', 'elementor' ) }
|
|
105
|
+
id="icon-library-search"
|
|
106
|
+
sx={ { px: ICON_LIBRARY_INLINE_SPACING, pb: 1 } }
|
|
107
|
+
/>
|
|
108
|
+
<Divider />
|
|
109
|
+
<Box sx={ { flex: 1, overflow: 'auto', minHeight: 0 } }>
|
|
110
|
+
<IconLibraryContent
|
|
111
|
+
isLoading={ isLoading }
|
|
112
|
+
items={ items }
|
|
113
|
+
selectedValue={ selectedValue }
|
|
114
|
+
searchValue={ searchValue }
|
|
115
|
+
onSelect={ handleSelect }
|
|
116
|
+
onClose={ handleClose }
|
|
117
|
+
onClearSearch={ handleClearSearch }
|
|
118
|
+
/>
|
|
119
|
+
</Box>
|
|
120
|
+
</PopoverBody>
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
type IconLibraryContentProps = {
|
|
125
|
+
isLoading: boolean;
|
|
126
|
+
items: IconLibraryItem[];
|
|
127
|
+
selectedValue: string | undefined;
|
|
128
|
+
searchValue: string;
|
|
129
|
+
onSelect: ( id: string ) => void;
|
|
130
|
+
onClose: () => void;
|
|
131
|
+
onClearSearch: () => void;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const IconLibraryContent = ( {
|
|
135
|
+
isLoading,
|
|
136
|
+
items,
|
|
137
|
+
selectedValue,
|
|
138
|
+
searchValue,
|
|
139
|
+
onSelect,
|
|
140
|
+
onClose,
|
|
141
|
+
onClearSearch,
|
|
142
|
+
}: IconLibraryContentProps ) => {
|
|
143
|
+
if ( isLoading ) {
|
|
144
|
+
return <IconLibraryLoadingState />;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<PopoverMenuList
|
|
149
|
+
items={ items }
|
|
150
|
+
selectedValue={ selectedValue }
|
|
151
|
+
menuListTemplate={ CompactIconLibraryMenuList }
|
|
152
|
+
onSelect={ onSelect }
|
|
153
|
+
onClose={ onClose }
|
|
154
|
+
itemHeight={ ICON_LIBRARY_ROW_HEIGHT }
|
|
155
|
+
menuItemContentTemplate={ IconLibraryRow }
|
|
156
|
+
noResultsComponent={ <IconLibraryEmptyState searchValue={ searchValue } onClear={ onClearSearch } /> }
|
|
157
|
+
data-testid="icon-library-list"
|
|
158
|
+
/>
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const IconLibraryLoadingState = () => (
|
|
163
|
+
<Stack alignItems="center" justifyContent="center" height="100%">
|
|
164
|
+
<CircularProgress role="progressbar" size={ 24 } />
|
|
165
|
+
</Stack>
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const IconLibraryEmptyState = ( { searchValue, onClear }: { searchValue: string; onClear: () => void } ) => {
|
|
169
|
+
if ( searchValue.trim() === '' ) {
|
|
170
|
+
return <CatalogUnavailable />;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return <NoResults searchValue={ searchValue } onClear={ onClear } />;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const IconLibraryRow = ( item: VirtualizedItem< string, string > ) => {
|
|
177
|
+
const icon = item as IconLibraryItem;
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<Stack direction="row" alignItems="center" gap={ 1 } sx={ { width: '100%' } }>
|
|
181
|
+
<Box
|
|
182
|
+
sx={ {
|
|
183
|
+
width: ICON_TILE_SIZE,
|
|
184
|
+
height: ICON_TILE_SIZE,
|
|
185
|
+
display: 'flex',
|
|
186
|
+
alignItems: 'center',
|
|
187
|
+
justifyContent: 'center',
|
|
188
|
+
border: 1,
|
|
189
|
+
borderColor: 'divider',
|
|
190
|
+
borderRadius: 1,
|
|
191
|
+
color: 'text.tertiary',
|
|
192
|
+
flexShrink: 0,
|
|
193
|
+
} }
|
|
194
|
+
>
|
|
195
|
+
{ icon.paths.length > 0 ? (
|
|
196
|
+
<FontAwesomeGlyph icon={ icon } size={ ICON_GLYPH_SIZE } color="currentColor" />
|
|
197
|
+
) : null }
|
|
198
|
+
</Box>
|
|
199
|
+
<Typography variant="caption" color="text.primary" noWrap>
|
|
200
|
+
{ icon.label }
|
|
201
|
+
</Typography>
|
|
202
|
+
</Stack>
|
|
203
|
+
);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const CatalogUnavailable = () => (
|
|
207
|
+
<Stack alignItems="center" justifyContent="center" height="100%" p={ 2.5 } gap={ 1.5 }>
|
|
208
|
+
<ComponentsIcon fontSize="large" />
|
|
209
|
+
<Typography align="center" variant="subtitle2" color="text.secondary">
|
|
210
|
+
{ __( "Icons couldn't be loaded.", 'elementor' ) }
|
|
211
|
+
</Typography>
|
|
212
|
+
</Stack>
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const NoResults = ( { searchValue, onClear }: { searchValue: string; onClear: () => void } ) => (
|
|
216
|
+
<Stack alignItems="center" justifyContent="center" height="100%" p={ 2.5 } gap={ 1.5 }>
|
|
217
|
+
<ComponentsIcon fontSize="large" />
|
|
218
|
+
<Typography align="center" variant="subtitle2" color="text.secondary">
|
|
219
|
+
{ __( 'Sorry, nothing matched', 'elementor' ) }
|
|
220
|
+
</Typography>
|
|
221
|
+
<Typography align="center" variant="subtitle2" color="text.secondary" noWrap sx={ { maxWidth: '80%' } }>
|
|
222
|
+
“{ searchValue }”.
|
|
223
|
+
</Typography>
|
|
224
|
+
<Link color="secondary" variant="caption" component="button" type="button" onClick={ onClear }>
|
|
225
|
+
{ __( 'Clear & try again', 'elementor' ) }
|
|
226
|
+
</Link>
|
|
227
|
+
</Stack>
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const createIconLibraryItems = ( icons: FontAwesome7Icon[], searchValue: string ): IconLibraryItem[] =>
|
|
231
|
+
filterFontAwesome7Icons( icons, searchValue ).map( ( icon ) => ( {
|
|
232
|
+
...icon,
|
|
233
|
+
type: 'item',
|
|
234
|
+
value: icon.id,
|
|
235
|
+
} ) );
|