@elementor/editor-controls 4.4.0-1110 → 4.4.0-1112
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 +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1069 -337
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +874 -142
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -16
- package/src/controls/icon-library/custom-icon-libraries.ts +556 -0
- package/src/controls/icon-library/font-awesome-7-catalog.ts +16 -7
- package/src/controls/icon-library/font-awesome-7-data.ts +2 -0
- package/src/controls/icon-library/font-awesome-glyph.tsx +62 -17
- package/src/controls/icon-library/fontello-svg-font.ts +227 -0
- package/src/controls/icon-library/icomoon-selection.ts +104 -0
- package/src/controls/icon-library/icon-library-filter.tsx +126 -61
- package/src/controls/icon-library/icon-library-grid.tsx +5 -7
- package/src/controls/icon-library/icon-library-popover.tsx +7 -5
- package/src/controls/icon-library/sanitize-icon-svg-markup.ts +53 -0
- package/src/controls/svg-media-preview.tsx +7 -2
- package/src/index.ts +1 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import { Box } from '@elementor/ui';
|
|
2
3
|
|
|
3
4
|
import { type FontAwesome7Icon } from './font-awesome-7-catalog';
|
|
5
|
+
import { sanitizeIconSvgMarkup } from './sanitize-icon-svg-markup';
|
|
4
6
|
|
|
5
7
|
type FontAwesomeGlyphProps = {
|
|
6
8
|
icon: FontAwesome7Icon;
|
|
@@ -10,21 +12,64 @@ type FontAwesomeGlyphProps = {
|
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
export const FontAwesomeGlyph = ( { icon, size, color, label }: FontAwesomeGlyphProps ) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
15
|
+
const svgMarkup = sanitizeIconSvgMarkup( icon.svgMarkup );
|
|
16
|
+
|
|
17
|
+
if ( svgMarkup ) {
|
|
18
|
+
return (
|
|
19
|
+
<Box
|
|
20
|
+
component="span"
|
|
21
|
+
aria-hidden={ label ? undefined : true }
|
|
22
|
+
aria-label={ label }
|
|
23
|
+
role={ label ? 'img' : undefined }
|
|
24
|
+
sx={ {
|
|
25
|
+
width: size,
|
|
26
|
+
height: size,
|
|
27
|
+
color,
|
|
28
|
+
display: 'inline-flex',
|
|
29
|
+
'& svg': {
|
|
30
|
+
width: '100%',
|
|
31
|
+
height: '100%',
|
|
32
|
+
fill: 'currentColor',
|
|
33
|
+
},
|
|
34
|
+
'& path': {
|
|
35
|
+
fill: 'currentColor',
|
|
36
|
+
},
|
|
37
|
+
} }
|
|
38
|
+
dangerouslySetInnerHTML={ { __html: svgMarkup } }
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if ( icon.paths.length > 0 ) {
|
|
44
|
+
return (
|
|
45
|
+
<svg
|
|
46
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
47
|
+
viewBox={ `0 0 ${ icon.width } ${ icon.height }` }
|
|
48
|
+
width={ size }
|
|
49
|
+
height={ size }
|
|
50
|
+
fill={ color }
|
|
51
|
+
overflow="visible"
|
|
52
|
+
aria-hidden={ label ? undefined : true }
|
|
53
|
+
aria-label={ label }
|
|
54
|
+
role={ label ? 'img' : undefined }
|
|
55
|
+
>
|
|
56
|
+
{ icon.paths.map( ( path, index ) => (
|
|
57
|
+
<path key={ `${ index }-${ path }` } d={ path } />
|
|
58
|
+
) ) }
|
|
59
|
+
</svg>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if ( icon.glyphClass ) {
|
|
64
|
+
return (
|
|
65
|
+
<i
|
|
66
|
+
className={ icon.glyphClass }
|
|
67
|
+
aria-hidden={ label ? undefined : true }
|
|
68
|
+
aria-label={ label }
|
|
69
|
+
style={ { fontSize: size, color, lineHeight: 1 } }
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return null;
|
|
30
75
|
};
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
type FontelloConfig = {
|
|
2
|
+
glyphs?: Array< { css?: unknown; code?: unknown; svg?: unknown } >;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
type GlyphRecord = {
|
|
6
|
+
d: string;
|
|
7
|
+
units: number;
|
|
8
|
+
advance: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type GlyphIndex = {
|
|
12
|
+
byName: Map< string, GlyphRecord >;
|
|
13
|
+
byCode: Map< number, GlyphRecord >;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function parseFontelloSvgFont(
|
|
17
|
+
configJson: string,
|
|
18
|
+
svgFont: string,
|
|
19
|
+
prefix: string,
|
|
20
|
+
displayPrefix: string,
|
|
21
|
+
iconNames: string[]
|
|
22
|
+
): Record< string, string > {
|
|
23
|
+
const codepoints = parseConfigCodepoints( configJson, prefix );
|
|
24
|
+
const configPaths = parseConfigSvgPaths( configJson, prefix );
|
|
25
|
+
const glyphs = parseSvgFontGlyphs( svgFont );
|
|
26
|
+
const resolvedDisplayPrefix = displayPrefix || prefix.replace( /-$/, '' );
|
|
27
|
+
const result: Record< string, string > = {};
|
|
28
|
+
|
|
29
|
+
for ( const name of iconNames ) {
|
|
30
|
+
const fromConfig =
|
|
31
|
+
configPaths.get( name ) ??
|
|
32
|
+
( prefix && name.startsWith( prefix ) ? configPaths.get( name.slice( prefix.length ) ) : undefined );
|
|
33
|
+
const glyph = fromConfig ?? findGlyph( glyphs, codepoints, name, prefix );
|
|
34
|
+
|
|
35
|
+
if ( ! glyph ) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const value = `${ resolvedDisplayPrefix } ${ prefix }${ name }`.trim();
|
|
40
|
+
result[ value ] = buildIconSvg( glyph );
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function fontelloSvgUrlFromConfig( fetchJson: string ): string | null {
|
|
47
|
+
try {
|
|
48
|
+
const url = new URL( fetchJson, window.location.origin );
|
|
49
|
+
url.pathname = url.pathname.replace( /\/[^/]+$/, '/font/fontello.svg' );
|
|
50
|
+
|
|
51
|
+
return url.href;
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseConfigCodepoints( configJson: string, prefix: string ): Map< string, number > {
|
|
58
|
+
const map = new Map< string, number >();
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const data = JSON.parse( configJson ) as FontelloConfig;
|
|
62
|
+
|
|
63
|
+
if ( ! Array.isArray( data.glyphs ) ) {
|
|
64
|
+
return map;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for ( const glyph of data.glyphs ) {
|
|
68
|
+
if ( typeof glyph.css !== 'string' || glyph.css === '' || typeof glyph.code !== 'number' ) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
map.set( glyph.css, glyph.code );
|
|
73
|
+
|
|
74
|
+
if ( prefix !== '' && ! glyph.css.startsWith( prefix ) ) {
|
|
75
|
+
map.set( `${ prefix }${ glyph.css }`, glyph.code );
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
return map;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return map;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseConfigSvgPaths( configJson: string, prefix: string ): Map< string, GlyphRecord > {
|
|
86
|
+
const map = new Map< string, GlyphRecord >();
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const data = JSON.parse( configJson ) as FontelloConfig;
|
|
90
|
+
|
|
91
|
+
if ( ! Array.isArray( data.glyphs ) ) {
|
|
92
|
+
return map;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for ( const glyph of data.glyphs ) {
|
|
96
|
+
if ( typeof glyph.css !== 'string' || glyph.css === '' ) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let path = '';
|
|
101
|
+
let advance = 1000;
|
|
102
|
+
|
|
103
|
+
if ( typeof glyph.svg === 'string' ) {
|
|
104
|
+
path = glyph.svg;
|
|
105
|
+
} else if (
|
|
106
|
+
glyph.svg &&
|
|
107
|
+
typeof glyph.svg === 'object' &&
|
|
108
|
+
'path' in glyph.svg &&
|
|
109
|
+
typeof glyph.svg.path === 'string'
|
|
110
|
+
) {
|
|
111
|
+
path = glyph.svg.path;
|
|
112
|
+
if ( 'width' in glyph.svg && typeof glyph.svg.width === 'number' && glyph.svg.width > 0 ) {
|
|
113
|
+
advance = glyph.svg.width;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if ( path === '' ) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const record = { d: path, units: 1000, advance };
|
|
122
|
+
map.set( glyph.css, record );
|
|
123
|
+
|
|
124
|
+
if ( prefix !== '' && ! glyph.css.startsWith( prefix ) ) {
|
|
125
|
+
map.set( `${ prefix }${ glyph.css }`, record );
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
return map;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return map;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseSvgFontGlyphs( svgFont: string ): GlyphIndex {
|
|
136
|
+
const byName = new Map< string, GlyphRecord >();
|
|
137
|
+
const byCode = new Map< number, GlyphRecord >();
|
|
138
|
+
const doc = new DOMParser().parseFromString( stripDoctype( svgFont ), 'image/svg+xml' );
|
|
139
|
+
const font = doc.querySelector( 'font' );
|
|
140
|
+
const face = doc.querySelector( 'font-face' );
|
|
141
|
+
const fontAdvance = positiveInt( font?.getAttribute( 'horiz-adv-x' ), 1000 );
|
|
142
|
+
const units = positiveInt( face?.getAttribute( 'units-per-em' ), fontAdvance );
|
|
143
|
+
|
|
144
|
+
doc.querySelectorAll( 'glyph' ).forEach( ( node ) => {
|
|
145
|
+
const d = node.getAttribute( 'd' ) ?? '';
|
|
146
|
+
|
|
147
|
+
if ( d === '' ) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const record: GlyphRecord = {
|
|
152
|
+
d,
|
|
153
|
+
units,
|
|
154
|
+
advance: positiveInt( node.getAttribute( 'horiz-adv-x' ), fontAdvance ),
|
|
155
|
+
};
|
|
156
|
+
const name = node.getAttribute( 'glyph-name' ) ?? '';
|
|
157
|
+
const unicode = node.getAttribute( 'unicode' ) ?? '';
|
|
158
|
+
const code = unicode.codePointAt( 0 );
|
|
159
|
+
|
|
160
|
+
if ( name ) {
|
|
161
|
+
byName.set( name, record );
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if ( code !== undefined ) {
|
|
165
|
+
byCode.set( code, record );
|
|
166
|
+
}
|
|
167
|
+
} );
|
|
168
|
+
|
|
169
|
+
return { byName, byCode };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function findGlyph(
|
|
173
|
+
glyphs: GlyphIndex,
|
|
174
|
+
codepoints: Map< string, number >,
|
|
175
|
+
iconName: string,
|
|
176
|
+
prefix: string
|
|
177
|
+
): GlyphRecord | null {
|
|
178
|
+
const names = [ iconName ];
|
|
179
|
+
|
|
180
|
+
if ( prefix !== '' && iconName.startsWith( prefix ) ) {
|
|
181
|
+
names.push( iconName.slice( prefix.length ) );
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for ( const name of names ) {
|
|
185
|
+
const byName = glyphs.byName.get( name );
|
|
186
|
+
|
|
187
|
+
if ( byName ) {
|
|
188
|
+
return byName;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const code = codepoints.get( name );
|
|
192
|
+
const byCode = code !== undefined ? glyphs.byCode.get( code ) : undefined;
|
|
193
|
+
|
|
194
|
+
if ( byCode ) {
|
|
195
|
+
return byCode;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function buildIconSvg( glyph: GlyphRecord ): string {
|
|
203
|
+
return (
|
|
204
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${ glyph.advance } ${ glyph.units }" fill="currentColor" width="100%" height="100%">` +
|
|
205
|
+
`<g transform="translate(0,${ glyph.units }) scale(1,-1)">` +
|
|
206
|
+
`<path d="${ escapeAttribute( glyph.d ) }" fill="currentColor"></path>` +
|
|
207
|
+
`</g></svg>`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function stripDoctype( svgFont: string ): string {
|
|
212
|
+
return svgFont.replace( /<!DOCTYPE[^>]*>/is, '' );
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function positiveInt( value: string | null | undefined, fallback: number ): number {
|
|
216
|
+
if ( ! value ) {
|
|
217
|
+
return fallback;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const parsed = Number.parseInt( value, 10 );
|
|
221
|
+
|
|
222
|
+
return parsed > 0 ? parsed : fallback;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function escapeAttribute( value: string ): string {
|
|
226
|
+
return value.replace( /&/g, '&' ).replace( /"/g, '"' ).replace( /</g, '<' ).replace( />/g, '>' );
|
|
227
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const DEFAULT_ICOMOON_VIEWBOX = 1024;
|
|
2
|
+
|
|
3
|
+
type IcomoonSelection = {
|
|
4
|
+
icons?: Array< {
|
|
5
|
+
icon?: { paths?: unknown; width?: unknown; tags?: unknown };
|
|
6
|
+
properties?: { name?: unknown };
|
|
7
|
+
} >;
|
|
8
|
+
preferences?: { fontPref?: { prefix?: unknown } };
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function parseIcomoonSelection(
|
|
12
|
+
selectionJson: string,
|
|
13
|
+
prefix: string,
|
|
14
|
+
displayPrefix: string,
|
|
15
|
+
iconNames: string[]
|
|
16
|
+
): Record< string, string > {
|
|
17
|
+
let data: IcomoonSelection;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
data = JSON.parse( selectionJson ) as IcomoonSelection;
|
|
21
|
+
} catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const resolvedDisplayPrefix = displayPrefix || prefix.replace( /-$/, '' );
|
|
26
|
+
const names = iconNames.length > 0 ? iconNames : namesFromSelection( data );
|
|
27
|
+
const result: Record< string, string > = {};
|
|
28
|
+
|
|
29
|
+
for ( const name of names ) {
|
|
30
|
+
const markup = svgForName( data, name, prefix );
|
|
31
|
+
|
|
32
|
+
if ( ! markup ) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
result[ `${ resolvedDisplayPrefix } ${ prefix }${ name }`.trim() ] = markup;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function namesFromSelection( data: IcomoonSelection ): string[] {
|
|
43
|
+
if ( ! Array.isArray( data.icons ) ) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return data.icons.flatMap( ( icon ) => {
|
|
48
|
+
const name = iconEntryName( icon );
|
|
49
|
+
|
|
50
|
+
return name ? [ name ] : [];
|
|
51
|
+
} );
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function svgForName( data: IcomoonSelection, iconName: string, prefix: string ): string | null {
|
|
55
|
+
const candidates = [ iconName ];
|
|
56
|
+
|
|
57
|
+
if ( prefix && iconName.startsWith( prefix ) ) {
|
|
58
|
+
candidates.push( iconName.slice( prefix.length ) );
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for ( const icon of data.icons ?? [] ) {
|
|
62
|
+
const name = iconEntryName( icon );
|
|
63
|
+
|
|
64
|
+
if ( ! name || ! candidates.includes( name ) ) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const paths = Array.isArray( icon.icon?.paths )
|
|
69
|
+
? icon.icon.paths.filter( ( path ): path is string => typeof path === 'string' && path !== '' )
|
|
70
|
+
: [];
|
|
71
|
+
|
|
72
|
+
if ( paths.length === 0 ) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const size =
|
|
77
|
+
typeof icon.icon?.width === 'number' && icon.icon.width > 0 ? icon.icon.width : DEFAULT_ICOMOON_VIEWBOX;
|
|
78
|
+
const pathMarkup = paths
|
|
79
|
+
.map( ( d ) => `<path d="${ escapeAttribute( d ) }" fill="currentColor"></path>` )
|
|
80
|
+
.join( '' );
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${ size } ${ size }" fill="currentColor" width="100%" height="100%">` +
|
|
84
|
+
pathMarkup +
|
|
85
|
+
'</svg>'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function iconEntryName( icon: NonNullable< IcomoonSelection[ 'icons' ] >[ number ] ): string {
|
|
93
|
+
if ( typeof icon.properties?.name === 'string' ) {
|
|
94
|
+
return icon.properties.name;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const tag = icon.icon?.tags;
|
|
98
|
+
|
|
99
|
+
return Array.isArray( tag ) && typeof tag[ 0 ] === 'string' ? tag[ 0 ] : '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function escapeAttribute( value: string ): string {
|
|
103
|
+
return value.replace( /&/g, '&' ).replace( /"/g, '"' ).replace( /</g, '<' ).replace( />/g, '>' );
|
|
104
|
+
}
|
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
bindMenu,
|
|
6
6
|
bindToggle,
|
|
7
7
|
Box,
|
|
8
|
+
Divider,
|
|
9
|
+
ListSubheader,
|
|
8
10
|
Menu,
|
|
9
11
|
MenuItem,
|
|
10
12
|
Stack,
|
|
@@ -15,43 +17,35 @@ import {
|
|
|
15
17
|
} from '@elementor/ui';
|
|
16
18
|
import { __ } from '@wordpress/i18n';
|
|
17
19
|
|
|
18
|
-
import {
|
|
19
|
-
FONT_AWESOME_7_LIBRARIES,
|
|
20
|
-
type FontAwesome7Library,
|
|
21
|
-
type FontAwesome7LibraryFilter,
|
|
22
|
-
} from './font-awesome-7-catalog';
|
|
20
|
+
import { type FontAwesome7LibraryFilter } from './font-awesome-7-catalog';
|
|
23
21
|
import { ICON_LIBRARY_ACTION_TOOLTIP_ENTER_DELAY } from './icon-library-tooltip';
|
|
24
22
|
|
|
25
23
|
const FILTER_MENU_WIDTH = 280;
|
|
26
24
|
const FILTER_INDICATOR_SIZE = 6;
|
|
27
25
|
const FILTER_INDICATOR_OFFSET = 4;
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
},
|
|
26
|
+
const FILTER_TYPE_ALL = 'all';
|
|
27
|
+
const FILTER_TYPE_GROUP = 'group';
|
|
28
|
+
const FILTER_TYPE_ITEM = 'item';
|
|
29
|
+
|
|
30
|
+
const FILTER_ICONS: Record< string, typeof ListIcon > = {
|
|
31
|
+
list: ListIcon,
|
|
32
|
+
star: StarIcon,
|
|
33
|
+
'star-filled': StarFilledIcon,
|
|
34
|
+
library: LibraryIcon,
|
|
53
35
|
};
|
|
54
36
|
|
|
37
|
+
const DEFAULT_FILTER = [
|
|
38
|
+
{ type: FILTER_TYPE_ALL, label: 'All icons', icon: 'list' },
|
|
39
|
+
{ type: FILTER_TYPE_ITEM, value: 'fa-regular', label: 'Font Awesome - Regular', icon: 'star' },
|
|
40
|
+
{ type: FILTER_TYPE_ITEM, value: 'fa-solid', label: 'Font Awesome - Solid', icon: 'star-filled' },
|
|
41
|
+
{ type: FILTER_TYPE_ITEM, value: 'fa-brands', label: 'Font Awesome - Brands', icon: 'library' },
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
44
|
+
type FilterEntry =
|
|
45
|
+
| { type: typeof FILTER_TYPE_ALL; label: string; icon?: string }
|
|
46
|
+
| { type: typeof FILTER_TYPE_GROUP; label: string }
|
|
47
|
+
| { type: typeof FILTER_TYPE_ITEM; value: string; label: string; icon?: string };
|
|
48
|
+
|
|
55
49
|
type IconLibraryFilterProps = {
|
|
56
50
|
value: FontAwesome7LibraryFilter;
|
|
57
51
|
onChange: ( value: FontAwesome7LibraryFilter ) => void;
|
|
@@ -63,27 +57,19 @@ export const IconLibraryFilter = ( { value, onChange }: IconLibraryFilterProps )
|
|
|
63
57
|
variant: 'popover',
|
|
64
58
|
popupId,
|
|
65
59
|
} );
|
|
66
|
-
const
|
|
67
|
-
|
|
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 );
|
|
60
|
+
const entries = getFilterEntries();
|
|
61
|
+
const selectableValues = entries.flatMap( ( entry ) => ( entry.type === FILTER_TYPE_ITEM ? [ entry.value ] : [] ) );
|
|
72
62
|
const isFiltered = value.length > 0;
|
|
73
63
|
const filterButtonLabel = isFiltered
|
|
74
64
|
? __( 'Filter by library, active', 'elementor' )
|
|
75
65
|
: __( 'Filter by library', 'elementor' );
|
|
76
66
|
|
|
77
|
-
const
|
|
78
|
-
onChange( [] );
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const handleLibraryClick = ( library: FontAwesome7Library ) => {
|
|
67
|
+
const handleLibraryClick = ( library: string ) => {
|
|
82
68
|
const nextValue = value.includes( library )
|
|
83
69
|
? value.filter( ( selectedLibrary ) => selectedLibrary !== library )
|
|
84
70
|
: [ ...value, library ];
|
|
85
71
|
|
|
86
|
-
onChange( nextValue.length ===
|
|
72
|
+
onChange( nextValue.length === selectableValues.length ? [] : nextValue );
|
|
87
73
|
};
|
|
88
74
|
|
|
89
75
|
return (
|
|
@@ -131,30 +117,62 @@ export const IconLibraryFilter = ( { value, onChange }: IconLibraryFilterProps )
|
|
|
131
117
|
} }
|
|
132
118
|
sx={ { '& .MuiPaper-root': { minWidth: FILTER_MENU_WIDTH } } }
|
|
133
119
|
>
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
) ) }
|
|
120
|
+
{ entries.flatMap( ( entry, index ) =>
|
|
121
|
+
renderFilterEntry( entry, index, value, isFiltered, () => onChange( [] ), handleLibraryClick )
|
|
122
|
+
) }
|
|
153
123
|
</Menu>
|
|
154
124
|
</>
|
|
155
125
|
);
|
|
156
126
|
};
|
|
157
127
|
|
|
128
|
+
const renderFilterEntry = (
|
|
129
|
+
entry: FilterEntry,
|
|
130
|
+
index: number,
|
|
131
|
+
selectedLibraries: FontAwesome7LibraryFilter,
|
|
132
|
+
isFiltered: boolean,
|
|
133
|
+
onAllIconsClick: () => void,
|
|
134
|
+
onLibraryClick: ( library: string ) => void
|
|
135
|
+
): React.ReactNode[] => {
|
|
136
|
+
if ( entry.type === FILTER_TYPE_GROUP ) {
|
|
137
|
+
return [
|
|
138
|
+
<Divider key={ `divider-${ index }` } />,
|
|
139
|
+
<ListSubheader key={ `group-${ index }` } disableSticky>
|
|
140
|
+
{ entry.label }
|
|
141
|
+
</ListSubheader>,
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if ( entry.type === FILTER_TYPE_ALL ) {
|
|
146
|
+
return [
|
|
147
|
+
<MenuItem
|
|
148
|
+
key={ `all-${ index }` }
|
|
149
|
+
role="menuitemcheckbox"
|
|
150
|
+
aria-checked={ ! isFiltered }
|
|
151
|
+
selected={ ! isFiltered }
|
|
152
|
+
onClick={ onAllIconsClick }
|
|
153
|
+
>
|
|
154
|
+
{ renderFilterMenuItemContent( entry.label, getFilterIcon( entry.icon ), ! isFiltered ) }
|
|
155
|
+
</MenuItem>,
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const isSelected = selectedLibraries.includes( entry.value );
|
|
160
|
+
|
|
161
|
+
return [
|
|
162
|
+
<MenuItem
|
|
163
|
+
key={ entry.value }
|
|
164
|
+
role="menuitemcheckbox"
|
|
165
|
+
aria-checked={ isSelected }
|
|
166
|
+
selected={ isSelected }
|
|
167
|
+
onClick={ () => onLibraryClick( entry.value ) }
|
|
168
|
+
>
|
|
169
|
+
{ renderFilterMenuItemContent( entry.label, getFilterIcon( entry.icon ), isSelected ) }
|
|
170
|
+
</MenuItem>,
|
|
171
|
+
];
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const getFilterIcon = ( icon?: string ) => FILTER_ICONS[ icon ?? '' ] ?? LibraryIcon;
|
|
175
|
+
|
|
158
176
|
const renderFilterMenuItemContent = ( label: string, Icon: typeof ListIcon, selected: boolean ) => (
|
|
159
177
|
<Stack direction="row" alignItems="center" gap={ 1 } width="100%">
|
|
160
178
|
<Icon fontSize="tiny" />
|
|
@@ -164,3 +182,50 @@ const renderFilterMenuItemContent = ( label: string, Icon: typeof ListIcon, sele
|
|
|
164
182
|
{ selected ? <CheckIcon fontSize="tiny" aria-hidden="true" /> : null }
|
|
165
183
|
</Stack>
|
|
166
184
|
);
|
|
185
|
+
|
|
186
|
+
function getFilterEntries(): FilterEntry[] {
|
|
187
|
+
const filter = window.elementorCommon?.config?.fontAwesome?.v7?.filter;
|
|
188
|
+
|
|
189
|
+
if ( ! Array.isArray( filter ) ) {
|
|
190
|
+
return [ ...DEFAULT_FILTER ];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const entries = filter.map( parseFilterEntry ).filter( ( entry ): entry is FilterEntry => entry !== null );
|
|
194
|
+
|
|
195
|
+
return entries.length > 0 ? entries : [ ...DEFAULT_FILTER ];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function parseFilterEntry( value: unknown ): FilterEntry | null {
|
|
199
|
+
if ( ! value || typeof value !== 'object' ) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const entry = value as { type?: unknown; label?: unknown; value?: unknown; icon?: unknown };
|
|
204
|
+
|
|
205
|
+
if ( typeof entry.label !== 'string' || entry.label === '' ) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if ( entry.type === FILTER_TYPE_GROUP ) {
|
|
210
|
+
return { type: FILTER_TYPE_GROUP, label: entry.label };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if ( entry.type === FILTER_TYPE_ALL ) {
|
|
214
|
+
return {
|
|
215
|
+
type: FILTER_TYPE_ALL,
|
|
216
|
+
label: entry.label,
|
|
217
|
+
icon: typeof entry.icon === 'string' ? entry.icon : undefined,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if ( entry.type === FILTER_TYPE_ITEM && typeof entry.value === 'string' && entry.value !== '' ) {
|
|
222
|
+
return {
|
|
223
|
+
type: FILTER_TYPE_ITEM,
|
|
224
|
+
value: entry.value,
|
|
225
|
+
label: entry.label,
|
|
226
|
+
icon: typeof entry.icon === 'string' ? entry.icon : undefined,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
@@ -322,13 +322,11 @@ export const IconLibraryGrid = ( {
|
|
|
322
322
|
},
|
|
323
323
|
} }
|
|
324
324
|
>
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
/>
|
|
331
|
-
) : null }
|
|
325
|
+
<FontAwesomeGlyph
|
|
326
|
+
icon={ item }
|
|
327
|
+
size={ ICON_GLYPH_SIZE }
|
|
328
|
+
color="currentColor"
|
|
329
|
+
/>
|
|
332
330
|
</Box>
|
|
333
331
|
</Tooltip>
|
|
334
332
|
</Box>
|