diff --git a/blocks/api/factory.js b/blocks/api/factory.js new file mode 100644 index 00000000000000..bb27df7e56d09c --- /dev/null +++ b/blocks/api/factory.js @@ -0,0 +1,19 @@ +/** + * External dependencies + */ +import uuid from 'uuid/v4'; + +/** + * Returns a block object given its type and attributes + * + * @param {Object} blockType BlockType + * @param {Object} attributes Block attributes + * @return {Object} Block object + */ +export function createBlock( blockType, attributes = {} ) { + return { + uid: uuid(), + blockType, + attributes + }; +} diff --git a/blocks/api/index.js b/blocks/api/index.js index 3abf5b70d8385a..2d43c110f70696 100644 --- a/blocks/api/index.js +++ b/blocks/api/index.js @@ -4,6 +4,7 @@ import * as query from 'hpq'; export { query }; +export { createBlock } from './factory'; export { default as parse } from './parser'; export { default as serialize } from './serializer'; export { getCategories } from './categories'; diff --git a/blocks/api/parser.js b/blocks/api/parser.js index 1b23bf7edd80a0..ea0a7b7183c625 100644 --- a/blocks/api/parser.js +++ b/blocks/api/parser.js @@ -2,13 +2,13 @@ * External dependencies */ import * as query from 'hpq'; -import uuid from 'uuid/v4'; /** * Internal dependencies */ import { parse as grammarParse } from './post.pegjs'; import { getBlockSettings, getUnknownTypeHandler } from './registration'; +import { createBlock } from './factory'; /** * Returns the block attributes of a registered block node given its settings. @@ -53,12 +53,9 @@ export default function parse( content ) { // Include in set only if settings were determined if ( settings ) { - memo.push( { - blockType, - uid: uuid(), - rawContent: blockNode.rawContent, - attributes: getBlockAttributes( blockNode, settings ) - } ); + memo.push( + createBlock( blockType, getBlockAttributes( blockNode, settings ) ) + ); } return memo; diff --git a/blocks/api/test/factory.js b/blocks/api/test/factory.js new file mode 100644 index 00000000000000..d8c5accc5fcbe6 --- /dev/null +++ b/blocks/api/test/factory.js @@ -0,0 +1,25 @@ +/** + * External dependencies + */ +import { expect } from 'chai'; + +/** + * Internal dependencies + */ +import { createBlock } from '../factory'; + +describe( 'block factory', () => { + describe( 'createBlock()', () => { + it( 'should create a block given its blockType and attributes', () => { + const block = createBlock( 'core/test-block', { + align: 'left' + } ); + + expect( block.blockType ).to.eql( 'core/test-block' ); + expect( block.attributes ).to.eql( { + align: 'left' + } ); + expect( block.uid ).to.be.a( 'string' ); + } ); + } ); +} ); diff --git a/blocks/api/test/parser.js b/blocks/api/test/parser.js index 45ac197d036e19..4ec62562f0b81a 100644 --- a/blocks/api/test/parser.js +++ b/blocks/api/test/parser.js @@ -101,7 +101,6 @@ describe( 'block parser', () => { expect( parsed[ 0 ].attributes ).to.eql( { content: 'Ribs & Chicken' } ); - expect( parsed[ 0 ].rawContent ).to.equal( 'Ribs' ); expect( parsed[ 0 ].uid ).to.be.a( 'string' ); } ); diff --git a/blocks/components/editable/index.js b/blocks/components/editable/index.js index d92d8999d73e7b..c2eebb4558541e 100644 --- a/blocks/components/editable/index.js +++ b/blocks/components/editable/index.js @@ -45,7 +45,8 @@ export default class Editable extends wp.element.Component { } onInit() { - this.editor.setContent( this.props.value ); + const { value = '' } = this.props; + this.editor.setContent( value ); } onChange() { diff --git a/editor/components/inserter/index.js b/editor/components/inserter/index.js index fe7914e463897c..9b00deffebcfd4 100644 --- a/editor/components/inserter/index.js +++ b/editor/components/inserter/index.js @@ -8,6 +8,7 @@ class Inserter extends wp.element.Component { constructor() { super( ...arguments ); this.toggle = this.toggle.bind( this ); + this.close = this.close.bind( this ); this.state = { opened: false }; @@ -19,6 +20,12 @@ class Inserter extends wp.element.Component { } ); } + close() { + this.setState( { + opened: false + } ); + } + render() { const { opened } = this.state; const { position } = this.props; @@ -30,7 +37,7 @@ class Inserter extends wp.element.Component { label={ wp.i18n.__( 'Insert block' ) } onClick={ this.toggle } className="editor-inserter__toggle" /> - { opened && } + { opened && } ); } diff --git a/editor/components/inserter/menu.js b/editor/components/inserter/menu.js index 8822eada9fc2d9..1e4a08b1ac85ab 100644 --- a/editor/components/inserter/menu.js +++ b/editor/components/inserter/menu.js @@ -1,46 +1,96 @@ +/** + * External dependencies + */ +import { connect } from 'react-redux'; + /** * Internal dependencies */ import './style.scss'; import Dashicon from 'components/dashicon'; -function InserterMenu( { position = 'top' } ) { - const blocks = wp.blocks.getBlocks(); - const blocksByCategory = blocks.reduce( ( groups, block ) => { - if ( ! groups[ block.category ] ) { - groups[ block.category ] = []; - } - groups[ block.category ].push( block ); - return groups; - }, {} ); - const categories = wp.blocks.getCategories(); +class InserterMenu extends wp.element.Component { + constructor() { + super( ...arguments ); + this.state = { + filterValue: '' + }; + this.filter = this.filter.bind( this ); + } - return ( -
-
-
- { categories - .map( ( category ) => !! blocksByCategory[ category.slug ] && ( -
-
{ category.title }
-
- { blocksByCategory[ category.slug ].map( ( { slug, title, icon } ) => ( -
- - { title } -
- ) ) } + filter( event ) { + this.setState( { + filterValue: event.target.value + } ); + } + + selectBlock( slug ) { + return () => { + this.props.onInsertBlock( slug ); + this.props.onSelect(); + this.setState( { filterValue: '' } ); + }; + } + + render() { + const { position = 'top' } = this.props; + const blocks = wp.blocks.getBlocks(); + const isShownBlock = block => block.title.toLowerCase().indexOf( this.state.filterValue.toLowerCase() ) !== -1; + const blocksByCategory = blocks.reduce( ( groups, block ) => { + if ( ! isShownBlock( block ) ) { + return groups; + } + if ( ! groups[ block.category ] ) { + groups[ block.category ] = []; + } + groups[ block.category ].push( block ); + return groups; + }, {} ); + const categories = wp.blocks.getCategories(); + + return ( +
+
+
+ { categories + .map( ( category ) => !! blocksByCategory[ category.slug ] && ( +
+
{ category.title }
+
+ { blocksByCategory[ category.slug ].map( ( { slug, title, icon } ) => ( + + ) ) } +
-
- ) ) - } + ) ) + } +
+
- -
- ); + ); + } } -export default InserterMenu; +export default connect( + undefined, + ( dispatch ) => ( { + onInsertBlock( slug ) { + dispatch( { + type: 'INSERT_BLOCK', + block: wp.blocks.createBlock( slug ) + } ); + } + } ) +)( InserterMenu ); diff --git a/editor/components/inserter/style.scss b/editor/components/inserter/style.scss index a7e58dbf87e545..cb45206ea87a50 100644 --- a/editor/components/inserter/style.scss +++ b/editor/components/inserter/style.scss @@ -7,7 +7,7 @@ font-family: $default-font; font-size: $default-font-size; line-height: $default-line-height; - z-index: 1; + z-index: 1; } .editor-inserter__toggle { @@ -140,6 +140,7 @@ input[type=search].editor-inserter__search { align-items: center; cursor: pointer; border: 1px solid transparent; + background: none; &:hover { border: 1px solid $dark-gray-500; diff --git a/editor/modes/visual-editor/block.js b/editor/modes/visual-editor/block.js index 45ec27159d86e3..32eafdb405c63c 100644 --- a/editor/modes/visual-editor/block.js +++ b/editor/modes/visual-editor/block.js @@ -84,8 +84,8 @@ function VisualEditorBlock( props ) { export default connect( ( state, ownProps ) => ( { block: state.blocks.byUid[ ownProps.uid ], - isSelected: !! state.blocks.selected[ ownProps.uid ], - isHovered: !! state.blocks.hovered[ ownProps.uid ] + isSelected: state.blocks.selected === ownProps.uid, + isHovered: state.blocks.hovered === ownProps.uid } ), ( dispatch, ownProps ) => ( { onChange( updates ) { diff --git a/editor/state.js b/editor/state.js index 96ab1cea733190..9ca71d2806e5b9 100644 --- a/editor/state.js +++ b/editor/state.js @@ -26,6 +26,12 @@ export const blocks = combineReducers( { ...action.updates } }; + + case 'INSERT_BLOCK': + return { + ...state, + [ action.block.uid ]: action.block + }; } return state; @@ -62,35 +68,53 @@ export const blocks = combineReducers( { action.uid, ...state.slice( index + 2 ) ]; + + case 'INSERT_BLOCK': + return [ + ...state, + action.block.uid + ]; } return state; }, - selected( state = {}, action ) { + selected( state = null, action ) { switch ( action.type ) { case 'TOGGLE_BLOCK_SELECTED': - return { - ...state, - [ action.uid ]: action.selected - }; + if ( action.selected ) { + return action.uid; + } + + if ( ! action.selected && action.uid === state ) { + return null; + } + + return state; + case 'MOVE_BLOCK_UP': + case 'MOVE_BLOCK_DOWN': + return action.uid; + case 'INSERT_BLOCK': + return action.block.uid; } return state; }, - hovered( state = {}, action ) { + hovered( state = null, action ) { switch ( action.type ) { case 'TOGGLE_BLOCK_HOVERED': - return { - ...state, - [ action.uid ]: action.hovered - }; + if ( action.hovered ) { + return action.uid; + } + + if ( ! action.hovered && action.uid === state ) { + return null; + } + + return state; case 'TOGGLE_BLOCK_SELECTED': - if ( state[ action.uid ] ) { - return { - ...state, - [ action.uid ]: false - }; + if ( state === action.uid ) { + return null; } break; } diff --git a/editor/test/state.js b/editor/test/state.js index 4ba6d8fcb41d68..8f203bf153e57b 100644 --- a/editor/test/state.js +++ b/editor/test/state.js @@ -30,8 +30,8 @@ describe( 'state', () => { expect( state ).to.eql( { byUid: {}, order: [], - selected: {}, - hovered: {} + selected: null, + hovered: null } ); } ); @@ -39,8 +39,8 @@ describe( 'state', () => { const original = deepFreeze( { byUid: {}, order: [], - selected: {}, - hovered: {} + selected: null, + hovered: null } ); const state = blocks( original, { type: 'REPLACE_BLOCKS', @@ -62,8 +62,8 @@ describe( 'state', () => { } }, order: [ 'kumquat' ], - selected: {}, - hovered: {} + selected: null, + hovered: null } ); const state = blocks( original, { type: 'UPDATE_BLOCK', @@ -88,8 +88,8 @@ describe( 'state', () => { } }, order: [ 'kumquat' ], - selected: {}, - hovered: {} + selected: null, + hovered: null } ); const state = blocks( original, { type: 'TOGGLE_BLOCK_HOVERED', @@ -97,7 +97,7 @@ describe( 'state', () => { hovered: true } ); - expect( state.hovered.kumquat ).to.be.true(); + expect( state.hovered ).to.equal( 'kumquat' ); } ); it( 'should return with block uid as selected, clearing hover', () => { @@ -110,10 +110,8 @@ describe( 'state', () => { } }, order: [ 'kumquat' ], - selected: {}, - hovered: { - kumquat: true - } + selected: null, + hovered: 'kumquat' } ); const state = blocks( original, { type: 'TOGGLE_BLOCK_SELECTED', @@ -121,11 +119,39 @@ describe( 'state', () => { selected: true } ); - expect( state.hovered.kumquat ).to.be.false(); - expect( state.selected.kumquat ).to.be.true(); + expect( state.hovered ).to.be.null(); + expect( state.selected ).to.equal( 'kumquat' ); + } ); + + it( 'should insert and select block', () => { + const original = deepFreeze( { + byUid: { + kumquat: { + uid: 'chicken', + blockType: 'core/test-block', + attributes: {} + } + }, + order: [ 'chicken' ], + selected: null, + hovered: null + } ); + const state = blocks( original, { + type: 'INSERT_BLOCK', + block: { + uid: 'ribs', + blockType: 'core/freeform' + } + } ); + + expect( Object.keys( state.byUid ) ).to.have.lengthOf( 2 ); + expect( values( state.byUid )[ 1 ].uid ).to.equal( 'ribs' ); + expect( state.order ).to.eql( [ 'chicken', 'ribs' ] ); + expect( state.hovered ).to.be.null(); + expect( state.selected ).to.equal( 'ribs' ); } ); - it( 'should move the block up', () => { + it( 'should move the block up and select it', () => { const original = deepFreeze( { byUid: { chicken: { @@ -149,6 +175,7 @@ describe( 'state', () => { } ); expect( state.order ).to.eql( [ 'ribs', 'chicken' ] ); + expect( state.selected ).to.eql( 'ribs' ); } ); it( 'should not move the first block up', () => { @@ -177,7 +204,7 @@ describe( 'state', () => { expect( state.order ).to.equal( original.order ); } ); - it( 'should move the block down', () => { + it( 'should move the block down and select it', () => { const original = deepFreeze( { byUid: { chicken: { @@ -201,6 +228,7 @@ describe( 'state', () => { } ); expect( state.order ).to.eql( [ 'ribs', 'chicken' ] ); + expect( state.selected ).to.eql( 'chicken' ); } ); it( 'should not move the last block down', () => { diff --git a/languages/gutenberg.pot b/languages/gutenberg.pot index 5430e7ec22659f..732ff5e7759610 100644 --- a/languages/gutenberg.pot +++ b/languages/gutenberg.pot @@ -42,11 +42,11 @@ msgstr "" msgid "Text" msgstr "" -#: editor/components/inserter/index.js:30 +#: editor/components/inserter/index.js:37 msgid "Insert block" msgstr "" -#: editor/components/inserter/menu.js:40 +#: editor/components/inserter/menu.js:77 msgid "Search…" msgstr ""