-
Notifications
You must be signed in to change notification settings - Fork 4.2k
/
utils.js
362 lines (323 loc) · 10.1 KB
/
utils.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
/**
* External dependencies
*/
import { colord, extend } from 'colord';
import namesPlugin from 'colord/plugins/names';
import a11yPlugin from 'colord/plugins/a11y';
/**
* WordPress dependencies
*/
import { Component, isValidElement } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { __unstableStripHTML as stripHTML } from '@wordpress/dom';
import { RichTextData } from '@wordpress/rich-text';
/**
* Internal dependencies
*/
import { BLOCK_ICON_DEFAULT } from './constants';
import { getBlockType, getDefaultBlockName } from './registration';
extend( [ namesPlugin, a11yPlugin ] );
/**
* Array of icon colors containing a color to be used if the icon color
* was not explicitly set but the icon background color was.
*
* @type {Object}
*/
const ICON_COLORS = [ '#191e23', '#f8f9f9' ];
/**
* Determines whether the block's attributes are equal to the default attributes
* which means the block is unmodified.
*
* @param {WPBlock} block Block Object
*
* @return {boolean} Whether the block is an unmodified block.
*/
export function isUnmodifiedBlock( block ) {
return Object.entries( getBlockType( block.name )?.attributes ?? {} ).every(
( [ key, definition ] ) => {
const value = block.attributes[ key ];
// Every attribute that has a default must match the default.
if ( definition.hasOwnProperty( 'default' ) ) {
return value === definition.default;
}
// The rich text type is a bit different from the rest because it
// has an implicit default value of an empty RichTextData instance,
// so check the length of the value.
if ( definition.type === 'rich-text' ) {
return ! value?.length;
}
// Every attribute that doesn't have a default should be undefined.
return value === undefined;
}
);
}
/**
* Determines whether the block is a default block and its attributes are equal
* to the default attributes which means the block is unmodified.
*
* @param {WPBlock} block Block Object
*
* @return {boolean} Whether the block is an unmodified default block.
*/
export function isUnmodifiedDefaultBlock( block ) {
return block.name === getDefaultBlockName() && isUnmodifiedBlock( block );
}
/**
* Function that checks if the parameter is a valid icon.
*
* @param {*} icon Parameter to be checked.
*
* @return {boolean} True if the parameter is a valid icon and false otherwise.
*/
export function isValidIcon( icon ) {
return (
!! icon &&
( typeof icon === 'string' ||
isValidElement( icon ) ||
typeof icon === 'function' ||
icon instanceof Component )
);
}
/**
* Function that receives an icon as set by the blocks during the registration
* and returns a new icon object that is normalized so we can rely on just on possible icon structure
* in the codebase.
*
* @param {WPBlockTypeIconRender} icon Render behavior of a block type icon;
* one of a Dashicon slug, an element, or a
* component.
*
* @return {WPBlockTypeIconDescriptor} Object describing the icon.
*/
export function normalizeIconObject( icon ) {
icon = icon || BLOCK_ICON_DEFAULT;
if ( isValidIcon( icon ) ) {
return { src: icon };
}
if ( 'background' in icon ) {
const colordBgColor = colord( icon.background );
const getColorContrast = ( iconColor ) =>
colordBgColor.contrast( iconColor );
const maxContrast = Math.max( ...ICON_COLORS.map( getColorContrast ) );
return {
...icon,
foreground: icon.foreground
? icon.foreground
: ICON_COLORS.find(
( iconColor ) =>
getColorContrast( iconColor ) === maxContrast
),
shadowColor: colordBgColor.alpha( 0.3 ).toRgbString(),
};
}
return icon;
}
/**
* Normalizes block type passed as param. When string is passed then
* it converts it to the matching block type object.
* It passes the original object otherwise.
*
* @param {string|Object} blockTypeOrName Block type or name.
*
* @return {?Object} Block type.
*/
export function normalizeBlockType( blockTypeOrName ) {
if ( typeof blockTypeOrName === 'string' ) {
return getBlockType( blockTypeOrName );
}
return blockTypeOrName;
}
/**
* Get the label for the block, usually this is either the block title,
* or the value of the block's `label` function when that's specified.
*
* @param {Object} blockType The block type.
* @param {Object} attributes The values of the block's attributes.
* @param {Object} context The intended use for the label.
*
* @return {string} The block label.
*/
export function getBlockLabel( blockType, attributes, context = 'visual' ) {
const { __experimentalLabel: getLabel, title } = blockType;
const label = getLabel && getLabel( attributes, { context } );
if ( ! label ) {
return title;
}
if ( label.toPlainText ) {
return label.toPlainText();
}
// Strip any HTML (i.e. RichText formatting) before returning.
return stripHTML( label );
}
/**
* Get a label for the block for use by screenreaders, this is more descriptive
* than the visual label and includes the block title and the value of the
* `getLabel` function if it's specified.
*
* @param {?Object} blockType The block type.
* @param {Object} attributes The values of the block's attributes.
* @param {?number} position The position of the block in the block list.
* @param {string} [direction='vertical'] The direction of the block layout.
*
* @return {string} The block label.
*/
export function getAccessibleBlockLabel(
blockType,
attributes,
position,
direction = 'vertical'
) {
// `title` is already localized, `label` is a user-supplied value.
const title = blockType?.title;
const label = blockType
? getBlockLabel( blockType, attributes, 'accessibility' )
: '';
const hasPosition = position !== undefined;
// getBlockLabel returns the block title as a fallback when there's no label,
// if it did return the title, this function needs to avoid adding the
// title twice within the accessible label. Use this `hasLabel` boolean to
// handle that.
const hasLabel = label && label !== title;
if ( hasPosition && direction === 'vertical' ) {
if ( hasLabel ) {
return sprintf(
/* translators: accessibility text. 1: The block title. 2: The block row number. 3: The block label.. */
__( '%1$s Block. Row %2$d. %3$s' ),
title,
position,
label
);
}
return sprintf(
/* translators: accessibility text. 1: The block title. 2: The block row number. */
__( '%1$s Block. Row %2$d' ),
title,
position
);
} else if ( hasPosition && direction === 'horizontal' ) {
if ( hasLabel ) {
return sprintf(
/* translators: accessibility text. 1: The block title. 2: The block column number. 3: The block label.. */
__( '%1$s Block. Column %2$d. %3$s' ),
title,
position,
label
);
}
return sprintf(
/* translators: accessibility text. 1: The block title. 2: The block column number. */
__( '%1$s Block. Column %2$d' ),
title,
position
);
}
if ( hasLabel ) {
return sprintf(
/* translators: accessibility text. %1: The block title. %2: The block label. */
__( '%1$s Block. %2$s' ),
title,
label
);
}
return sprintf(
/* translators: accessibility text. %s: The block title. */
__( '%s Block' ),
title
);
}
export function getDefault( attributeSchema ) {
if ( attributeSchema.default !== undefined ) {
return attributeSchema.default;
}
if ( attributeSchema.type === 'rich-text' ) {
return new RichTextData();
}
}
/**
* Ensure attributes contains only values defined by block type, and merge
* default values for missing attributes.
*
* @param {string} name The block's name.
* @param {Object} attributes The block's attributes.
* @return {Object} The sanitized attributes.
*/
export function __experimentalSanitizeBlockAttributes( name, attributes ) {
// Get the type definition associated with a registered block.
const blockType = getBlockType( name );
if ( undefined === blockType ) {
throw new Error( `Block type '${ name }' is not registered.` );
}
return Object.entries( blockType.attributes ).reduce(
( accumulator, [ key, schema ] ) => {
const value = attributes[ key ];
if ( undefined !== value ) {
if ( schema.type === 'rich-text' ) {
if ( value instanceof RichTextData ) {
accumulator[ key ] = value;
} else if ( typeof value === 'string' ) {
accumulator[ key ] =
RichTextData.fromHTMLString( value );
}
} else if (
schema.type === 'string' &&
value instanceof RichTextData
) {
accumulator[ key ] = value.toHTMLString();
} else {
accumulator[ key ] = value;
}
} else {
const _default = getDefault( schema );
if ( undefined !== _default ) {
accumulator[ key ] = _default;
}
}
if ( [ 'node', 'children' ].indexOf( schema.source ) !== -1 ) {
// Ensure value passed is always an array, which we're expecting in
// the RichText component to handle the deprecated value.
if ( typeof accumulator[ key ] === 'string' ) {
accumulator[ key ] = [ accumulator[ key ] ];
} else if ( ! Array.isArray( accumulator[ key ] ) ) {
accumulator[ key ] = [];
}
}
return accumulator;
},
{}
);
}
/**
* Filter block attributes by `role` and return their names.
*
* @param {string} name Block attribute's name.
* @param {string} role The role of a block attribute.
*
* @return {string[]} The attribute names that have the provided role.
*/
export function __experimentalGetBlockAttributesNamesByRole( name, role ) {
const attributes = getBlockType( name )?.attributes;
if ( ! attributes ) {
return [];
}
const attributesNames = Object.keys( attributes );
if ( ! role ) {
return attributesNames;
}
return attributesNames.filter(
( attributeName ) =>
attributes[ attributeName ]?.__experimentalRole === role
);
}
/**
* Return a new object with the specified keys omitted.
*
* @param {Object} object Original object.
* @param {Array} keys Keys to be omitted.
*
* @return {Object} Object with omitted keys.
*/
export function omit( object, keys ) {
return Object.fromEntries(
Object.entries( object ).filter( ( [ key ] ) => ! keys.includes( key ) )
);
}