From c102331c4c7e3a833155878fdfee3f66c16b9f85 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 14 Jul 2017 13:18:31 -0400 Subject: [PATCH] Enhance attributes to specify types and defaults --- blocks/api/factory.js | 35 ++++-- blocks/api/parser.js | 106 +++++++++++++----- blocks/api/paste.js | 12 +- blocks/api/serializer.js | 59 +++++----- blocks/api/test/factory.js | 41 ++++++- blocks/api/test/parser.js | 106 ++++++++++-------- blocks/api/test/serializer.js | 19 ++-- blocks/library/button/index.js | 6 +- blocks/library/cover-image/index.js | 10 +- blocks/library/embed/index.js | 4 +- blocks/library/gallery/index.js | 21 +++- blocks/library/heading/index.js | 13 ++- blocks/library/image/index.js | 4 +- blocks/library/latest-posts/index.js | 13 ++- blocks/library/list/index.js | 16 ++- blocks/library/pullquote/index.js | 6 +- blocks/library/quote/index.js | 13 ++- blocks/library/table/index.js | 19 ++-- blocks/library/text/index.js | 3 + blocks/test/fixtures/core__cover-image.json | 3 +- .../core__cover-image.serialized.html | 6 +- blocks/test/fixtures/core__gallery.json | 4 +- .../fixtures/core__gallery.serialized.html | 4 +- blocks/test/fixtures/core__pullquote.json | 3 +- .../fixtures/core__pullquote.serialized.html | 4 +- .../test/fixtures/core__quote__style-1.json | 8 +- .../core__quote__style-1.serialized.html | 4 +- .../test/fixtures/core__quote__style-2.json | 8 +- .../core__quote__style-2.serialized.html | 4 +- editor/test/effects.js | 6 + 30 files changed, 371 insertions(+), 189 deletions(-) diff --git a/blocks/api/factory.js b/blocks/api/factory.js index 000468224adbb..1c7ba00ddccd0 100644 --- a/blocks/api/factory.js +++ b/blocks/api/factory.js @@ -2,11 +2,19 @@ * External dependencies */ import uuid from 'uuid/v4'; -import { get, castArray, findIndex, isObjectLike, find } from 'lodash'; +import { + get, + reduce, + castArray, + findIndex, + isObjectLike, + find, +} from 'lodash'; /** * Internal dependencies */ +import { getNormalizedAttributeSource } from './parser'; import { getBlockType } from './registration'; /** @@ -20,21 +28,28 @@ export function createBlock( name, attributes = {} ) { // Get the type definition associated with a registered block. const blockType = getBlockType( name ); - // Do we need this? What purpose does it have? - let defaultAttributes; - if ( blockType ) { - defaultAttributes = blockType.defaultAttributes; - } + // Ensure attributes contains only values defined by block type, and merge + // default values for missing attributes. + attributes = reduce( blockType.attributes, ( result, source, key ) => { + const value = attributes[ key ]; + if ( undefined !== value ) { + result[ key ] = value; + } else { + source = getNormalizedAttributeSource( source ); + if ( source.defaultValue ) { + result[ key ] = source.defaultValue; + } + } + + return result; + }, {} ); // Blocks are stored with a unique ID, the assigned type name, // and the block attributes. return { uid: uuid(), name, - attributes: { - ...defaultAttributes, - ...attributes, - }, + attributes, }; } diff --git a/blocks/api/parser.js b/blocks/api/parser.js index 63ad01f98ab35..c44ea917134ba 100644 --- a/blocks/api/parser.js +++ b/blocks/api/parser.js @@ -2,7 +2,7 @@ * External dependencies */ import { parse as hpqParse } from 'hpq'; -import { pickBy } from 'lodash'; +import { isPlainObject, mapValues, pickBy } from 'lodash'; /** * Internal dependencies @@ -11,29 +11,62 @@ import { parse as grammarParse } from './post.pegjs'; import { getBlockType, getUnknownTypeHandler } from './registration'; import { createBlock } from './factory'; +/** + * Returns true if the provided function is a valid attribute matcher, or false + * otherwise. + * + * Matchers are implemented as functions receiving a DOM node to select data + * from. Using the DOM is incidental and we shouldn't guarantee a contract that + * this be provided, else block implementers may feel inclined to use the node. + * Instead, matchers are intended as a generic interface to query data from any + * tree shape. Here we pick only matchers which include an internal flag. + * + * @param {Function} matcher Function to test + * @return {Boolean} Whether function is an attribute matcher + */ +export function isValidMatcher( matcher ) { + return !! matcher && '_wpBlocksKnownMatcher' in matcher; +} + /** * Returns the block attributes parsed from raw content. * - * @param {String} rawContent Raw block content - * @param {Object} attributes Block attribute matchers - * @return {Object} Block attributes + * @param {String} rawContent Raw block content + * @param {Object} sources Block attribute matchers + * @return {Object} Block attributes */ -export function parseBlockAttributes( rawContent, attributes ) { - if ( 'function' === typeof attributes ) { - return attributes( rawContent ); - } else if ( attributes ) { - // Matchers are implemented as functions that receive a DOM node from - // which to select data. Use of the DOM is incidental and we shouldn't - // guarantee a contract that this be provided, else block implementers - // may feel compelled to use the node. Instead, matchers are intended - // as a generic interface to query data from any tree shape. Here we - // pick only matchers which include an internal flag. - const knownMatchers = pickBy( attributes, '_wpBlocksKnownMatcher' ); - - return hpqParse( rawContent, knownMatchers ); - } +export function getMatcherAttributes( rawContent, sources ) { + const matchers = mapValues( + // Parse only sources with matcher defined + pickBy( sources, ( source ) => isValidMatcher( source.matcher ) ), + + // Transform to object where matcher is value + ( source ) => source.matcher + ); - return {}; + return hpqParse( rawContent, matchers ); +} + +/** + * Returns an attribute source in normalized (object) form. A source may be + * specified in shorthand form as a constructor or attribute matcher, or in its + * expanded form as an object with any of `type`, `matcher`, and `defaultValue` + * values. + * + * @param {(Object|Function)} source Source to normalize + * @return {Object} Normalized source + */ +export function getNormalizedAttributeSource( source ) { + if ( isPlainObject( source ) ) { + return source; + } if ( 'function' === typeof source ) { + // Function may be either matcher or a constructor. Quack quack. + if ( isValidMatcher( source ) ) { + return { matcher: source }; + } + + return { type: source }; + } } /** @@ -45,18 +78,31 @@ export function parseBlockAttributes( rawContent, attributes ) { * @return {Object} All block attributes */ export function getBlockAttributes( blockType, rawContent, attributes ) { - // Merge any attributes present in comment delimiters with those - // that are specified in the block implementation. - attributes = attributes || {}; - if ( blockType ) { - attributes = { - ...blockType.defaultAttributes, - ...attributes, - ...parseBlockAttributes( rawContent, blockType.attributes ), - }; - } + const sources = mapValues( blockType.attributes, getNormalizedAttributeSource ); + + // Merge matcher values into attributes parsed from comment delimiters + attributes = { + ...attributes, + ...getMatcherAttributes( rawContent, sources ), + }; + + return mapValues( sources, ( source, key ) => { + const value = attributes[ key ]; + + // Return default if attribute value not assigned + if ( undefined === value ) { + // Nest the condition so that constructor coercion never occurs if + // value is undefined and block type doesn't specify default value + if ( 'defaultValue' in source ) { + return source.defaultValue; + } + } else if ( source.type && source.type.prototype.valueOf ) { + // Coerce to constructor value if source type + return ( new source.type( value ) ).valueOf(); + } - return attributes; + return value; + } ); } /** diff --git a/blocks/api/paste.js b/blocks/api/paste.js index 71d51901dc857..e1e63bdc01312 100644 --- a/blocks/api/paste.js +++ b/blocks/api/paste.js @@ -2,7 +2,7 @@ * External dependencies */ import { nodeListToReact } from 'dom-react'; -import { find, get } from 'lodash'; +import { find, get, mapValues } from 'lodash'; /** * WordPress dependencies @@ -14,7 +14,7 @@ import { createElement } from 'element'; */ import { createBlock } from './factory'; import { getBlockTypes, getUnknownTypeHandler } from './registration'; -import { parseBlockAttributes } from './parser'; +import { getMatcherAttributes, getNormalizedAttributeSource } from './parser'; /** * Normalises array nodes of any node type to an array of block level nodes. @@ -89,10 +89,12 @@ export default function( nodes ) { return acc; } - const { name, defaultAttributes = [] } = blockType; - const attributes = parseBlockAttributes( node.outerHTML, transform.attributes ); + const attributes = getMatcherAttributes( + node.outerHTML, + mapValues( transform.attributes, getNormalizedAttributeSource ) + ); - return createBlock( name, { ...defaultAttributes, ...attributes } ); + return createBlock( blockType.name, attributes ); }, null ); if ( block ) { diff --git a/blocks/api/serializer.js b/blocks/api/serializer.js index aed8cb12f3c88..69b22d1fde874 100644 --- a/blocks/api/serializer.js +++ b/blocks/api/serializer.js @@ -14,7 +14,7 @@ import { Component, createElement, renderToString, cloneElement, Children } from * Internal dependencies */ import { getBlockType } from './registration'; -import { parseBlockAttributes } from './parser'; +import { getNormalizedAttributeSource } from './parser'; /** * Returns the block's default classname from its name @@ -71,36 +71,39 @@ export function getSaveContent( blockType, attributes ) { } /** - * Returns attributes which ought to be saved - * and serialized into the block comment header + * Returns attributes which are to be saved and serialized into the block + * comment delimiter. * - * When a block exists in memory it contains as its attributes - * both those which come from the block comment header _and_ - * those which come from parsing the contents of the block. + * When a block exists in memory it contains as its attributes both those + * parsed the block comment delimiter _and_ those which matched from the + * contents of the block. * - * This function returns only those attributes which are - * needed to persist and which cannot already be inferred - * from the block content. + * This function returns only those attributes which are needed to persist and + * which cannot be matched from the block content. * - * @param {Object} allAttributes Attributes from in-memory block data - * @param {Object} attributesFromContent Attributes which are inferred from block content - * @returns {Object} filtered set of attributes for minimum save/serialization + * @param {Object} allAttributes Attributes from in-memory block data + * @param {Object} sources Block type attributes definition + * @returns {Object} Subset of attributes for comment serialization */ -export function getCommentAttributes( allAttributes, attributesFromContent ) { - // Iterate over attributes and produce the set to save - return reduce( - Object.keys( allAttributes ), - ( toSave, key ) => { - const allValue = allAttributes[ key ]; - const contentValue = attributesFromContent[ key ]; - - // save only if attribute if not inferred from the content and if valued - return ! ( contentValue !== undefined || allValue === undefined ) - ? Object.assign( toSave, { [ key ]: allValue } ) - : toSave; - }, - {}, - ); +export function getCommentAttributes( allAttributes, sources ) { + return reduce( sources, ( result, source, key ) => { + const value = allAttributes[ key ]; + + // Ignore undefined values + if ( undefined === value ) { + return result; + } + + // Ignore values sources from content + source = getNormalizedAttributeSource( source ); + if ( source.matcher ) { + return result; + } + + // Otherwise, include in comment set + result[ key ] = value; + return result; + }, {} ); } export function serializeAttributes( attrs ) { @@ -115,7 +118,7 @@ export function serializeBlock( block ) { const blockName = block.name; const blockType = getBlockType( blockName ); const saveContent = getSaveContent( blockType, block.attributes ); - const saveAttributes = getCommentAttributes( block.attributes, parseBlockAttributes( saveContent, blockType.attributes ) ); + const saveAttributes = getCommentAttributes( block.attributes, blockType.attributes ); if ( 'wp:core/more' === blockName ) { return `${ saveAttributes.noTeaser ? '\n' : '' }`; diff --git a/blocks/api/test/factory.js b/blocks/api/test/factory.js index 390e3b08f7914..fc43cedc40fa7 100644 --- a/blocks/api/test/factory.js +++ b/blocks/api/test/factory.js @@ -10,7 +10,12 @@ import { createBlock, switchToBlockType } from '../factory'; import { getBlockTypes, unregisterBlockType, setUnknownTypeHandler, registerBlockType } from '../registration'; describe( 'block factory', () => { - const defaultBlockSettings = { save: noop }; + const defaultBlockSettings = { + attributes: { + value: String, + }, + save: noop, + }; afterEach( () => { setUnknownTypeHandler( undefined ); @@ -22,8 +27,11 @@ describe( 'block factory', () => { describe( 'createBlock()', () => { it( 'should create a block given its blockType and attributes', () => { registerBlockType( 'core/test-block', { - defaultAttributes: { - includesDefault: true, + attributes: { + align: String, + includesDefault: { + defaultValue: true, + }, }, save: noop, } ); @@ -43,6 +51,9 @@ describe( 'block factory', () => { describe( 'switchToBlockType()', () => { it( 'should switch the blockType of a block using the "transform form"', () => { registerBlockType( 'core/updated-text-block', { + attributes: { + value: String, + }, transforms: { from: [ { blocks: [ 'core/text-block' ], @@ -79,6 +90,9 @@ describe( 'block factory', () => { it( 'should switch the blockType of a block using the "transform to"', () => { registerBlockType( 'core/updated-text-block', defaultBlockSettings ); registerBlockType( 'core/text-block', { + attributes: { + value: String, + }, transforms: { to: [ { blocks: [ 'core/updated-text-block' ], @@ -130,6 +144,9 @@ describe( 'block factory', () => { it( 'should reject transformations that return null', () => { registerBlockType( 'core/updated-text-block', { + attributes: { + value: String, + }, transforms: { from: [ { blocks: [ 'core/text-block' ], @@ -155,6 +172,9 @@ describe( 'block factory', () => { it( 'should reject transformations that return an empty array', () => { registerBlockType( 'core/updated-text-block', { + attributes: { + value: String, + }, transforms: { from: [ { blocks: [ 'core/text-block' ], @@ -180,6 +200,9 @@ describe( 'block factory', () => { it( 'should reject single transformations that do not include block types', () => { registerBlockType( 'core/updated-text-block', { + attributes: { + value: String, + }, transforms: { from: [ { blocks: [ 'core/text-block' ], @@ -211,6 +234,9 @@ describe( 'block factory', () => { it( 'should reject array transformations that do not include block types', () => { registerBlockType( 'core/updated-text-block', { + attributes: { + value: String, + }, transforms: { from: [ { blocks: [ 'core/text-block' ], @@ -248,6 +274,9 @@ describe( 'block factory', () => { it( 'should reject single transformations with unexpected block types', () => { registerBlockType( 'core/updated-text-block', defaultBlockSettings ); registerBlockType( 'core/text-block', { + attributes: { + value: String, + }, transforms: { to: [ { blocks: [ 'core/updated-text-block' ], @@ -277,6 +306,9 @@ describe( 'block factory', () => { it( 'should reject array transformations with unexpected block types', () => { registerBlockType( 'core/updated-text-block', defaultBlockSettings ); registerBlockType( 'core/text-block', { + attributes: { + value: String, + }, transforms: { to: [ { blocks: [ 'core/updated-text-block' ], @@ -311,6 +343,9 @@ describe( 'block factory', () => { it( 'should accept valid array transformations', () => { registerBlockType( 'core/updated-text-block', defaultBlockSettings ); registerBlockType( 'core/text-block', { + attributes: { + value: String, + }, transforms: { to: [ { blocks: [ 'core/updated-text-block' ], diff --git a/blocks/api/test/parser.js b/blocks/api/test/parser.js index 4d2ba51318180..0a6bf50c66041 100644 --- a/blocks/api/test/parser.js +++ b/blocks/api/test/parser.js @@ -6,10 +6,11 @@ import { noop } from 'lodash'; /** * Internal dependencies */ -import { text } from '../query'; +import { text, attr, html } from '../query'; import { + isValidMatcher, getBlockAttributes, - parseBlockAttributes, + getMatcherAttributes, createBlockWithFallback, default as parse, } from '../parser'; @@ -21,7 +22,12 @@ import { } from '../registration'; describe( 'block parser', () => { - const defaultBlockSettings = { save: noop }; + const defaultBlockSettings = { + attributes: { + attr: String, + }, + save: noop, + }; afterEach( () => { setUnknownTypeHandler( undefined ); @@ -30,61 +36,72 @@ describe( 'block parser', () => { } ); } ); - describe( 'parseBlockAttributes()', () => { - it( 'should use the function implementation', () => { - const attributes = function( rawContent ) { - return { - content: rawContent + ' & Chicken', - }; - }; + describe( 'isValidMatcher()', () => { + it( 'returns false if falsey argument', () => { + expect( isValidMatcher() ).toBe( false ); + } ); - expect( parseBlockAttributes( 'Ribs', attributes ) ).toEqual( { - content: 'Ribs & Chicken', - } ); + it( 'returns true if valid matcher argument', () => { + expect( isValidMatcher( text() ) ).toBe( true ); + } ); + + it( 'returns false if invalid matcher argument', () => { + expect( isValidMatcher( () => {} ) ).toBe( false ); } ); + } ); - it( 'should use the query object implementation', () => { - const attributes = { - emphasis: text( 'strong' ), - ignoredDomMatcher: ( node ) => node.innerHTML, + describe( 'getMatcherAttributes()', () => { + it( 'should return matched attributes from valid matchers', () => { + const sources = { + number: Number, + emphasis: { + matcher: text( 'strong' ), + }, }; const rawContent = 'Ribs & Chicken'; - expect( parseBlockAttributes( rawContent, attributes ) ).toEqual( { + expect( getMatcherAttributes( rawContent, sources ) ).toEqual( { emphasis: '& Chicken', } ); } ); - it( 'should return an empty object if no attributes defined', () => { - const attributes = {}; + it( 'should return an empty object if no sources defined', () => { + const sources = {}; const rawContent = 'Ribs & Chicken'; - expect( parseBlockAttributes( rawContent, attributes ) ).toEqual( {} ); + expect( getMatcherAttributes( rawContent, sources ) ).toEqual( {} ); } ); } ); describe( 'getBlockAttributes()', () => { it( 'should merge attributes with the parsed and default attributes', () => { const blockType = { - attributes: function( rawContent ) { - return { - content: rawContent + ' & Chicken', - }; - }, - defaultAttributes: { - content: '', - topic: 'none', + attributes: { + content: text( 'div' ), + number: { + type: Number, + matcher: attr( 'div', 'data-number' ), + }, + align: String, + topic: { + type: String, + defaultValue: 'none', + }, + ignoredDomMatcher: { + matcher: ( node ) => node.innerHTML, + }, }, }; - const rawContent = 'Ribs'; - const attrs = { align: 'left' }; + const rawContent = '
Ribs
'; + const attrs = { align: 'left', invalid: true }; expect( getBlockAttributes( blockType, rawContent, attrs ) ).toEqual( { + content: 'Ribs', + number: 10, align: 'left', topic: 'none', - content: 'Ribs & Chicken', } ); } ); } ); @@ -141,11 +158,11 @@ describe( 'block parser', () => { describe( 'parse()', () => { it( 'should parse the post content, including block attributes', () => { registerBlockType( 'core/test-block', { - // Currently this is the only way to test block content parsing? - attributes: function( rawContent ) { - return { - content: rawContent, - }; + attributes: { + content: text(), + smoked: String, + url: String, + chicken: String, }, save: noop, } ); @@ -175,10 +192,8 @@ describe( 'block parser', () => { it( 'should parse the post content, ignoring unknown blocks', () => { registerBlockType( 'core/test-block', { - attributes: function( rawContent ) { - return { - content: rawContent + ' & Chicken', - }; + attributes: { + content: text(), }, save: noop, } ); @@ -192,7 +207,7 @@ describe( 'block parser', () => { expect( parsed ).toHaveLength( 1 ); expect( parsed[ 0 ].name ).toBe( 'core/test-block' ); expect( parsed[ 0 ].attributes ).toEqual( { - content: 'Ribs & Chicken', + content: 'Ribs', } ); expect( typeof parsed[ 0 ].uid ).toBe( 'string' ); } ); @@ -220,11 +235,8 @@ describe( 'block parser', () => { it( 'should parse the post content, including raw HTML at each end', () => { registerBlockType( 'core/test-block', defaultBlockSettings ); registerBlockType( 'core/unknown-block', { - // Currently this is the only way to test block content parsing? - attributes: function( rawContent ) { - return { - content: rawContent, - }; + attributes: { + content: html(), }, save: noop, } ); diff --git a/blocks/api/test/serializer.js b/blocks/api/test/serializer.js index 73705b0846c10..47de23dc31e80 100644 --- a/blocks/api/test/serializer.js +++ b/blocks/api/test/serializer.js @@ -6,6 +6,7 @@ import { createElement, Component } from 'element'; /** * Internal dependencies */ +import { text } from '../query'; import serialize, { getCommentAttributes, getSaveContent, serializeAttributes } from '../serializer'; import { getBlockTypes, registerBlockType, unregisterBlockType } from '../registration'; @@ -125,13 +126,15 @@ describe( 'block serializer', () => { expect( attributes ).toEqual( {} ); } ); - it( 'should only return attributes which cannot be inferred from the content', () => { + it( 'should only return attributes which are not matched from content', () => { const attributes = getCommentAttributes( { fruit: 'bananas', category: 'food', ripeness: 'ripe', }, { - fruit: 'bananas', + fruit: text(), + category: String, + ripeness: String, } ); expect( attributes ).toEqual( { @@ -144,7 +147,10 @@ describe( 'block serializer', () => { const attributes = getCommentAttributes( { fruit: 'bananas', ripeness: undefined, - }, {} ); + }, { + fruit: String, + ripeness: String, + } ); expect( attributes ).toEqual( { fruit: 'bananas' } ); } ); @@ -168,10 +174,9 @@ describe( 'block serializer', () => { describe( 'serialize()', () => { it( 'should serialize the post content properly', () => { const blockType = { - attributes: ( rawContent ) => { - return { - content: rawContent, - }; + attributes: { + content: text(), + stuff: String, }, save( { attributes } ) { return

; diff --git a/blocks/library/button/index.js b/blocks/library/button/index.js index a8c87145858b3..df1617c555e17 100644 --- a/blocks/library/button/index.js +++ b/blocks/library/button/index.js @@ -27,6 +27,10 @@ registerBlockType( 'core/button', { url: attr( 'a', 'href' ), title: attr( 'a', 'title' ), text: children( 'a' ), + align: { + type: String, + defaultValue: 'none', + }, }, getEditWrapperProps( attributes ) { @@ -76,7 +80,7 @@ registerBlockType( 'core/button', { }, save( { attributes } ) { - const { url, text, title, align = 'none' } = attributes; + const { url, text, title, align } = attributes; return (

diff --git a/blocks/library/cover-image/index.js b/blocks/library/cover-image/index.js index 18f185b5649b6..f116c7428e49a 100644 --- a/blocks/library/cover-image/index.js +++ b/blocks/library/cover-image/index.js @@ -32,6 +32,14 @@ registerBlockType( 'core/cover-image', { attributes: { title: text( 'h2' ), + url: String, + align: String, + id: Number, + hasParallax: Boolean, + hasBackgroundDim: { + type: Boolean, + defaultValue: true, + }, }, getEditWrapperProps( attributes ) { @@ -42,7 +50,7 @@ registerBlockType( 'core/cover-image', { }, edit( { attributes, setAttributes, focus, setFocus, className } ) { - const { url, title, align, id, hasParallax, hasBackgroundDim = true } = attributes; + const { url, title, align, id, hasParallax, hasBackgroundDim } = attributes; const updateAlignment = ( nextAlign ) => setAttributes( { align: nextAlign } ); const onSelectImage = ( media ) => setAttributes( { url: media.url, id: media.id } ); diff --git a/blocks/library/embed/index.js b/blocks/library/embed/index.js index 7c74a7f097b57..f7001644668c6 100644 --- a/blocks/library/embed/index.js +++ b/blocks/library/embed/index.js @@ -36,6 +36,8 @@ function getEmbedBlockSettings( { title, icon, category = 'embed' } ) { attributes: { title: attr( 'iframe', 'title' ), caption: children( 'figcaption' ), + url: String, + align: String, }, getEditWrapperProps( attributes ) { @@ -198,7 +200,7 @@ function getEmbedBlockSettings( { title, icon, category = 'embed' } ) { }, save( { attributes } ) { - const { url, caption = [], align } = attributes; + const { url, caption, align } = attributes; return (
diff --git a/blocks/library/gallery/index.js b/blocks/library/gallery/index.js index cfc9d107e29ba..1a1c5cd6fc09a 100644 --- a/blocks/library/gallery/index.js +++ b/blocks/library/gallery/index.js @@ -70,6 +70,22 @@ registerBlockType( 'core/gallery', { icon: 'format-gallery', category: 'common', + attributes: { + align: { + type: String, + defaultValue: 'none', + }, + images: { + type: Object, // Array + defaultValue: [], + }, + columns: Number, + imageCrop: { + type: Boolean, + defaultValue: true, + }, + }, + getEditWrapperProps( attributes ) { const { align } = attributes; if ( 'left' === align || 'right' === align || 'wide' === align || 'full' === align ) { @@ -78,10 +94,9 @@ registerBlockType( 'core/gallery', { }, edit( { attributes, setAttributes, focus, className } ) { - const { images = [], columns = defaultColumnsNumber( attributes ), align = 'none' } = attributes; + const { images, columns = defaultColumnsNumber( attributes ), align, imageCrop } = attributes; const setColumnsNumber = ( event ) => setAttributes( { columns: event.target.value } ); const updateAlignment = ( nextAlign ) => setAttributes( { align: nextAlign } ); - const { imageCrop = true } = attributes; const toggleImageCrop = () => setAttributes( { imageCrop: ! imageCrop } ); const controls = ( @@ -158,7 +173,7 @@ registerBlockType( 'core/gallery', { }, save( { attributes } ) { - const { images, columns = defaultColumnsNumber( attributes ), align = 'none', imageCrop = true } = attributes; + const { images, columns = defaultColumnsNumber( attributes ), align, imageCrop } = attributes; return (
{ images.map( ( img ) => ( diff --git a/blocks/library/heading/index.js b/blocks/library/heading/index.js index 74d829db30a9c..a8b3ff83df2c7 100644 --- a/blocks/library/heading/index.js +++ b/blocks/library/heading/index.js @@ -34,11 +34,14 @@ registerBlockType( 'core/heading', { attributes: { content: children( 'h1,h2,h3,h4,h5,h6' ), - nodeName: prop( 'h1,h2,h3,h4,h5,h6', 'nodeName' ), - }, - - defaultAttributes: { - nodeName: 'H2', + nodeName: { + matcher: prop( 'h1,h2,h3,h4,h5,h6', 'nodeName' ), + defaultValue: 'H2', + }, + align: String, + placeholder: { + type: Object, // Array + }, }, transforms: { diff --git a/blocks/library/image/index.js b/blocks/library/image/index.js index 739293d9c6262..3cde6c5788043 100644 --- a/blocks/library/image/index.js +++ b/blocks/library/image/index.js @@ -32,6 +32,8 @@ registerBlockType( 'core/image', { alt: attr( 'img', 'alt' ), caption: children( 'figcaption' ), href: attr( 'a', 'href' ), + id: Number, + align: String, }, transforms: { @@ -152,7 +154,7 @@ registerBlockType( 'core/image', { }, save( { attributes } ) { - const { url, alt, caption = [], align, href } = attributes; + const { url, alt, caption, align, href } = attributes; const image = {; return ( diff --git a/blocks/library/latest-posts/index.js b/blocks/library/latest-posts/index.js index 6709df177f1fd..d16973aaf45ee 100644 --- a/blocks/library/latest-posts/index.js +++ b/blocks/library/latest-posts/index.js @@ -30,9 +30,16 @@ registerBlockType( 'core/latest-posts', { category: 'widgets', - defaultAttributes: { - postsToShow: 5, - displayPostDate: false, + attributes: { + postsToShow: { + type: Number, + defaultValue: 5, + }, + displayPostDate: { + type: Boolean, + defaultValue: false, + }, + align: String, }, getEditWrapperProps( attributes ) { diff --git a/blocks/library/list/index.js b/blocks/library/list/index.js index eb80484c52950..2ad288a90f3f0 100644 --- a/blocks/library/list/index.js +++ b/blocks/library/list/index.js @@ -70,8 +70,14 @@ registerBlockType( 'core/list', { category: 'common', attributes: { - nodeName: prop( 'ol,ul', 'nodeName' ), - values: children( 'ol,ul' ), + nodeName: { + matcher: prop( 'ol,ul', 'nodeName' ), + defaultValue: 'OL', + }, + values: { + matcher: children( 'ol,ul' ), + defaultValue: [], + }, }, className: false, @@ -148,7 +154,7 @@ registerBlockType( 'core/list', { isListActive( listType ) { const { internalListType } = this.state; - const { nodeName = 'OL' } = this.props.attributes; + const { nodeName } = this.props.attributes; return listType === ( internalListType ? internalListType : nodeName ); } @@ -218,7 +224,7 @@ registerBlockType( 'core/list', { render() { const { attributes, focus, setFocus } = this.props; - const { nodeName = 'OL', values = [] } = attributes; + const { nodeName, values } = attributes; return [ focus && ( @@ -268,7 +274,7 @@ registerBlockType( 'core/list', { }, save( { attributes } ) { - const { nodeName = 'OL', values = [] } = attributes; + const { nodeName, values } = attributes; return createElement( nodeName.toLowerCase(), diff --git a/blocks/library/pullquote/index.js b/blocks/library/pullquote/index.js index c2ecd058ab93a..2fc3c087ff906 100644 --- a/blocks/library/pullquote/index.js +++ b/blocks/library/pullquote/index.js @@ -26,6 +26,10 @@ registerBlockType( 'core/pullquote', { attributes: { value: query( 'blockquote > p', children() ), citation: children( 'footer' ), + align: { + type: String, + defaultValue: 'none', + }, }, getEditWrapperProps( attributes ) { @@ -82,7 +86,7 @@ registerBlockType( 'core/pullquote', { }, save( { attributes } ) { - const { value, citation, align = 'none' } = attributes; + const { value, citation, align } = attributes; return (
diff --git a/blocks/library/quote/index.js b/blocks/library/quote/index.js index 151126ab2d2d7..93ce43b949aab 100644 --- a/blocks/library/quote/index.js +++ b/blocks/library/quote/index.js @@ -28,10 +28,11 @@ registerBlockType( 'core/quote', { attributes: { value: query( 'blockquote > p', node() ), citation: children( 'footer' ), - }, - - defaultAttributes: { - value: [], + align: String, + style: { + type: Number, + defaultValue: 1, + }, }, transforms: { @@ -114,7 +115,7 @@ registerBlockType( 'core/quote', { }, edit( { attributes, setAttributes, focus, setFocus, mergeBlocks, className } ) { - const { align, value, citation, style = 1 } = attributes; + const { align, value, citation, style } = attributes; const focusedEditable = focus ? focus.editable || 'value' : null; return [ @@ -174,7 +175,7 @@ registerBlockType( 'core/quote', { }, save( { attributes } ) { - const { align, value, citation, style = 1 } = attributes; + const { align, value, citation, style } = attributes; return (
diff --git a/blocks/library/table/index.js b/blocks/library/table/index.js index ce5508b2e5c6d..89943a3d24ee4 100644 --- a/blocks/library/table/index.js +++ b/blocks/library/table/index.js @@ -15,16 +15,15 @@ registerBlockType( 'core/table', { category: 'formatting', attributes: { - content: children( 'table' ), - }, - - defaultAttributes: { - content: [ - -

-

- , - ], + content: { + matcher: children( 'table' ), + defaultValue: [ + +

+

+ , + ], + }, }, getEditWrapperProps( attributes ) { diff --git a/blocks/library/text/index.js b/blocks/library/text/index.js index f3e22fc418674..ca15978a12a33 100644 --- a/blocks/library/text/index.js +++ b/blocks/library/text/index.js @@ -29,6 +29,9 @@ registerBlockType( 'core/text', { attributes: { content: query( 'p', children() ), + align: String, + dropCap: Boolean, + placeholder: Object, // Array }, transforms: { diff --git a/blocks/test/fixtures/core__cover-image.json b/blocks/test/fixtures/core__cover-image.json index e76615bc0388e..546e54b13e159 100644 --- a/blocks/test/fixtures/core__cover-image.json +++ b/blocks/test/fixtures/core__cover-image.json @@ -3,8 +3,9 @@ "uid": "_uid_0", "name": "core/cover-image", "attributes": { + "title": "Guten Berg!", "url": "https://cldup.com/uuUqE_dXzy.jpg", - "title": "Guten Berg!" + "hasBackgroundDim": true } } ] diff --git a/blocks/test/fixtures/core__cover-image.serialized.html b/blocks/test/fixtures/core__cover-image.serialized.html index 71dbaaf017f04..4b27a95043b89 100644 --- a/blocks/test/fixtures/core__cover-image.serialized.html +++ b/blocks/test/fixtures/core__cover-image.serialized.html @@ -1,7 +1,7 @@ - +
-
+

Guten Berg!

- + \ No newline at end of file diff --git a/blocks/test/fixtures/core__gallery.json b/blocks/test/fixtures/core__gallery.json index aa42f70c3990a..4babfd480520d 100644 --- a/blocks/test/fixtures/core__gallery.json +++ b/blocks/test/fixtures/core__gallery.json @@ -3,6 +3,7 @@ "uid": "_uid_0", "name": "core/gallery", "attributes": { + "align": "none", "images": [ { "sizes": { @@ -36,7 +37,8 @@ "url": "http://google.com/hi.png", "alt": "title" } - ] + ], + "imageCrop": true } } ] diff --git a/blocks/test/fixtures/core__gallery.serialized.html b/blocks/test/fixtures/core__gallery.serialized.html index f3be93a9cc842..73baee9debce3 100644 --- a/blocks/test/fixtures/core__gallery.serialized.html +++ b/blocks/test/fixtures/core__gallery.serialized.html @@ -1,6 +1,6 @@ - + - + \ No newline at end of file diff --git a/blocks/test/fixtures/core__pullquote.json b/blocks/test/fixtures/core__pullquote.json index 088e26a2d92f0..c556b794e5de5 100644 --- a/blocks/test/fixtures/core__pullquote.json +++ b/blocks/test/fixtures/core__pullquote.json @@ -10,7 +10,8 @@ ], "citation": [ "...with a caption" - ] + ], + "align": "none" } } ] diff --git a/blocks/test/fixtures/core__pullquote.serialized.html b/blocks/test/fixtures/core__pullquote.serialized.html index 2957236d56242..5b6e1cc5583e2 100644 --- a/blocks/test/fixtures/core__pullquote.serialized.html +++ b/blocks/test/fixtures/core__pullquote.serialized.html @@ -1,6 +1,6 @@ - +

Testing pullquote block...

...with a caption
- + \ No newline at end of file diff --git a/blocks/test/fixtures/core__quote__style-1.json b/blocks/test/fixtures/core__quote__style-1.json index dd76ad46a3da7..2dbf4d885bad9 100644 --- a/blocks/test/fixtures/core__quote__style-1.json +++ b/blocks/test/fixtures/core__quote__style-1.json @@ -3,16 +3,16 @@ "uid": "_uid_0", "name": "core/quote", "attributes": { - "style": "1", "value": [ { - "children": "The editor will endeavour to create a new page and post building experience that makes writing rich posts effortless, and has “blocks” to make it easy what today might take shortcodes, custom HTML, or “mystery meat” embed discovery.", - "type": "p" + "type": "p", + "children": "The editor will endeavour to create a new page and post building experience that makes writing rich posts effortless, and has “blocks” to make it easy what today might take shortcodes, custom HTML, or “mystery meat” embed discovery." } ], "citation": [ "Matt Mullenweg, 2017" - ] + ], + "style": 1 } } ] diff --git a/blocks/test/fixtures/core__quote__style-1.serialized.html b/blocks/test/fixtures/core__quote__style-1.serialized.html index b6f8ab20095fc..d9ced2abb1a04 100644 --- a/blocks/test/fixtures/core__quote__style-1.serialized.html +++ b/blocks/test/fixtures/core__quote__style-1.serialized.html @@ -1,6 +1,6 @@ - +

The editor will endeavour to create a new page and post building experience that makes writing rich posts effortless, and has “blocks” to make it easy what today might take shortcodes, custom HTML, or “mystery meat” embed discovery.

Matt Mullenweg, 2017
- + \ No newline at end of file diff --git a/blocks/test/fixtures/core__quote__style-2.json b/blocks/test/fixtures/core__quote__style-2.json index 114a32462aea3..3fc23ef6d6c97 100644 --- a/blocks/test/fixtures/core__quote__style-2.json +++ b/blocks/test/fixtures/core__quote__style-2.json @@ -3,16 +3,16 @@ "uid": "_uid_0", "name": "core/quote", "attributes": { - "style": "2", "value": [ { - "children": "There is no greater agony than bearing an untold story inside you.", - "type": "p" + "type": "p", + "children": "There is no greater agony than bearing an untold story inside you." } ], "citation": [ "Maya Angelou" - ] + ], + "style": 2 } } ] diff --git a/blocks/test/fixtures/core__quote__style-2.serialized.html b/blocks/test/fixtures/core__quote__style-2.serialized.html index 109f466bbd19c..55564572abe7f 100644 --- a/blocks/test/fixtures/core__quote__style-2.serialized.html +++ b/blocks/test/fixtures/core__quote__style-2.serialized.html @@ -1,6 +1,6 @@ - +

There is no greater agony than bearing an untold story inside you.

Maya Angelou
- + \ No newline at end of file diff --git a/editor/test/effects.js b/editor/test/effects.js index 62eecdeabf981..d90f44831883b 100644 --- a/editor/test/effects.js +++ b/editor/test/effects.js @@ -102,6 +102,9 @@ describe( 'effects', () => { it( 'should transform and merge the blocks', () => { registerBlockType( 'core/test-block', { + attributes: { + content: String, + }, merge( attributes, attributesToMerge ) { return { content: attributes.content + ' ' + attributesToMerge.content, @@ -110,6 +113,9 @@ describe( 'effects', () => { save: noop, } ); registerBlockType( 'core/test-block-2', { + attributes: { + content: String, + }, transforms: { to: [ { type: 'blocks',