- { 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',