@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
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.3.0-
|
|
4
|
+
"version": "4.3.0-beta3",
|
|
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.3.0-
|
|
44
|
-
"@elementor/editor-elements": "4.3.0-
|
|
45
|
-
"@elementor/editor-props": "4.3.0-
|
|
46
|
-
"@elementor/editor-responsive": "4.3.0-
|
|
47
|
-
"@elementor/editor-ui": "4.3.0-
|
|
48
|
-
"@elementor/editor-v1-adapters": "4.3.0-
|
|
49
|
-
"@elementor/env": "4.3.0-
|
|
50
|
-
"@elementor/events": "4.3.0-
|
|
51
|
-
"@elementor/http-client": "4.3.0-
|
|
43
|
+
"@elementor/editor-current-user": "4.3.0-beta3",
|
|
44
|
+
"@elementor/editor-elements": "4.3.0-beta3",
|
|
45
|
+
"@elementor/editor-props": "4.3.0-beta3",
|
|
46
|
+
"@elementor/editor-responsive": "4.3.0-beta3",
|
|
47
|
+
"@elementor/editor-ui": "4.3.0-beta3",
|
|
48
|
+
"@elementor/editor-v1-adapters": "4.3.0-beta3",
|
|
49
|
+
"@elementor/env": "4.3.0-beta3",
|
|
50
|
+
"@elementor/events": "4.3.0-beta3",
|
|
51
|
+
"@elementor/http-client": "4.3.0-beta3",
|
|
52
52
|
"@elementor/icons": "~1.75.1",
|
|
53
|
-
"@elementor/locations": "4.3.0-
|
|
54
|
-
"@elementor/query": "4.3.0-
|
|
55
|
-
"@elementor/schema": "4.3.0-
|
|
56
|
-
"@elementor/session": "4.3.0-
|
|
53
|
+
"@elementor/locations": "4.3.0-beta3",
|
|
54
|
+
"@elementor/query": "4.3.0-beta3",
|
|
55
|
+
"@elementor/schema": "4.3.0-beta3",
|
|
56
|
+
"@elementor/session": "4.3.0-beta3",
|
|
57
57
|
"@elementor/ui": "1.37.5",
|
|
58
|
-
"@elementor/utils": "4.3.0-
|
|
59
|
-
"@elementor/wp-media": "4.3.0-
|
|
58
|
+
"@elementor/utils": "4.3.0-beta3",
|
|
59
|
+
"@elementor/wp-media": "4.3.0-beta3",
|
|
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",
|
|
@@ -0,0 +1,113 @@
|
|
|
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 FontAwesome7Library = ( typeof FONT_AWESOME_7_LIBRARIES )[ number ][ 'library' ];
|
|
12
|
+
export type FontAwesome7LibraryFilter = FontAwesome7Library[];
|
|
13
|
+
|
|
14
|
+
export type FontAwesome7Icon = FontAwesome7IconDefinition & {
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
library: FontAwesome7Library;
|
|
18
|
+
value: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export async function loadFontAwesome7Catalog( signal?: AbortSignal ): Promise< FontAwesome7Icon[] > {
|
|
22
|
+
const config = getFontAwesome7EditorConfig();
|
|
23
|
+
|
|
24
|
+
if ( ! config ) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const catalogs = await Promise.all(
|
|
29
|
+
FONT_AWESOME_7_LIBRARIES.map( async ( { file, library } ) => {
|
|
30
|
+
if ( ! config.jsonFiles.includes( file ) ) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const icons = await loadFontAwesome7Library( file, signal );
|
|
35
|
+
|
|
36
|
+
return icons.map( ( icon ) => toCatalogIcon( icon, library ) );
|
|
37
|
+
} )
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return catalogs.flat();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function filterFontAwesome7Icons(
|
|
44
|
+
icons: FontAwesome7Icon[],
|
|
45
|
+
searchValue?: string | null,
|
|
46
|
+
libraries: FontAwesome7LibraryFilter = []
|
|
47
|
+
): FontAwesome7Icon[] {
|
|
48
|
+
const query = searchValue?.trim().toLowerCase() ?? '';
|
|
49
|
+
const libraryIcons =
|
|
50
|
+
libraries.length === 0 ? icons : icons.filter( ( icon ) => libraries.includes( icon.library ) );
|
|
51
|
+
|
|
52
|
+
if ( query === '' ) {
|
|
53
|
+
return libraryIcons;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return libraryIcons.filter( ( icon ) => {
|
|
57
|
+
if ( icon.name.includes( query ) || icon.label.toLowerCase().includes( query ) ) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return icon.aliases.some( ( alias ) => alias.toLowerCase().includes( query ) );
|
|
62
|
+
} );
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createIconSelectionValue( library: string, name: string ): string {
|
|
66
|
+
return `${ library } fa-${ name }`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getSelectedIconId( iconClass: string | null, library: string | null ): string | undefined {
|
|
70
|
+
if ( ! iconClass || ! library ) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const name = getFontAwesome7IconName( iconClass );
|
|
75
|
+
|
|
76
|
+
if ( ! name ) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return `${ library }:${ name }`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function findFontAwesome7Icon(
|
|
84
|
+
icons: FontAwesome7Icon[],
|
|
85
|
+
iconClass: string | null,
|
|
86
|
+
library: string | null
|
|
87
|
+
): FontAwesome7Icon | undefined {
|
|
88
|
+
const selectedId = getSelectedIconId( iconClass, library );
|
|
89
|
+
|
|
90
|
+
if ( ! selectedId || ! library ) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const selectedName = selectedId.slice( `${ library }:`.length );
|
|
95
|
+
|
|
96
|
+
return icons.find( ( icon ) => {
|
|
97
|
+
if ( icon.library !== library ) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return icon.id === selectedId || icon.name === selectedName || icon.aliases.includes( selectedName );
|
|
102
|
+
} );
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function toCatalogIcon( icon: FontAwesome7IconDefinition, library: FontAwesome7Library ): FontAwesome7Icon {
|
|
106
|
+
return {
|
|
107
|
+
...icon,
|
|
108
|
+
id: `${ library }:${ icon.name }`,
|
|
109
|
+
label: icon.name.replace( /-/g, ' ' ),
|
|
110
|
+
library,
|
|
111
|
+
value: createIconSelectionValue( library, icon.name ),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -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,166 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useId } from 'react';
|
|
3
|
+
import { CheckIcon, FilterIcon, LibraryIcon, ListIcon, StarFilledIcon, StarIcon } from '@elementor/icons';
|
|
4
|
+
import {
|
|
5
|
+
bindMenu,
|
|
6
|
+
bindToggle,
|
|
7
|
+
Box,
|
|
8
|
+
Menu,
|
|
9
|
+
MenuItem,
|
|
10
|
+
Stack,
|
|
11
|
+
ToggleButton,
|
|
12
|
+
Tooltip,
|
|
13
|
+
Typography,
|
|
14
|
+
usePopupState,
|
|
15
|
+
} from '@elementor/ui';
|
|
16
|
+
import { __ } from '@wordpress/i18n';
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
FONT_AWESOME_7_LIBRARIES,
|
|
20
|
+
type FontAwesome7Library,
|
|
21
|
+
type FontAwesome7LibraryFilter,
|
|
22
|
+
} from './font-awesome-7-catalog';
|
|
23
|
+
import { ICON_LIBRARY_ACTION_TOOLTIP_ENTER_DELAY } from './icon-library-tooltip';
|
|
24
|
+
|
|
25
|
+
const FILTER_MENU_WIDTH = 280;
|
|
26
|
+
const FILTER_INDICATOR_SIZE = 6;
|
|
27
|
+
const FILTER_INDICATOR_OFFSET = 4;
|
|
28
|
+
const LIBRARY_FILTER_ORDER = {
|
|
29
|
+
regular: 0,
|
|
30
|
+
solid: 1,
|
|
31
|
+
brands: 2,
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
const LIBRARY_FILTER_CONFIG: Record<
|
|
35
|
+
FontAwesome7Library,
|
|
36
|
+
{ getLabel: () => string; Icon: typeof StarIcon; order: number }
|
|
37
|
+
> = {
|
|
38
|
+
'fa-regular': {
|
|
39
|
+
getLabel: () => __( 'Font Awesome - Regular', 'elementor' ),
|
|
40
|
+
Icon: StarIcon,
|
|
41
|
+
order: LIBRARY_FILTER_ORDER.regular,
|
|
42
|
+
},
|
|
43
|
+
'fa-solid': {
|
|
44
|
+
getLabel: () => __( 'Font Awesome - Solid', 'elementor' ),
|
|
45
|
+
Icon: StarFilledIcon,
|
|
46
|
+
order: LIBRARY_FILTER_ORDER.solid,
|
|
47
|
+
},
|
|
48
|
+
'fa-brands': {
|
|
49
|
+
getLabel: () => __( 'Font Awesome - Brands', 'elementor' ),
|
|
50
|
+
Icon: LibraryIcon,
|
|
51
|
+
order: LIBRARY_FILTER_ORDER.brands,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type IconLibraryFilterProps = {
|
|
56
|
+
value: FontAwesome7LibraryFilter;
|
|
57
|
+
onChange: ( value: FontAwesome7LibraryFilter ) => void;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const IconLibraryFilter = ( { value, onChange }: IconLibraryFilterProps ) => {
|
|
61
|
+
const popupId = useId();
|
|
62
|
+
const popupState = usePopupState( {
|
|
63
|
+
variant: 'popover',
|
|
64
|
+
popupId,
|
|
65
|
+
} );
|
|
66
|
+
const options = FONT_AWESOME_7_LIBRARIES.map( ( { library } ) => ( {
|
|
67
|
+
value: library,
|
|
68
|
+
label: LIBRARY_FILTER_CONFIG[ library ].getLabel(),
|
|
69
|
+
Icon: LIBRARY_FILTER_CONFIG[ library ].Icon,
|
|
70
|
+
order: LIBRARY_FILTER_CONFIG[ library ].order,
|
|
71
|
+
} ) ).sort( ( firstOption, secondOption ) => firstOption.order - secondOption.order );
|
|
72
|
+
const isFiltered = value.length > 0;
|
|
73
|
+
const filterButtonLabel = isFiltered
|
|
74
|
+
? __( 'Filter by library, active', 'elementor' )
|
|
75
|
+
: __( 'Filter by library', 'elementor' );
|
|
76
|
+
|
|
77
|
+
const handleAllIconsClick = () => {
|
|
78
|
+
onChange( [] );
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const handleLibraryClick = ( library: FontAwesome7Library ) => {
|
|
82
|
+
const nextValue = value.includes( library )
|
|
83
|
+
? value.filter( ( selectedLibrary ) => selectedLibrary !== library )
|
|
84
|
+
: [ ...value, library ];
|
|
85
|
+
|
|
86
|
+
onChange( nextValue.length === FONT_AWESOME_7_LIBRARIES.length ? [] : nextValue );
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<>
|
|
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
|
+
>
|
|
98
|
+
<ToggleButton
|
|
99
|
+
aria-label={ filterButtonLabel }
|
|
100
|
+
value="filter"
|
|
101
|
+
size="tiny"
|
|
102
|
+
selected={ popupState.isOpen }
|
|
103
|
+
sx={ { position: 'relative', flexShrink: 0 } }
|
|
104
|
+
{ ...bindToggle( popupState ) }
|
|
105
|
+
aria-expanded={ popupState.isOpen }
|
|
106
|
+
>
|
|
107
|
+
<FilterIcon fontSize="tiny" />
|
|
108
|
+
{ isFiltered ? (
|
|
109
|
+
<Box
|
|
110
|
+
component="span"
|
|
111
|
+
aria-hidden="true"
|
|
112
|
+
sx={ {
|
|
113
|
+
position: 'absolute',
|
|
114
|
+
insetBlockStart: FILTER_INDICATOR_OFFSET,
|
|
115
|
+
insetInlineEnd: FILTER_INDICATOR_OFFSET,
|
|
116
|
+
width: FILTER_INDICATOR_SIZE,
|
|
117
|
+
height: FILTER_INDICATOR_SIZE,
|
|
118
|
+
borderRadius: '50%',
|
|
119
|
+
bgcolor: 'secondary.main',
|
|
120
|
+
} }
|
|
121
|
+
/>
|
|
122
|
+
) : null }
|
|
123
|
+
</ToggleButton>
|
|
124
|
+
</Tooltip>
|
|
125
|
+
<Menu
|
|
126
|
+
{ ...bindMenu( popupState ) }
|
|
127
|
+
MenuListProps={ {
|
|
128
|
+
dense: true,
|
|
129
|
+
autoFocusItem: true,
|
|
130
|
+
'aria-label': __( 'Filter by library', 'elementor' ),
|
|
131
|
+
} }
|
|
132
|
+
sx={ { '& .MuiPaper-root': { minWidth: FILTER_MENU_WIDTH } } }
|
|
133
|
+
>
|
|
134
|
+
<MenuItem
|
|
135
|
+
role="menuitemcheckbox"
|
|
136
|
+
aria-checked={ ! isFiltered }
|
|
137
|
+
selected={ ! isFiltered }
|
|
138
|
+
onClick={ handleAllIconsClick }
|
|
139
|
+
>
|
|
140
|
+
{ renderFilterMenuItemContent( __( 'All icons', 'elementor' ), ListIcon, ! isFiltered ) }
|
|
141
|
+
</MenuItem>
|
|
142
|
+
{ options.map( ( { value: library, label, Icon } ) => (
|
|
143
|
+
<MenuItem
|
|
144
|
+
key={ library }
|
|
145
|
+
role="menuitemcheckbox"
|
|
146
|
+
aria-checked={ value.includes( library ) }
|
|
147
|
+
selected={ value.includes( library ) }
|
|
148
|
+
onClick={ () => handleLibraryClick( library ) }
|
|
149
|
+
>
|
|
150
|
+
{ renderFilterMenuItemContent( label, Icon, value.includes( library ) ) }
|
|
151
|
+
</MenuItem>
|
|
152
|
+
) ) }
|
|
153
|
+
</Menu>
|
|
154
|
+
</>
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const renderFilterMenuItemContent = ( label: string, Icon: typeof ListIcon, selected: boolean ) => (
|
|
159
|
+
<Stack direction="row" alignItems="center" gap={ 1 } width="100%">
|
|
160
|
+
<Icon fontSize="tiny" />
|
|
161
|
+
<Typography variant="caption" sx={ { flex: 1 } }>
|
|
162
|
+
{ label }
|
|
163
|
+
</Typography>
|
|
164
|
+
{ selected ? <CheckIcon fontSize="tiny" aria-hidden="true" /> : null }
|
|
165
|
+
</Stack>
|
|
166
|
+
);
|