From dea003b701a6129f49f0abb148cdf180520d29e5 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 30 Nov 2017 16:16:56 -0500 Subject: [PATCH 01/13] Editor: Move slot provider to top-level Nesting editors still want to make use of ancestored slots, e.g. inserter, toolbar, inspector controls --- edit-post/index.js | 9 +++++++-- editor/components/provider/index.js | 15 +-------------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/edit-post/index.js b/edit-post/index.js index 3fb2777fe1bc01..f1c1f132415e8e 100644 --- a/edit-post/index.js +++ b/edit-post/index.js @@ -11,6 +11,7 @@ import { createProvider } from 'react-redux'; import { render, unmountComponentAtNode } from '@wordpress/element'; import { settings as dateSettings } from '@wordpress/date'; import { EditorProvider, ErrorBoundary } from '@wordpress/editor'; +import { SlotFillProvider } from '@wordpress/components'; /** * Internal dependencies @@ -63,7 +64,9 @@ export function reinitializeEditor( target, settings ) { - + + + , @@ -92,7 +95,9 @@ export function initializeEditor( id, post, settings ) { - + + + , diff --git a/editor/components/provider/index.js b/editor/components/provider/index.js index 906092835d2c74..543b4115aa6cec 100644 --- a/editor/components/provider/index.js +++ b/editor/components/provider/index.js @@ -10,11 +10,7 @@ import { flow, pick, noop } from 'lodash'; */ import { createElement, Component } from '@wordpress/element'; import { RichTextProvider } from '@wordpress/blocks'; -import { - APIProvider, - DropZoneProvider, - SlotFillProvider, -} from '@wordpress/components'; +import { APIProvider, DropZoneProvider } from '@wordpress/components'; /** * Internal Dependencies @@ -112,15 +108,6 @@ class EditorProvider extends Component { }, this.store.dispatch ), ], - // Slot / Fill provider: - // - // - context.getSlot - // - context.registerSlot - // - context.unregisterSlot - [ - SlotFillProvider, - ], - // APIProvider // // - context.getAPISchema From acb160e2efbeb19fd3de972fd9a989ad43739515 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 30 Nov 2017 16:18:09 -0500 Subject: [PATCH 02/13] Block API: Implement support for parsing, generating, serializing nested blocks --- blocks/api/factory.js | 4 +- blocks/api/parser.js | 27 +++++++++--- blocks/api/serializer.js | 22 +++++----- blocks/api/test/parser.js | 44 +++++++++++-------- blocks/test/fixtures/core-embed__animoto.json | 1 + blocks/test/fixtures/core-embed__cloudup.json | 1 + .../fixtures/core-embed__collegehumor.json | 1 + .../fixtures/core-embed__dailymotion.json | 1 + .../test/fixtures/core-embed__facebook.json | 1 + blocks/test/fixtures/core-embed__flickr.json | 1 + .../test/fixtures/core-embed__funnyordie.json | 1 + blocks/test/fixtures/core-embed__hulu.json | 1 + blocks/test/fixtures/core-embed__imgur.json | 1 + .../test/fixtures/core-embed__instagram.json | 1 + blocks/test/fixtures/core-embed__issuu.json | 1 + .../fixtures/core-embed__kickstarter.json | 1 + .../test/fixtures/core-embed__meetup-com.json | 1 + .../test/fixtures/core-embed__mixcloud.json | 1 + .../fixtures/core-embed__photobucket.json | 1 + .../test/fixtures/core-embed__polldaddy.json | 1 + blocks/test/fixtures/core-embed__reddit.json | 1 + .../fixtures/core-embed__reverbnation.json | 1 + .../test/fixtures/core-embed__screencast.json | 1 + blocks/test/fixtures/core-embed__scribd.json | 1 + .../test/fixtures/core-embed__slideshare.json | 1 + blocks/test/fixtures/core-embed__smugmug.json | 1 + .../test/fixtures/core-embed__soundcloud.json | 1 + blocks/test/fixtures/core-embed__speaker.json | 1 + blocks/test/fixtures/core-embed__spotify.json | 1 + blocks/test/fixtures/core-embed__ted.json | 1 + blocks/test/fixtures/core-embed__tumblr.json | 1 + blocks/test/fixtures/core-embed__twitter.json | 1 + .../test/fixtures/core-embed__videopress.json | 1 + blocks/test/fixtures/core-embed__vimeo.json | 1 + .../fixtures/core-embed__wordpress-tv.json | 1 + .../test/fixtures/core-embed__wordpress.json | 1 + blocks/test/fixtures/core-embed__youtube.json | 1 + .../core__4-invalid-starting-letter.json | 1 + blocks/test/fixtures/core__audio.json | 1 + blocks/test/fixtures/core__block.json | 1 + .../test/fixtures/core__button__center.json | 1 + blocks/test/fixtures/core__categories.json | 1 + blocks/test/fixtures/core__code.json | 1 + blocks/test/fixtures/core__cover-image.json | 1 + blocks/test/fixtures/core__embed.json | 1 + blocks/test/fixtures/core__freeform.json | 1 + .../fixtures/core__freeform__undelimited.json | 1 + blocks/test/fixtures/core__gallery.json | 1 + .../test/fixtures/core__gallery__columns.json | 1 + .../test/fixtures/core__heading__h2-em.json | 1 + blocks/test/fixtures/core__heading__h2.json | 1 + blocks/test/fixtures/core__html.json | 1 + blocks/test/fixtures/core__image.json | 1 + .../fixtures/core__image__center-caption.json | 1 + .../test/fixtures/core__invalid-Capitals.json | 1 + .../test/fixtures/core__invalid-special.json | 1 + blocks/test/fixtures/core__latest-posts.json | 1 + .../core__latest-posts__displayPostDate.json | 1 + blocks/test/fixtures/core__list__ul.json | 1 + blocks/test/fixtures/core__more.json | 1 + .../core__more__custom-text-teaser.json | 1 + .../core__paragraph__align-right.json | 1 + blocks/test/fixtures/core__preformatted.json | 1 + blocks/test/fixtures/core__pullquote.json | 1 + .../core__pullquote__multi-paragraph.json | 1 + .../test/fixtures/core__quote__style-1.json | 1 + .../test/fixtures/core__quote__style-2.json | 1 + blocks/test/fixtures/core__separator.json | 1 + blocks/test/fixtures/core__shortcode.json | 1 + blocks/test/fixtures/core__subhead.json | 1 + blocks/test/fixtures/core__table.json | 1 + blocks/test/fixtures/core__text-columns.json | 1 + .../core__text__converts-to-paragraph.json | 1 + blocks/test/fixtures/core__verse.json | 1 + blocks/test/fixtures/core__video.json | 1 + blocks/test/full-content.js | 6 +++ 76 files changed, 138 insertions(+), 36 deletions(-) diff --git a/blocks/api/factory.js b/blocks/api/factory.js index a0b490cb44939d..112a7b3ae09038 100644 --- a/blocks/api/factory.js +++ b/blocks/api/factory.js @@ -32,10 +32,11 @@ import { getBlockType, getBlockTypes } from './registration'; * * @param {string} name Block name. * @param {Object} blockAttributes Block attributes. + * @param {?Array} innerBlocks Nested blocks. * * @return {Object} Block object. */ -export function createBlock( name, blockAttributes = {} ) { +export function createBlock( name, blockAttributes = {}, innerBlocks = [] ) { // Get the type definition associated with a registered block. const blockType = getBlockType( name ); @@ -59,6 +60,7 @@ export function createBlock( name, blockAttributes = {} ) { name, isValid: true, attributes, + innerBlocks, }; } diff --git a/blocks/api/parser.js b/blocks/api/parser.js index 2011a213db5839..609e270e91d49b 100644 --- a/blocks/api/parser.js +++ b/blocks/api/parser.js @@ -178,13 +178,23 @@ export function getAttributesFromDeprecatedVersion( blockType, innerHTML, attrib /** * Creates a block with fallback to the unknown type handler. * - * @param {?string} name Block type name. - * @param {string} innerHTML Raw block content. - * @param {?Object} attributes Attributes obtained from block delimiters. + * @param {Object} blockNode Parsed block node. * * @return {?Object} An initialized block object (if possible). */ -export function createBlockWithFallback( name, innerHTML, attributes ) { +export function createBlockWithFallback( blockNode ) { + let { + blockName: name, + attrs: attributes, + innerBlocks = [], + innerHTML, + } = blockNode; + + attributes = attributes || {}; + + // Trim content to avoid creation of intermediary freeform segments + innerHTML = innerHTML.trim(); + // Use type from block content, otherwise find unknown handler. name = name || getUnknownTypeHandlerName(); @@ -217,6 +227,9 @@ export function createBlockWithFallback( name, innerHTML, attributes ) { blockType = getBlockType( name ); } + // Coerce inner blocks from parse form to canonical form + innerBlocks = innerBlocks.map( createBlockWithFallback ); + // Include in set only if type were determined. if ( ! blockType || ( ! innerHTML && name === fallbackBlock ) ) { return; @@ -224,7 +237,8 @@ export function createBlockWithFallback( name, innerHTML, attributes ) { const block = createBlock( name, - getBlockAttributes( blockType, innerHTML, attributes ) + getBlockAttributes( blockType, innerHTML, attributes ), + innerBlocks ); // Validate that the parsed block is valid, meaning that if we were to @@ -264,8 +278,7 @@ export function createBlockWithFallback( name, innerHTML, attributes ) { */ export function parseWithGrammar( content ) { return grammarParse( content ).reduce( ( memo, blockNode ) => { - const { blockName, innerHTML, attrs } = blockNode; - const block = createBlockWithFallback( blockName, innerHTML.trim(), attrs ); + const block = createBlockWithFallback( blockNode ); if ( block ) { memo.push( block ); } diff --git a/blocks/api/serializer.js b/blocks/api/serializer.js index d93b189784217f..56594d5d9db760 100644 --- a/blocks/api/serializer.js +++ b/blocks/api/serializer.js @@ -32,12 +32,13 @@ export function getBlockDefaultClassname( blockName ) { * Given a block type containg a save render implementation and attributes, returns the * enhanced element to be saved or string when raw HTML expected. * - * @param {Object} blockType Block type. - * @param {Object} attributes Block attributes. + * @param {Object} blockType Block type. + * @param {Object} attributes Block attributes. + * @param {?Array} innerBlocks Nested blocks. * - * @return {Object|string} Save content. + * @return {Object|string} Save element or raw HTML string. */ -export function getSaveElement( blockType, attributes ) { +export function getSaveElement( blockType, attributes, innerBlocks = [] ) { let { save } = blockType; // Component classes are unsupported for save since serialization must @@ -48,7 +49,7 @@ export function getSaveElement( blockType, attributes ) { save = instance.render.bind( instance ); } - let element = save( { attributes } ); + let element = save( { attributes, innerBlocks } ); if ( isObject( element ) && hasFilter( 'blocks.getSaveContent.extraProps' ) ) { /** @@ -84,13 +85,14 @@ export function getSaveElement( blockType, attributes ) { * Given a block type containg a save render implementation and attributes, returns the * static markup to be saved. * - * @param {Object} blockType Block type. - * @param {Object} attributes Block attributes. + * @param {Object} blockType Block type. + * @param {Object} attributes Block attributes. + * @param {?Array} innerBlocks Nested blocks. * * @return {string} Save content. */ -export function getSaveContent( blockType, attributes ) { - return renderToString( getSaveElement( blockType, attributes ) ); +export function getSaveContent( blockType, attributes, innerBlocks ) { + return renderToString( getSaveElement( blockType, attributes, innerBlocks ) ); } /** @@ -175,7 +177,7 @@ export function getBlockContent( block ) { let saveContent = block.originalContent; if ( block.isValid ) { try { - saveContent = getSaveContent( blockType, block.attributes ); + saveContent = getSaveContent( blockType, block.attributes, block.innerBlocks ); } catch ( error ) {} } diff --git a/blocks/api/test/parser.js b/blocks/api/test/parser.js index 0213dbf2257666..608ba501d91f7a 100644 --- a/blocks/api/test/parser.js +++ b/blocks/api/test/parser.js @@ -231,11 +231,11 @@ describe( 'block parser', () => { it( 'should create the requested block if it exists', () => { registerBlockType( 'core/test-block', defaultBlockSettings ); - const block = createBlockWithFallback( - 'core/test-block', - 'Bananas', - { fruit: 'Bananas' } - ); + const block = createBlockWithFallback( { + blockName: 'core/test-block', + innerHTML: 'Bananas', + attrs: { fruit: 'Bananas' }, + } ); expect( block.name ).toEqual( 'core/test-block' ); expect( block.attributes ).toEqual( { fruit: 'Bananas' } ); } ); @@ -243,7 +243,10 @@ describe( 'block parser', () => { it( 'should create the requested block with no attributes if it exists', () => { registerBlockType( 'core/test-block', defaultBlockSettings ); - const block = createBlockWithFallback( 'core/test-block', '' ); + const block = createBlockWithFallback( { + blockName: 'core/test-block', + innerHTML: '', + } ); expect( block.name ).toEqual( 'core/test-block' ); expect( block.attributes ).toEqual( {} ); } ); @@ -252,11 +255,11 @@ describe( 'block parser', () => { registerBlockType( 'core/unknown-block', unknownBlockSettings ); setUnknownTypeHandlerName( 'core/unknown-block' ); - const block = createBlockWithFallback( - 'core/test-block', - 'Bananas', - { fruit: 'Bananas' } - ); + const block = createBlockWithFallback( { + blockName: 'core/test-block', + innerHTML: 'Bananas', + attrs: { fruit: 'Bananas' }, + } ); expect( block.name ).toBe( 'core/unknown-block' ); expect( block.attributes.content ).toContain( 'wp:test-block' ); } ); @@ -265,13 +268,18 @@ describe( 'block parser', () => { registerBlockType( 'core/unknown-block', unknownBlockSettings ); setUnknownTypeHandlerName( 'core/unknown-block' ); - const block = createBlockWithFallback( null, 'content' ); + const block = createBlockWithFallback( { + innerHTML: 'content', + } ); expect( block.name ).toEqual( 'core/unknown-block' ); expect( block.attributes ).toEqual( { content: '

content

' } ); } ); it( 'should not create a block if no unknown type handler', () => { - const block = createBlockWithFallback( 'core/test-block', '' ); + const block = createBlockWithFallback( { + blockName: 'core/test-block', + innerHTML: '', + } ); expect( block ).toBeUndefined(); } ); @@ -301,11 +309,11 @@ describe( 'block parser', () => { ], } ); - const block = createBlockWithFallback( - 'core/test-block', - 'Bananas', - { fruit: 'Bananas' } - ); + const block = createBlockWithFallback( { + blockName: 'core/test-block', + innerHTML: 'Bananas', + attrs: { fruit: 'Bananas' }, + } ); expect( block.name ).toEqual( 'core/test-block' ); expect( block.attributes ).toEqual( { fruit: 'Big Bananas' } ); expect( block.isValid ).toBe( true ); diff --git a/blocks/test/fixtures/core-embed__animoto.json b/blocks/test/fixtures/core-embed__animoto.json index 9ffca1aefa663c..973bfe9bd485fe 100644 --- a/blocks/test/fixtures/core-embed__animoto.json +++ b/blocks/test/fixtures/core-embed__animoto.json @@ -9,6 +9,7 @@ "Embedded content from animoto" ] }, + "innerBlocks": [], "originalContent": "
\n https://animoto.com/\n
Embedded content from animoto
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__cloudup.json b/blocks/test/fixtures/core-embed__cloudup.json index 5b093bf14ab046..111dc5d58921d3 100644 --- a/blocks/test/fixtures/core-embed__cloudup.json +++ b/blocks/test/fixtures/core-embed__cloudup.json @@ -9,6 +9,7 @@ "Embedded content from cloudup" ] }, + "innerBlocks": [], "originalContent": "
\n https://cloudup.com/\n
Embedded content from cloudup
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__collegehumor.json b/blocks/test/fixtures/core-embed__collegehumor.json index 7aa5dab8f4ecce..cccdbc2ba161c3 100644 --- a/blocks/test/fixtures/core-embed__collegehumor.json +++ b/blocks/test/fixtures/core-embed__collegehumor.json @@ -9,6 +9,7 @@ "Embedded content from collegehumor" ] }, + "innerBlocks": [], "originalContent": "
\n https://collegehumor.com/\n
Embedded content from collegehumor
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__dailymotion.json b/blocks/test/fixtures/core-embed__dailymotion.json index 644af7bf6e0288..d47c9e592c7f68 100644 --- a/blocks/test/fixtures/core-embed__dailymotion.json +++ b/blocks/test/fixtures/core-embed__dailymotion.json @@ -9,6 +9,7 @@ "Embedded content from dailymotion" ] }, + "innerBlocks": [], "originalContent": "
\n https://dailymotion.com/\n
Embedded content from dailymotion
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__facebook.json b/blocks/test/fixtures/core-embed__facebook.json index 23050a60bbe44b..15e57c9103b729 100644 --- a/blocks/test/fixtures/core-embed__facebook.json +++ b/blocks/test/fixtures/core-embed__facebook.json @@ -9,6 +9,7 @@ "Embedded content from facebook" ] }, + "innerBlocks": [], "originalContent": "
\n https://facebook.com/\n
Embedded content from facebook
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__flickr.json b/blocks/test/fixtures/core-embed__flickr.json index 98e4d671330dcc..3d202ece36f59e 100644 --- a/blocks/test/fixtures/core-embed__flickr.json +++ b/blocks/test/fixtures/core-embed__flickr.json @@ -9,6 +9,7 @@ "Embedded content from flickr" ] }, + "innerBlocks": [], "originalContent": "
\n https://flickr.com/\n
Embedded content from flickr
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__funnyordie.json b/blocks/test/fixtures/core-embed__funnyordie.json index 707a5eac89db56..8cef138ad02cd2 100644 --- a/blocks/test/fixtures/core-embed__funnyordie.json +++ b/blocks/test/fixtures/core-embed__funnyordie.json @@ -9,6 +9,7 @@ "Embedded content from funnyordie" ] }, + "innerBlocks": [], "originalContent": "
\n https://funnyordie.com/\n
Embedded content from funnyordie
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__hulu.json b/blocks/test/fixtures/core-embed__hulu.json index 8a3af46b91cf30..0fb42064add739 100644 --- a/blocks/test/fixtures/core-embed__hulu.json +++ b/blocks/test/fixtures/core-embed__hulu.json @@ -9,6 +9,7 @@ "Embedded content from hulu" ] }, + "innerBlocks": [], "originalContent": "
\n https://hulu.com/\n
Embedded content from hulu
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__imgur.json b/blocks/test/fixtures/core-embed__imgur.json index e37ea33dac37b0..b1d5ca44fc301d 100644 --- a/blocks/test/fixtures/core-embed__imgur.json +++ b/blocks/test/fixtures/core-embed__imgur.json @@ -9,6 +9,7 @@ "Embedded content from imgur" ] }, + "innerBlocks": [], "originalContent": "
\n https://imgur.com/\n
Embedded content from imgur
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__instagram.json b/blocks/test/fixtures/core-embed__instagram.json index 9756e0b3268e6e..ebaf856850fdb6 100644 --- a/blocks/test/fixtures/core-embed__instagram.json +++ b/blocks/test/fixtures/core-embed__instagram.json @@ -9,6 +9,7 @@ "Embedded content from instagram" ] }, + "innerBlocks": [], "originalContent": "
\n https://instagram.com/\n
Embedded content from instagram
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__issuu.json b/blocks/test/fixtures/core-embed__issuu.json index 8b37c67162c719..406b16d852d60d 100644 --- a/blocks/test/fixtures/core-embed__issuu.json +++ b/blocks/test/fixtures/core-embed__issuu.json @@ -9,6 +9,7 @@ "Embedded content from issuu" ] }, + "innerBlocks": [], "originalContent": "
\n https://issuu.com/\n
Embedded content from issuu
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__kickstarter.json b/blocks/test/fixtures/core-embed__kickstarter.json index 044df72425bee6..a31b8342ab3921 100644 --- a/blocks/test/fixtures/core-embed__kickstarter.json +++ b/blocks/test/fixtures/core-embed__kickstarter.json @@ -9,6 +9,7 @@ "Embedded content from kickstarter" ] }, + "innerBlocks": [], "originalContent": "
\n https://kickstarter.com/\n
Embedded content from kickstarter
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__meetup-com.json b/blocks/test/fixtures/core-embed__meetup-com.json index b5bfda1c2154d6..574330da51c367 100644 --- a/blocks/test/fixtures/core-embed__meetup-com.json +++ b/blocks/test/fixtures/core-embed__meetup-com.json @@ -9,6 +9,7 @@ "Embedded content from meetup-com" ] }, + "innerBlocks": [], "originalContent": "
\n https://meetup.com/\n
Embedded content from meetup-com
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__mixcloud.json b/blocks/test/fixtures/core-embed__mixcloud.json index afa52ccab22abb..c234164d20ee46 100644 --- a/blocks/test/fixtures/core-embed__mixcloud.json +++ b/blocks/test/fixtures/core-embed__mixcloud.json @@ -9,6 +9,7 @@ "Embedded content from mixcloud" ] }, + "innerBlocks": [], "originalContent": "
\n https://mixcloud.com/\n
Embedded content from mixcloud
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__photobucket.json b/blocks/test/fixtures/core-embed__photobucket.json index 4a9e74cd575f4a..76b33d9373e52b 100644 --- a/blocks/test/fixtures/core-embed__photobucket.json +++ b/blocks/test/fixtures/core-embed__photobucket.json @@ -9,6 +9,7 @@ "Embedded content from photobucket" ] }, + "innerBlocks": [], "originalContent": "
\n https://photobucket.com/\n
Embedded content from photobucket
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__polldaddy.json b/blocks/test/fixtures/core-embed__polldaddy.json index d5d6e007d7c8ea..f355eb0f8e41e9 100644 --- a/blocks/test/fixtures/core-embed__polldaddy.json +++ b/blocks/test/fixtures/core-embed__polldaddy.json @@ -9,6 +9,7 @@ "Embedded content from polldaddy" ] }, + "innerBlocks": [], "originalContent": "
\n https://polldaddy.com/\n
Embedded content from polldaddy
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__reddit.json b/blocks/test/fixtures/core-embed__reddit.json index 1b9ca92d2ffd39..516155b3c50d19 100644 --- a/blocks/test/fixtures/core-embed__reddit.json +++ b/blocks/test/fixtures/core-embed__reddit.json @@ -9,6 +9,7 @@ "Embedded content from reddit" ] }, + "innerBlocks": [], "originalContent": "
\n https://reddit.com/\n
Embedded content from reddit
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__reverbnation.json b/blocks/test/fixtures/core-embed__reverbnation.json index e7345026cb11e1..f76235a1120128 100644 --- a/blocks/test/fixtures/core-embed__reverbnation.json +++ b/blocks/test/fixtures/core-embed__reverbnation.json @@ -9,6 +9,7 @@ "Embedded content from reverbnation" ] }, + "innerBlocks": [], "originalContent": "
\n https://reverbnation.com/\n
Embedded content from reverbnation
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__screencast.json b/blocks/test/fixtures/core-embed__screencast.json index 4ff4aa953f0008..8fe863a7dcc65e 100644 --- a/blocks/test/fixtures/core-embed__screencast.json +++ b/blocks/test/fixtures/core-embed__screencast.json @@ -9,6 +9,7 @@ "Embedded content from screencast" ] }, + "innerBlocks": [], "originalContent": "
\n https://screencast.com/\n
Embedded content from screencast
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__scribd.json b/blocks/test/fixtures/core-embed__scribd.json index 6a49c821012e93..ce090cf5d7a516 100644 --- a/blocks/test/fixtures/core-embed__scribd.json +++ b/blocks/test/fixtures/core-embed__scribd.json @@ -9,6 +9,7 @@ "Embedded content from scribd" ] }, + "innerBlocks": [], "originalContent": "
\n https://scribd.com/\n
Embedded content from scribd
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__slideshare.json b/blocks/test/fixtures/core-embed__slideshare.json index 05e785b0dbaa32..097f06411708fb 100644 --- a/blocks/test/fixtures/core-embed__slideshare.json +++ b/blocks/test/fixtures/core-embed__slideshare.json @@ -9,6 +9,7 @@ "Embedded content from slideshare" ] }, + "innerBlocks": [], "originalContent": "
\n https://slideshare.com/\n
Embedded content from slideshare
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__smugmug.json b/blocks/test/fixtures/core-embed__smugmug.json index a7d05bbffe4011..72d3442ae92100 100644 --- a/blocks/test/fixtures/core-embed__smugmug.json +++ b/blocks/test/fixtures/core-embed__smugmug.json @@ -9,6 +9,7 @@ "Embedded content from smugmug" ] }, + "innerBlocks": [], "originalContent": "
\n https://smugmug.com/\n
Embedded content from smugmug
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__soundcloud.json b/blocks/test/fixtures/core-embed__soundcloud.json index 7b2ab832954832..1fdfe140b3ffe3 100644 --- a/blocks/test/fixtures/core-embed__soundcloud.json +++ b/blocks/test/fixtures/core-embed__soundcloud.json @@ -9,6 +9,7 @@ "Embedded content from soundcloud" ] }, + "innerBlocks": [], "originalContent": "
\n https://soundcloud.com/\n
Embedded content from soundcloud
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__speaker.json b/blocks/test/fixtures/core-embed__speaker.json index 27b7dcc291debc..1998640c395835 100644 --- a/blocks/test/fixtures/core-embed__speaker.json +++ b/blocks/test/fixtures/core-embed__speaker.json @@ -9,6 +9,7 @@ "Embedded content from speaker" ] }, + "innerBlocks": [], "originalContent": "
\n https://speaker.com/\n
Embedded content from speaker
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__spotify.json b/blocks/test/fixtures/core-embed__spotify.json index f769e12a5b1cc4..ec59ab05cdfec2 100644 --- a/blocks/test/fixtures/core-embed__spotify.json +++ b/blocks/test/fixtures/core-embed__spotify.json @@ -9,6 +9,7 @@ "Embedded content from spotify" ] }, + "innerBlocks": [], "originalContent": "
\n https://spotify.com/\n
Embedded content from spotify
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__ted.json b/blocks/test/fixtures/core-embed__ted.json index 8160876659851f..306e48d7b9484c 100644 --- a/blocks/test/fixtures/core-embed__ted.json +++ b/blocks/test/fixtures/core-embed__ted.json @@ -9,6 +9,7 @@ "Embedded content from ted" ] }, + "innerBlocks": [], "originalContent": "
\n https://ted.com/\n
Embedded content from ted
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__tumblr.json b/blocks/test/fixtures/core-embed__tumblr.json index 7b69e5746c6ecf..7ff6190c971519 100644 --- a/blocks/test/fixtures/core-embed__tumblr.json +++ b/blocks/test/fixtures/core-embed__tumblr.json @@ -9,6 +9,7 @@ "Embedded content from tumblr" ] }, + "innerBlocks": [], "originalContent": "
\n https://tumblr.com/\n
Embedded content from tumblr
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__twitter.json b/blocks/test/fixtures/core-embed__twitter.json index 85953b791641ff..458d69398354fb 100644 --- a/blocks/test/fixtures/core-embed__twitter.json +++ b/blocks/test/fixtures/core-embed__twitter.json @@ -9,6 +9,7 @@ "We are Automattic" ] }, + "innerBlocks": [], "originalContent": "
\n https://twitter.com/automattic\n
We are Automattic
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__videopress.json b/blocks/test/fixtures/core-embed__videopress.json index 2547accbabf84c..81f8b7bed2dddd 100644 --- a/blocks/test/fixtures/core-embed__videopress.json +++ b/blocks/test/fixtures/core-embed__videopress.json @@ -9,6 +9,7 @@ "Embedded content from videopress" ] }, + "innerBlocks": [], "originalContent": "
\n https://videopress.com/\n
Embedded content from videopress
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__vimeo.json b/blocks/test/fixtures/core-embed__vimeo.json index 459909bfb9afbf..930291ae2d150f 100644 --- a/blocks/test/fixtures/core-embed__vimeo.json +++ b/blocks/test/fixtures/core-embed__vimeo.json @@ -9,6 +9,7 @@ "Embedded content from vimeo" ] }, + "innerBlocks": [], "originalContent": "
\n https://vimeo.com/\n
Embedded content from vimeo
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__wordpress-tv.json b/blocks/test/fixtures/core-embed__wordpress-tv.json index 74bd05f431c655..9e55cc69e9fffb 100644 --- a/blocks/test/fixtures/core-embed__wordpress-tv.json +++ b/blocks/test/fixtures/core-embed__wordpress-tv.json @@ -9,6 +9,7 @@ "Embedded content from wordpress-tv" ] }, + "innerBlocks": [], "originalContent": "
\n https://wordpress.tv/\n
Embedded content from wordpress-tv
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__wordpress.json b/blocks/test/fixtures/core-embed__wordpress.json index 45dd54a275f308..9386dc743d3cf0 100644 --- a/blocks/test/fixtures/core-embed__wordpress.json +++ b/blocks/test/fixtures/core-embed__wordpress.json @@ -9,6 +9,7 @@ "Embedded content from WordPress" ] }, + "innerBlocks": [], "originalContent": "
\n https://wordpress.com/\n
Embedded content from WordPress
\n
" } ] diff --git a/blocks/test/fixtures/core-embed__youtube.json b/blocks/test/fixtures/core-embed__youtube.json index f4f42cb78de857..73e380d4dfa272 100644 --- a/blocks/test/fixtures/core-embed__youtube.json +++ b/blocks/test/fixtures/core-embed__youtube.json @@ -9,6 +9,7 @@ "Embedded content from youtube" ] }, + "innerBlocks": [], "originalContent": "
\n https://youtube.com/\n
Embedded content from youtube
\n
" } ] diff --git a/blocks/test/fixtures/core__4-invalid-starting-letter.json b/blocks/test/fixtures/core__4-invalid-starting-letter.json index 5515c7e9eade7d..792d06fd6ce239 100644 --- a/blocks/test/fixtures/core__4-invalid-starting-letter.json +++ b/blocks/test/fixtures/core__4-invalid-starting-letter.json @@ -6,6 +6,7 @@ "attributes": { "content": "

" }, + "innerBlocks": [], "originalContent": "

" } ] diff --git a/blocks/test/fixtures/core__audio.json b/blocks/test/fixtures/core__audio.json index 99b43e37d313d4..b1ff7c032168e7 100644 --- a/blocks/test/fixtures/core__audio.json +++ b/blocks/test/fixtures/core__audio.json @@ -8,6 +8,7 @@ "align": "right", "caption": [] }, + "innerBlocks": [], "originalContent": "
\n \n
" } ] diff --git a/blocks/test/fixtures/core__block.json b/blocks/test/fixtures/core__block.json index 46433ea6c4ee44..02490abb1936ae 100644 --- a/blocks/test/fixtures/core__block.json +++ b/blocks/test/fixtures/core__block.json @@ -6,6 +6,7 @@ "attributes": { "ref": 123 }, + "innerBlocks": [], "originalContent": "" } ] diff --git a/blocks/test/fixtures/core__button__center.json b/blocks/test/fixtures/core__button__center.json index 0ef63ab8671605..5965af4df12e8c 100644 --- a/blocks/test/fixtures/core__button__center.json +++ b/blocks/test/fixtures/core__button__center.json @@ -10,6 +10,7 @@ ], "align": "center" }, + "innerBlocks": [], "originalContent": "" } ] diff --git a/blocks/test/fixtures/core__categories.json b/blocks/test/fixtures/core__categories.json index 0c72a917e9e5e9..96f2240e6b7889 100644 --- a/blocks/test/fixtures/core__categories.json +++ b/blocks/test/fixtures/core__categories.json @@ -8,6 +8,7 @@ "displayAsDropdown": false, "showHierarchy": false }, + "innerBlocks": [], "originalContent": "" } ] diff --git a/blocks/test/fixtures/core__code.json b/blocks/test/fixtures/core__code.json index eabc18c2a24ece..01009fbb640422 100644 --- a/blocks/test/fixtures/core__code.json +++ b/blocks/test/fixtures/core__code.json @@ -6,6 +6,7 @@ "attributes": { "content": "export default function MyButton() {\n\treturn ;\n}" }, + "innerBlocks": [], "originalContent": "
export default function MyButton() {\n\treturn <Button>Click Me!</Button>;\n}
" } ] diff --git a/blocks/test/fixtures/core__cover-image.json b/blocks/test/fixtures/core__cover-image.json index 8fa8eccc0308b2..f001fe32e4ef43 100644 --- a/blocks/test/fixtures/core__cover-image.json +++ b/blocks/test/fixtures/core__cover-image.json @@ -12,6 +12,7 @@ "hasParallax": false, "dimRatio": 40 }, + "innerBlocks": [], "originalContent": "
\n

Guten Berg!

\n
" } ] diff --git a/blocks/test/fixtures/core__embed.json b/blocks/test/fixtures/core__embed.json index 7f105a115092c2..69b11621eaac16 100644 --- a/blocks/test/fixtures/core__embed.json +++ b/blocks/test/fixtures/core__embed.json @@ -9,6 +9,7 @@ "Embedded content from an example URL" ] }, + "innerBlocks": [], "originalContent": "
\n https://example.com/\n
Embedded content from an example URL
\n
" } ] diff --git a/blocks/test/fixtures/core__freeform.json b/blocks/test/fixtures/core__freeform.json index cdeb5e8293fa29..e71a19e0decaea 100644 --- a/blocks/test/fixtures/core__freeform.json +++ b/blocks/test/fixtures/core__freeform.json @@ -6,6 +6,7 @@ "attributes": { "content": "

Testing freeform block with some\n

\n\tHTML content\n
" }, + "innerBlocks": [], "originalContent": "

Testing freeform block with some\n

\n\tHTML content\n
" } ] diff --git a/blocks/test/fixtures/core__freeform__undelimited.json b/blocks/test/fixtures/core__freeform__undelimited.json index cdeb5e8293fa29..e71a19e0decaea 100644 --- a/blocks/test/fixtures/core__freeform__undelimited.json +++ b/blocks/test/fixtures/core__freeform__undelimited.json @@ -6,6 +6,7 @@ "attributes": { "content": "

Testing freeform block with some\n

\n\tHTML content\n
" }, + "innerBlocks": [], "originalContent": "

Testing freeform block with some\n

\n\tHTML content\n
" } ] diff --git a/blocks/test/fixtures/core__gallery.json b/blocks/test/fixtures/core__gallery.json index 1f1f20b45b79eb..c8aebc9301e4b8 100644 --- a/blocks/test/fixtures/core__gallery.json +++ b/blocks/test/fixtures/core__gallery.json @@ -20,6 +20,7 @@ "imageCrop": true, "linkTo": "none" }, + "innerBlocks": [], "originalContent": "" } ] diff --git a/blocks/test/fixtures/core__gallery__columns.json b/blocks/test/fixtures/core__gallery__columns.json index c6fbbd5ef0cbe7..41614c3387ecb6 100644 --- a/blocks/test/fixtures/core__gallery__columns.json +++ b/blocks/test/fixtures/core__gallery__columns.json @@ -21,6 +21,7 @@ "imageCrop": true, "linkTo": "none" }, + "innerBlocks": [], "originalContent": "" } ] diff --git a/blocks/test/fixtures/core__heading__h2-em.json b/blocks/test/fixtures/core__heading__h2-em.json index 39674931b4429a..213be79cfa972b 100644 --- a/blocks/test/fixtures/core__heading__h2-em.json +++ b/blocks/test/fixtures/core__heading__h2-em.json @@ -14,6 +14,7 @@ ], "nodeName": "H2" }, + "innerBlocks": [], "originalContent": "

The Inserter Tool

" } ] diff --git a/blocks/test/fixtures/core__heading__h2.json b/blocks/test/fixtures/core__heading__h2.json index ad97334855f74f..f07a75b91f5c6f 100644 --- a/blocks/test/fixtures/core__heading__h2.json +++ b/blocks/test/fixtures/core__heading__h2.json @@ -9,6 +9,7 @@ ], "nodeName": "H2" }, + "innerBlocks": [], "originalContent": "

A picture is worth a thousand words, or so the saying goes

" } ] diff --git a/blocks/test/fixtures/core__html.json b/blocks/test/fixtures/core__html.json index d96020b083d2e2..1ce1fdf0b91800 100644 --- a/blocks/test/fixtures/core__html.json +++ b/blocks/test/fixtures/core__html.json @@ -6,6 +6,7 @@ "attributes": { "content": "

Some HTML code

\nThis text will scroll from right to left" }, + "innerBlocks": [], "originalContent": "

Some HTML code

\nThis text will scroll from right to left" } ] diff --git a/blocks/test/fixtures/core__image.json b/blocks/test/fixtures/core__image.json index 71d725f76d9106..2ac6b609a3a8cb 100644 --- a/blocks/test/fixtures/core__image.json +++ b/blocks/test/fixtures/core__image.json @@ -8,6 +8,7 @@ "alt": "", "caption": [] }, + "innerBlocks": [], "originalContent": "
\"\"
" } ] diff --git a/blocks/test/fixtures/core__image__center-caption.json b/blocks/test/fixtures/core__image__center-caption.json index 1b6164a90508aa..22ccf40cd5655c 100644 --- a/blocks/test/fixtures/core__image__center-caption.json +++ b/blocks/test/fixtures/core__image__center-caption.json @@ -11,6 +11,7 @@ ], "align": "center" }, + "innerBlocks": [], "originalContent": "
\"\"
Give it a try. Press the "really wide" button on the image toolbar.
" } ] diff --git a/blocks/test/fixtures/core__invalid-Capitals.json b/blocks/test/fixtures/core__invalid-Capitals.json index 8a320995a22240..a5711c158ceafd 100644 --- a/blocks/test/fixtures/core__invalid-Capitals.json +++ b/blocks/test/fixtures/core__invalid-Capitals.json @@ -6,6 +6,7 @@ "attributes": { "content": "

" }, + "innerBlocks": [], "originalContent": "

" } ] diff --git a/blocks/test/fixtures/core__invalid-special.json b/blocks/test/fixtures/core__invalid-special.json index 08728b7a4d8e7c..f950a3e0272f7a 100644 --- a/blocks/test/fixtures/core__invalid-special.json +++ b/blocks/test/fixtures/core__invalid-special.json @@ -6,6 +6,7 @@ "attributes": { "content": "

" }, + "innerBlocks": [], "originalContent": "

" } ] diff --git a/blocks/test/fixtures/core__latest-posts.json b/blocks/test/fixtures/core__latest-posts.json index 0ef356b6788d5f..bfebd9ec16455c 100644 --- a/blocks/test/fixtures/core__latest-posts.json +++ b/blocks/test/fixtures/core__latest-posts.json @@ -12,6 +12,7 @@ "order": "desc", "orderBy": "date" }, + "innerBlocks": [], "originalContent": "" } ] diff --git a/blocks/test/fixtures/core__latest-posts__displayPostDate.json b/blocks/test/fixtures/core__latest-posts__displayPostDate.json index c0e38f52cf8285..82f20c7a306cca 100644 --- a/blocks/test/fixtures/core__latest-posts__displayPostDate.json +++ b/blocks/test/fixtures/core__latest-posts__displayPostDate.json @@ -12,6 +12,7 @@ "order": "desc", "orderBy": "date" }, + "innerBlocks": [], "originalContent": "" } ] diff --git a/blocks/test/fixtures/core__list__ul.json b/blocks/test/fixtures/core__list__ul.json index 15bc029cf8de16..4f86b13d9acd42 100644 --- a/blocks/test/fixtures/core__list__ul.json +++ b/blocks/test/fixtures/core__list__ul.json @@ -39,6 +39,7 @@ } ] }, + "innerBlocks": [], "originalContent": "" } ] diff --git a/blocks/test/fixtures/core__more.json b/blocks/test/fixtures/core__more.json index 14cf7688d9edda..4ba7ca6254470c 100644 --- a/blocks/test/fixtures/core__more.json +++ b/blocks/test/fixtures/core__more.json @@ -6,6 +6,7 @@ "attributes": { "noTeaser": false }, + "innerBlocks": [], "originalContent": "" } ] diff --git a/blocks/test/fixtures/core__more__custom-text-teaser.json b/blocks/test/fixtures/core__more__custom-text-teaser.json index 1632474c51fff5..c37da83a834bd7 100644 --- a/blocks/test/fixtures/core__more__custom-text-teaser.json +++ b/blocks/test/fixtures/core__more__custom-text-teaser.json @@ -7,6 +7,7 @@ "customText": "Continue Reading", "noTeaser": true }, + "innerBlocks": [], "originalContent": "" } ] diff --git a/blocks/test/fixtures/core__paragraph__align-right.json b/blocks/test/fixtures/core__paragraph__align-right.json index ee501720801afb..731657d06705f8 100644 --- a/blocks/test/fixtures/core__paragraph__align-right.json +++ b/blocks/test/fixtures/core__paragraph__align-right.json @@ -10,6 +10,7 @@ "align": "right", "dropCap": false }, + "innerBlocks": [], "originalContent": "

... like this one, which is separate from the above and right aligned.

" } ] diff --git a/blocks/test/fixtures/core__preformatted.json b/blocks/test/fixtures/core__preformatted.json index 37ca66e8c95ad5..d8e03462e4c687 100644 --- a/blocks/test/fixtures/core__preformatted.json +++ b/blocks/test/fixtures/core__preformatted.json @@ -17,6 +17,7 @@ "And more!" ] }, + "innerBlocks": [], "originalContent": "
Some preformatted text...
And more!
" } ] diff --git a/blocks/test/fixtures/core__pullquote.json b/blocks/test/fixtures/core__pullquote.json index 5ebe0b5564fb25..e044ae447f65f8 100644 --- a/blocks/test/fixtures/core__pullquote.json +++ b/blocks/test/fixtures/core__pullquote.json @@ -23,6 +23,7 @@ ], "align": "none" }, + "innerBlocks": [], "originalContent": "
\n

Testing pullquote block...

...with a caption\n
" } ] diff --git a/blocks/test/fixtures/core__pullquote__multi-paragraph.json b/blocks/test/fixtures/core__pullquote__multi-paragraph.json index 2105a8afb0aecc..7d81b414d2811e 100644 --- a/blocks/test/fixtures/core__pullquote__multi-paragraph.json +++ b/blocks/test/fixtures/core__pullquote__multi-paragraph.json @@ -47,6 +47,7 @@ ], "align": "none" }, + "innerBlocks": [], "originalContent": "
\n

Paragraph one

\n

Paragraph two

\n by whomever\n
" } ] diff --git a/blocks/test/fixtures/core__quote__style-1.json b/blocks/test/fixtures/core__quote__style-1.json index bea152f89f32c6..1a1f57668d6bba 100644 --- a/blocks/test/fixtures/core__quote__style-1.json +++ b/blocks/test/fixtures/core__quote__style-1.json @@ -23,6 +23,7 @@ ], "style": 1 }, + "innerBlocks": [], "originalContent": "

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
" } ] diff --git a/blocks/test/fixtures/core__quote__style-2.json b/blocks/test/fixtures/core__quote__style-2.json index ea48f03aef42fe..2462f8aa0a8fc4 100644 --- a/blocks/test/fixtures/core__quote__style-2.json +++ b/blocks/test/fixtures/core__quote__style-2.json @@ -23,6 +23,7 @@ ], "style": 2 }, + "innerBlocks": [], "originalContent": "

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

Maya Angelou
" } ] diff --git a/blocks/test/fixtures/core__separator.json b/blocks/test/fixtures/core__separator.json index 43119546033803..7060deba1c063c 100644 --- a/blocks/test/fixtures/core__separator.json +++ b/blocks/test/fixtures/core__separator.json @@ -4,6 +4,7 @@ "name": "core/separator", "isValid": true, "attributes": {}, + "innerBlocks": [], "originalContent": "
" } ] diff --git a/blocks/test/fixtures/core__shortcode.json b/blocks/test/fixtures/core__shortcode.json index ded5113b30e699..37471b9d68f37d 100644 --- a/blocks/test/fixtures/core__shortcode.json +++ b/blocks/test/fixtures/core__shortcode.json @@ -6,6 +6,7 @@ "attributes": { "text": "[gallery ids=\"238,338\"]" }, + "innerBlocks": [], "originalContent": "[gallery ids=\"238,338\"]" } ] diff --git a/blocks/test/fixtures/core__subhead.json b/blocks/test/fixtures/core__subhead.json index 20e5037001563c..c2373ee2781c04 100644 --- a/blocks/test/fixtures/core__subhead.json +++ b/blocks/test/fixtures/core__subhead.json @@ -13,6 +13,7 @@ "." ] }, + "innerBlocks": [], "originalContent": "

This is a subhead.

" } ] diff --git a/blocks/test/fixtures/core__table.json b/blocks/test/fixtures/core__table.json index 2ecdee94dd749c..532566a4908cd9 100644 --- a/blocks/test/fixtures/core__table.json +++ b/blocks/test/fixtures/core__table.json @@ -196,6 +196,7 @@ } ] }, + "innerBlocks": [], "originalContent": "
VersionMusicianDate
.70No musician chosen.May 27, 2003
1.0Miles DavisJanuary 3, 2004
Lots of versions skipped, see the full list
4.4Clifford BrownDecember 8, 2015
4.5Coleman HawkinsApril 12, 2016
4.6Pepper AdamsAugust 16, 2016
4.7Sarah VaughanDecember 6, 2016
" } ] diff --git a/blocks/test/fixtures/core__text-columns.json b/blocks/test/fixtures/core__text-columns.json index f0caa26a89003a..44d2a980a68bdb 100644 --- a/blocks/test/fixtures/core__text-columns.json +++ b/blocks/test/fixtures/core__text-columns.json @@ -19,6 +19,7 @@ "columns": 2, "width": "center" }, + "innerBlocks": [], "originalContent": "
\n
\n

One

\n
\n
\n

Two

\n
\n
" } ] diff --git a/blocks/test/fixtures/core__text__converts-to-paragraph.json b/blocks/test/fixtures/core__text__converts-to-paragraph.json index 0efed063a93be2..f9ff727423d80a 100644 --- a/blocks/test/fixtures/core__text__converts-to-paragraph.json +++ b/blocks/test/fixtures/core__text__converts-to-paragraph.json @@ -14,6 +14,7 @@ ], "dropCap": false }, + "innerBlocks": [], "originalContent": "

This is an old-style text block. Changed to paragraph in #2135.

" } ] diff --git a/blocks/test/fixtures/core__verse.json b/blocks/test/fixtures/core__verse.json index 0baa254be043a9..0f784623281357 100644 --- a/blocks/test/fixtures/core__verse.json +++ b/blocks/test/fixtures/core__verse.json @@ -17,6 +17,7 @@ "And more!" ] }, + "innerBlocks": [], "originalContent": "
A verse
And more!
" } ] diff --git a/blocks/test/fixtures/core__video.json b/blocks/test/fixtures/core__video.json index fd7b9da559965e..6905f4d7b9ccba 100644 --- a/blocks/test/fixtures/core__video.json +++ b/blocks/test/fixtures/core__video.json @@ -7,6 +7,7 @@ "src": "https://awesome-fake.video/file.mp4", "caption": [] }, + "innerBlocks": [], "originalContent": "
" } ] diff --git a/blocks/test/full-content.js b/blocks/test/full-content.js index be355e6d242a37..a1fd277048fa70 100644 --- a/blocks/test/full-content.js +++ b/blocks/test/full-content.js @@ -77,13 +77,19 @@ function normalizeParsedBlocks( blocks ) { // Clone and remove React-instance-specific stuff; also, attribute // values that equal `undefined` will be removed block = JSON.parse( JSON.stringify( block ) ); + // Change unique UIDs to a predictable value block.uid = '_uid_' + index; + // Walk each attribute and get a more concise representation of any // React elements for ( const k in block.attributes ) { block.attributes[ k ] = normalizeReactTree( block.attributes[ k ] ); } + + // Recurse to normalize inner blocks + block.innerBlocks = normalizeParsedBlocks( block.innerBlocks ); + return block; } ); } From 86fd2bc156a30c0baddf801341f7270d2cf4da45 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 30 Nov 2017 16:18:44 -0500 Subject: [PATCH 03/13] Block List: Enable block edit to revise inner blocks --- editor/components/block-list/block.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/editor/components/block-list/block.js b/editor/components/block-list/block.js index 973ca34de52def..c9e1f472b21c8c 100644 --- a/editor/components/block-list/block.js +++ b/editor/components/block-list/block.js @@ -41,6 +41,7 @@ import { focusBlock, insertBlocks, mergeBlocks, + updateBlock, removeBlock, replaceBlocks, selectBlock, @@ -467,6 +468,8 @@ export class BlockListBlock extends Component { onReplace={ isLocked ? undefined : onReplace } setFocus={ partial( onFocus, block.uid ) } mergeBlocks={ isLocked ? undefined : this.mergeBlocks } + innerBlocks={ block.innerBlocks } + setInnerBlocks={ isLocked ? undefined : this.props.setInnerBlocks } id={ block.uid } isSelectionEnabled={ this.props.isSelectionEnabled } toggleSelection={ this.props.toggleSelection } @@ -573,6 +576,10 @@ const mapDispatchToProps = ( dispatch, ownProps ) => ( { toggleSelection( selectionEnabled ) { dispatch( toggleSelection( selectionEnabled ) ); }, + + setInnerBlocks( innerBlocks ) { + dispatch( updateBlock( ownProps.uid, { innerBlocks } ) ); + }, } ); BlockListBlock.className = 'editor-block-list__block-edit'; From 2f556b376d43072608d462fb762ef08db76214ff Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 30 Nov 2017 16:19:01 -0500 Subject: [PATCH 04/13] Blocks: Add InnerBlocks component for nested editor --- .eslintrc.json | 3 + blocks/index.js | 1 + blocks/inner-blocks/index.js | 90 +++++++++++++++++++++++++++++ edit-post/index.js | 6 ++ editor/components/provider/index.js | 4 +- editor/store/index.js | 42 ++++++++++++-- 6 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 blocks/inner-blocks/index.js diff --git a/.eslintrc.json b/.eslintrc.json index f529be3eb28c26..7e5974eceee0a4 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -165,6 +165,9 @@ "react/jsx-indent": [ "error", "tab" ], "react/jsx-indent-props": [ "error", "tab" ], "react/jsx-key": "error", + "react/jsx-no-undef": [ "error", { + "allowGlobals": true + } ], "react/jsx-tag-spacing": "error", "react/no-children-prop": "off", "react/no-find-dom-node": "warn", diff --git a/blocks/index.js b/blocks/index.js index 4ef35d2d54ab47..6d30e999887963 100644 --- a/blocks/index.js +++ b/blocks/index.js @@ -22,6 +22,7 @@ export { default as BlockEdit } from './block-edit'; export { default as BlockIcon } from './block-icon'; export { default as ColorPalette } from './color-palette'; export { default as Editable } from './rich-text/editable'; +export { default as InnerBlocks } from './inner-blocks'; export { default as InspectorControls } from './inspector-controls'; export { default as PlainText } from './plain-text'; export { default as MediaUpload } from './media-upload'; diff --git a/blocks/inner-blocks/index.js b/blocks/inner-blocks/index.js new file mode 100644 index 00000000000000..d5af5157c83722 --- /dev/null +++ b/blocks/inner-blocks/index.js @@ -0,0 +1,90 @@ +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; +import { withContext } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { serialize } from '../api'; + +class InnerBlocks extends Component { + constructor() { + super( ...arguments ); + + const store = wp.editor.createStore(); + store.dispatch( { + type: 'RESET_BLOCKS', + blocks: this.props.value, + } ); + + this.store = store; + this.editor = store.getState().editor; + + this.maybeChange = this.maybeChange.bind( this ); + + // For child store, monitor changes to emit back up to parent store + store.subscribe( this.maybeChange ); + } + + shouldComponentUpdate() { + // TODO: Investigate if we can avoid disabling render reconciliation. + // Needed currently because render passes a new settings reference by + // presence of types (via shallow clone). Maybe assign in constructor. + return false; + } + + maybeChange() { + const state = this.store.getState(); + const { editor } = state; + if ( editor === this.editor ) { + return; + } + + const blocks = wp.editor.selectors.getBlocks( state ); + this.props.onChange( blocks ); + this.editor = editor; + } + + render() { + const { types } = this.props; + + let { settings } = this.props; + if ( types ) { + settings = { + ...settings, + blockTypes: types, + }; + } + + // TODO: We should not be referencing editor on the global object, but + // this is a circular dependency between editor and blocks. + + // TODO: The DIV wrapper inside EditorProvider should be eliminated, + // and exists because provider supports only a single child. + + return ( + +
+ + +
+
+ ); + } +} + +InnerBlocks = withContext( 'editor' )()( InnerBlocks ); + +InnerBlocks.Content = ( { value } ) => { + if ( ! value ) { + return null; + } + + const html = serialize( value ); + + return ; +}; + +export default InnerBlocks; diff --git a/edit-post/index.js b/edit-post/index.js index f1c1f132415e8e..fa8dc3beb2bb4e 100644 --- a/edit-post/index.js +++ b/edit-post/index.js @@ -18,8 +18,14 @@ import { SlotFillProvider } from '@wordpress/components'; */ import './assets/stylesheets/main.scss'; import Layout from './components/layout'; +import * as selectors from './store/selectors'; +import * as actions from './store/actions'; import store from './store'; +export { createStore } from './store'; +export { selectors }; +export { actions }; + // Configure moment globally moment.locale( dateSettings.l10n.locale ); if ( dateSettings.timezone.string ) { diff --git a/editor/components/provider/index.js b/editor/components/provider/index.js index 543b4115aa6cec..656b82e0efddd2 100644 --- a/editor/components/provider/index.js +++ b/editor/components/provider/index.js @@ -54,7 +54,7 @@ class EditorProvider extends Component { constructor( props ) { super( ...arguments ); - this.store = store; + this.store = props.store || store; this.initializeMetaBoxes = this.initializeMetaBoxes.bind( this ); this.settings = { @@ -63,7 +63,7 @@ class EditorProvider extends Component { }; // Assume that we don't need to initialize in the case of an error recovery. - if ( ! props.recovery ) { + if ( ! props.recovery && props.post ) { this.store.dispatch( setupEditor( props.post, this.settings ) ); } } diff --git a/editor/store/index.js b/editor/store/index.js index fd9bbd9f226542..eca962b4aefffb 100644 --- a/editor/store/index.js +++ b/editor/store/index.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import { createStore as createReduxStore } from 'redux'; + /** * WordPress Dependencies */ @@ -7,7 +12,9 @@ import { registerReducer, registerSelectors, withRehydratation, loadAndPersist } * Internal dependencies */ import reducer from './reducer'; +import enhanceWithBrowserSize from './mobile'; import applyMiddlewares from './middlewares'; +import { BREAK_MEDIUM } from './constants'; import { getCurrentPostType, getEditedPostContent, @@ -22,10 +29,35 @@ import { const STORAGE_KEY = `GUTENBERG_PREFERENCES_${ window.userSettings.uid }`; const MODULE_KEY = 'core/editor'; -const store = applyMiddlewares( - registerReducer( MODULE_KEY, withRehydratation( reducer, 'preferences', STORAGE_KEY ) ) -); -loadAndPersist( store, reducer, 'preferences', STORAGE_KEY ); +/** + * Creates a Redux store for editor state, enhanced with middlewares, persistence, + * and browser size observer. + * + * @return {Object} Redux store + */ +export function createStore() { + const store = applyMiddlewares( createReduxStore( withRehydratation( reducer, 'preferences', STORAGE_KEY ) ) ); + loadAndPersist( store, reducer, 'preferences', STORAGE_KEY ); + enhanceWithBrowserSize( store, BREAK_MEDIUM ); + + return store; +} + +/** + * Registers an editor state store, enhanced with middlewares, persistence, and + * browser size observer. + * + * @return {Object} Registered data store + */ +export function createRegisteredStore() { + const store = applyMiddlewares( + registerReducer( 'core/editor', withRehydratation( reducer, 'preferences', STORAGE_KEY ) ) + ); + loadAndPersist( store, reducer, 'preferences', STORAGE_KEY ); + enhanceWithBrowserSize( store, BREAK_MEDIUM ); + + return store; +} registerSelectors( MODULE_KEY, { getCurrentPostType, @@ -35,4 +67,4 @@ registerSelectors( MODULE_KEY, { getCurrentPostSlug, } ); -export default store; +export default createRegisteredStore(); From 38165b9e389e536dc78b39f417f2f120dcb88aa1 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 30 Nov 2017 16:19:09 -0500 Subject: [PATCH 05/13] Blocks: Add Columns block --- blocks/library/columns/editor.scss | 9 ++ blocks/library/columns/index.js | 136 ++++++++++++++++++ blocks/library/index.js | 2 + blocks/test/fixtures/core__columns.html | 11 ++ blocks/test/fixtures/core__columns.json | 42 ++++++ .../test/fixtures/core__columns.parsed.json | 30 ++++ .../fixtures/core__columns.serialized.html | 10 ++ .../core__pullquote__multi-paragraph.json | 2 +- 8 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 blocks/library/columns/editor.scss create mode 100644 blocks/library/columns/index.js create mode 100644 blocks/test/fixtures/core__columns.html create mode 100644 blocks/test/fixtures/core__columns.json create mode 100644 blocks/test/fixtures/core__columns.parsed.json create mode 100644 blocks/test/fixtures/core__columns.serialized.html diff --git a/blocks/library/columns/editor.scss b/blocks/library/columns/editor.scss new file mode 100644 index 00000000000000..cdf58342c74a3b --- /dev/null +++ b/blocks/library/columns/editor.scss @@ -0,0 +1,9 @@ +.wp-block-columns { + display: flex; + padding: $item-spacing 0; + + > div { + flex-basis: 100%; + flex-shrink: 1; + } +} diff --git a/blocks/library/columns/index.js b/blocks/library/columns/index.js new file mode 100644 index 00000000000000..ab1e011e3e3c2d --- /dev/null +++ b/blocks/library/columns/index.js @@ -0,0 +1,136 @@ +/** + * External dependencies + */ +import { repeat, identity } from 'lodash'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import './editor.scss'; +import RangeControl from '../../inspector-controls/range-control'; +import InnerBlocks from '../../inner-blocks'; +import InspectorControls from '../../inspector-controls'; + +function mapInnerBlocks( innerBlocks, columns, callback ) { + return columns.map( ( endOffset, index ) => { + const startOffset = columns[ index - 1 ] || 0; + return callback( innerBlocks.slice( startOffset, endOffset ), index ); + } ); +} + +export const name = 'core/columns'; + +export const settings = { + title: __( 'Columns' ), + + icon: 'columns', + + category: 'layout', + + attributes: { + columns: { + type: 'array', + default: [ 1, 2 ], + }, + }, + + description: __( 'A multi-column layout of content.' ), + + getEditWrapperProps() { + return { 'data-align': 'wide' }; + }, + + edit( { attributes, setAttributes, setInnerBlocks, innerBlocks, className, focus } ) { + const { columns } = attributes; + + // TODO: Refactor setInnerBlockFragment to be less awful, separated, tested. + + const setInnerBlockFragment = ( index ) => ( nextInnerBlocks ) => { + const fragmented = mapInnerBlocks( innerBlocks, columns, identity ); + const origNumInnerBlocks = fragmented[ index ].length; + fragmented.splice( index, 1, nextInnerBlocks ); + + let runningOffset = 0; + const nextColumns = []; + const allNextInnerBlocks = []; + fragmented.forEach( ( innerBlockSet, innerBlockSetIndex ) => { + runningOffset += innerBlockSet.length; + if ( innerBlockSetIndex > index ) { + runningOffset += nextInnerBlocks.length - origNumInnerBlocks; + } + + nextColumns.push( runningOffset ); + allNextInnerBlocks.push( ...innerBlockSet ); + } ); + + setAttributes( { columns: nextColumns } ); + setInnerBlocks( allNextInnerBlocks ); + }; + + const setNextColumnsCount = ( count ) => { + const nextColumns = count < columns.length ? + // Take first X of columns... + columns.slice( 0, count ) : + // ...or fill with new columns + [ ...columns, repeat( count - columns.length, () => [] ) ]; + + if ( count < columns.length ) { + // When trimming columns, reassign inner blocks to include only + // those up to the now-last column. + + // TODO: Ideally we'd simply flatten eliminated columns into + // the last available column, which is simply assigning the new + // last column to have inner blocks length as its final offset, + // but this is made difficult by the fact that we'd need to + // reset the block state of the rendered InnerBlocks. + const lastInnerBlockIndex = nextColumns[ nextColumns.length - 1 ]; + setInnerBlocks( innerBlocks.slice( 0, lastInnerBlockIndex ) ); + } + + setAttributes( { columns: nextColumns } ); + }; + + return [ + focus && ( + + + + ), +
+ { mapInnerBlocks( innerBlocks, columns, ( innerBlockSet, index ) => ( + + ) ) } +
, + ]; + }, + + save( { attributes, innerBlocks } ) { + const { columns } = attributes; + + return ( +
+ { mapInnerBlocks( innerBlocks, columns, ( innerBlockSet, index ) => ( + + ) ) } +
+ ); + }, +}; diff --git a/blocks/library/index.js b/blocks/library/index.js index 17e7534aa35cd1..b55e5b2025cf50 100644 --- a/blocks/library/index.js +++ b/blocks/library/index.js @@ -10,6 +10,7 @@ import * as audio from './audio'; import * as button from './button'; import * as categories from './categories'; import * as code from './code'; +import * as columns from './columns'; import * as coverImage from './cover-image'; import * as embed from './embed'; import * as freeform from './freeform'; @@ -59,6 +60,7 @@ export const registerCoreBlocks = () => { button, categories, code, + columns, coverImage, embed, ...embed.common, diff --git a/blocks/test/fixtures/core__columns.html b/blocks/test/fixtures/core__columns.html new file mode 100644 index 00000000000000..82fec967b1c493 --- /dev/null +++ b/blocks/test/fixtures/core__columns.html @@ -0,0 +1,11 @@ + +
+ +

Column One

+ + + +

Column Two

+ +
+ diff --git a/blocks/test/fixtures/core__columns.json b/blocks/test/fixtures/core__columns.json new file mode 100644 index 00000000000000..94745068beb5ce --- /dev/null +++ b/blocks/test/fixtures/core__columns.json @@ -0,0 +1,42 @@ +[ + { + "uid": "_uid_0", + "name": "core/columns", + "isValid": true, + "attributes": { + "columns": [ + 1, + 2 + ] + }, + "innerBlocks": [ + { + "uid": "_uid_0", + "name": "core/paragraph", + "isValid": true, + "attributes": { + "content": [ + "Column One" + ], + "dropCap": false + }, + "innerBlocks": [], + "originalContent": "

Column One

" + }, + { + "uid": "_uid_1", + "name": "core/paragraph", + "isValid": true, + "attributes": { + "content": [ + "Column Two" + ], + "dropCap": false + }, + "innerBlocks": [], + "originalContent": "

Column Two

" + } + ], + "originalContent": "
\n\t\n\n\t\n
" + } +] diff --git a/blocks/test/fixtures/core__columns.parsed.json b/blocks/test/fixtures/core__columns.parsed.json new file mode 100644 index 00000000000000..ef3d7daba8c557 --- /dev/null +++ b/blocks/test/fixtures/core__columns.parsed.json @@ -0,0 +1,30 @@ +[ + { + "blockName": "core/columns", + "attrs": { + "columns": [ + 1, + 2 + ] + }, + "innerBlocks": [ + { + "blockName": "core/paragraph", + "attrs": null, + "innerBlocks": [], + "innerHTML": "\n\t

Column One

\n\t" + }, + { + "blockName": "core/paragraph", + "attrs": null, + "innerBlocks": [], + "innerHTML": "\n\t

Column Two

\n\t" + } + ], + "innerHTML": "\n
\n\t\n\n\t\n
\n" + }, + { + "attrs": {}, + "innerHTML": "\n" + } +] diff --git a/blocks/test/fixtures/core__columns.serialized.html b/blocks/test/fixtures/core__columns.serialized.html new file mode 100644 index 00000000000000..66d88f197c3a22 --- /dev/null +++ b/blocks/test/fixtures/core__columns.serialized.html @@ -0,0 +1,10 @@ + +
+ +

Column One

+ + +

Column Two

+ +
+ diff --git a/blocks/test/fixtures/core__pullquote__multi-paragraph.json b/blocks/test/fixtures/core__pullquote__multi-paragraph.json index 7d81b414d2811e..95473fe0ac7e29 100644 --- a/blocks/test/fixtures/core__pullquote__multi-paragraph.json +++ b/blocks/test/fixtures/core__pullquote__multi-paragraph.json @@ -15,7 +15,7 @@ "Paragraph ", { "type": "strong", - "key": "_domReact67", + "key": "_domReact69", "ref": null, "props": { "children": "one" From 80d66e6d56088e9db3a05e338b465a0d47ceb594 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 12 Jan 2018 14:35:04 -0500 Subject: [PATCH 06/13] Revert "Editor: Move slot provider to top-level" This reverts commit 7b6adced07889fe8eaf2b769f7708b0fc58033aa. --- edit-post/index.js | 9 ++------- editor/components/provider/index.js | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/edit-post/index.js b/edit-post/index.js index fa8dc3beb2bb4e..6f3efe1007e9af 100644 --- a/edit-post/index.js +++ b/edit-post/index.js @@ -11,7 +11,6 @@ import { createProvider } from 'react-redux'; import { render, unmountComponentAtNode } from '@wordpress/element'; import { settings as dateSettings } from '@wordpress/date'; import { EditorProvider, ErrorBoundary } from '@wordpress/editor'; -import { SlotFillProvider } from '@wordpress/components'; /** * Internal dependencies @@ -70,9 +69,7 @@ export function reinitializeEditor( target, settings ) { - - - + , @@ -101,9 +98,7 @@ export function initializeEditor( id, post, settings ) { - - - + , diff --git a/editor/components/provider/index.js b/editor/components/provider/index.js index 656b82e0efddd2..b52f5684c753c2 100644 --- a/editor/components/provider/index.js +++ b/editor/components/provider/index.js @@ -10,7 +10,11 @@ import { flow, pick, noop } from 'lodash'; */ import { createElement, Component } from '@wordpress/element'; import { RichTextProvider } from '@wordpress/blocks'; -import { APIProvider, DropZoneProvider } from '@wordpress/components'; +import { + APIProvider, + DropZoneProvider, + SlotFillProvider, +} from '@wordpress/components'; /** * Internal Dependencies @@ -108,6 +112,15 @@ class EditorProvider extends Component { }, this.store.dispatch ), ], + // Slot / Fill provider: + // + // - context.getSlot + // - context.registerSlot + // - context.unregisterSlot + [ + SlotFillProvider, + ], + // APIProvider // // - context.getAPISchema From 06252a173ac6f432cb67b4c613ea05680c7d3d07 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 25 Jan 2018 16:09:53 +0000 Subject: [PATCH 07/13] Block List: Move BlockList to layout file Aiding in GitHub rebase history as explicit move --- editor/components/block-list/{index.js => layout.js} | 0 editor/components/index.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename editor/components/block-list/{index.js => layout.js} (100%) diff --git a/editor/components/block-list/index.js b/editor/components/block-list/layout.js similarity index 100% rename from editor/components/block-list/index.js rename to editor/components/block-list/layout.js diff --git a/editor/components/index.js b/editor/components/index.js index c13e32820817dc..252b8965b39ec3 100644 --- a/editor/components/index.js +++ b/editor/components/index.js @@ -54,7 +54,7 @@ export { default as WordCount } from './word-count'; // Content Related Components export { default as BlockInspector } from './block-inspector'; -export { default as BlockList } from './block-list'; +export { default as BlockList } from './block-list/layout'; export { default as BlockMover } from './block-mover'; export { default as BlockSelectionClearer } from './block-selection-clearer'; export { default as BlockSettingsMenu } from './block-settings-menu'; From 1dd6f6fcf0bdbe2c932ba3cc2eecb9614db103ce Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 19 Jan 2018 19:45:55 -0500 Subject: [PATCH 08/13] Framework: Add store and BlockList support for nested context --- .eslintrc.json | 3 - blocks/api/factory.js | 22 ++ blocks/api/index.js | 8 +- blocks/api/serializer.js | 16 +- blocks/api/test/factory.js | 94 ++++- blocks/api/test/parser.js | 4 +- blocks/api/test/registration.js | 27 ++ blocks/api/test/serializer.js | 71 +++- blocks/block-content-provider/README.md | 12 + blocks/block-content-provider/index.js | 50 +++ blocks/hooks/index.js | 1 + blocks/hooks/layout.js | 53 +++ blocks/hooks/test/layout.js | 45 +++ blocks/inner-blocks/README.md | 147 ++++++++ blocks/inner-blocks/index.js | 86 +---- blocks/library/columns/editor.scss | 9 - blocks/library/columns/index.js | 115 ++---- blocks/library/columns/style.scss | 16 + blocks/test/fixtures/core__columns.html | 21 +- blocks/test/fixtures/core__columns.json | 49 ++- .../test/fixtures/core__columns.parsed.json | 35 +- .../fixtures/core__columns.serialized.html | 21 +- blocks/test/fixtures/core__latest-posts.json | 1 - .../core__latest-posts__displayPostDate.json | 1 - .../core__pullquote__multi-paragraph.json | 2 +- .../components/modes/visual-editor/index.js | 2 - edit-post/index.js | 6 - editor/components/block-drop-zone/index.js | 35 +- editor/components/block-list/block.js | 150 ++++++-- .../block-list/ignore-nested-events.js | 78 ++++ editor/components/block-list/index.js | 62 ++++ .../components/block-list/insertion-point.js | 10 +- editor/components/block-list/layout.js | 47 ++- .../components/block-list/multi-controls.js | 3 +- editor/components/block-list/utils.js | 67 ++++ editor/components/block-mover/index.js | 57 +-- .../default-block-appender/index.js | 69 ++-- .../test/__snapshots__/index.js.snap | 52 ++- .../default-block-appender/test/index.js | 63 ++-- .../editor-global-keyboard-shortcuts/index.js | 4 +- editor/components/index.js | 2 +- editor/components/inserter/index.js | 16 +- editor/components/provider/index.js | 4 +- editor/components/writing-flow/index.js | 38 +- editor/store/actions.js | 43 ++- editor/store/effects.js | 12 +- editor/store/index.js | 42 +-- editor/store/reducer.js | 210 ++++++++--- editor/store/selectors.js | 247 ++++++++----- editor/store/test/actions.js | 12 +- editor/store/test/reducer.js | 215 ++++++++++- editor/store/test/selectors.js | 334 +++++++++++++----- 52 files changed, 2075 insertions(+), 714 deletions(-) create mode 100644 blocks/block-content-provider/README.md create mode 100644 blocks/block-content-provider/index.js create mode 100644 blocks/hooks/layout.js create mode 100644 blocks/hooks/test/layout.js create mode 100644 blocks/inner-blocks/README.md delete mode 100644 blocks/library/columns/editor.scss create mode 100644 blocks/library/columns/style.scss create mode 100644 editor/components/block-list/ignore-nested-events.js create mode 100644 editor/components/block-list/index.js create mode 100644 editor/components/block-list/utils.js diff --git a/.eslintrc.json b/.eslintrc.json index 7e5974eceee0a4..f529be3eb28c26 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -165,9 +165,6 @@ "react/jsx-indent": [ "error", "tab" ], "react/jsx-indent-props": [ "error", "tab" ], "react/jsx-key": "error", - "react/jsx-no-undef": [ "error", { - "allowGlobals": true - } ], "react/jsx-tag-spacing": "error", "react/no-children-prop": "off", "react/no-find-dom-node": "warn", diff --git a/blocks/api/factory.js b/blocks/api/factory.js index 112a7b3ae09038..3b6f58f8efed10 100644 --- a/blocks/api/factory.js +++ b/blocks/api/factory.js @@ -64,6 +64,28 @@ export function createBlock( name, blockAttributes = {}, innerBlocks = [] ) { }; } +/** + * Given a block object, returns a copy of the block object, optionally merging + * new attributes and/or replacing its inner blocks. + * + * @param {Object} block Block object. + * @param {Object} mergeAttributes Block attributes. + * @param {?Array} innerBlocks Nested blocks. + * + * @return {Object} A cloned block. + */ +export function cloneBlock( block, mergeAttributes = {}, innerBlocks = block.innerBlocks ) { + return { + ...block, + uid: uuid(), + attributes: { + ...block.attributes, + ...mergeAttributes, + }, + innerBlocks, + }; +} + /** * Returns a predicate that receives a transformation and returns true if the * given transformation is able to execute in the situation specified in the diff --git a/blocks/api/index.js b/blocks/api/index.js index 7f0653a133c5b3..b3bce77afc1238 100644 --- a/blocks/api/index.js +++ b/blocks/api/index.js @@ -1,4 +1,10 @@ -export { createBlock, getPossibleBlockTransformations, switchToBlockType, createReusableBlock } from './factory'; +export { + createBlock, + cloneBlock, + getPossibleBlockTransformations, + switchToBlockType, + createReusableBlock, +} from './factory'; export { default as parse, getBlockAttributes } from './parser'; export { default as rawHandler } from './raw-handling'; export { diff --git a/blocks/api/serializer.js b/blocks/api/serializer.js index 56594d5d9db760..d98132fe20eecf 100644 --- a/blocks/api/serializer.js +++ b/blocks/api/serializer.js @@ -15,6 +15,7 @@ import { hasFilter, applyFilters } from '@wordpress/hooks'; * Internal dependencies */ import { getBlockType, getUnknownTypeHandlerName } from './registration'; +import BlockContentProvider from '../block-content-provider'; /** * Returns the block's default classname from its name. @@ -78,7 +79,13 @@ export function getSaveElement( blockType, attributes, innerBlocks = [] ) { * @param {WPBlockType} blockType Block type definition. * @param {Object} attributes Block attributes. */ - return applyFilters( 'blocks.getSaveElement', element, blockType, attributes ); + element = applyFilters( 'blocks.getSaveElement', element, blockType, attributes ); + + return ( + + { element } + + ); } /** @@ -173,9 +180,12 @@ export function getBlockContent( block ) { const blockType = getBlockType( block.name ); // If block was parsed as invalid or encounters an error while generating - // save content, use original content instead to avoid content loss. + // save content, use original content instead to avoid content loss. If a + // block contains nested content, exempt it from this condition because we + // otherwise have no access to its original content and content loss would + // still occur. let saveContent = block.originalContent; - if ( block.isValid ) { + if ( block.isValid || block.innerBlocks.length ) { try { saveContent = getSaveContent( blockType, block.attributes, block.innerBlocks ); } catch ( error ) {} diff --git a/blocks/api/test/factory.js b/blocks/api/test/factory.js index 9932007d88f82b..e24a0a726a6d0f 100644 --- a/blocks/api/test/factory.js +++ b/blocks/api/test/factory.js @@ -1,12 +1,19 @@ /** * External dependencies */ +import deepFreeze from 'deep-freeze'; import { noop } from 'lodash'; /** * Internal dependencies */ -import { createBlock, getPossibleBlockTransformations, switchToBlockType, createReusableBlock } from '../factory'; +import { + createBlock, + cloneBlock, + getPossibleBlockTransformations, + switchToBlockType, + createReusableBlock, +} from '../factory'; import { getBlockTypes, unregisterBlockType, setUnknownTypeHandlerName, registerBlockType } from '../registration'; describe( 'block factory', () => { @@ -34,7 +41,7 @@ describe( 'block factory', () => { } ); describe( 'createBlock()', () => { - it( 'should create a block given its blockType and attributes', () => { + it( 'should create a block given its blockType, attributes, inner blocks', () => { registerBlockType( 'core/test-block', { attributes: { align: { @@ -53,9 +60,11 @@ describe( 'block factory', () => { category: 'common', title: 'test block', } ); - const block = createBlock( 'core/test-block', { - align: 'left', - } ); + const block = createBlock( + 'core/test-block', + { align: 'left' }, + [ createBlock( 'core/test-block' ) ], + ); expect( block.name ).toEqual( 'core/test-block' ); expect( block.attributes ).toEqual( { @@ -64,6 +73,8 @@ describe( 'block factory', () => { align: 'left', } ); expect( block.isValid ).toBe( true ); + expect( block.innerBlocks ).toHaveLength( 1 ); + expect( block.innerBlocks[ 0 ].name ).toBe( 'core/test-block' ); expect( typeof block.uid ).toBe( 'string' ); } ); @@ -103,6 +114,79 @@ describe( 'block factory', () => { } ); } ); + describe( 'cloneBlock()', () => { + it( 'should merge attributes into the existing block', () => { + registerBlockType( 'core/test-block', { + attributes: { + align: { + type: 'string', + }, + isDifferent: { + type: 'boolean', + default: false, + }, + }, + save: noop, + category: 'common', + title: 'test block', + } ); + const block = deepFreeze( + createBlock( + 'core/test-block', + { align: 'left' }, + [ createBlock( 'core/test-block' ) ], + ) + ); + + const clonedBlock = cloneBlock( block, { + isDifferent: true, + } ); + + expect( clonedBlock.name ).toEqual( block.name ); + expect( clonedBlock.attributes ).toEqual( { + align: 'left', + isDifferent: true, + } ); + expect( clonedBlock.innerBlocks ).toHaveLength( 1 ); + expect( typeof clonedBlock.uid ).toBe( 'string' ); + expect( clonedBlock.uid ).not.toBe( block.uid ); + } ); + + it( 'should replace inner blocks of the existing block', () => { + registerBlockType( 'core/test-block', { + attributes: { + align: { + type: 'string', + }, + isDifferent: { + type: 'boolean', + default: false, + }, + }, + save: noop, + category: 'common', + title: 'test block', + } ); + const block = deepFreeze( + createBlock( + 'core/test-block', + { align: 'left' }, + [ + createBlock( 'core/test-block', { align: 'right' } ), + createBlock( 'core/test-block', { align: 'left' } ), + ], + ) + ); + + const clonedBlock = cloneBlock( block, undefined, [ + createBlock( 'core/test-block' ), + ] ); + + expect( clonedBlock.innerBlocks ).toHaveLength( 1 ); + expect( clonedBlock.innerBlocks[ 0 ].attributes ).not.toHaveProperty( 'align' ); + } ); + } ); + describe( 'getPossibleBlockTransformations()', () => { it( 'should should show as available a simple "from" transformation"', () => { registerBlockType( 'core/updated-text-block', { diff --git a/blocks/api/test/parser.js b/blocks/api/test/parser.js index 608ba501d91f7a..ea08c371f3da89 100644 --- a/blocks/api/test/parser.js +++ b/blocks/api/test/parser.js @@ -23,7 +23,7 @@ describe( 'block parser', () => { type: 'string', }, }, - save: ( { attributes } ) => attributes.fruit, + save: ( { attributes } ) => attributes.fruit || null, category: 'common', title: 'block title', }; @@ -37,7 +37,7 @@ describe( 'block parser', () => { source: 'html', }, }, - save: ( { attributes } ) => attributes.content, + save: ( { attributes } ) => attributes.content || null, }; beforeAll( () => { diff --git a/blocks/api/test/registration.js b/blocks/api/test/registration.js index bebd5ae5bc85d9..f8cf2aa8fb8f42 100644 --- a/blocks/api/test/registration.js +++ b/blocks/api/test/registration.js @@ -95,6 +95,9 @@ describe( 'blocks', () => { className: { type: 'string', }, + layout: { + type: 'string', + }, }, } ); } ); @@ -183,6 +186,9 @@ describe( 'blocks', () => { className: { type: 'string', }, + layout: { + type: 'string', + }, }, } ); } ); @@ -202,6 +208,9 @@ describe( 'blocks', () => { className: { type: 'string', }, + layout: { + type: 'string', + }, }, } ); } ); @@ -227,6 +236,9 @@ describe( 'blocks', () => { className: { type: 'string', }, + layout: { + type: 'string', + }, }, }, ] ); @@ -242,6 +254,9 @@ describe( 'blocks', () => { className: { type: 'string', }, + layout: { + type: 'string', + }, }, } ); expect( getBlockTypes() ).toEqual( [] ); @@ -289,6 +304,9 @@ describe( 'blocks', () => { className: { type: 'string', }, + layout: { + type: 'string', + }, }, } ); } ); @@ -307,6 +325,9 @@ describe( 'blocks', () => { className: { type: 'string', }, + layout: { + type: 'string', + }, }, } ); } ); @@ -332,6 +353,9 @@ describe( 'blocks', () => { className: { type: 'string', }, + layout: { + type: 'string', + }, }, }, { @@ -345,6 +369,9 @@ describe( 'blocks', () => { className: { type: 'string', }, + layout: { + type: 'string', + }, }, }, ] ); diff --git a/blocks/api/test/serializer.js b/blocks/api/test/serializer.js index f029d555db6c96..ec2bfde75c36f4 100644 --- a/blocks/api/test/serializer.js +++ b/blocks/api/test/serializer.js @@ -16,12 +16,14 @@ import serialize, { getBlockContent, } from '../serializer'; import { + getBlockType, getBlockTypes, registerBlockType, unregisterBlockType, setUnknownTypeHandlerName, } from '../registration'; import { createBlock } from '../'; +import InnerBlocks from '../../inner-blocks'; describe( 'block serializer', () => { beforeAll( () => { @@ -119,6 +121,45 @@ describe( 'block serializer', () => { expect( saved ).toBe( '
Bananas
' ); } ); + + it( 'should return element as string, with inner blocks', () => { + registerBlockType( 'core/fruit', { + category: 'common', + + title: 'fruit', + + attributes: { + fruit: { + type: 'string', + }, + }, + + supports: { + className: false, + }, + + save( { attributes } ) { + return ( +
+ { attributes.fruit } + +
+ ); + }, + } ); + + const saved = getSaveContent( + getBlockType( 'core/fruit' ), + { fruit: 'Bananas' }, + [ createBlock( 'core/fruit', { fruit: 'Apples' } ) ], + ); + + expect( saved ).toBe( + '
Bananas\n' + + '
Apples
\n' + + '
' + ); + } ); } ); } ); @@ -334,7 +375,12 @@ describe( 'block serializer', () => { throw new Error(); } - return

; + return ( +

+ { attributes.content } + +

+ ); }, category: 'common', title: 'block title', @@ -347,7 +393,7 @@ describe( 'block serializer', () => { content: 'Ribs & Chicken', stuff: 'left & right -- but ', } ); - const expectedPostContent = '\n

Ribs & Chicken

\n'; + const expectedPostContent = '\n

Ribs & Chicken

\n'; expect( serialize( [ block ] ) ).toEqual( expectedPostContent ); expect( serialize( block ) ).toEqual( expectedPostContent ); @@ -366,6 +412,27 @@ describe( 'block serializer', () => { ); } ); + it( 'should force serialize for invalid block with inner blocks', () => { + const block = createBlock( + 'core/test-block', + { content: 'Invalid' }, + [ createBlock( 'core/test-block' ) ] + ); + + block.isValid = false; + block.originalContent = 'Original'; + + expect( serialize( block ) ).toEqual( + '\n' + + '

Invalid\n' + + ' \n' + + '

\n' + + ' \n' + + '

\n' + + '' + ); + } ); + it( 'should preserve content for crashing block', () => { const block = createBlock( 'core/test-block', { content: 'Incorrect', diff --git a/blocks/block-content-provider/README.md b/blocks/block-content-provider/README.md new file mode 100644 index 00000000000000..45ad712a49e8b0 --- /dev/null +++ b/blocks/block-content-provider/README.md @@ -0,0 +1,12 @@ +BlockContentProvider +==================== + +An internal block component used in block content serialization to inject nested block content within the `save` implementation of the ancestor component in which it is nested. The component provides a pre-bound `BlockContent` component via context, which is used by the developer-facing `InnerBlocks.Content` component to render block content. + +## Usage + +```jsx + + { blockSaveElement } + +``` diff --git a/blocks/block-content-provider/index.js b/blocks/block-content-provider/index.js new file mode 100644 index 00000000000000..6829078682858f --- /dev/null +++ b/blocks/block-content-provider/index.js @@ -0,0 +1,50 @@ +/** + * WordPress dependencies + */ +import { Component, RawHTML } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { serialize } from '../api'; + +/** + * An internal block component used in block content serialization to inject + * nested block content within the `save` implementation of the ancestor + * component in which it is nested. The component provides a pre-bound + * `BlockContent` component via context, which is used by the developer-facing + * `InnerBlocks.Content` component to render block content. + * + * @example + * + * ```jsx + * + * { blockSaveElement } + * + * ``` + */ +class BlockContentProvider extends Component { + getChildContext() { + const { innerBlocks } = this.props; + + return { + BlockContent() { + // Value is an array of blocks, so defer to block serializer + const html = serialize( innerBlocks ); + + // Use special-cased raw HTML tag to avoid default escaping + return { html }; + }, + }; + } + + render() { + return this.props.children; + } +} + +BlockContentProvider.childContextTypes = { + BlockContent: () => {}, +}; + +export default BlockContentProvider; diff --git a/blocks/hooks/index.js b/blocks/hooks/index.js index 33489dd8e4315e..4bc4cebbf3119e 100644 --- a/blocks/hooks/index.js +++ b/blocks/hooks/index.js @@ -5,4 +5,5 @@ import './anchor'; import './custom-class-name'; import './deprecated'; import './generated-class-name'; +import './layout'; import './matchers'; diff --git a/blocks/hooks/layout.js b/blocks/hooks/layout.js new file mode 100644 index 00000000000000..92761a2405bbb6 --- /dev/null +++ b/blocks/hooks/layout.js @@ -0,0 +1,53 @@ +/** + * External dependencies + */ +import { assign, compact } from 'lodash'; + +/** + * WordPress dependencies + */ +import { addFilter } from '@wordpress/hooks'; + +/** + * Filters registered block settings, extending attributes with layout. + * + * @param {Object} settings Original block settings. + * + * @return {Object} Filtered block settings. + */ +export function addAttribute( settings ) { + // Use Lodash's assign to gracefully handle if attributes are undefined + settings.attributes = assign( settings.attributes, { + layout: { + type: 'string', + }, + } ); + + return settings; +} + +/** + * Override props assigned to save component to inject layout class. This is + * only applied if the block's save result is an element and not a markup + * string. + * + * @param {Object} extraProps Additional props applied to save element. + * @param {Object} blockType Block type. + * @param {Object} attributes Current block attributes. + * + * @return {Object} Filtered props applied to save element. + */ +export function addSaveProps( extraProps, blockType, attributes ) { + const { layout } = attributes; + if ( layout ) { + extraProps.className = compact( [ + extraProps.className, + 'layout-' + layout, + ] ).join( ' ' ); + } + + return extraProps; +} + +addFilter( 'blocks.registerBlockType', 'core/layout/attribute', addAttribute ); +addFilter( 'blocks.getSaveContent.extraProps', 'core/layout/save-props', addSaveProps ); diff --git a/blocks/hooks/test/layout.js b/blocks/hooks/test/layout.js new file mode 100644 index 00000000000000..308547753f5650 --- /dev/null +++ b/blocks/hooks/test/layout.js @@ -0,0 +1,45 @@ +/** + * External dependencies + */ +import { noop } from 'lodash'; + +/** + * WordPress dependencies + */ +import { applyFilters } from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import '../layout'; + +describe( 'layout', () => { + const blockSettings = { + save: noop, + category: 'common', + title: 'block title', + }; + + describe( 'addAttribute()', () => { + const registerBlockType = applyFilters.bind( null, 'blocks.registerBlockType' ); + + it( 'should assign a new layout attribute', () => { + const settings = registerBlockType( blockSettings ); + + expect( settings.attributes ).toHaveProperty( 'layout' ); + } ); + } ); + + describe( 'addSaveProps', () => { + const getSaveContentExtraProps = applyFilters.bind( null, 'blocks.getSaveContent.extraProps' ); + + it( 'should merge layout class name', () => { + const attributes = { layout: 'wide' }; + const extraProps = getSaveContentExtraProps( { + className: 'wizard', + }, blockSettings, attributes ); + + expect( extraProps.className ).toBe( 'wizard layout-wide' ); + } ); + } ); +} ); diff --git a/blocks/inner-blocks/README.md b/blocks/inner-blocks/README.md new file mode 100644 index 00000000000000..5d6b39b4295e85 --- /dev/null +++ b/blocks/inner-blocks/README.md @@ -0,0 +1,147 @@ +InnerBlocks +=========== + +InnerBlocks exports a pair of components which can be used in block implementations to enable nested block content. + +Refer to the [implementation of the Columns block](https://github.com/WordPress/gutenberg/tree/master/blocks/library/columns) as an example resource. + +## Usage + +In a block's `edit` implementation, simply render `InnerBlocks`, optionally with `layouts` of available nest areas: + +Then, in the `save` implementation, render `InnerBlocks.Content`. This will be replaced automatically with the content of the nested blocks. + +```jsx +import { registerBlockType, InnerBlocks } from '@wordpress/blocks'; + +registerBlockType( 'my-plugin/my-block', { + // ... + + edit( { className } ) { + return ( +
+ +
+ ); + }, + + save() { + return ( +
+ +
+ ); + } +} ); +``` + +_Note:_ A block can render at most a single `InnerBlocks` and `InnerBlocks.Content` element in `edit` and `save` respectively. To create distinct arrangements of nested blocks, refer to the `layouts` prop documented below. + +_Note:_ Since the save step will automatically apply props to the element returned by `save`, it is important to include the wrapping `div` in the above simple example even though we are applying no props of our own. In a real-world example, you may have your own attributes to apply to the saved markup, or sibling content adjacent to the rendered nested blocks. + +## Props + +### `InnerBlocks` + +#### `layouts` + +* **Type:** `Array|Object` + +To achieve distinct arrangements of nested blocks, you may assign layout as an array of objects, or an object. When assigned, a user will be provided with the option to move blocks between layouts, and the rendered output will assign a layout-specific class which can be used in your block stylesheet to effect the visual arrangement of nested blocks. + +Because `InnerBlocks.Content` will generate a single continuous flow of block markup for nested content, it may be advisable to use [CSS Grid Layout](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout) to assign layout positions. Be aware that CSS grid is [not suported in legacy browsers](https://caniuse.com/#feat=css-grid), and therefore you should consider how this might impact your block's appearance when viewed on the front end of a site in such browsers. + +Layouts can be assigned either either as an object (ungrouped layouts) or an array of objects (grouped layouts). These are documented below. + +In both cases, each layout consists of: + +- Name: A slug to use in generating the layout class applied to nested blocks +- Icon (`icon: string`): The slug of the Dashicon to use in controls presented to the user in moving between layouts + - Reference: https://developer.wordpress.org/resource/dashicons/ +- Label (`label: string`): The text to display in the controls presented to the user in moving between layouts + +_Ungrouped Layouts:_ + +If you do not depend on a particular order of markup for your nested content and need merely to assign a layout class to each nested block, you should assign `layouts` as an object, where each key is the name of a layout: + +```jsx + +``` + +_Grouped Layouts:_ + +If your nested content depends on having each layout grouped in its markup, you should assign `layouts` as an array of layout objects, where the name of the layout is set as a property of the object: + +```jsx + +``` + +Consider a Columns block. When the user changes the layout of a block from one column to another, it is not sufficient to simply reassign the class name of the block to the new layout, as the user may then proceed to attempt to move the block up or down within the new column. The expected behavior here requires that the markup of the block itself be moved in relation to blocks already present in the new layout. + +```html + +
+ +

First Paragraph

+ + + +

Second Paragraph

+ + + +

Third Paragraph

+ +
+ +``` + +In the above example markup, if the user moved the first nested paragraph block to the second column, we must ensure that if they then proceed to move the block down, that the block would be the last item in the markup, otherwise it would not appear to move because it would still exist in markup prior to the third paragraph. + +_Bad:_ + +```html + +
+ +

First Paragraph

+ + + +

Second Paragraph

+ + + +

Third Paragraph

+ +
+ +``` + +We achieve this by ensuring that the markup of each layout is kept grouped together. + +_Good:_ + +```html + +
+ +

Second Paragraph

+ + + +

First Paragraph

+ + + +

Third Paragraph

+ +
+ +``` diff --git a/blocks/inner-blocks/index.js b/blocks/inner-blocks/index.js index d5af5157c83722..37ae97e13c71a7 100644 --- a/blocks/inner-blocks/index.js +++ b/blocks/inner-blocks/index.js @@ -1,90 +1,18 @@ /** * WordPress dependencies */ -import { Component } from '@wordpress/element'; import { withContext } from '@wordpress/components'; -/** - * Internal dependencies - */ -import { serialize } from '../api'; - -class InnerBlocks extends Component { - constructor() { - super( ...arguments ); - - const store = wp.editor.createStore(); - store.dispatch( { - type: 'RESET_BLOCKS', - blocks: this.props.value, - } ); - - this.store = store; - this.editor = store.getState().editor; - - this.maybeChange = this.maybeChange.bind( this ); - - // For child store, monitor changes to emit back up to parent store - store.subscribe( this.maybeChange ); - } - - shouldComponentUpdate() { - // TODO: Investigate if we can avoid disabling render reconciliation. - // Needed currently because render passes a new settings reference by - // presence of types (via shallow clone). Maybe assign in constructor. - return false; - } - - maybeChange() { - const state = this.store.getState(); - const { editor } = state; - if ( editor === this.editor ) { - return; - } - - const blocks = wp.editor.selectors.getBlocks( state ); - this.props.onChange( blocks ); - this.editor = editor; - } - - render() { - const { types } = this.props; - - let { settings } = this.props; - if ( types ) { - settings = { - ...settings, - blockTypes: types, - }; - } - - // TODO: We should not be referencing editor on the global object, but - // this is a circular dependency between editor and blocks. - - // TODO: The DIV wrapper inside EditorProvider should be eliminated, - // and exists because provider supports only a single child. - - return ( - -
- - -
-
- ); - } +function InnerBlocks( { BlockList, layouts } ) { + return ; } -InnerBlocks = withContext( 'editor' )()( InnerBlocks ); - -InnerBlocks.Content = ( { value } ) => { - if ( ! value ) { - return null; - } +InnerBlocks = withContext( 'BlockList' )()( InnerBlocks ); - const html = serialize( value ); - - return ; +InnerBlocks.Content = ( { BlockContent } ) => { + return ; }; +InnerBlocks.Content = withContext( 'BlockContent' )()( InnerBlocks.Content ); + export default InnerBlocks; diff --git a/blocks/library/columns/editor.scss b/blocks/library/columns/editor.scss deleted file mode 100644 index cdf58342c74a3b..00000000000000 --- a/blocks/library/columns/editor.scss +++ /dev/null @@ -1,9 +0,0 @@ -.wp-block-columns { - display: flex; - padding: $item-spacing 0; - - > div { - flex-basis: 100%; - flex-shrink: 1; - } -} diff --git a/blocks/library/columns/index.js b/blocks/library/columns/index.js index ab1e011e3e3c2d..3422e8cf82c633 100644 --- a/blocks/library/columns/index.js +++ b/blocks/library/columns/index.js @@ -1,27 +1,21 @@ /** * External dependencies */ -import { repeat, identity } from 'lodash'; +import { times } from 'lodash'; +import classnames from 'classnames'; /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies */ -import './editor.scss'; +import './style.scss'; import RangeControl from '../../inspector-controls/range-control'; -import InnerBlocks from '../../inner-blocks'; import InspectorControls from '../../inspector-controls'; - -function mapInnerBlocks( innerBlocks, columns, callback ) { - return columns.map( ( endOffset, index ) => { - const startOffset = columns[ index - 1 ] || 0; - return callback( innerBlocks.slice( startOffset, endOffset ), index ); - } ); -} +import InnerBlocks from '../../inner-blocks'; export const name = 'core/columns'; @@ -34,102 +28,55 @@ export const settings = { attributes: { columns: { - type: 'array', - default: [ 1, 2 ], + type: 'number', + default: 2, }, }, description: __( 'A multi-column layout of content.' ), - getEditWrapperProps() { - return { 'data-align': 'wide' }; - }, - - edit( { attributes, setAttributes, setInnerBlocks, innerBlocks, className, focus } ) { + edit( { attributes, setAttributes, className, focus } ) { const { columns } = attributes; - - // TODO: Refactor setInnerBlockFragment to be less awful, separated, tested. - - const setInnerBlockFragment = ( index ) => ( nextInnerBlocks ) => { - const fragmented = mapInnerBlocks( innerBlocks, columns, identity ); - const origNumInnerBlocks = fragmented[ index ].length; - fragmented.splice( index, 1, nextInnerBlocks ); - - let runningOffset = 0; - const nextColumns = []; - const allNextInnerBlocks = []; - fragmented.forEach( ( innerBlockSet, innerBlockSetIndex ) => { - runningOffset += innerBlockSet.length; - if ( innerBlockSetIndex > index ) { - runningOffset += nextInnerBlocks.length - origNumInnerBlocks; - } - - nextColumns.push( runningOffset ); - allNextInnerBlocks.push( ...innerBlockSet ); - } ); - - setAttributes( { columns: nextColumns } ); - setInnerBlocks( allNextInnerBlocks ); - }; - - const setNextColumnsCount = ( count ) => { - const nextColumns = count < columns.length ? - // Take first X of columns... - columns.slice( 0, count ) : - // ...or fill with new columns - [ ...columns, repeat( count - columns.length, () => [] ) ]; - - if ( count < columns.length ) { - // When trimming columns, reassign inner blocks to include only - // those up to the now-last column. - - // TODO: Ideally we'd simply flatten eliminated columns into - // the last available column, which is simply assigning the new - // last column to have inner blocks length as its final offset, - // but this is made difficult by the fact that we'd need to - // reset the block state of the rendered InnerBlocks. - const lastInnerBlockIndex = nextColumns[ nextColumns.length - 1 ]; - setInnerBlocks( innerBlocks.slice( 0, lastInnerBlockIndex ) ); - } - - setAttributes( { columns: nextColumns } ); - }; + const classes = classnames( className, `has-${ columns }-columns` ); + + // Define columns as a set of layouts within the inner block list. This + // will enable the user to move blocks between columns, and will apply + // a layout-specific class name to the rendered output which can be + // styled by the columns wrapper to visually place the columns. + const layouts = times( columns, ( n ) => ( { + name: `column-${ n + 1 }`, + label: sprintf( __( 'Column %d' ), n + 1 ), + icon: 'columns', + } ) ); return [ focus && ( { + setAttributes( { + columns: nextColumns, + } ); + } } min={ 2 } - max={ 4 } + max={ 6 } /> ), -
- { mapInnerBlocks( innerBlocks, columns, ( innerBlockSet, index ) => ( - - ) ) } +
+
, ]; }, - save( { attributes, innerBlocks } ) { + save( { attributes } ) { const { columns } = attributes; return ( -
- { mapInnerBlocks( innerBlocks, columns, ( innerBlockSet, index ) => ( - - ) ) } +
+
); }, diff --git a/blocks/library/columns/style.scss b/blocks/library/columns/style.scss new file mode 100644 index 00000000000000..9ec2e09c5c8753 --- /dev/null +++ b/blocks/library/columns/style.scss @@ -0,0 +1,16 @@ +.wp-block-columns { + display: grid; + grid-auto-flow: dense; + + @for $i from 2 through 6 { + &.has-#{ $i }-columns { + grid-auto-columns: #{ 100% / $i }; + } + } + + @for $i from 1 through 6 { + .layout-column-#{ $i } { + grid-column: #{ $i }; + } + } +} diff --git a/blocks/test/fixtures/core__columns.html b/blocks/test/fixtures/core__columns.html index 82fec967b1c493..ae96a2e33b146c 100644 --- a/blocks/test/fixtures/core__columns.html +++ b/blocks/test/fixtures/core__columns.html @@ -1,11 +1,16 @@ - -
- -

Column One

- - - -

Column Two

+ +
+ +

Column One, Paragraph One

+ + +

Column One, Paragraph Two

+ + +

Column Two, Paragraph One

+ + +

Column Three, Paragraph One

diff --git a/blocks/test/fixtures/core__columns.json b/blocks/test/fixtures/core__columns.json index 94745068beb5ce..baf28cfd7f291b 100644 --- a/blocks/test/fixtures/core__columns.json +++ b/blocks/test/fixtures/core__columns.json @@ -4,10 +4,7 @@ "name": "core/columns", "isValid": true, "attributes": { - "columns": [ - 1, - 2 - ] + "columns": 3 }, "innerBlocks": [ { @@ -16,12 +13,13 @@ "isValid": true, "attributes": { "content": [ - "Column One" + "Column One, Paragraph One" ], - "dropCap": false + "dropCap": false, + "layout": "column-1" }, "innerBlocks": [], - "originalContent": "

Column One

" + "originalContent": "

Column One, Paragraph One

" }, { "uid": "_uid_1", @@ -29,14 +27,43 @@ "isValid": true, "attributes": { "content": [ - "Column Two" + "Column One, Paragraph Two" ], - "dropCap": false + "dropCap": false, + "layout": "column-1" }, "innerBlocks": [], - "originalContent": "

Column Two

" + "originalContent": "

Column One, Paragraph Two

" + }, + { + "uid": "_uid_2", + "name": "core/paragraph", + "isValid": true, + "attributes": { + "content": [ + "Column Two, Paragraph One" + ], + "dropCap": false, + "layout": "column-2" + }, + "innerBlocks": [], + "originalContent": "

Column Two, Paragraph One

" + }, + { + "uid": "_uid_3", + "name": "core/paragraph", + "isValid": true, + "attributes": { + "content": [ + "Column Three, Paragraph One" + ], + "dropCap": false, + "layout": "column-3" + }, + "innerBlocks": [], + "originalContent": "

Column Three, Paragraph One

" } ], - "originalContent": "
\n\t\n\n\t\n
" + "originalContent": "
\n\t\n\t\n\t\n\t\n
" } ] diff --git a/blocks/test/fixtures/core__columns.parsed.json b/blocks/test/fixtures/core__columns.parsed.json index ef3d7daba8c557..80b670bf1e8492 100644 --- a/blocks/test/fixtures/core__columns.parsed.json +++ b/blocks/test/fixtures/core__columns.parsed.json @@ -2,26 +2,43 @@ { "blockName": "core/columns", "attrs": { - "columns": [ - 1, - 2 - ] + "columns": 3 }, "innerBlocks": [ { "blockName": "core/paragraph", - "attrs": null, + "attrs": { + "layout": "column-1" + }, "innerBlocks": [], - "innerHTML": "\n\t

Column One

\n\t" + "innerHTML": "\n\t

Column One, Paragraph One

\n\t" }, { "blockName": "core/paragraph", - "attrs": null, + "attrs": { + "layout": "column-1" + }, "innerBlocks": [], - "innerHTML": "\n\t

Column Two

\n\t" + "innerHTML": "\n\t

Column One, Paragraph Two

\n\t" + }, + { + "blockName": "core/paragraph", + "attrs": { + "layout": "column-2" + }, + "innerBlocks": [], + "innerHTML": "\n\t

Column Two, Paragraph One

\n\t" + }, + { + "blockName": "core/paragraph", + "attrs": { + "layout": "column-3" + }, + "innerBlocks": [], + "innerHTML": "\n\t

Column Three, Paragraph One

\n\t" } ], - "innerHTML": "\n
\n\t\n\n\t\n
\n" + "innerHTML": "\n
\n\t\n\t\n\t\n\t\n
\n" }, { "attrs": {}, diff --git a/blocks/test/fixtures/core__columns.serialized.html b/blocks/test/fixtures/core__columns.serialized.html index 66d88f197c3a22..0296c04a5ad00b 100644 --- a/blocks/test/fixtures/core__columns.serialized.html +++ b/blocks/test/fixtures/core__columns.serialized.html @@ -1,10 +1,19 @@ - -
- -

Column One

+ +
+ +

Column One, Paragraph One

- -

Column Two

+ + +

Column One, Paragraph Two

+ + + +

Column Two, Paragraph One

+ + + +

Column Three, Paragraph One

diff --git a/blocks/test/fixtures/core__latest-posts.json b/blocks/test/fixtures/core__latest-posts.json index bfebd9ec16455c..f3dd4953876cba 100644 --- a/blocks/test/fixtures/core__latest-posts.json +++ b/blocks/test/fixtures/core__latest-posts.json @@ -6,7 +6,6 @@ "attributes": { "postsToShow": 5, "displayPostDate": false, - "layout": "list", "columns": 3, "align": "center", "order": "desc", diff --git a/blocks/test/fixtures/core__latest-posts__displayPostDate.json b/blocks/test/fixtures/core__latest-posts__displayPostDate.json index 82f20c7a306cca..be9c487c2f0a86 100644 --- a/blocks/test/fixtures/core__latest-posts__displayPostDate.json +++ b/blocks/test/fixtures/core__latest-posts__displayPostDate.json @@ -6,7 +6,6 @@ "attributes": { "postsToShow": 5, "displayPostDate": true, - "layout": "list", "columns": 3, "align": "center", "order": "desc", diff --git a/blocks/test/fixtures/core__pullquote__multi-paragraph.json b/blocks/test/fixtures/core__pullquote__multi-paragraph.json index 95473fe0ac7e29..c2a6c0d770de73 100644 --- a/blocks/test/fixtures/core__pullquote__multi-paragraph.json +++ b/blocks/test/fixtures/core__pullquote__multi-paragraph.json @@ -15,7 +15,7 @@ "Paragraph ", { "type": "strong", - "key": "_domReact69", + "key": "_domReact71", "ref": null, "props": { "children": "one" diff --git a/edit-post/components/modes/visual-editor/index.js b/edit-post/components/modes/visual-editor/index.js index 6bf5cb77a42b3d..13a828437a94b7 100644 --- a/edit-post/components/modes/visual-editor/index.js +++ b/edit-post/components/modes/visual-editor/index.js @@ -10,7 +10,6 @@ import { BlockList, PostTitle, WritingFlow, - DefaultBlockAppender, EditorGlobalKeyboardShortcuts, BlockSelectionClearer, } from '@wordpress/editor'; @@ -38,7 +37,6 @@ function VisualEditor( props ) { ) } /> - ); diff --git a/edit-post/index.js b/edit-post/index.js index 6f3efe1007e9af..3fb2777fe1bc01 100644 --- a/edit-post/index.js +++ b/edit-post/index.js @@ -17,14 +17,8 @@ import { EditorProvider, ErrorBoundary } from '@wordpress/editor'; */ import './assets/stylesheets/main.scss'; import Layout from './components/layout'; -import * as selectors from './store/selectors'; -import * as actions from './store/actions'; import store from './store'; -export { createStore } from './store'; -export { selectors }; -export { actions }; - // Configure moment globally moment.locale( dateSettings.l10n.locale ); if ( dateSettings.timezone.string ) { diff --git a/editor/components/block-drop-zone/index.js b/editor/components/block-drop-zone/index.js index 29f76029940b14..1654048f20436f 100644 --- a/editor/components/block-drop-zone/index.js +++ b/editor/components/block-drop-zone/index.js @@ -2,13 +2,13 @@ * External Dependencies */ import { connect } from 'react-redux'; -import { reduce, get, find } from 'lodash'; +import { reduce, get, find, castArray } from 'lodash'; /** * WordPress dependencies */ import { DropZone, withContext } from '@wordpress/components'; -import { getBlockTypes, rawHandler } from '@wordpress/blocks'; +import { getBlockTypes, rawHandler, cloneBlock } from '@wordpress/blocks'; import { compose } from '@wordpress/element'; /** @@ -21,7 +21,7 @@ function BlockDropZone( { index, isLocked, ...props } ) { return null; } - const getInsertPosition = ( position ) => { + const getInsertIndex = ( position ) => { if ( index !== undefined ) { return position.y === 'top' ? index : index + 1; } @@ -39,9 +39,9 @@ function BlockDropZone( { index, isLocked, ...props } ) { }, false ); if ( transformation ) { - const insertPosition = getInsertPosition( position ); + const insertIndex = getInsertIndex( position ); const blocks = transformation.transform( files, props.updateBlockAttributes ); - props.insertBlocks( blocks, insertPosition ); + props.insertBlocks( blocks, insertIndex ); } }; @@ -49,7 +49,7 @@ function BlockDropZone( { index, isLocked, ...props } ) { const blocks = rawHandler( { HTML, mode: 'BLOCKS' } ); if ( blocks.length ) { - props.insertBlocks( blocks, getInsertPosition( position ) ); + props.insertBlocks( blocks, getInsertIndex( position ) ); } }; @@ -64,7 +64,28 @@ function BlockDropZone( { index, isLocked, ...props } ) { export default compose( connect( undefined, - { insertBlocks, updateBlockAttributes } + ( dispatch, ownProps ) => { + return { + insertBlocks( blocks, insertIndex ) { + const { rootUID, layout } = ownProps; + + if ( layout ) { + // A block's transform function may return a single + // transformed block or an array of blocks, so ensure + // to first coerce to an array before mapping to inject + // the layout attribute. + blocks = castArray( blocks ).map( ( block ) => ( + cloneBlock( block, { layout } ) + ) ); + } + + dispatch( insertBlocks( blocks, insertIndex, rootUID ) ); + }, + updateBlockAttributes( ...args ) { + dispatch( updateBlockAttributes( ...args ) ); + }, + }; + } ), withContext( 'editor' )( ( settings ) => { const { templateLock } = settings; diff --git a/editor/components/block-list/block.js b/editor/components/block-list/block.js index c9e1f472b21c8c..11313641860bcb 100644 --- a/editor/components/block-list/block.js +++ b/editor/components/block-list/block.js @@ -3,16 +3,17 @@ */ import { connect } from 'react-redux'; import classnames from 'classnames'; -import { get, partial, reduce, size } from 'lodash'; +import { get, partial, reduce, size, castArray, noop } from 'lodash'; /** * WordPress dependencies */ -import { Component, compose } from '@wordpress/element'; +import { Component, findDOMNode, compose } from '@wordpress/element'; import { keycodes } from '@wordpress/utils'; import { BlockEdit, createBlock, + cloneBlock, getBlockType, getSaveElement, isReusableBlock, @@ -35,13 +36,14 @@ import BlockContextualToolbar from './block-contextual-toolbar'; import BlockMultiControls from './multi-controls'; import BlockMobileToolbar from './block-mobile-toolbar'; import BlockInsertionPoint from './insertion-point'; +import IgnoreNestedEvents from './ignore-nested-events'; +import { createInnerBlockList } from './utils'; import { clearSelectedBlock, editPost, focusBlock, insertBlocks, mergeBlocks, - updateBlock, removeBlock, replaceBlocks, selectBlock, @@ -123,6 +125,22 @@ export class BlockListBlock extends Component { }; } + getChildContext() { + const { + uid, + renderBlockMenu, + showContextualToolbar, + } = this.props; + + return { + BlockList: createInnerBlockList( + uid, + renderBlockMenu, + showContextualToolbar + ), + }; + } + componentDidMount() { if ( this.props.focus ) { this.node.focus(); @@ -179,11 +197,23 @@ export class BlockListBlock extends Component { } setBlockListRef( node ) { + // Disable reason: The root return element uses a component to manage + // event nesting, but the parent block list layout needs the raw DOM + // node to track multi-selection. + // + // eslint-disable-next-line react/no-find-dom-node + node = findDOMNode( node ); + this.props.blockRef( node, this.props.uid ); } bindBlockNode( node ) { - this.node = node; + // Disable reason: The block element uses a component to manage event + // nesting, but we rely on a raw DOM node for focusing and preserving + // scroll offset on move. + // + // eslint-disable-next-line react/no-find-dom-node + this.node = findDOMNode( node ); } setAttributes( attributes ) { @@ -217,6 +247,16 @@ export class BlockListBlock extends Component { this.hadTouchStart = false; } + /** + * A mouseover event handler to apply hover effect when a pointer device is + * placed within the bounds of the block. The mouseover event is preferred + * over mouseenter because it may be the case that a previous mouseenter + * event was blocked from being handled by a IgnoreNestedEvents component, + * therefore transitioning out of a nested block to the bounds of the block + * would otherwise not trigger a hover effect. + * + * @see https://developer.mozilla.org/en-US/docs/Web/Events/mouseenter + */ maybeHover() { const { isHovered, isSelected, isMultiSelected, onHover } = this.props; @@ -398,7 +438,18 @@ export class BlockListBlock extends Component { } render() { - const { block, order, mode, showContextualToolbar, isLocked, renderBlockMenu } = this.props; + const { + block, + order, + mode, + showContextualToolbar, + isLocked, + isFirst, + isLast, + rootUID, + layout, + renderBlockMenu, + } = this.props; const { name: blockName, isValid } = block; const blockType = getBlockType( blockName ); // translators: %s: Type of block (i.e. Text, Image etc) @@ -426,13 +477,19 @@ export class BlockListBlock extends Component { wrapperProps = blockType.getEditWrapperProps( block.attributes ); } - // Disable reason: Each block can be selected by clicking on it - /* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */ + // Disable reasons: + // + // jsx-a11y/mouse-events-have-key-events: + // - onMouseOver is explicitly handling hover effects + // + // jsx-a11y/no-static-element-interactions: + // - Each block can be selected by clicking on it + + /* eslint-disable jsx-a11y/mouse-events-have-key-events, jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */ return ( -
- - { ( showUI || isHovered ) && } - { ( showUI || isHovered ) && } - { ( showUI || isHovered ) && } + + { ( showUI || isHovered ) && ( + + ) } + { ( showUI || isHovered ) && ( + + ) } + { ( showUI || isHovered ) && ( + + ) } { showUI && isValid && showContextualToolbar && } - { isFirstMultiSelected && } -
} + { showUI && } -
+ { !! error && } - -
+ + ); /* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */ } } -const mapStateToProps = ( state, { uid } ) => ( { +const mapStateToProps = ( state, { uid, rootUID } ) => ( { previousBlock: getPreviousBlock( state, uid ), nextBlock: getNextBlock( state, uid ), block: getBlock( state, uid ), @@ -508,7 +590,7 @@ const mapStateToProps = ( state, { uid } ) => ( { isHovered: isBlockHovered( state, uid ) && ! isMultiSelecting( state ), focus: getBlockFocus( state, uid ), isTyping: isTyping( state ), - order: getBlockIndex( state, uid ), + order: getBlockIndex( state, uid, rootUID ), meta: getEditedPostAttribute( state, 'meta' ), mode: getBlockMode( state, uid ), isSelectionEnabled: isSelectionEnabled( state ), @@ -549,8 +631,12 @@ const mapDispatchToProps = ( dispatch, ownProps ) => ( { } ); }, - onInsertBlocks( blocks, position ) { - dispatch( insertBlocks( blocks, position ) ); + onInsertBlocks( blocks, index ) { + const { rootUID, layout } = ownProps; + + blocks = blocks.map( ( block ) => cloneBlock( block, { layout } ) ); + + dispatch( insertBlocks( blocks, index, rootUID ) ); }, onFocus( ...args ) { @@ -566,6 +652,12 @@ const mapDispatchToProps = ( dispatch, ownProps ) => ( { }, onReplace( blocks ) { + const { layout } = ownProps; + + blocks = castArray( blocks ).map( ( block ) => ( + cloneBlock( block, { layout } ) + ) ); + dispatch( replaceBlocks( [ ownProps.uid ], blocks ) ); }, @@ -576,14 +668,14 @@ const mapDispatchToProps = ( dispatch, ownProps ) => ( { toggleSelection( selectionEnabled ) { dispatch( toggleSelection( selectionEnabled ) ); }, - - setInnerBlocks( innerBlocks ) { - dispatch( updateBlock( ownProps.uid, { innerBlocks } ) ); - }, } ); BlockListBlock.className = 'editor-block-list__block-edit'; +BlockListBlock.childContextTypes = { + BlockList: noop, +}; + export default compose( connect( mapStateToProps, mapDispatchToProps ), withContext( 'editor' )( ( settings ) => { diff --git a/editor/components/block-list/ignore-nested-events.js b/editor/components/block-list/ignore-nested-events.js new file mode 100644 index 00000000000000..e045071209c832 --- /dev/null +++ b/editor/components/block-list/ignore-nested-events.js @@ -0,0 +1,78 @@ +/** + * External dependencies + */ +import { reduce } from 'lodash'; + +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; + +/** + * Component which renders a div with all passed props applied, replacing all + * event prop handlers with a proxying event handler to capture and prevent + * events from being handled by its ancestor IgnoreNestedEvents components by + * testing the presence of a private flag value on the event object. + * + * @type {Component} + */ +class IgnoreNestedEvents extends Component { + constructor() { + super( ...arguments ); + + this.proxyEvent = this.proxyEvent.bind( this ); + + // The event map is responsible for tracking an event type to a React + // component prop name, since it is easy to determine event type from + // a React prop name, but not the other way around. + this.eventMap = {}; + } + + /** + * General event handler which only calls to its original props callback if + * it has not already been handled by a descendant IgnoreNestedEvents. + * + * @param {Event} event Event object. + * + * @return {void} + */ + proxyEvent( event ) { + // Skip if already handled (i.e. assume nested block) + if ( event.nativeEvent._blockHandled ) { + return; + } + + // Assign into the native event, since React will reuse their synthetic + // event objects and this property assignment could otherwise leak. + // + // See: https://reactjs.org/docs/events.html#event-pooling + event.nativeEvent._blockHandled = true; + + // Invoke original prop handler + const propKey = this.eventMap[ event.type ]; + this.props[ propKey ]( event ); + } + + render() { + const eventHandlers = reduce( this.props, ( result, handler, key ) => { + // Try to match prop key as event handler + const match = key.match( /^on([A-Z][a-zA-Z]+)$/ ); + if ( match ) { + // Re-map the prop to the local proxy handler to check whether + // the event has already been handled. + result[ key ] = this.proxyEvent; + + // Assign event -> propName into an instance variable, so as to + // avoid re-renders which could be incurred either by setState + // or in mapping values to a newly created function. + this.eventMap[ match[ 1 ].toLowerCase() ] = key; + } + + return result; + }, {} ); + + return
; + } +} + +export default IgnoreNestedEvents; diff --git a/editor/components/block-list/index.js b/editor/components/block-list/index.js new file mode 100644 index 00000000000000..bb2311d7b92117 --- /dev/null +++ b/editor/components/block-list/index.js @@ -0,0 +1,62 @@ +/** + * External dependencies + */ +import { connect } from 'react-redux'; +import { + filter, + get, + map, +} from 'lodash'; +/** + * Internal dependencies + */ +import './style.scss'; +import BlockListLayout from './layout'; +import { getBlocks } from '../../store/selectors'; + +function BlockList( { + blocks, + renderBlockMenu, + layouts = {}, + rootUID, + showContextualToolbar, +} ) { + // BlockList can be provided with a layouts configuration, either grouped + // (blocks adjacent in markup) or ungrouped. This is inferred by the shape + // of the layouts configuration passed (grouped layout as array). + const isGroupedByLayout = Array.isArray( layouts ); + + // In case of ungrouped layout, we still emulate a layout merely for the + // purposes of normalizing layout rendering, even though there will only + // be a single layout, and no filtering applied. + if ( ! isGroupedByLayout ) { + layouts = [ { name: 'default' } ]; + } + + return map( layouts, ( layout ) => { + // When rendering grouped layouts, filter to blocks assigned to layout. + const layoutBlocks = isGroupedByLayout ? + filter( blocks, ( block ) => ( + get( block, [ 'attributes', 'layout' ] ) === layout.name + ) ) : + blocks; + + return ( + + ); + } ); +} + +export default connect( + ( state, ownProps ) => ( { + blocks: getBlocks( state, ownProps.rootUID ), + } ), +)( BlockList ); diff --git a/editor/components/block-list/insertion-point.js b/editor/components/block-list/insertion-point.js index f95c6335a310ca..919df2c499d4a6 100644 --- a/editor/components/block-list/insertion-point.js +++ b/editor/components/block-list/insertion-point.js @@ -7,7 +7,7 @@ import { connect } from 'react-redux'; * Internal dependencies */ import { - getBlockUids, + getBlockIndex, getBlockInsertionPoint, isBlockInsertionPointVisible, } from '../../store/selectors'; @@ -21,14 +21,14 @@ function BlockInsertionPoint( { showInsertionPoint } ) { } export default connect( - ( state, { uid } ) => { - const blockIndex = uid ? getBlockUids( state ).indexOf( uid ) : -1; + ( state, { uid, rootUID, layout } ) => { + const blockIndex = uid ? getBlockIndex( state, uid, rootUID ) : -1; const insertIndex = blockIndex > -1 ? blockIndex + 1 : 0; return { showInsertionPoint: ( - isBlockInsertionPointVisible( state ) && - getBlockInsertionPoint( state ) === insertIndex + isBlockInsertionPointVisible( state, rootUID, layout ) && + getBlockInsertionPoint( state, rootUID ) === insertIndex ), }; }, diff --git a/editor/components/block-list/layout.js b/editor/components/block-list/layout.js index 532d36a855dfe1..b0cedfe918e23a 100644 --- a/editor/components/block-list/layout.js +++ b/editor/components/block-list/layout.js @@ -10,6 +10,8 @@ import { mapValues, sortBy, throttle, + get, + last, } from 'lodash'; import scrollIntoView from 'dom-scroll-into-view'; import 'element-closest'; @@ -18,7 +20,7 @@ import 'element-closest'; * WordPress dependencies */ import { Component } from '@wordpress/element'; -import { serialize } from '@wordpress/blocks'; +import { serialize, getDefaultBlockName } from '@wordpress/blocks'; /** * Internal dependencies @@ -26,8 +28,8 @@ import { serialize } from '@wordpress/blocks'; import './style.scss'; import BlockListBlock from './block'; import BlockSelectionClearer from '../block-selection-clearer'; +import DefaultBlockAppender from '../default-block-appender'; import { - getBlockUids, getMultiSelectedBlocksStartUid, getMultiSelectedBlocksEndUid, getMultiSelectedBlocks, @@ -39,7 +41,7 @@ import { import { startMultiSelect, stopMultiSelect, multiSelect, selectBlock } from '../../store/actions'; import { documentHasSelection } from '../../utils/dom'; -class BlockList extends Component { +class BlockListLayout extends Component { constructor( props ) { super( props ); @@ -245,21 +247,47 @@ class BlockList extends Component { } render() { - const { blocks, showContextualToolbar, renderBlockMenu } = this.props; + const { + blocks, + showContextualToolbar, + layout, + isGroupedByLayout, + rootUID, + renderBlockMenu, + } = this.props; + + let defaultLayout; + if ( isGroupedByLayout ) { + defaultLayout = layout; + } + + const isLastBlockDefault = get( last( blocks ), 'name' ) === getDefaultBlockName(); return ( - - { map( blocks, ( uid ) => ( + + { map( blocks, ( block, blockIndex ) => ( ) ) } + { ( ! blocks.length || ! isLastBlockDefault ) && ( + + ) } ); } @@ -267,7 +295,6 @@ class BlockList extends Component { export default connect( ( state ) => ( { - blocks: getBlockUids( state ), selectionStart: getMultiSelectedBlocksStartUid( state ), selectionEnd: getMultiSelectedBlocksEndUid( state ), multiSelectedBlocks: getMultiSelectedBlocks( state ), @@ -293,4 +320,4 @@ export default connect( dispatch( { type: 'REMOVE_BLOCKS', uids } ); }, } ) -)( BlockList ); +)( BlockListLayout ); diff --git a/editor/components/block-list/multi-controls.js b/editor/components/block-list/multi-controls.js index b6e4970f457c9f..d1dcffa7b38a4b 100644 --- a/editor/components/block-list/multi-controls.js +++ b/editor/components/block-list/multi-controls.js @@ -13,7 +13,7 @@ import { isMultiSelecting, } from '../../store/selectors'; -function BlockListMultiControls( { multiSelectedBlockUids, isSelecting } ) { +function BlockListMultiControls( { multiSelectedBlockUids, rootUID, isSelecting } ) { if ( isSelecting ) { return null; } @@ -21,6 +21,7 @@ function BlockListMultiControls( { multiSelectedBlockUids, isSelecting } ) { return [ , + ); + } + }, + + // A counter tracking active mounted instances: + 0, + ]; + } + + return INNER_BLOCK_LIST_CACHE[ uid ][ 0 ]; +} diff --git a/editor/components/block-mover/index.js b/editor/components/block-mover/index.js index 3110a34ef0e73b..013414f02bd539 100644 --- a/editor/components/block-mover/index.js +++ b/editor/components/block-mover/index.js @@ -2,7 +2,7 @@ * External dependencies */ import { connect } from 'react-redux'; -import { first, last } from 'lodash'; +import { first } from 'lodash'; /** * WordPress dependencies @@ -17,7 +17,7 @@ import { compose } from '@wordpress/element'; */ import './style.scss'; import { getBlockMoverLabel } from './mover-label'; -import { isFirstBlock, isLastBlock, getBlockIndex, getBlock } from '../../store/selectors'; +import { getBlockIndex, getBlock } from '../../store/selectors'; import { selectBlock } from '../../store/actions'; export function BlockMover( { onMoveUp, onMoveDown, isFirst, isLast, uids, blockType, firstIndex, isLocked } ) { @@ -65,39 +65,42 @@ export function BlockMover( { onMoveUp, onMoveDown, isFirst, isLast, uids, block ); } +/** + * Action creator creator which, given the action type to dispatch and the + * arguments of mapDispatchToProps, creates a prop dispatcher callback for + * managing block movement. + * + * @param {string} type Action type to dispatch. + * @param {Function} dispatch Store dispatch. + * @param {Object} ownProps The wrapped component's own props. + * + * @return {Function} Prop dispatcher callback. + */ +function createOnMove( type, dispatch, ownProps ) { + return () => { + const { uids, rootUID } = ownProps; + if ( uids.length === 1 ) { + dispatch( selectBlock( first( uids ) ) ); + } + + dispatch( { type, uids, rootUID } ); + }; +} + export default compose( connect( ( state, ownProps ) => { - const block = getBlock( state, first( ownProps.uids ) ); + const { uids, rootUID } = ownProps; + const block = getBlock( state, first( uids ) ); return ( { - isFirst: isFirstBlock( state, first( ownProps.uids ) ), - isLast: isLastBlock( state, last( ownProps.uids ) ), - firstIndex: getBlockIndex( state, first( ownProps.uids ) ), + firstIndex: getBlockIndex( state, first( uids ), rootUID ), blockType: block ? getBlockType( block.name ) : null, } ); }, - ( dispatch, ownProps ) => ( { - onMoveDown() { - if ( ownProps.uids.length === 1 ) { - dispatch( selectBlock( first( ownProps.uids ) ) ); - } - - dispatch( { - type: 'MOVE_BLOCKS_DOWN', - uids: ownProps.uids, - } ); - }, - onMoveUp() { - if ( ownProps.uids.length === 1 ) { - dispatch( selectBlock( first( ownProps.uids ) ) ); - } - - dispatch( { - type: 'MOVE_BLOCKS_UP', - uids: ownProps.uids, - } ); - }, + ( ...args ) => ( { + onMoveDown: createOnMove( 'MOVE_BLOCKS_DOWN', ...args ), + onMoveUp: createOnMove( 'MOVE_BLOCKS_UP', ...args ), } ) ), withContext( 'editor' )( ( settings ) => { diff --git a/editor/components/default-block-appender/index.js b/editor/components/default-block-appender/index.js index a7cf5a6d2ffeef..bd96ab0537510e 100644 --- a/editor/components/default-block-appender/index.js +++ b/editor/components/default-block-appender/index.js @@ -2,14 +2,11 @@ * External dependencies */ import { connect } from 'react-redux'; -import { last } from 'lodash'; /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { Component } from '@wordpress/element'; -import { getDefaultBlockName } from '@wordpress/blocks'; /** * Internal dependencies @@ -17,48 +14,36 @@ import { getDefaultBlockName } from '@wordpress/blocks'; import './style.scss'; import BlockDropZone from '../block-drop-zone'; import { appendDefaultBlock } from '../../store/actions'; -import { getBlockCount, getBlocks } from '../../store/selectors'; -export class DefaultBlockAppender extends Component { - render() { - const { count, blocks } = this.props; - const lastBlock = last( blocks ); - const showAppender = lastBlock && lastBlock.name !== getDefaultBlockName(); - - return ( -
- { ( count === 0 || showAppender ) && } - { count === 0 && - - } - { count !== 0 && showAppender && - - } -
- ); - } +export function DefaultBlockAppender( { onAppend, showPrompt = true } ) { + return ( +
+ + +
+ ); } export default connect( - ( state ) => ( { - count: getBlockCount( state ), - blocks: getBlocks( state ), + null, + ( dispatch, ownProps ) => ( { + onAppend() { + const { layout, rootUID } = ownProps; + + let attributes; + if ( layout ) { + attributes = { layout }; + } + + dispatch( appendDefaultBlock( attributes, rootUID ) ); + }, } ), - { appendDefaultBlock } )( DefaultBlockAppender ); diff --git a/editor/components/default-block-appender/test/__snapshots__/index.js.snap b/editor/components/default-block-appender/test/__snapshots__/index.js.snap index 4b68fc5e90559c..a8cafd88e0773e 100644 --- a/editor/components/default-block-appender/test/__snapshots__/index.js.snap +++ b/editor/components/default-block-appender/test/__snapshots__/index.js.snap @@ -1,12 +1,41 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`DefaultBlockAppender blocks present should match snapshot 1`] = ` +exports[`DefaultBlockAppender should append a default block when input focused 1`] = `
+> + + +
`; -exports[`DefaultBlockAppender no block present should match snapshot 1`] = ` +exports[`DefaultBlockAppender should match snapshot 1`] = `
@@ -22,3 +51,20 @@ exports[`DefaultBlockAppender no block present should match snapshot 1`] = ` />
`; + +exports[`DefaultBlockAppender should optionally show without prompt 1`] = ` +
+ + +
+`; diff --git a/editor/components/default-block-appender/test/index.js b/editor/components/default-block-appender/test/index.js index cc681b935ac220..02a512388c0bd5 100644 --- a/editor/components/default-block-appender/test/index.js +++ b/editor/components/default-block-appender/test/index.js @@ -9,52 +9,47 @@ import { shallow } from 'enzyme'; import { DefaultBlockAppender } from '../'; describe( 'DefaultBlockAppender', () => { - const expectAppendDefaultBlockCalled = ( appendDefaultBlock ) => { - expect( appendDefaultBlock ).toHaveBeenCalledTimes( 1 ); - expect( appendDefaultBlock ).toHaveBeenCalledWith(); + const expectOnAppendCalled = ( onAppend ) => { + expect( onAppend ).toHaveBeenCalledTimes( 1 ); + expect( onAppend ).toHaveBeenCalledWith(); }; - describe( 'no block present', () => { - it( 'should match snapshot', () => { - const appendDefaultBlock = jest.fn(); - const wrapper = shallow( ); + it( 'should match snapshot', () => { + const onAppend = jest.fn(); + const wrapper = shallow( ); - expect( wrapper ).toMatchSnapshot(); - } ); + expect( wrapper ).toMatchSnapshot(); + } ); - it( 'should append a default block when input clicked', () => { - const appendDefaultBlock = jest.fn(); - const wrapper = shallow( ); + it( 'should append a default block when input clicked', () => { + const onAppend = jest.fn(); + const wrapper = shallow( ); + const input = wrapper.find( 'input.editor-default-block-appender__content' ); - wrapper.find( 'input.editor-default-block-appender__content' ).simulate( 'click' ); + expect( input.prop( 'value' ) ).toEqual( 'Write your story' ); + input.simulate( 'click' ); - expectAppendDefaultBlockCalled( appendDefaultBlock ); - } ); + expectOnAppendCalled( onAppend ); + } ); - it( 'should append a default block when input focused', () => { - const appendDefaultBlock = jest.fn(); - const wrapper = shallow( ); + it( 'should append a default block when input focused', () => { + const onAppend = jest.fn(); + const wrapper = shallow( ); - wrapper.find( 'input.editor-default-block-appender__content' ).simulate( 'focus' ); + wrapper.find( 'input.editor-default-block-appender__content' ).simulate( 'focus' ); - expectAppendDefaultBlockCalled( appendDefaultBlock ); - } ); - } ); + expect( wrapper ).toMatchSnapshot(); - describe( 'blocks present', () => { - it( 'should match snapshot', () => { - const wrapper = shallow( ); - - expect( wrapper ).toMatchSnapshot(); - } ); + expectOnAppendCalled( onAppend ); + } ); - it( 'should append a default block when button clicked', () => { - const insertBlock = jest.fn(); - const wrapper = shallow( ); + it( 'should optionally show without prompt', () => { + const onAppend = jest.fn(); + const wrapper = shallow( ); + const input = wrapper.find( 'input.editor-default-block-appender__content' ); - wrapper.find( 'input.editor-default-block-appender__content' ).simulate( 'click' ); + expect( input.prop( 'value' ) ).toEqual( '' ); - expectAppendDefaultBlockCalled( insertBlock ); - } ); + expect( wrapper ).toMatchSnapshot(); } ); } ); diff --git a/editor/components/editor-global-keyboard-shortcuts/index.js b/editor/components/editor-global-keyboard-shortcuts/index.js index 493136635cd010..cb591b7b1104f2 100644 --- a/editor/components/editor-global-keyboard-shortcuts/index.js +++ b/editor/components/editor-global-keyboard-shortcuts/index.js @@ -13,7 +13,7 @@ import { KeyboardShortcuts, withContext } from '@wordpress/components'; /** * Internal dependencies */ -import { getBlockUids, getMultiSelectedBlockUids } from '../../store/selectors'; +import { getBlockOrder, getMultiSelectedBlockUids } from '../../store/selectors'; import { clearSelectedBlock, multiSelect, redo, undo, removeBlocks } from '../../store/actions'; class EditorGlobalKeyboardShortcuts extends Component { @@ -69,7 +69,7 @@ export default compose( connect( ( state ) => { return { - uids: getBlockUids( state ), + uids: getBlockOrder( state ), multiSelectedBlockUids: getMultiSelectedBlockUids( state ), }; }, diff --git a/editor/components/index.js b/editor/components/index.js index 252b8965b39ec3..c13e32820817dc 100644 --- a/editor/components/index.js +++ b/editor/components/index.js @@ -54,7 +54,7 @@ export { default as WordCount } from './word-count'; // Content Related Components export { default as BlockInspector } from './block-inspector'; -export { default as BlockList } from './block-list/layout'; +export { default as BlockList } from './block-list'; export { default as BlockMover } from './block-mover'; export { default as BlockSelectionClearer } from './block-selection-clearer'; export { default as BlockSettingsMenu } from './block-settings-menu'; diff --git a/editor/components/inserter/index.js b/editor/components/inserter/index.js index e7063ff88c811b..e06a2ce5f0a171 100644 --- a/editor/components/inserter/index.js +++ b/editor/components/inserter/index.js @@ -81,6 +81,7 @@ class Inserter extends Component { renderContent={ ( { onClose } ) => { const onSelect = ( item ) => { onInsertBlock( item, insertionPoint ); + onClose(); }; @@ -93,16 +94,19 @@ class Inserter extends Component { export default compose( [ connect( - ( state ) => { + ( state, ownProps ) => { return { - insertionPoint: getBlockInsertionPoint( state ), + insertionPoint: getBlockInsertionPoint( state, ownProps.rootUID ), }; }, - ( dispatch ) => ( { - onInsertBlock( item, position ) { + ( dispatch, ownProps ) => ( { + onInsertBlock( item, index ) { + const { rootUID, layout } = ownProps; + const { name, initialAttributes } = item; dispatch( insertBlock( - createBlock( item.name, item.initialAttributes ), - position + createBlock( name, { ...initialAttributes, layout } ), + index, + rootUID, ) ); }, ...bindActionCreators( { diff --git a/editor/components/provider/index.js b/editor/components/provider/index.js index b52f5684c753c2..906092835d2c74 100644 --- a/editor/components/provider/index.js +++ b/editor/components/provider/index.js @@ -58,7 +58,7 @@ class EditorProvider extends Component { constructor( props ) { super( ...arguments ); - this.store = props.store || store; + this.store = store; this.initializeMetaBoxes = this.initializeMetaBoxes.bind( this ); this.settings = { @@ -67,7 +67,7 @@ class EditorProvider extends Component { }; // Assume that we don't need to initialize in the case of an error recovery. - if ( ! props.recovery && props.post ) { + if ( ! props.recovery ) { this.store.dispatch( setupEditor( props.post, this.settings ) ); } } diff --git a/editor/components/writing-flow/index.js b/editor/components/writing-flow/index.js index 157a2a21d02ab5..f4f0ceeaee8a13 100644 --- a/editor/components/writing-flow/index.js +++ b/editor/components/writing-flow/index.js @@ -3,7 +3,7 @@ */ import { connect } from 'react-redux'; import 'element-closest'; -import { find, last, reverse, clamp } from 'lodash'; +import { find, last, reverse } from 'lodash'; /** * WordPress dependencies */ @@ -22,7 +22,8 @@ import { placeCaretAtVerticalEdge, } from '../../utils/dom'; import { - getBlockUids, + getPreviousBlock, + getNextBlock, getMultiSelectedBlocksStartUid, getMultiSelectedBlocksEndUid, getMultiSelectedBlocks, @@ -134,16 +135,22 @@ class WritingFlow extends Component { blockEl.contains( el ) && isElementNonEmpty( el ) ); } - expandSelection( blocks, currentStartUid, currentEndUid, delta ) { - const lastIndex = blocks.indexOf( currentEndUid ); - const nextIndex = clamp( lastIndex + delta, 0, blocks.length - 1 ); - this.props.onMultiSelect( currentStartUid, blocks[ nextIndex ] ); + expandSelection( currentStartUid, isReverse ) { + const { previousBlock, nextBlock } = this.props; + + const expandedBlock = isReverse ? previousBlock : nextBlock; + if ( expandedBlock ) { + this.props.onMultiSelect( currentStartUid, expandedBlock.uid ); + } } - moveSelection( blocks, currentUid, delta ) { - const currentIndex = blocks.indexOf( currentUid ); - const nextIndex = clamp( currentIndex + delta, 0, blocks.length - 1 ); - this.props.onFocusBlock( blocks[ nextIndex ] ); + moveSelection( currentUid, isReverse ) { + const { previousBlock, nextBlock } = this.props; + + const focusedBlock = isReverse ? previousBlock : nextBlock; + if ( focusedBlock ) { + this.props.onFocusBlock( focusedBlock.uid ); + } } isEditableEdge( moveUp, target ) { @@ -154,7 +161,7 @@ class WritingFlow extends Component { } onKeyDown( event ) { - const { selectedBlock, selectionStart, selectionEnd, blocks, hasMultiSelection } = this.props; + const { selectedBlock, selectionStart, selectionEnd, hasMultiSelection } = this.props; const { keyCode, target } = event; const isUp = keyCode === UP; @@ -178,15 +185,15 @@ class WritingFlow extends Component { if ( isNav && isShift && hasMultiSelection ) { // Shift key is down and existing block selection event.preventDefault(); - this.expandSelection( blocks, selectionStart, selectionEnd, isReverse ? -1 : +1 ); + this.expandSelection( selectionStart, isReverse ); } else if ( isNav && isShift && this.isEditableEdge( isReverse, target ) && isNavEdge( target, isReverse, true ) ) { // Shift key is down, but no existing block selection event.preventDefault(); - this.expandSelection( blocks, selectedBlock.uid, selectedBlock.uid, isReverse ? -1 : +1 ); + this.expandSelection( selectedBlock.uid, isReverse ); } else if ( isNav && hasMultiSelection ) { // Moving from multi block selection to single block selection event.preventDefault(); - this.moveSelection( blocks, selectionEnd, isReverse ? -1 : +1 ); + this.moveSelection( selectionEnd, isReverse ); } else if ( isVertical && isVerticalEdge( target, isReverse, isShift ) ) { const closestTabbable = this.getClosestTabbable( target, isReverse ); placeCaretAtVerticalEdge( closestTabbable, isReverse, this.verticalRect ); @@ -226,7 +233,8 @@ class WritingFlow extends Component { export default connect( ( state ) => ( { - blocks: getBlockUids( state ), + previousBlock: getPreviousBlock( state ), + nextBlock: getNextBlock( state ), selectionStart: getMultiSelectedBlocksStartUid( state ), selectionEnd: getMultiSelectedBlocksEndUid( state ), hasMultiSelection: getMultiSelectedBlocks( state ).length > 1, diff --git a/editor/store/actions.js b/editor/store/actions.js index 8240520008572e..9727be15ac05a3 100644 --- a/editor/store/actions.js +++ b/editor/store/actions.js @@ -187,15 +187,36 @@ export function replaceBlock( uid, block ) { return replaceBlocks( uid, block ); } -export function insertBlock( block, position ) { - return insertBlocks( [ block ], position ); +/** + * Returns an action object used in signalling that a single block should be + * inserted, optionally at a specific index respective a root block list. + * + * @param {Object} block Block object to insert. + * @param {?number} index Index at which block should be inserted. + * @param {?string} rootUID Optional root UID of block list to insert. + * + * @return {Object} Action object. + */ +export function insertBlock( block, index, rootUID ) { + return insertBlocks( [ block ], index, rootUID ); } -export function insertBlocks( blocks, position ) { +/** + * Returns an action object used in signalling that an array of blocks should + * be inserted, optionally at a specific index respective a root block list. + * + * @param {Object[]} blocks Block objects to insert. + * @param {?number} index Index at which block should be inserted. + * @param {?string} rootUID Optional root UID of block list to insert. + * + * @return {Object} Action object. + */ +export function insertBlocks( blocks, index, rootUID ) { return { type: 'INSERT_BLOCKS', blocks: castArray( blocks ), - position, + index, + rootUID, }; } @@ -541,9 +562,19 @@ export function convertBlockToReusable( uid ) { uid, }; } - -export function appendDefaultBlock() { +/** + * Returns an action object used in signalling that a new block of the default + * type should be appended to the block list. + * + * @param {?Object} attributes Optional attributes of the block to assign. + * @param {?string} rootUID Optional root UID of block list to append. + * + * @return {Object} Action object + */ +export function appendDefaultBlock( attributes, rootUID ) { return { type: 'APPEND_DEFAULT_BLOCK', + attributes, + rootUID, }; } diff --git a/editor/store/effects.js b/editor/store/effects.js index 646d317b5dee71..5a6c69c2af330b 100644 --- a/editor/store/effects.js +++ b/editor/store/effects.js @@ -458,13 +458,19 @@ export default { const oldBlock = getBlock( getState(), action.uid ); const reusableBlock = createReusableBlock( oldBlock.name, oldBlock.attributes ); - const newBlock = createBlock( 'core/block', { ref: reusableBlock.id } ); + const newBlock = createBlock( 'core/block', { + ref: reusableBlock.id, + layout: oldBlock.attributes.layout, + } ); dispatch( updateReusableBlock( reusableBlock.id, reusableBlock ) ); dispatch( saveReusableBlock( reusableBlock.id ) ); dispatch( replaceBlocks( [ oldBlock.uid ], [ newBlock ] ) ); }, - APPEND_DEFAULT_BLOCK() { - return insertBlock( createBlock( getDefaultBlockName() ) ); + APPEND_DEFAULT_BLOCK( action ) { + const { attributes, rootUID } = action; + const block = createBlock( getDefaultBlockName(), attributes ); + + return insertBlock( block, undefined, rootUID ); }, CREATE_NOTICE( { notice: { content, spokenMessage } } ) { const message = spokenMessage || content; diff --git a/editor/store/index.js b/editor/store/index.js index eca962b4aefffb..fd9bbd9f226542 100644 --- a/editor/store/index.js +++ b/editor/store/index.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { createStore as createReduxStore } from 'redux'; - /** * WordPress Dependencies */ @@ -12,9 +7,7 @@ import { registerReducer, registerSelectors, withRehydratation, loadAndPersist } * Internal dependencies */ import reducer from './reducer'; -import enhanceWithBrowserSize from './mobile'; import applyMiddlewares from './middlewares'; -import { BREAK_MEDIUM } from './constants'; import { getCurrentPostType, getEditedPostContent, @@ -29,35 +22,10 @@ import { const STORAGE_KEY = `GUTENBERG_PREFERENCES_${ window.userSettings.uid }`; const MODULE_KEY = 'core/editor'; -/** - * Creates a Redux store for editor state, enhanced with middlewares, persistence, - * and browser size observer. - * - * @return {Object} Redux store - */ -export function createStore() { - const store = applyMiddlewares( createReduxStore( withRehydratation( reducer, 'preferences', STORAGE_KEY ) ) ); - loadAndPersist( store, reducer, 'preferences', STORAGE_KEY ); - enhanceWithBrowserSize( store, BREAK_MEDIUM ); - - return store; -} - -/** - * Registers an editor state store, enhanced with middlewares, persistence, and - * browser size observer. - * - * @return {Object} Registered data store - */ -export function createRegisteredStore() { - const store = applyMiddlewares( - registerReducer( 'core/editor', withRehydratation( reducer, 'preferences', STORAGE_KEY ) ) - ); - loadAndPersist( store, reducer, 'preferences', STORAGE_KEY ); - enhanceWithBrowserSize( store, BREAK_MEDIUM ); - - return store; -} +const store = applyMiddlewares( + registerReducer( MODULE_KEY, withRehydratation( reducer, 'preferences', STORAGE_KEY ) ) +); +loadAndPersist( store, reducer, 'preferences', STORAGE_KEY ); registerSelectors( MODULE_KEY, { getCurrentPostType, @@ -67,4 +35,4 @@ registerSelectors( MODULE_KEY, { getCurrentPostSlug, } ); -export default createRegisteredStore(); +export default store; diff --git a/editor/store/reducer.js b/editor/store/reducer.js index 00c96794373098..861c5b1defe0b3 100644 --- a/editor/store/reducer.js +++ b/editor/store/reducer.js @@ -7,7 +7,6 @@ import { flow, partialRight, reduce, - keyBy, first, last, omit, @@ -45,6 +44,56 @@ export function getPostRawValue( value ) { return value; } +/** + * Given an array of blocks, returns an object where each key is a nesting + * context, the value of which is an array of block UIDs existing within that + * nesting context. + * + * @param {Array} blocks Blocks to map. + * @param {?string} rootUID Assumed root UID. + * + * @return {Object} Block order map object. + */ +function mapBlockOrder( blocks, rootUID = '' ) { + const result = { [ rootUID ]: [] }; + + blocks.forEach( ( block ) => { + const { uid, innerBlocks } = block; + + result[ rootUID ].push( uid ); + + Object.assign( result, mapBlockOrder( innerBlocks, uid ) ); + } ); + + return result; +} + +/** + * Given an array of blocks, returns an object containing all blocks, recursing + * into inner blocks. Keys correspond to the block UID, the value of which is + * the block object. + * + * @param {Array} blocks Blocks to flatten. + * + * @return {Object} Flattened blocks object. + */ +function getFlattenedBlocks( blocks ) { + const flattenedBlocks = {}; + + const stack = [ ...blocks ]; + while ( stack.length ) { + // `innerBlocks` is redundant data which can fall out of sync, since + // this is reflected in `blockOrder`, so exclude from appended block. + const { innerBlocks, ...block } = stack.shift(); + + stack.push( ...innerBlocks ); + + flattenedBlocks[ block.uid ] = block; + } + + return flattenedBlocks; +} + /** * Undoable reducer returning the editor post state, including blocks parsed * from current HTML markup. @@ -53,7 +102,8 @@ export function getPostRawValue( value ) { * - edits: an object describing changes to be made to the current post, in * the format accepted by the WP REST API * - blocksByUid: post content blocks keyed by UID - * - blockOrder: list of block UIDs in order + * - blockOrder: object where each key is a UID, its value an array of uids + * representing the order of its inner blocks * * @param {Object} state Current state. * @param {Object} action Dispatched action. @@ -117,7 +167,7 @@ export const editor = flow( [ blocksByUid( state = {}, action ) { switch ( action.type ) { case 'RESET_BLOCKS': - return keyBy( action.blocks, 'uid' ); + return getFlattenedBlocks( action.blocks ); case 'UPDATE_BLOCK_ATTRIBUTES': // Ignore updates if block isn't known @@ -171,19 +221,18 @@ export const editor = flow( [ case 'INSERT_BLOCKS': return { ...state, - ...keyBy( action.blocks, 'uid' ), + ...getFlattenedBlocks( action.blocks ), }; case 'REPLACE_BLOCKS': if ( ! action.blocks ) { return state; } - return action.blocks.reduce( ( memo, block ) => { - return { - ...memo, - [ block.uid ]: block, - }; - }, omit( state, action.uids ) ); + + return { + ...omit( state, action.uids ), + ...getFlattenedBlocks( action.blocks ), + }; case 'REMOVE_BLOCKS': return omit( state, action.uids ); @@ -218,80 +267,127 @@ export const editor = flow( [ return state; }, - blockOrder( state = [], action ) { + blockOrder( state = {}, action ) { switch ( action.type ) { case 'RESET_BLOCKS': - return action.blocks.map( ( { uid } ) => uid ); + return mapBlockOrder( action.blocks ); case 'INSERT_BLOCKS': { - const position = action.position !== undefined ? action.position : state.length; - return [ - ...state.slice( 0, position ), - ...action.blocks.map( block => block.uid ), - ...state.slice( position ), - ]; + const { rootUID = '', blocks } = action; + + const subState = state[ rootUID ] || []; + const mappedBlocks = mapBlockOrder( blocks, rootUID ); + + const { index = subState.length } = action; + + return { + ...state, + ...mappedBlocks, + [ rootUID ]: [ + ...subState.slice( 0, index ), + ...mappedBlocks[ rootUID ], + ...subState.slice( index ), + ], + }; } case 'MOVE_BLOCKS_UP': { - const firstUid = first( action.uids ); - const lastUid = last( action.uids ); + const { uids, rootUID = '' } = action; + const firstUid = first( uids ); + const lastUid = last( uids ); + const subState = state[ rootUID ]; - if ( ! state.length || firstUid === first( state ) ) { + if ( ! subState.length || firstUid === first( subState ) ) { return state; } - const firstIndex = state.indexOf( firstUid ); - const lastIndex = state.indexOf( lastUid ); - const swappedUid = state[ firstIndex - 1 ]; + const firstIndex = subState.indexOf( firstUid ); + const lastIndex = subState.indexOf( lastUid ); + const swappedUid = subState[ firstIndex - 1 ]; - return [ - ...state.slice( 0, firstIndex - 1 ), - ...action.uids, - swappedUid, - ...state.slice( lastIndex + 1 ), - ]; + return { + ...state, + [ rootUID ]: [ + ...subState.slice( 0, firstIndex - 1 ), + ...uids, + swappedUid, + ...subState.slice( lastIndex + 1 ), + ], + }; } case 'MOVE_BLOCKS_DOWN': { - const firstUid = first( action.uids ); - const lastUid = last( action.uids ); + const { uids, rootUID = '' } = action; + const firstUid = first( uids ); + const lastUid = last( uids ); + const subState = state[ rootUID ]; - if ( ! state.length || lastUid === last( state ) ) { + if ( ! subState.length || lastUid === last( subState ) ) { return state; } - const firstIndex = state.indexOf( firstUid ); - const lastIndex = state.indexOf( lastUid ); - const swappedUid = state[ lastIndex + 1 ]; + const firstIndex = subState.indexOf( firstUid ); + const lastIndex = subState.indexOf( lastUid ); + const swappedUid = subState[ lastIndex + 1 ]; - return [ - ...state.slice( 0, firstIndex ), - swappedUid, - ...action.uids, - ...state.slice( lastIndex + 2 ), - ]; + return { + ...state, + [ rootUID ]: [ + ...subState.slice( 0, firstIndex ), + swappedUid, + ...uids, + ...subState.slice( lastIndex + 2 ), + ], + }; } - case 'REPLACE_BLOCKS': - if ( ! action.blocks ) { + case 'REPLACE_BLOCKS': { + const { blocks, uids } = action; + if ( ! blocks ) { return state; } - return state.reduce( ( memo, uid ) => { - if ( uid === action.uids[ 0 ] ) { - return memo.concat( action.blocks.map( ( block ) => block.uid ) ); - } - if ( action.uids.indexOf( uid ) === -1 ) { - memo.push( uid ); - } - return memo; - }, [] ); + const mappedBlocks = mapBlockOrder( blocks ); + + return flow( [ + ( nextState ) => omit( nextState, uids ), + ( nextState ) => mapValues( nextState, ( subState ) => ( + reduce( subState, ( result, uid ) => { + if ( uid === uids[ 0 ] ) { + return [ + ...result, + ...mappedBlocks[ '' ], + ]; + } + + if ( uids.indexOf( uid ) === -1 ) { + result.push( uid ); + } + + return result; + }, [] ) + ) ), + ] )( { + ...state, + ...omit( mappedBlocks, '' ), + } ); + } case 'REMOVE_BLOCKS': - return without( state, ...action.uids ); - - case 'REMOVE_REUSABLE_BLOCK': - return without( state, ...action.associatedBlockUids ); + case 'REMOVE_REUSABLE_BLOCK': { + const { type, uids, associatedBlockUids } = action; + const uidsToRemove = type === 'REMOVE_BLOCKS' ? uids : associatedBlockUids; + + return flow( [ + // Remove inner block ordering for removed blocks + ( nextState ) => omit( nextState, uidsToRemove ), + + // Remove deleted blocks from other blocks' orderings + ( nextState ) => mapValues( nextState, ( subState ) => ( + without( subState, ...uidsToRemove ) + ) ), + ] )( state ); + } } return state; diff --git a/editor/store/selectors.js b/editor/store/selectors.js index ffa12d79549179..80bd6ccb88ee8e 100644 --- a/editor/store/selectors.js +++ b/editor/store/selectors.js @@ -3,6 +3,7 @@ */ import moment from 'moment'; import { + map, first, get, has, @@ -12,6 +13,7 @@ import { find, some, unionWith, + includes, } from 'lodash'; import createSelector from 'rememo'; @@ -28,6 +30,15 @@ import { addQueryArgs } from '@wordpress/url'; const MAX_RECENT_BLOCKS = 8; export const POST_UPDATE_TRANSACTION_ID = 'post-update'; +/** + * Shared reference to an empty array used as the default block order return + * value when the state value is not explicitly assigned, since we want to + * avoid returning a new array reference on every invocation. + * + * @type {Array} + */ +const DEFAULT_BLOCK_ORDER = []; + /** * Returns the state of legacy meta boxes. * @@ -387,7 +398,7 @@ export function getEditedPostPreviewLink( state ) { * @param {Object} state Global application state. * @param {string} uid Block unique ID. * - * @returns {Object} Parsed block object. + * @return {Object} Parsed block object. */ export const getBlock = createSelector( ( state, uid ) => { @@ -439,13 +450,17 @@ function getPostMeta( state, key ) { * the order they appear in the post. * Note: It's important to memoize this selector to avoid return a new instance on each call * - * @param {Object} state Global application state. + * @param {Object} state Global application state. + * @param {?String} rootUID Optional root UID of block list. * - * @returns {Object[]} Post blocks. + * @return {Object[]} Post blocks. */ export const getBlocks = createSelector( - ( state ) => { - return state.editor.present.blockOrder.map( ( uid ) => getBlock( state, uid ) ); + ( state, rootUID ) => { + return map( getBlockOrder( state, rootUID ), ( uid ) => ( { + ...getBlock( state, uid ), + innerBlocks: getBlocks( state, uid ), + } ) ); }, ( state ) => [ state.editor.present.blockOrder, @@ -456,12 +471,13 @@ export const getBlocks = createSelector( /** * Returns the number of blocks currently present in the post. * - * @param {Object} state Global application state. + * @param {Object} state Global application state. + * @param {?string} rootUID Optional root UID of block list. * * @return {number} Number of blocks in the post. */ -export function getBlockCount( state ) { - return getBlockUids( state ).length; +export function getBlockCount( state, rootUID ) { + return getBlockOrder( state, rootUID ).length; } /** @@ -497,22 +513,134 @@ export function getSelectedBlock( state ) { return getBlock( state, start ); } +/** + * Given a block UID, returns the root block from which the block is nested, an + * empty string for top-level blocks, or null if the block does not exist. + * + * @param {Object} state Global application state. + * @param {string} uid Block from which to find root UID. + * + * @return {?string} Root UID, if exists + */ +export function getBlockRootUID( state, uid ) { + const { blockOrder } = state.editor.present; + + for ( const rootUID in blockOrder ) { + if ( includes( blockOrder[ rootUID ], uid ) ) { + return rootUID; + } + } + + return null; +} + +/** + * Returns the block adjacent one at the given reference startUID and modifier + * directionality. Defaults start UID to the selected block, and direction as + * next block. Returns null if there is no adjacent block. + * + * @param {Object} state Global application state. + * @param {?string} startUID Optional UID of block from which to search. + * @param {?number} modifier Directionality multiplier (1 next, -1 previous). + * + * @return {?Object} Adjacent block object, or null if none exists. + */ +export function getAdjacentBlock( state, startUID, modifier = 1 ) { + // Default to selected block. + if ( startUID === undefined ) { + startUID = get( getSelectedBlock( state ), 'uid' ); + } + + // Try multi-selection starting at extent based on modifier. + if ( startUID === undefined ) { + if ( modifier < 0 ) { + startUID = getFirstMultiSelectedBlockUid( state ); + } else { + startUID = getLastMultiSelectedBlockUid( state ); + } + } + + // Validate working start UID. + if ( ! startUID ) { + return null; + } + + // Retrieve start block root UID, being careful to allow the falsey empty + // string top-level root UID by explicitly testing against null. + const rootUID = getBlockRootUID( state, startUID ); + if ( rootUID === null ) { + return null; + } + + const { blockOrder } = state.editor.present; + const orderSet = blockOrder[ rootUID ]; + const index = orderSet.indexOf( startUID ); + const nextIndex = ( index + ( 1 * modifier ) ); + + // Block was first in set and we're attempting to get previous. + if ( nextIndex < 0 ) { + return null; + } + + // Block was last in set and we're attempting to get next. + if ( nextIndex === orderSet.length ) { + return null; + } + + // Assume incremented index is within the set. + return getBlock( state, orderSet[ nextIndex ] ); +} + +/** + * Returns the previous block from the given reference startUID. Defaults start + * UID to the selected block. Returns null if there is no previous block. + * + * @param {Object} state Global application state. + * @param {?string} startUID Optional UID of block from which to search. + * + * @return {?Object} Adjacent block object, or null if none exists. + */ +export function getPreviousBlock( state, startUID ) { + return getAdjacentBlock( state, startUID, -1 ); +} + +/** + * Returns the next block from the given reference startUID. Defaults start UID + * to the selected block. Returns null if there is no next block. + * + * @param {Object} state Global application state. + * @param {?string} startUID Optional UID of block from which to search. + * + * @return {?Object} Adjacent block object, or null if none exists. + */ +export function getNextBlock( state, startUID ) { + return getAdjacentBlock( state, startUID, 1 ); +} + /** * Returns the current multi-selection set of blocks unique IDs, or an empty * array if there is no multi-selection. * * @param {Object} state Global application state. * - * @returns {Array} Multi-selected block unique IDs. + * @return {Array} Multi-selected block unique IDs. */ export const getMultiSelectedBlockUids = createSelector( ( state ) => { - const { blockOrder } = state.editor.present; const { start, end } = state.blockSelection; if ( start === end ) { return []; } + // Retrieve root UID to aid in retrieving relevant nested block order, + // being careful to allow the falsey empty string top-level root UID by + // explicitly testing against null. + const rootUID = getBlockRootUID( state, start ); + if ( rootUID === null ) { + return []; + } + + const blockOrder = getBlockOrder( state, rootUID ); const startIndex = blockOrder.indexOf( start ); const endIndex = blockOrder.indexOf( end ); @@ -535,7 +663,7 @@ export const getMultiSelectedBlockUids = createSelector( * * @param {Object} state Global application state. * - * @returns {Array} Multi-selected block objects. + * @return {Array} Multi-selected block objects. */ export const getMultiSelectedBlocks = createSelector( ( state ) => getMultiSelectedBlockUids( state ).map( ( uid ) => getBlock( state, uid ) ), @@ -640,81 +768,31 @@ export function getMultiSelectedBlocksEndUid( state ) { /** * Returns an array containing all block unique IDs of the post being edited, - * in the order they appear in the post. + * in the order they appear in the post. Optionally accepts a root UID of the + * block list for which the order should be returned, defaulting to the top- + * level block order. * - * @param {Object} state Global application state. + * @param {Object} state Global application state. + * @param {?string} rootUID Optional root UID of block list. * * @return {Array} Ordered unique IDs of post blocks. */ -export function getBlockUids( state ) { - return state.editor.present.blockOrder; +export function getBlockOrder( state, rootUID ) { + return state.editor.present.blockOrder[ rootUID || '' ] || DEFAULT_BLOCK_ORDER; } /** * Returns the index at which the block corresponding to the specified unique ID * occurs within the post block order, or `-1` if the block does not exist. * - * @param {Object} state Global application state. - * @param {string} uid Block unique ID. + * @param {Object} state Global application state. + * @param {string} uid Block unique ID. + * @param {?string} rootUID Optional root UID of block list. * * @return {number} Index at which block exists in order. */ -export function getBlockIndex( state, uid ) { - return state.editor.present.blockOrder.indexOf( uid ); -} - -/** - * Returns true if the block corresponding to the specified unique ID is the - * first block of the post, or false otherwise. - * - * @param {Object} state Global application state. - * @param {string} uid Block unique ID. - * - * @return {boolean} Whether block is first in post. - */ -export function isFirstBlock( state, uid ) { - return first( state.editor.present.blockOrder ) === uid; -} - -/** - * Returns true if the block corresponding to the specified unique ID is the - * last block of the post, or false otherwise. - * - * @param {Object} state Global application state. - * @param {string} uid Block unique ID. - * - * @return {boolean} Whether block is last in post. - */ -export function isLastBlock( state, uid ) { - return last( state.editor.present.blockOrder ) === uid; -} - -/** - * Returns the block object occurring before the one corresponding to the - * specified unique ID. - * - * @param {Object} state Global application state. - * @param {string} uid Block unique ID. - * - * @return {Object} Block occurring before specified unique ID. - */ -export function getPreviousBlock( state, uid ) { - const order = getBlockIndex( state, uid ); - return state.editor.present.blocksByUid[ state.editor.present.blockOrder[ order - 1 ] ] || null; -} - -/** - * Returns the block object occurring after the one corresponding to the - * specified unique ID. - * - * @param {Object} state Global application state. - * @param {string} uid Block unique ID. - * - * @return {Object} Block occurring after specified unique ID. - */ -export function getNextBlock( state, uid ) { - const order = getBlockIndex( state, uid ); - return state.editor.present.blocksByUid[ state.editor.present.blockOrder[ order + 1 ] ] || null; +export function getBlockIndex( state, uid, rootUID ) { + return getBlockOrder( state, rootUID ).indexOf( uid ); } /** @@ -837,24 +915,25 @@ export function isTyping( state ) { /** * Returns the insertion point, the index at which the new inserted block would - * be placed. Defaults to the last position. + * be placed. Defaults to the last index. * - * @param {Object} state Global application state. + * @param {Object} state Global application state. + * @param {?string} rootUID Optional root UID of block list. * * @return {?string} Unique ID after which insertion will occur. */ -export function getBlockInsertionPoint( state ) { +export function getBlockInsertionPoint( state, rootUID ) { const lastMultiSelectedBlock = getLastMultiSelectedBlockUid( state ); if ( lastMultiSelectedBlock ) { - return getBlockIndex( state, lastMultiSelectedBlock ) + 1; + return getBlockIndex( state, lastMultiSelectedBlock, rootUID ) + 1; } const selectedBlock = getSelectedBlock( state ); if ( selectedBlock ) { - return getBlockIndex( state, selectedBlock.uid ) + 1; + return getBlockIndex( state, selectedBlock.uid, rootUID ) + 1; } - return state.editor.present.blockOrder.length; + return getBlockOrder( state, rootUID ).length; } /** @@ -913,20 +992,20 @@ export function didPostSaveRequestFail( state ) { * @return {?string} Suggested post format. */ export function getSuggestedPostFormat( state ) { - const blocks = state.editor.present.blockOrder; + const blocks = getBlockOrder( state ); let name; // If there is only one block in the content of the post grab its name // so we can derive a suitable post format from it. if ( blocks.length === 1 ) { - name = state.editor.present.blocksByUid[ blocks[ 0 ] ].name; + name = getBlock( state, blocks[ 0 ] ).name; } // If there are two blocks in the content and the last one is a text blocks // grab the name of the first one to also suggest a post format from it. if ( blocks.length === 2 ) { - if ( state.editor.present.blocksByUid[ blocks[ 1 ] ].name === 'core/paragraph' ) { - name = state.editor.present.blocksByUid[ blocks[ 0 ] ].name; + if ( getBlock( state, blocks[ 1 ] ).name === 'core/paragraph' ) { + name = getBlock( state, blocks[ 0 ] ).name; } } @@ -958,7 +1037,7 @@ export function getSuggestedPostFormat( state ) { * * @param {Object} state Global application state. * - * @returns {string} Post content. + * @return {string} Post content. */ export const getEditedPostContent = createSelector( ( state ) => { diff --git a/editor/store/test/actions.js b/editor/store/test/actions.js index 83d0a1a504d5c7..404f65b7261aa4 100644 --- a/editor/store/test/actions.js +++ b/editor/store/test/actions.js @@ -215,11 +215,11 @@ describe( 'actions', () => { const block = { uid: 'ribs', }; - const position = 5; - expect( insertBlock( block, position ) ).toEqual( { + const index = 5; + expect( insertBlock( block, index ) ).toEqual( { type: 'INSERT_BLOCKS', blocks: [ block ], - position, + index, } ); } ); } ); @@ -229,11 +229,11 @@ describe( 'actions', () => { const blocks = [ { uid: 'ribs', } ]; - const position = 3; - expect( insertBlocks( blocks, position ) ).toEqual( { + const index = 3; + expect( insertBlocks( blocks, index ) ).toEqual( { type: 'INSERT_BLOCKS', blocks, - position, + index, } ); } ); } ); diff --git a/editor/store/test/reducer.js b/editor/store/test/reducer.js index fd53c7a4a57950..a0d75132f1d27d 100644 --- a/editor/store/test/reducer.js +++ b/editor/store/test/reducer.js @@ -11,6 +11,7 @@ import { registerCoreBlocks, registerBlockType, unregisterBlockType, + createBlock, } from '@wordpress/blocks'; /** @@ -75,20 +76,41 @@ describe( 'state', () => { expect( state.future ).toEqual( [] ); expect( state.present.edits ).toEqual( {} ); expect( state.present.blocksByUid ).toEqual( {} ); - expect( state.present.blockOrder ).toEqual( [] ); + expect( state.present.blockOrder ).toEqual( {} ); expect( state.isDirty ).toBe( false ); } ); - it( 'should key by replaced blocks uid', () => { + it( 'should key by reset blocks uid', () => { const original = editor( undefined, {} ); const state = editor( original, { type: 'RESET_BLOCKS', - blocks: [ { uid: 'bananas' } ], + blocks: [ { uid: 'bananas', innerBlocks: [] } ], } ); expect( Object.keys( state.present.blocksByUid ) ).toHaveLength( 1 ); expect( values( state.present.blocksByUid )[ 0 ].uid ).toBe( 'bananas' ); - expect( state.present.blockOrder ).toEqual( [ 'bananas' ] ); + expect( state.present.blockOrder ).toEqual( { + '': [ 'bananas' ], + bananas: [], + } ); + } ); + + it( 'should key by reset blocks uid, including inner blocks', () => { + const original = editor( undefined, {} ); + const state = editor( original, { + type: 'RESET_BLOCKS', + blocks: [ { + uid: 'bananas', + innerBlocks: [ { uid: 'apples', innerBlocks: [] } ], + } ], + } ); + + expect( Object.keys( state.present.blocksByUid ) ).toHaveLength( 2 ); + expect( state.present.blockOrder ).toEqual( { + '': [ 'bananas' ], + apples: [], + bananas: [ 'apples' ], + } ); } ); it( 'should insert block', () => { @@ -98,6 +120,7 @@ describe( 'state', () => { uid: 'chicken', name: 'core/test-block', attributes: {}, + innerBlocks: [], } ], } ); const state = editor( original, { @@ -105,12 +128,17 @@ describe( 'state', () => { blocks: [ { uid: 'ribs', name: 'core/freeform', + innerBlocks: [], } ], } ); expect( Object.keys( state.present.blocksByUid ) ).toHaveLength( 2 ); expect( values( state.present.blocksByUid )[ 1 ].uid ).toBe( 'ribs' ); - expect( state.present.blockOrder ).toEqual( [ 'chicken', 'ribs' ] ); + expect( state.present.blockOrder ).toEqual( { + '': [ 'chicken', 'ribs' ], + chicken: [], + ribs: [], + } ); } ); it( 'should replace the block', () => { @@ -120,6 +148,7 @@ describe( 'state', () => { uid: 'chicken', name: 'core/test-block', attributes: {}, + innerBlocks: [], } ], } ); const state = editor( original, { @@ -128,13 +157,39 @@ describe( 'state', () => { blocks: [ { uid: 'wings', name: 'core/freeform', + innerBlocks: [], } ], } ); expect( Object.keys( state.present.blocksByUid ) ).toHaveLength( 1 ); expect( values( state.present.blocksByUid )[ 0 ].name ).toBe( 'core/freeform' ); expect( values( state.present.blocksByUid )[ 0 ].uid ).toBe( 'wings' ); - expect( state.present.blockOrder ).toEqual( [ 'wings' ] ); + expect( state.present.blockOrder ).toEqual( { + '': [ 'wings' ], + wings: [], + } ); + } ); + + it( 'should replace the nested block', () => { + const nestedBlock = createBlock( 'core/test-block' ); + const wrapperBlock = createBlock( 'core/test-block', {}, [ nestedBlock ] ); + const replacementBlock = createBlock( 'core/test-block' ); + const original = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ wrapperBlock ], + } ); + + const state = editor( original, { + type: 'REPLACE_BLOCKS', + uids: [ nestedBlock.uid ], + blocks: [ replacementBlock ], + } ); + + expect( state.present.blockOrder ).toEqual( { + '': [ wrapperBlock.uid ], + [ wrapperBlock.uid ]: [ replacementBlock.uid ], + [ replacementBlock.uid ]: [], + } ); } ); it( 'should update the block', () => { @@ -145,6 +200,7 @@ describe( 'state', () => { name: 'core/test-block', attributes: {}, isValid: false, + innerBlocks: [], } ], } ); const state = editor( deepFreeze( original ), { @@ -174,6 +230,7 @@ describe( 'state', () => { ref: 'random-uid', }, isValid: false, + innerBlocks: [], } ], } ); @@ -200,10 +257,12 @@ describe( 'state', () => { uid: 'chicken', name: 'core/test-block', attributes: {}, + innerBlocks: [], }, { uid: 'ribs', name: 'core/test-block', attributes: {}, + innerBlocks: [], } ], } ); const state = editor( original, { @@ -211,7 +270,29 @@ describe( 'state', () => { uids: [ 'ribs' ], } ); - expect( state.present.blockOrder ).toEqual( [ 'ribs', 'chicken' ] ); + expect( state.present.blockOrder[ '' ] ).toEqual( [ 'ribs', 'chicken' ] ); + } ); + + it( 'should move the nested block up', () => { + const movedBlock = createBlock( 'core/test-block' ); + const siblingBlock = createBlock( 'core/test-block' ); + const wrapperBlock = createBlock( 'core/test-block', {}, [ siblingBlock, movedBlock ] ); + const original = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ wrapperBlock ], + } ); + const state = editor( original, { + type: 'MOVE_BLOCKS_UP', + uids: [ movedBlock.uid ], + rootUID: wrapperBlock.uid, + } ); + + expect( state.present.blockOrder ).toEqual( { + '': [ wrapperBlock.uid ], + [ wrapperBlock.uid ]: [ movedBlock.uid, siblingBlock.uid ], + [ movedBlock.uid ]: [], + [ siblingBlock.uid ]: [], + } ); } ); it( 'should move multiple blocks up', () => { @@ -221,14 +302,17 @@ describe( 'state', () => { uid: 'chicken', name: 'core/test-block', attributes: {}, + innerBlocks: [], }, { uid: 'ribs', name: 'core/test-block', attributes: {}, + innerBlocks: [], }, { uid: 'veggies', name: 'core/test-block', attributes: {}, + innerBlocks: [], } ], } ); const state = editor( original, { @@ -236,7 +320,31 @@ describe( 'state', () => { uids: [ 'ribs', 'veggies' ], } ); - expect( state.present.blockOrder ).toEqual( [ 'ribs', 'veggies', 'chicken' ] ); + expect( state.present.blockOrder[ '' ] ).toEqual( [ 'ribs', 'veggies', 'chicken' ] ); + } ); + + it( 'should move multiple nested blocks up', () => { + const movedBlockA = createBlock( 'core/test-block' ); + const movedBlockB = createBlock( 'core/test-block' ); + const siblingBlock = createBlock( 'core/test-block' ); + const wrapperBlock = createBlock( 'core/test-block', {}, [ siblingBlock, movedBlockA, movedBlockB ] ); + const original = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ wrapperBlock ], + } ); + const state = editor( original, { + type: 'MOVE_BLOCKS_UP', + uids: [ movedBlockA.uid, movedBlockB.uid ], + rootUID: wrapperBlock.uid, + } ); + + expect( state.present.blockOrder ).toEqual( { + '': [ wrapperBlock.uid ], + [ wrapperBlock.uid ]: [ movedBlockA.uid, movedBlockB.uid, siblingBlock.uid ], + [ movedBlockA.uid ]: [], + [ movedBlockB.uid ]: [], + [ siblingBlock.uid ]: [], + } ); } ); it( 'should not move the first block up', () => { @@ -246,10 +354,12 @@ describe( 'state', () => { uid: 'chicken', name: 'core/test-block', attributes: {}, + innerBlocks: [], }, { uid: 'ribs', name: 'core/test-block', attributes: {}, + innerBlocks: [], } ], } ); const state = editor( original, { @@ -267,10 +377,12 @@ describe( 'state', () => { uid: 'chicken', name: 'core/test-block', attributes: {}, + innerBlocks: [], }, { uid: 'ribs', name: 'core/test-block', attributes: {}, + innerBlocks: [], } ], } ); const state = editor( original, { @@ -278,7 +390,29 @@ describe( 'state', () => { uids: [ 'chicken' ], } ); - expect( state.present.blockOrder ).toEqual( [ 'ribs', 'chicken' ] ); + expect( state.present.blockOrder[ '' ] ).toEqual( [ 'ribs', 'chicken' ] ); + } ); + + it( 'should move the nested block down', () => { + const movedBlock = createBlock( 'core/test-block' ); + const siblingBlock = createBlock( 'core/test-block' ); + const wrapperBlock = createBlock( 'core/test-block', {}, [ movedBlock, siblingBlock ] ); + const original = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ wrapperBlock ], + } ); + const state = editor( original, { + type: 'MOVE_BLOCKS_DOWN', + uids: [ movedBlock.uid ], + rootUID: wrapperBlock.uid, + } ); + + expect( state.present.blockOrder ).toEqual( { + '': [ wrapperBlock.uid ], + [ wrapperBlock.uid ]: [ siblingBlock.uid, movedBlock.uid ], + [ movedBlock.uid ]: [], + [ siblingBlock.uid ]: [], + } ); } ); it( 'should move multiple blocks down', () => { @@ -288,14 +422,17 @@ describe( 'state', () => { uid: 'chicken', name: 'core/test-block', attributes: {}, + innerBlocks: [], }, { uid: 'ribs', name: 'core/test-block', attributes: {}, + innerBlocks: [], }, { uid: 'veggies', name: 'core/test-block', attributes: {}, + innerBlocks: [], } ], } ); const state = editor( original, { @@ -303,7 +440,31 @@ describe( 'state', () => { uids: [ 'chicken', 'ribs' ], } ); - expect( state.present.blockOrder ).toEqual( [ 'veggies', 'chicken', 'ribs' ] ); + expect( state.present.blockOrder[ '' ] ).toEqual( [ 'veggies', 'chicken', 'ribs' ] ); + } ); + + it( 'should move multiple nested blocks down', () => { + const movedBlockA = createBlock( 'core/test-block' ); + const movedBlockB = createBlock( 'core/test-block' ); + const siblingBlock = createBlock( 'core/test-block' ); + const wrapperBlock = createBlock( 'core/test-block', {}, [ movedBlockA, movedBlockB, siblingBlock ] ); + const original = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ wrapperBlock ], + } ); + const state = editor( original, { + type: 'MOVE_BLOCKS_DOWN', + uids: [ movedBlockA.uid, movedBlockB.uid ], + rootUID: wrapperBlock.uid, + } ); + + expect( state.present.blockOrder ).toEqual( { + '': [ wrapperBlock.uid ], + [ wrapperBlock.uid ]: [ siblingBlock.uid, movedBlockA.uid, movedBlockB.uid ], + [ movedBlockA.uid ]: [], + [ movedBlockB.uid ]: [], + [ siblingBlock.uid ]: [], + } ); } ); it( 'should not move the last block down', () => { @@ -313,10 +474,12 @@ describe( 'state', () => { uid: 'chicken', name: 'core/test-block', attributes: {}, + innerBlocks: [], }, { uid: 'ribs', name: 'core/test-block', attributes: {}, + innerBlocks: [], } ], } ); const state = editor( original, { @@ -334,10 +497,12 @@ describe( 'state', () => { uid: 'chicken', name: 'core/test-block', attributes: {}, + innerBlocks: [], }, { uid: 'ribs', name: 'core/test-block', attributes: {}, + innerBlocks: [], } ], } ); const state = editor( original, { @@ -345,7 +510,8 @@ describe( 'state', () => { uids: [ 'chicken' ], } ); - expect( state.present.blockOrder ).toEqual( [ 'ribs' ] ); + expect( state.present.blockOrder[ '' ] ).toEqual( [ 'ribs' ] ); + expect( state.present.blockOrder ).not.toHaveProperty( 'chicken' ); expect( state.present.blocksByUid ).toEqual( { ribs: { uid: 'ribs', @@ -362,14 +528,17 @@ describe( 'state', () => { uid: 'chicken', name: 'core/test-block', attributes: {}, + innerBlocks: [], }, { uid: 'ribs', name: 'core/test-block', attributes: {}, + innerBlocks: [], }, { uid: 'veggies', name: 'core/test-block', attributes: {}, + innerBlocks: [], } ], } ); const state = editor( original, { @@ -377,7 +546,9 @@ describe( 'state', () => { uids: [ 'chicken', 'veggies' ], } ); - expect( state.present.blockOrder ).toEqual( [ 'ribs' ] ); + expect( state.present.blockOrder[ '' ] ).toEqual( [ 'ribs' ] ); + expect( state.present.blockOrder ).not.toHaveProperty( 'chicken' ); + expect( state.present.blockOrder ).not.toHaveProperty( 'veggies' ); expect( state.present.blocksByUid ).toEqual( { ribs: { uid: 'ribs', @@ -387,31 +558,34 @@ describe( 'state', () => { } ); } ); - it( 'should insert at the specified position', () => { + it( 'should insert at the specified index', () => { const original = editor( undefined, { type: 'RESET_BLOCKS', blocks: [ { uid: 'kumquat', name: 'core/test-block', attributes: {}, + innerBlocks: [], }, { uid: 'loquat', name: 'core/test-block', attributes: {}, + innerBlocks: [], } ], } ); const state = editor( original, { type: 'INSERT_BLOCKS', - position: 1, + index: 1, blocks: [ { uid: 'persimmon', name: 'core/freeform', + innerBlocks: [], } ], } ); expect( Object.keys( state.present.blocksByUid ) ).toHaveLength( 3 ); - expect( state.present.blockOrder ).toEqual( [ 'kumquat', 'persimmon', 'loquat' ] ); + expect( state.present.blockOrder[ '' ] ).toEqual( [ 'kumquat', 'persimmon', 'loquat' ] ); } ); it( 'should remove associated blocks when deleting a reusable block', () => { @@ -421,10 +595,12 @@ describe( 'state', () => { uid: 'chicken', name: 'core/test-block', attributes: {}, + innerBlocks: [], }, { uid: 'ribs', name: 'core/test-block', attributes: {}, + innerBlocks: [], } ], } ); const state = editor( original, { @@ -433,7 +609,7 @@ describe( 'state', () => { associatedBlockUids: [ 'chicken', 'veggies' ], } ); - expect( state.present.blockOrder ).toEqual( [ 'ribs' ] ); + expect( state.present.blockOrder[ '' ] ).toEqual( [ 'ribs' ] ); expect( state.present.blocksByUid ).toEqual( { ribs: { uid: 'ribs', @@ -546,10 +722,12 @@ describe( 'state', () => { uid: 'kumquat', name: 'core/test-block', attributes: {}, + innerBlocks: [], }, { uid: 'loquat', name: 'core/test-block', attributes: {}, + innerBlocks: [], } ], } ); @@ -564,6 +742,7 @@ describe( 'state', () => { blocks: [ { uid: 'kumquat', attributes: {}, + innerBlocks: [], } ], } ) ); const state = editor( original, { @@ -585,6 +764,7 @@ describe( 'state', () => { attributes: { updated: true, }, + innerBlocks: [], } ], } ) ); const state = editor( original, { @@ -625,6 +805,7 @@ describe( 'state', () => { attributes: { updated: true, }, + innerBlocks: [], } ], } ) ); const state = editor( original, { @@ -700,6 +881,7 @@ describe( 'state', () => { blocks: [ { uid: 'wings', name: 'core/freeform', + innerBlocks: [], } ], } ); @@ -713,6 +895,7 @@ describe( 'state', () => { blocks: [ { uid: 'wings', name: 'core/freeform', + innerBlocks: [], } ], } ); diff --git a/editor/store/test/selectors.js b/editor/store/test/selectors.js index cebb550803b332..0f15fe911b0788 100644 --- a/editor/store/test/selectors.js +++ b/editor/store/test/selectors.js @@ -39,15 +39,14 @@ import { getBlocks, getBlockCount, getSelectedBlock, + getBlockRootUID, getEditedPostContent, getMultiSelectedBlockUids, getMultiSelectedBlocks, getMultiSelectedBlocksStartUid, getMultiSelectedBlocksEndUid, - getBlockUids, + getBlockOrder, getBlockIndex, - isFirstBlock, - isLastBlock, getPreviousBlock, getNextBlock, isBlockSelected, @@ -515,7 +514,7 @@ describe( 'selectors', () => { present: { edits: {}, blocksByUid: {}, - blockOrder: [], + blockOrder: {}, }, isDirty: false, }, @@ -555,7 +554,7 @@ describe( 'selectors', () => { present: { edits: {}, blocksByUid: {}, - blockOrder: [], + blockOrder: {}, }, isDirty: false, }, @@ -576,7 +575,7 @@ describe( 'selectors', () => { present: { edits: {}, blocksByUid: {}, - blockOrder: [], + blockOrder: {}, }, isDirty: true, }, @@ -837,7 +836,7 @@ describe( 'selectors', () => { editor: { present: { blocksByUid: {}, - blockOrder: [], + blockOrder: {}, edits: {}, }, }, @@ -852,7 +851,7 @@ describe( 'selectors', () => { editor: { present: { blocksByUid: {}, - blockOrder: [], + blockOrder: {}, edits: {}, }, }, @@ -869,7 +868,7 @@ describe( 'selectors', () => { editor: { present: { blocksByUid: {}, - blockOrder: [], + blockOrder: {}, edits: {}, }, }, @@ -894,7 +893,9 @@ describe( 'selectors', () => { }, }, }, - blockOrder: [ 123 ], + blockOrder: { + '': [ 123 ], + }, edits: {}, }, }, @@ -1032,21 +1033,23 @@ describe( 'selectors', () => { 23: { uid: 23, name: 'core/heading' }, 123: { uid: 123, name: 'core/paragraph' }, }, - blockOrder: [ 123, 23 ], + blockOrder: { + '': [ 123, 23 ], + }, edits: {}, }, }, }; expect( getBlocks( state ) ).toEqual( [ - { uid: 123, name: 'core/paragraph' }, - { uid: 23, name: 'core/heading' }, + { uid: 123, name: 'core/paragraph', innerBlocks: [] }, + { uid: 23, name: 'core/heading', innerBlocks: [] }, ] ); } ); } ); describe( 'getBlockCount', () => { - it( 'should return the number of blocks in the post', () => { + it( 'should return the number of top-level blocks in the post', () => { const state = { editor: { present: { @@ -1054,13 +1057,35 @@ describe( 'selectors', () => { 23: { uid: 23, name: 'core/heading' }, 123: { uid: 123, name: 'core/paragraph' }, }, - blockOrder: [ 123, 23 ], + blockOrder: { + '': [ 123, 23 ], + }, }, }, }; expect( getBlockCount( state ) ).toBe( 2 ); } ); + + it( 'should return the number of blocks in a nested context', () => { + const state = { + editor: { + present: { + blocksByUid: { + 123: { uid: 123, name: 'core/columns' }, + 456: { uid: 456, name: 'core/paragraph' }, + 789: { uid: 789, name: 'core/paragraph' }, + }, + blockOrder: { + '': [ 123 ], + 123: [ 456, 789 ], + }, + }, + }, + }; + + expect( getBlockCount( state, '123' ) ).toBe( 2 ); + } ); } ); describe( 'getSelectedBlock', () => { @@ -1115,12 +1140,43 @@ describe( 'selectors', () => { } ); } ); + describe( 'getBlockRootUID', () => { + it( 'should return null if the block does not exist', () => { + const state = { + editor: { + present: { + blockOrder: {}, + }, + }, + }; + + expect( getBlockRootUID( state, 56 ) ).toBeNull(); + } ); + + it( 'should return root UID relative the block UID', () => { + const state = { + editor: { + present: { + blockOrder: { + '': [ 123, 23 ], + 123: [ 456, 56 ], + }, + }, + }, + }; + + expect( getBlockRootUID( state, 56 ) ).toBe( '123' ); + } ); + } ); + describe( 'getMultiSelectedBlockUids', () => { it( 'should return empty if there is no multi selection', () => { const state = { editor: { present: { - blockOrder: [ 123, 23 ], + blockOrder: { + '': [ 123, 23 ], + }, }, }, blockSelection: { start: null, end: null }, @@ -1133,7 +1189,9 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: [ 5, 4, 3, 2, 1 ], + blockOrder: { + '': [ 5, 4, 3, 2, 1 ], + }, }, }, blockSelection: { start: 2, end: 4 }, @@ -1141,16 +1199,27 @@ describe( 'selectors', () => { expect( getMultiSelectedBlockUids( state ) ).toEqual( [ 4, 3, 2 ] ); } ); - } ); - describe( 'getMultiSelectedBlocksStartUid', () => { - it( 'returns null if there is no multi selection', () => { + it( 'should return selected block uids if there is multi selection (nested context)', () => { const state = { editor: { present: { - blockOrder: [ 123, 23 ], + blockOrder: { + '': [ 5, 4, 3, 2, 1 ], + 4: [ 9, 8, 7, 6 ], + }, }, }, + blockSelection: { start: 7, end: 9 }, + }; + + expect( getMultiSelectedBlockUids( state ) ).toEqual( [ 9, 8, 7 ] ); + } ); + } ); + + describe( 'getMultiSelectedBlocksStartUid', () => { + it( 'returns null if there is no multi selection', () => { + const state = { blockSelection: { start: null, end: null }, }; @@ -1159,11 +1228,6 @@ describe( 'selectors', () => { it( 'returns multi selection start', () => { const state = { - editor: { - present: { - blockOrder: [ 5, 4, 3, 2, 1 ], - }, - }, blockSelection: { start: 2, end: 4 }, }; @@ -1174,11 +1238,6 @@ describe( 'selectors', () => { describe( 'getMultiSelectedBlocksEndUid', () => { it( 'returns null if there is no multi selection', () => { const state = { - editor: { - present: { - blockOrder: [ 123, 23 ], - }, - }, blockSelection: { start: null, end: null }, }; @@ -1187,11 +1246,6 @@ describe( 'selectors', () => { it( 'returns multi selection end', () => { const state = { - editor: { - present: { - blockOrder: [ 5, 4, 3, 2, 1 ], - }, - }, blockSelection: { start: 2, end: 4 }, }; @@ -1199,88 +1253,109 @@ describe( 'selectors', () => { } ); } ); - describe( 'getBlockUids', () => { - it( 'should return the ordered block UIDs', () => { + describe( 'getBlockOrder', () => { + it( 'should return the ordered block UIDs of top-level blocks by default', () => { const state = { editor: { present: { - blockOrder: [ 123, 23 ], + blockOrder: { + '': [ 123, 23 ], + }, }, }, }; - expect( getBlockUids( state ) ).toEqual( [ 123, 23 ] ); + expect( getBlockOrder( state ) ).toEqual( [ 123, 23 ] ); } ); - } ); - describe( 'getBlockIndex', () => { - it( 'should return the block order', () => { + it( 'should return the ordered block UIDs at a specified rootUID', () => { const state = { editor: { present: { - blockOrder: [ 123, 23 ], + blockOrder: { + '': [ 123, 23 ], + 123: [ 456 ], + }, }, }, }; - expect( getBlockIndex( state, 23 ) ).toBe( 1 ); + expect( getBlockOrder( state, '123' ) ).toEqual( [ 456 ] ); } ); } ); - describe( 'isFirstBlock', () => { - it( 'should return true when the block is first', () => { + describe( 'getBlockIndex', () => { + it( 'should return the block order', () => { const state = { editor: { present: { - blockOrder: [ 123, 23 ], + blockOrder: { + '': [ 123, 23 ], + }, }, }, }; - expect( isFirstBlock( state, 123 ) ).toBe( true ); + expect( getBlockIndex( state, 23 ) ).toBe( 1 ); } ); - it( 'should return false when the block is not first', () => { + it( 'should return the block order (nested context)', () => { const state = { editor: { present: { - blockOrder: [ 123, 23 ], + blockOrder: { + '': [ 123, 23 ], + 123: [ 456, 56 ], + }, }, }, }; - expect( isFirstBlock( state, 23 ) ).toBe( false ); + expect( getBlockIndex( state, 56, '123' ) ).toBe( 1 ); } ); } ); - describe( 'isLastBlock', () => { - it( 'should return true when the block is last', () => { + describe( 'getPreviousBlock', () => { + it( 'should return the previous block', () => { const state = { editor: { present: { - blockOrder: [ 123, 23 ], + blocksByUid: { + 23: { uid: 23, name: 'core/heading' }, + 123: { uid: 123, name: 'core/paragraph' }, + }, + blockOrder: { + '': [ 123, 23 ], + }, }, }, }; - expect( isLastBlock( state, 23 ) ).toBe( true ); + expect( getPreviousBlock( state, 23 ) ).toEqual( { uid: 123, name: 'core/paragraph' } ); } ); - it( 'should return false when the block is not last', () => { + it( 'should return the previous block (nested context)', () => { const state = { editor: { present: { - blockOrder: [ 123, 23 ], + blocksByUid: { + 23: { uid: 23, name: 'core/heading' }, + 123: { uid: 123, name: 'core/paragraph' }, + 56: { uid: 56, name: 'core/heading' }, + 456: { uid: 456, name: 'core/paragraph' }, + }, + blockOrder: { + '': [ 123, 23 ], + 123: [ 456, 56 ], + }, }, }, }; - expect( isLastBlock( state, 123 ) ).toBe( false ); + expect( getPreviousBlock( state, 56, '123' ) ).toEqual( { uid: 456, name: 'core/paragraph' } ); } ); - } ); - describe( 'getPreviousBlock', () => { - it( 'should return the previous block', () => { + it( 'should return null for the first block', () => { const state = { editor: { present: { @@ -1288,28 +1363,35 @@ describe( 'selectors', () => { 23: { uid: 23, name: 'core/heading' }, 123: { uid: 123, name: 'core/paragraph' }, }, - blockOrder: [ 123, 23 ], + blockOrder: { + '': [ 123, 23 ], + }, }, }, }; - expect( getPreviousBlock( state, 23 ) ).toEqual( { uid: 123, name: 'core/paragraph' } ); + expect( getPreviousBlock( state, 123 ) ).toBeNull(); } ); - it( 'should return null for the first block', () => { + it( 'should return null for the first block (nested context)', () => { const state = { editor: { present: { blocksByUid: { 23: { uid: 23, name: 'core/heading' }, 123: { uid: 123, name: 'core/paragraph' }, + 56: { uid: 56, name: 'core/heading' }, + 456: { uid: 456, name: 'core/paragraph' }, + }, + blockOrder: { + '': [ 123, 23 ], + 123: [ 456, 56 ], }, - blockOrder: [ 123, 23 ], }, }, }; - expect( getPreviousBlock( state, 123 ) ).toBeNull(); + expect( getPreviousBlock( state, 456, '123' ) ).toBeNull(); } ); } ); @@ -1322,7 +1404,9 @@ describe( 'selectors', () => { 23: { uid: 23, name: 'core/heading' }, 123: { uid: 123, name: 'core/paragraph' }, }, - blockOrder: [ 123, 23 ], + blockOrder: { + '': [ 123, 23 ], + }, }, }, }; @@ -1330,6 +1414,27 @@ describe( 'selectors', () => { expect( getNextBlock( state, 123 ) ).toEqual( { uid: 23, name: 'core/heading' } ); } ); + it( 'should return the following block (nested context)', () => { + const state = { + editor: { + present: { + blocksByUid: { + 23: { uid: 23, name: 'core/heading' }, + 123: { uid: 123, name: 'core/paragraph' }, + 56: { uid: 56, name: 'core/heading' }, + 456: { uid: 456, name: 'core/paragraph' }, + }, + blockOrder: { + '': [ 123, 23 ], + 123: [ 456, 56 ], + }, + }, + }, + }; + + expect( getNextBlock( state, 456, '123' ) ).toEqual( { uid: 56, name: 'core/heading' } ); + } ); + it( 'should return null for the last block', () => { const state = { editor: { @@ -1338,13 +1443,36 @@ describe( 'selectors', () => { 23: { uid: 23, name: 'core/heading' }, 123: { uid: 123, name: 'core/paragraph' }, }, - blockOrder: [ 123, 23 ], + blockOrder: { + '': [ 123, 23 ], + }, }, }, }; expect( getNextBlock( state, 23 ) ).toBeNull(); } ); + + it( 'should return null for the last block (nested context)', () => { + const state = { + editor: { + present: { + blocksByUid: { + 23: { uid: 23, name: 'core/heading' }, + 123: { uid: 123, name: 'core/paragraph' }, + 56: { uid: 56, name: 'core/heading' }, + 456: { uid: 456, name: 'core/paragraph' }, + }, + blockOrder: { + '': [ 123, 23 ], + 123: [ 456, 56 ], + }, + }, + }, + }; + + expect( getNextBlock( state, 56, '123' ) ).toBeNull(); + } ); } ); describe( 'isBlockSelected', () => { @@ -1379,7 +1507,9 @@ describe( 'selectors', () => { blockSelection: { start: 5, end: 3 }, editor: { present: { - blockOrder: [ 5, 4, 3, 2, 1 ], + blockOrder: { + '': [ 5, 4, 3, 2, 1 ], + }, }, }, }; @@ -1392,7 +1522,9 @@ describe( 'selectors', () => { blockSelection: { start: 5, end: 3 }, editor: { present: { - blockOrder: [ 5, 4, 3, 2, 1 ], + blockOrder: { + '': [ 5, 4, 3, 2, 1 ], + }, }, }, }; @@ -1405,7 +1537,9 @@ describe( 'selectors', () => { blockSelection: { start: 5, end: 3 }, editor: { present: { - blockOrder: [ 5, 4, 3, 2, 1 ], + blockOrder: { + '': [ 5, 4, 3, 2, 1 ], + }, }, }, }; @@ -1418,7 +1552,9 @@ describe( 'selectors', () => { blockSelection: {}, editor: { present: { - blockOrder: [ 5, 4, 3, 2, 1 ], + blockOrder: { + '': [ 5, 4, 3, 2, 1 ], + }, }, }, }; @@ -1431,7 +1567,9 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: [ 5, 4, 3, 2, 1 ], + blockOrder: { + '': [ 5, 4, 3, 2, 1 ], + }, }, }, blockSelection: { start: 2, end: 4 }, @@ -1450,7 +1588,9 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: [ 5, 4, 3, 2, 1 ], + blockOrder: { + '': [ 5, 4, 3, 2, 1 ], + }, }, }, blockSelection: { start: 2, end: 4 }, @@ -1607,7 +1747,9 @@ describe( 'selectors', () => { blocksByUid: { 2: { uid: 2 }, }, - blockOrder: [ 1, 2, 3 ], + blockOrder: { + '': [ 1, 2, 3 ], + }, edits: {}, }, }, @@ -1626,7 +1768,9 @@ describe( 'selectors', () => { }, editor: { present: { - blockOrder: [ 1, 2, 3 ], + blockOrder: { + '': [ 1, 2, 3 ], + }, }, }, isInsertionPointVisible: false, @@ -1641,7 +1785,9 @@ describe( 'selectors', () => { blockSelection: { start: null, end: null }, editor: { present: { - blockOrder: [ 1, 2, 3 ], + blockOrder: { + '': [ 1, 2, 3 ], + }, }, }, isInsertionPointVisible: false, @@ -1745,7 +1891,7 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: [], + blockOrder: {}, blocksByUid: {}, }, }, @@ -1758,7 +1904,9 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: [ 123, 456 ], + blockOrder: { + '': [ 123, 456 ], + }, blocksByUid: { 123: { uid: 123, name: 'core/image' }, 456: { uid: 456, name: 'core/quote' }, @@ -1774,7 +1922,9 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: [ 123 ], + blockOrder: { + '': [ 123 ], + }, blocksByUid: { 123: { uid: 123, name: 'core/image' }, }, @@ -1789,7 +1939,9 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: [ 456 ], + blockOrder: { + '': [ 456 ], + }, blocksByUid: { 456: { uid: 456, name: 'core/quote' }, }, @@ -1804,7 +1956,9 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: [ 567 ], + blockOrder: { + '': [ 567 ], + }, blocksByUid: { 567: { uid: 567, name: 'core-embed/youtube' }, }, @@ -1819,7 +1973,9 @@ describe( 'selectors', () => { const state = { editor: { present: { - blockOrder: [ 456, 789 ], + blockOrder: { + '': [ 456, 789 ], + }, blocksByUid: { 456: { uid: 456, name: 'core/quote' }, 789: { uid: 789, name: 'core/paragraph' }, @@ -1851,7 +2007,7 @@ describe( 'selectors', () => { editor: { present: { blocksByUid: {}, - blockOrder: [], + blockOrder: {}, }, }, reusableBlocks: { @@ -1868,7 +2024,7 @@ describe( 'selectors', () => { editor: { present: { blocksByUid: {}, - blockOrder: [], + blockOrder: {}, }, }, reusableBlocks: { @@ -1897,7 +2053,9 @@ describe( 'selectors', () => { blocksByUid: { 1: { uid: 1, name: 'core/test-block', attributes: {} }, }, - blockOrder: [ 1 ], + blockOrder: { + '': [ 1 ], + }, }, }, reusableBlocks: { @@ -1914,7 +2072,7 @@ describe( 'selectors', () => { editor: { present: { blocksByUid: {}, - blockOrder: [], + blockOrder: {}, }, }, reusableBlocks: { From 377a1f29eadb8ea1c01fde295056bbbe207d4e1a Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Sun, 28 Jan 2018 11:29:58 +0000 Subject: [PATCH 09/13] Block: Stop propagation but don't handle child events --- editor/components/block-list/block.js | 7 +++ .../block-list/ignore-nested-events.js | 27 +++++++--- .../block-list/test/ignore-nested-events.js | 54 +++++++++++++++++++ 3 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 editor/components/block-list/test/ignore-nested-events.js diff --git a/editor/components/block-list/block.js b/editor/components/block-list/block.js index 11313641860bcb..63ef3da3ffc9ff 100644 --- a/editor/components/block-list/block.js +++ b/editor/components/block-list/block.js @@ -495,6 +495,13 @@ export class BlockListBlock extends Component { data-type={ block.name } onTouchStart={ this.onTouchStart } onClick={ this.onClick } + childHandledEvents={ [ + 'onKeyPress', + 'onDragStart', + 'onMouseDown', + 'onKeyDown', + 'onFocus', + ] } { ...wrapperProps } > { + const { childHandledEvents = [], ...props } = this.props; + + const eventHandlers = reduce( [ + ...childHandledEvents, + ...Object.keys( props ), + ], ( result, key ) => { // Try to match prop key as event handler const match = key.match( /^on([A-Z][a-zA-Z]+)$/ ); if ( match ) { @@ -71,7 +84,7 @@ class IgnoreNestedEvents extends Component { return result; }, {} ); - return
; + return
; } } diff --git a/editor/components/block-list/test/ignore-nested-events.js b/editor/components/block-list/test/ignore-nested-events.js new file mode 100644 index 00000000000000..73f9c636607062 --- /dev/null +++ b/editor/components/block-list/test/ignore-nested-events.js @@ -0,0 +1,54 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; + +/** + * Internal dependencies + */ +import IgnoreNestedEvents from '../ignore-nested-events'; + +describe( 'IgnoreNestedEvents', () => { + it( 'passes props to its rendered div', () => { + const wrapper = shallow( + + ); + + expect( wrapper.type() ).toBe( 'div' ); + expect( wrapper.prop( 'className' ) ).toBe( 'foo' ); + } ); + + it( 'stops propagation of events to ancestor IgnoreNestedEvents', () => { + const spyOuter = jest.fn(); + const spyInner = jest.fn(); + const wrapper = shallow( + + + + ); + + wrapper.childAt( 0 ).simulate( 'click' ); + + expect( spyInner ).toHaveBeenCalled(); + expect( spyOuter ).not.toHaveBeenCalled(); + } ); + + it( 'stops propagation of child handled events', () => { + const spyOuter = jest.fn(); + const spyInner = jest.fn(); + const wrapper = shallow( + + +
+ + + + ); + + const div = wrapper.childAt( 0 ).childAt( 0 ); + div.simulate( 'click' ); + + expect( spyInner ).not.toHaveBeenCalled(); + expect( spyOuter ).not.toHaveBeenCalled(); + } ); +} ); From 60c7158a729bba332ed5361c1d676a8de690c997 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 30 Jan 2018 16:43:19 -0500 Subject: [PATCH 10/13] Blocks: Rename latest posts layout attribute as postLayout Otherwise conflicting with newly-introduced "layout" attribute applying to all blocks for nesting arrangement --- blocks/library/latest-posts/index.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/blocks/library/latest-posts/index.php b/blocks/library/latest-posts/index.php index 5358c2aa44546d..4ea93fa5130501 100644 --- a/blocks/library/latest-posts/index.php +++ b/blocks/library/latest-posts/index.php @@ -48,11 +48,11 @@ function gutenberg_render_block_core_latest_posts( $attributes ) { } $class = "wp-block-latest-posts align{$attributes['align']}"; - if ( isset( $attributes['layout'] ) && 'grid' === $attributes['layout'] ) { + if ( isset( $attributes['postLayout'] ) && 'grid' === $attributes['postLayout'] ) { $class .= ' is-grid'; } - if ( isset( $attributes['columns'] ) && 'grid' === $attributes['layout'] ) { + if ( isset( $attributes['columns'] ) && 'grid' === $attributes['postLayout'] ) { $class .= ' columns-' . $attributes['columns']; } @@ -78,7 +78,7 @@ function gutenberg_render_block_core_latest_posts( $attributes ) { 'type' => 'boolean', 'default' => false, ), - 'layout' => array( + 'postLayout' => array( 'type' => 'string', 'default' => 'list', ), From 326cdac1f4f8352f1d9f6085b3118925dd2965a8 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 6 Feb 2018 13:46:03 -0500 Subject: [PATCH 11/13] Blocks: Add wide / full align options --- blocks/library/columns/index.js | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/blocks/library/columns/index.js b/blocks/library/columns/index.js index 3422e8cf82c633..6f5cc754845904 100644 --- a/blocks/library/columns/index.js +++ b/blocks/library/columns/index.js @@ -15,6 +15,8 @@ import { __, sprintf } from '@wordpress/i18n'; import './style.scss'; import RangeControl from '../../inspector-controls/range-control'; import InspectorControls from '../../inspector-controls'; +import BlockControls from '../../block-controls'; +import BlockAlignmentToolbar from '../../block-alignment-toolbar'; import InnerBlocks from '../../inner-blocks'; export const name = 'core/columns'; @@ -31,12 +33,21 @@ export const settings = { type: 'number', default: 2, }, + align: { + type: 'string', + }, }, description: __( 'A multi-column layout of content.' ), + getEditWrapperProps( attributes ) { + const { align } = attributes; + + return { 'data-align': align }; + }, + edit( { attributes, setAttributes, className, focus } ) { - const { columns } = attributes; + const { align, columns } = attributes; const classes = classnames( className, `has-${ columns }-columns` ); // Define columns as a set of layouts within the inner block list. This @@ -50,7 +61,16 @@ export const settings = { } ) ); return [ - focus && ( + ...focus ? [ + + { + setAttributes( { align: nextAlign } ); + } } + /> + , - - ), + , + ] : [],
, From ab4be6ae9e83b2169f4e737f44dda0fa60e9f6bf Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 6 Feb 2018 13:57:18 -0500 Subject: [PATCH 12/13] Layout: Preserve layout attribute via transform hook --- blocks/api/factory.js | 25 +++++++++++++++++++------ blocks/hooks/layout.js | 24 +++++++++++++++++++++++- blocks/hooks/test/layout.js | 11 +++++++++++ 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/blocks/api/factory.js b/blocks/api/factory.js index 3b6f58f8efed10..e603795eaa858f 100644 --- a/blocks/api/factory.js +++ b/blocks/api/factory.js @@ -21,6 +21,7 @@ import { * WordPress dependencies */ import { __ } from '@wordpress/i18n'; +import { applyFilters } from '@wordpress/hooks'; /** * Internal dependencies @@ -237,12 +238,24 @@ export function switchToBlockType( blocks, name ) { return null; } - return transformationResults.map( ( result, index ) => ( { - ...result, - // The first transformed block whose type matches the "destination" - // type gets to keep the existing UID of the first block. - uid: index === firstSwitchedBlock ? firstBlock.uid : result.uid, - } ) ); + return transformationResults.map( ( result, index ) => { + const transformedBlock = { + ...result, + // The first transformed block whose type matches the "destination" + // type gets to keep the existing UID of the first block. + uid: index === firstSwitchedBlock ? firstBlock.uid : result.uid, + }; + + /** + * Filters an individual transform result from block transformation. + * All of the original blocks are passed, since transformations are + * many-to-many, not one-to-one. + * + * @param {Object} transformedBlock The transformed block. + * @param {Object[]} blocks Original blocks transformed. + */ + return applyFilters( 'blocks.switchToBlockType.transformedBlock', transformedBlock, blocks ); + } ); } /** diff --git a/blocks/hooks/layout.js b/blocks/hooks/layout.js index 92761a2405bbb6..a313c6ee35f18f 100644 --- a/blocks/hooks/layout.js +++ b/blocks/hooks/layout.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { assign, compact } from 'lodash'; +import { assign, compact, get } from 'lodash'; /** * WordPress dependencies @@ -49,5 +49,27 @@ export function addSaveProps( extraProps, blockType, attributes ) { return extraProps; } +/** + * Given a transformed block, assigns the layout from the original block. Since + * layout is a "global" attribute implemented via hooks, the individual block + * transforms are not expected to handle this themselves, and a transform would + * otherwise lose assigned layout. + * + * @param {Object} transformedBlock Original transformed block. + * @param {Object} blocks Blocks on which transform was applied. + * + * @return {Object} Modified transformed block, with layout preserved. + */ +function preserveLayoutAttribute( transformedBlock, blocks ) { + // Since block transforms are many-to-many, use the layout attribute from + // the first of the source blocks. + const layout = get( blocks, [ 0, 'attributes', 'layout' ] ); + + transformedBlock.attributes.layout = layout; + + return transformedBlock; +} + addFilter( 'blocks.registerBlockType', 'core/layout/attribute', addAttribute ); addFilter( 'blocks.getSaveContent.extraProps', 'core/layout/save-props', addSaveProps ); +addFilter( 'blocks.switchToBlockType.transformedBlock', 'core/layout/preserve-layout', preserveLayoutAttribute ); diff --git a/blocks/hooks/test/layout.js b/blocks/hooks/test/layout.js index 308547753f5650..24ac163077807b 100644 --- a/blocks/hooks/test/layout.js +++ b/blocks/hooks/test/layout.js @@ -42,4 +42,15 @@ describe( 'layout', () => { expect( extraProps.className ).toBe( 'wizard layout-wide' ); } ); } ); + + describe( 'preserveLayoutAttribute', () => { + const transformBlock = applyFilters.bind( null, 'blocks.switchToBlockType.transformedBlock' ); + + it( 'should preserve layout attribute', () => { + const blocks = [ { attributes: { layout: 'wide' } } ]; + const transformedBlock = transformBlock( { attributes: {} }, blocks ); + + expect( transformedBlock.attributes.layout ).toBe( 'wide' ); + } ); + } ); } ); From 31a2ce0f3786fc029e17505cb10ce5af6bb848da Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 6 Feb 2018 14:10:20 -0500 Subject: [PATCH 13/13] Blocks: Add experimental modifier to Columns block title --- blocks/library/columns/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/blocks/library/columns/index.js b/blocks/library/columns/index.js index 6f5cc754845904..5331df92262a96 100644 --- a/blocks/library/columns/index.js +++ b/blocks/library/columns/index.js @@ -22,7 +22,12 @@ import InnerBlocks from '../../inner-blocks'; export const name = 'core/columns'; export const settings = { - title: __( 'Columns' ), + title: sprintf( + /* translators: Block title modifier */ + __( '%1$s (%2$s)' ), + __( 'Columns' ), + __( 'Experimental' ) + ), icon: 'columns',