-
Notifications
You must be signed in to change notification settings - Fork 4.2k
/
registration.js
708 lines (652 loc) · 22.7 KB
/
registration.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
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
/* eslint no-console: [ 'error', { allow: [ 'error', 'warn' ] } ] */
/**
* External dependencies
*/
import {
camelCase,
isArray,
isEmpty,
isFunction,
isNil,
isObject,
isPlainObject,
isString,
mapKeys,
omit,
pick,
pickBy,
some,
} from 'lodash';
/**
* WordPress dependencies
*/
import { applyFilters } from '@wordpress/hooks';
import { select, dispatch } from '@wordpress/data';
import { _x } from '@wordpress/i18n';
import { blockDefault } from '@wordpress/icons';
/**
* Internal dependencies
*/
import i18nBlockSchema from './i18n-block.json';
import { isValidIcon, normalizeIconObject } from './utils';
import { DEPRECATED_ENTRY_KEYS } from './constants';
import { store as blocksStore } from '../store';
/**
* An icon type definition. One of a Dashicon slug, an element,
* or a component.
*
* @typedef {(string|WPElement|WPComponent)} WPIcon
*
* @see https://developer.wordpress.org/resource/dashicons/
*/
/**
* Render behavior of a block type icon; one of a Dashicon slug, an element,
* or a component.
*
* @typedef {WPIcon} WPBlockTypeIconRender
*/
/**
* An object describing a normalized block type icon.
*
* @typedef {Object} WPBlockTypeIconDescriptor
*
* @property {WPBlockTypeIconRender} src Render behavior of the icon,
* one of a Dashicon slug, an
* element, or a component.
* @property {string} background Optimal background hex string
* color when displaying icon.
* @property {string} foreground Optimal foreground hex string
* color when displaying icon.
* @property {string} shadowColor Optimal shadow hex string
* color when displaying icon.
*/
/**
* Value to use to render the icon for a block type in an editor interface,
* either a Dashicon slug, an element, a component, or an object describing
* the icon.
*
* @typedef {(WPBlockTypeIconDescriptor|WPBlockTypeIconRender)} WPBlockTypeIcon
*/
/**
* Named block variation scopes.
*
* @typedef {'block'|'inserter'|'transform'} WPBlockVariationScope
*/
/**
* An object describing a variation defined for the block type.
*
* @typedef {Object} WPBlockVariation
*
* @property {string} name The unique and machine-readable name.
* @property {string} title A human-readable variation title.
* @property {string} [description] A detailed variation description.
* @property {string} [category] Block type category classification,
* used in search interfaces to arrange
* block types by category.
* @property {WPIcon} [icon] An icon helping to visualize the variation.
* @property {boolean} [isDefault] Indicates whether the current variation is
* the default one. Defaults to `false`.
* @property {Object} [attributes] Values which override block attributes.
* @property {Array[]} [innerBlocks] Initial configuration of nested blocks.
* @property {Object} [example] Example provides structured data for
* the block preview. You can set to
* `undefined` to disable the preview shown
* for the block type.
* @property {WPBlockVariationScope[]} [scope] The list of scopes where the variation
* is applicable. When not provided, it
* assumes all available scopes.
* @property {string[]} [keywords] An array of terms (which can be translated)
* that help users discover the variation
* while searching.
* @property {Function|string[]} [isActive] This can be a function or an array of block attributes.
* Function that accepts a block's attributes and the
* variation's attributes and determines if a variation is active.
* This function doesn't try to find a match dynamically based
* on all block's attributes, as in many cases some attributes are irrelevant.
* An example would be for `embed` block where we only care
* about `providerNameSlug` attribute's value.
* We can also use a `string[]` to tell which attributes
* should be compared as a shorthand. Each attributes will
* be matched and the variation will be active if all of them are matching.
*/
/**
* Defined behavior of a block type.
*
* @typedef {Object} WPBlock
*
* @property {string} name Block type's namespaced name.
* @property {string} title Human-readable block type label.
* @property {string} [description] A detailed block type description.
* @property {string} [category] Block type category classification,
* used in search interfaces to arrange
* block types by category.
* @property {WPBlockTypeIcon} [icon] Block type icon.
* @property {string[]} [keywords] Additional keywords to produce block
* type as result in search interfaces.
* @property {Object} [attributes] Block type attributes.
* @property {WPComponent} [save] Optional component describing
* serialized markup structure of a
* block type.
* @property {WPComponent} edit Component rendering an element to
* manipulate the attributes of a block
* in the context of an editor.
* @property {WPBlockVariation[]} [variations] The list of block variations.
* @property {Object} [example] Example provides structured data for
* the block preview. When not defined
* then no preview is shown.
*/
/**
* Mapping of legacy category slugs to their latest normal values, used to
* accommodate updates of the default set of block categories.
*
* @type {Record<string,string>}
*/
const LEGACY_CATEGORY_MAPPING = {
common: 'text',
formatting: 'text',
layout: 'design',
};
export const serverSideBlockDefinitions = {};
/**
* Sets the server side block definition of blocks.
*
* @param {Object} definitions Server-side block definitions
*/
// eslint-disable-next-line camelcase
export function unstable__bootstrapServerSideBlockDefinitions( definitions ) {
for ( const blockName of Object.keys( definitions ) ) {
// Don't overwrite if already set. It covers the case when metadata
// was initialized from the server.
if ( serverSideBlockDefinitions[ blockName ] ) {
// We still need to polyfill `apiVersion` for WordPress version
// lower than 5.7. If it isn't present in the definition shared
// from the server, we try to fallback to the definition passed.
// @see https://github.com/WordPress/gutenberg/pull/29279
if (
serverSideBlockDefinitions[ blockName ].apiVersion ===
undefined &&
definitions[ blockName ].apiVersion
) {
serverSideBlockDefinitions[ blockName ].apiVersion =
definitions[ blockName ].apiVersion;
}
continue;
}
serverSideBlockDefinitions[ blockName ] = mapKeys(
pickBy( definitions[ blockName ], ( value ) => ! isNil( value ) ),
( value, key ) => camelCase( key )
);
}
}
/**
* Registers a new block provided a unique name and an object defining its
* behavior. Once registered, the block is made available as an option to any
* editor interface where blocks are implemented.
*
* @param {string} name Block name.
* @param {Object} settings Block settings.
*
* @return {?WPBlock} The block, if it has been successfully registered;
* otherwise `undefined`.
*/
export function registerBlockType( name, settings ) {
settings = {
name,
icon: blockDefault,
keywords: [],
attributes: {},
providesContext: {},
usesContext: [],
supports: {},
styles: [],
save: () => null,
...serverSideBlockDefinitions?.[ name ],
...settings,
};
if ( typeof name !== 'string' ) {
console.error( 'Block names must be strings.' );
return;
}
if ( ! /^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/.test( name ) ) {
console.error(
'Block names must contain a namespace prefix, include only lowercase alphanumeric characters or dashes, and start with a letter. Example: my-plugin/my-custom-block'
);
return;
}
if ( select( blocksStore ).getBlockType( name ) ) {
console.error( 'Block "' + name + '" is already registered.' );
return;
}
const preFilterSettings = { ...settings };
settings = applyFilters( 'blocks.registerBlockType', settings, name );
if ( settings.deprecated ) {
settings.deprecated = settings.deprecated.map( ( deprecation ) =>
pick(
// Only keep valid deprecation keys.
applyFilters(
'blocks.registerBlockType',
// Merge deprecation keys with pre-filter settings
// so that filters that depend on specific keys being
// present don't fail.
{
// Omit deprecation keys here so that deprecations
// can opt out of specific keys like "supports".
...omit( preFilterSettings, DEPRECATED_ENTRY_KEYS ),
...deprecation,
},
name
),
DEPRECATED_ENTRY_KEYS
)
);
}
if ( ! isPlainObject( settings ) ) {
console.error( 'Block settings must be a valid object.' );
return;
}
if ( ! isFunction( settings.save ) ) {
console.error( 'The "save" property must be a valid function.' );
return;
}
if ( 'edit' in settings && ! isFunction( settings.edit ) ) {
console.error( 'The "edit" property must be a valid function.' );
return;
}
// Canonicalize legacy categories to equivalent fallback.
if ( LEGACY_CATEGORY_MAPPING.hasOwnProperty( settings.category ) ) {
settings.category = LEGACY_CATEGORY_MAPPING[ settings.category ];
}
if (
'category' in settings &&
! some( select( blocksStore ).getCategories(), {
slug: settings.category,
} )
) {
console.warn(
'The block "' +
name +
'" is registered with an invalid category "' +
settings.category +
'".'
);
delete settings.category;
}
if ( ! ( 'title' in settings ) || settings.title === '' ) {
console.error( 'The block "' + name + '" must have a title.' );
return;
}
if ( typeof settings.title !== 'string' ) {
console.error( 'Block titles must be strings.' );
return;
}
settings.icon = normalizeIconObject( settings.icon );
if ( ! isValidIcon( settings.icon.src ) ) {
console.error(
'The icon passed is invalid. ' +
'The icon should be a string, an element, a function, or an object following the specifications documented in https://developer.wordpress.org/block-editor/developers/block-api/block-registration/#icon-optional'
);
return;
}
dispatch( blocksStore ).addBlockTypes( settings );
return settings;
}
/**
* Translates block settings provided with metadata using the i18n schema.
*
* @param {string|string[]|Object[]} i18nSchema I18n schema for the block setting.
* @param {string|string[]|Object[]} settingValue Value for the block setting.
* @param {string} textdomain Textdomain to use with translations.
*
* @return {string|string[]|Object[]} Translated setting.
*/
function translateBlockSettingUsingI18nSchema(
i18nSchema,
settingValue,
textdomain
) {
if ( isString( i18nSchema ) && isString( settingValue ) ) {
// eslint-disable-next-line @wordpress/i18n-no-variables, @wordpress/i18n-text-domain
return _x( settingValue, i18nSchema, textdomain );
}
if (
isArray( i18nSchema ) &&
! isEmpty( i18nSchema ) &&
isArray( settingValue )
) {
return settingValue.map( ( value ) =>
translateBlockSettingUsingI18nSchema(
i18nSchema[ 0 ],
value,
textdomain
)
);
}
if (
isObject( i18nSchema ) &&
! isEmpty( i18nSchema ) &&
isObject( settingValue )
) {
return Object.keys( settingValue ).reduce( ( accumulator, key ) => {
if ( ! i18nSchema[ key ] ) {
accumulator[ key ] = settingValue[ key ];
return accumulator;
}
accumulator[ key ] = translateBlockSettingUsingI18nSchema(
i18nSchema[ key ],
settingValue[ key ],
textdomain
);
return accumulator;
}, {} );
}
return settingValue;
}
/**
* Registers a new block provided from metadata stored in `block.json` file.
* It uses `registerBlockType` internally.
*
* @see registerBlockType
*
* @param {Object} metadata Block metadata loaded from `block.json`.
* @param {string} metadata.name Block name.
* @param {string} metadata.textdomain Textdomain to use with translations.
* @param {Object} additionalSettings Additional block settings.
*
* @return {?WPBlock} The block, if it has been successfully registered;
* otherwise `undefined`.
*/
export function registerBlockTypeFromMetadata(
{ name, textdomain, ...metadata },
additionalSettings
) {
const allowedFields = [
'apiVersion',
'title',
'category',
'parent',
'icon',
'description',
'keywords',
'attributes',
'providesContext',
'usesContext',
'supports',
'styles',
'example',
'variations',
];
const settings = pick( metadata, allowedFields );
if ( textdomain ) {
Object.keys( i18nBlockSchema ).forEach( ( key ) => {
if ( ! settings[ key ] ) {
return;
}
settings[ key ] = translateBlockSettingUsingI18nSchema(
i18nBlockSchema[ key ],
settings[ key ],
textdomain
);
} );
}
unstable__bootstrapServerSideBlockDefinitions( {
[ name ]: settings,
} );
return registerBlockType( name, additionalSettings );
}
/**
* Registers a new block collection to group blocks in the same namespace in the inserter.
*
* @param {string} namespace The namespace to group blocks by in the inserter; corresponds to the block namespace.
* @param {Object} settings The block collection settings.
* @param {string} settings.title The title to display in the block inserter.
* @param {Object} [settings.icon] The icon to display in the block inserter.
*/
export function registerBlockCollection( namespace, { title, icon } ) {
dispatch( blocksStore ).addBlockCollection( namespace, title, icon );
}
/**
* Unregisters a block collection
*
* @param {string} namespace The namespace to group blocks by in the inserter; corresponds to the block namespace
*
*/
export function unregisterBlockCollection( namespace ) {
dispatch( blocksStore ).removeBlockCollection( namespace );
}
/**
* Unregisters a block.
*
* @param {string} name Block name.
*
* @return {?WPBlock} The previous block value, if it has been successfully
* unregistered; otherwise `undefined`.
*/
export function unregisterBlockType( name ) {
const oldBlock = select( blocksStore ).getBlockType( name );
if ( ! oldBlock ) {
console.error( 'Block "' + name + '" is not registered.' );
return;
}
dispatch( blocksStore ).removeBlockTypes( name );
return oldBlock;
}
/**
* Assigns name of block for handling non-block content.
*
* @param {string} blockName Block name.
*/
export function setFreeformContentHandlerName( blockName ) {
dispatch( blocksStore ).setFreeformFallbackBlockName( blockName );
}
/**
* Retrieves name of block handling non-block content, or undefined if no
* handler has been defined.
*
* @return {?string} Block name.
*/
export function getFreeformContentHandlerName() {
return select( blocksStore ).getFreeformFallbackBlockName();
}
/**
* Retrieves name of block used for handling grouping interactions.
*
* @return {?string} Block name.
*/
export function getGroupingBlockName() {
return select( blocksStore ).getGroupingBlockName();
}
/**
* Assigns name of block handling unregistered block types.
*
* @param {string} blockName Block name.
*/
export function setUnregisteredTypeHandlerName( blockName ) {
dispatch( blocksStore ).setUnregisteredFallbackBlockName( blockName );
}
/**
* Retrieves name of block handling unregistered block types, or undefined if no
* handler has been defined.
*
* @return {?string} Block name.
*/
export function getUnregisteredTypeHandlerName() {
return select( blocksStore ).getUnregisteredFallbackBlockName();
}
/**
* Assigns the default block name.
*
* @param {string} name Block name.
*/
export function setDefaultBlockName( name ) {
dispatch( blocksStore ).setDefaultBlockName( name );
}
/**
* Assigns name of block for handling block grouping interactions.
*
* @param {string} name Block name.
*/
export function setGroupingBlockName( name ) {
dispatch( blocksStore ).setGroupingBlockName( name );
}
/**
* Retrieves the default block name.
*
* @return {?string} Block name.
*/
export function getDefaultBlockName() {
return select( blocksStore ).getDefaultBlockName();
}
/**
* Returns a registered block type.
*
* @param {string} name Block name.
*
* @return {?Object} Block type.
*/
export function getBlockType( name ) {
return select( blocksStore ).getBlockType( name );
}
/**
* Returns all registered blocks.
*
* @return {Array} Block settings.
*/
export function getBlockTypes() {
return select( blocksStore ).getBlockTypes();
}
/**
* Returns the block support value for a feature, if defined.
*
* @param {(string|Object)} nameOrType Block name or type object
* @param {string} feature Feature to retrieve
* @param {*} defaultSupports Default value to return if not
* explicitly defined
*
* @return {?*} Block support value
*/
export function getBlockSupport( nameOrType, feature, defaultSupports ) {
return select( blocksStore ).getBlockSupport(
nameOrType,
feature,
defaultSupports
);
}
/**
* Returns true if the block defines support for a feature, or false otherwise.
*
* @param {(string|Object)} nameOrType Block name or type object.
* @param {string} feature Feature to test.
* @param {boolean} defaultSupports Whether feature is supported by
* default if not explicitly defined.
*
* @return {boolean} Whether block supports feature.
*/
export function hasBlockSupport( nameOrType, feature, defaultSupports ) {
return select( blocksStore ).hasBlockSupport(
nameOrType,
feature,
defaultSupports
);
}
/**
* Determines whether or not the given block is a reusable block. This is a
* special block type that is used to point to a global block stored via the
* API.
*
* @param {Object} blockOrType Block or Block Type to test.
*
* @return {boolean} Whether the given block is a reusable block.
*/
export function isReusableBlock( blockOrType ) {
return blockOrType.name === 'core/block';
}
/**
* Determines whether or not the given block is a template part. This is a
* special block type that allows composing a page template out of reusable
* design elements.
*
* @param {Object} blockOrType Block or Block Type to test.
*
* @return {boolean} Whether the given block is a template part.
*/
export function isTemplatePart( blockOrType ) {
return blockOrType.name === 'core/template-part';
}
/**
* Returns an array with the child blocks of a given block.
*
* @param {string} blockName Name of block (example: “latest-posts”).
*
* @return {Array} Array of child block names.
*/
export const getChildBlockNames = ( blockName ) => {
return select( blocksStore ).getChildBlockNames( blockName );
};
/**
* Returns a boolean indicating if a block has child blocks or not.
*
* @param {string} blockName Name of block (example: “latest-posts”).
*
* @return {boolean} True if a block contains child blocks and false otherwise.
*/
export const hasChildBlocks = ( blockName ) => {
return select( blocksStore ).hasChildBlocks( blockName );
};
/**
* Returns a boolean indicating if a block has at least one child block with inserter support.
*
* @param {string} blockName Block type name.
*
* @return {boolean} True if a block contains at least one child blocks with inserter support
* and false otherwise.
*/
export const hasChildBlocksWithInserterSupport = ( blockName ) => {
return select( blocksStore ).hasChildBlocksWithInserterSupport( blockName );
};
/**
* Registers a new block style variation for the given block.
*
* @param {string} blockName Name of block (example: “core/latest-posts”).
* @param {Object} styleVariation Object containing `name` which is the class name applied to the block and `label` which identifies the variation to the user.
*/
export const registerBlockStyle = ( blockName, styleVariation ) => {
dispatch( blocksStore ).addBlockStyles( blockName, styleVariation );
};
/**
* Unregisters a block style variation for the given block.
*
* @param {string} blockName Name of block (example: “core/latest-posts”).
* @param {string} styleVariationName Name of class applied to the block.
*/
export const unregisterBlockStyle = ( blockName, styleVariationName ) => {
dispatch( blocksStore ).removeBlockStyles( blockName, styleVariationName );
};
/**
* Returns an array with the variations of a given block type.
*
* @param {string} blockName Name of block (example: “core/columns”).
* @param {WPBlockVariationScope} [scope] Block variation scope name.
*
* @return {(WPBlockVariation[]|void)} Block variations.
*/
export const getBlockVariations = ( blockName, scope ) => {
return select( blocksStore ).getBlockVariations( blockName, scope );
};
/**
* Registers a new block variation for the given block type.
*
* @param {string} blockName Name of the block (example: “core/columns”).
* @param {WPBlockVariation} variation Object describing a block variation.
*/
export const registerBlockVariation = ( blockName, variation ) => {
dispatch( blocksStore ).addBlockVariations( blockName, variation );
};
/**
* Unregisters a block variation defined for the given block type.
*
* @param {string} blockName Name of the block (example: “core/columns”).
* @param {string} variationName Name of the variation defined for the block.
*/
export const unregisterBlockVariation = ( blockName, variationName ) => {
dispatch( blocksStore ).removeBlockVariations( blockName, variationName );
};