Skip to content

Commit

Permalink
Edit Post: Add block management modal (#14224)
Browse files Browse the repository at this point in the history
  • Loading branch information
aduth authored and youknowriad committed Mar 20, 2019
1 parent e205195 commit 7417eb5
Show file tree
Hide file tree
Showing 21 changed files with 830 additions and 40 deletions.
15 changes: 15 additions & 0 deletions docs/designers-developers/developers/data/data-core-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,21 @@ Returns true if the block defines support for a feature, or false otherwise.

Whether block supports feature.

### isMatchingSearchTerm

Returns true if the block type by the given name or object value matches a
search term, or false otherwise.

*Parameters*

* state: Blocks state.
* nameOrType: Block name or type object.
* searchTerm: Search term by which to filter.

*Returns*

Wheter block type matches search term.

### hasChildBlocks

Returns a boolean indicating if a block has child blocks or not.
Expand Down
18 changes: 18 additions & 0 deletions docs/designers-developers/developers/data/data-core-edit-post.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,24 @@ Returns an action object used to toggle a plugin name flag.

* pluginName: Plugin name.

### hideBlockTypes

Returns an action object used in signalling that block types by the given
name(s) should be hidden.

*Parameters*

* blockNames: Names of block types to hide.

### showBlockTypes

Returns an action object used in signalling that block types by the given
name(s) should be shown.

*Parameters*

* blockNames: Names of block types to show.

### setAvailableMetaBoxesPerLocation

Returns an action object used in signaling
Expand Down
65 changes: 61 additions & 4 deletions packages/blocks/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,22 @@
* External dependencies
*/
import createSelector from 'rememo';
import { filter, get, includes, map, some } from 'lodash';
import { filter, get, includes, map, some, flow, deburr } from 'lodash';

/**
* Given a block name or block type object, returns the corresponding
* normalized block type object.
*
* @param {Object} state Blocks state.
* @param {(string|Object)} nameOrType Block name or type object
*
* @return {Object} Block type object.
*/
const getNormalizedBlockType = ( state, nameOrType ) => (
'string' === typeof nameOrType ?
getBlockType( state, nameOrType ) :
nameOrType
);

/**
* Returns all the available block types.
Expand Down Expand Up @@ -120,9 +135,7 @@ export const getChildBlockNames = createSelector(
* @return {?*} Block support value
*/
export const getBlockSupport = ( state, nameOrType, feature, defaultSupports ) => {
const blockType = 'string' === typeof nameOrType ?
getBlockType( state, nameOrType ) :
nameOrType;
const blockType = getNormalizedBlockType( state, nameOrType );

return get( blockType, [
'supports',
Expand All @@ -145,6 +158,50 @@ export function hasBlockSupport( state, nameOrType, feature, defaultSupports ) {
return !! getBlockSupport( state, nameOrType, feature, defaultSupports );
}

/**
* Returns true if the block type by the given name or object value matches a
* search term, or false otherwise.
*
* @param {Object} state Blocks state.
* @param {(string|Object)} nameOrType Block name or type object.
* @param {string} searchTerm Search term by which to filter.
*
* @return {Object[]} Wheter block type matches search term.
*/
export function isMatchingSearchTerm( state, nameOrType, searchTerm ) {
const blockType = getNormalizedBlockType( state, nameOrType );

const getNormalizedSearchTerm = flow( [
// Disregard diacritics.
// Input: "média"
deburr,

// Lowercase.
// Input: "MEDIA"
( term ) => term.toLowerCase(),

// Strip leading and trailing whitespace.
// Input: " media "
( term ) => term.trim(),
] );

const normalizedSearchTerm = getNormalizedSearchTerm( searchTerm );

const isSearchMatch = flow( [
getNormalizedSearchTerm,
( normalizedCandidate ) => includes(
normalizedCandidate,
normalizedSearchTerm
),
] );

return (
isSearchMatch( blockType.title ) ||
some( blockType.keywords, isSearchMatch ) ||
isSearchMatch( blockType.category )
);
}

/**
* Returns a boolean indicating if a block has child blocks or not.
*
Expand Down
67 changes: 66 additions & 1 deletion packages/blocks/src/store/test/selectors.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
/**
* Internal dependencies
*/
import { getChildBlockNames } from '../selectors';
import {
getChildBlockNames,
isMatchingSearchTerm,
} from '../selectors';

describe( 'selectors', () => {
describe( 'getChildBlockNames', () => {
Expand Down Expand Up @@ -134,4 +137,66 @@ describe( 'selectors', () => {
expect( getChildBlockNames( state, 'parent2' ) ).toEqual( [ 'child2' ] );
} );
} );

describe( 'isMatchingSearchTerm', () => {
const name = 'core/paragraph';
const blockType = {
title: 'Paragraph',
category: 'common',
keywords: [ 'text' ],
};

const state = {
blockTypes: {
[ name ]: blockType,
},
};

describe.each( [
[ 'name', name ],
[ 'block type', blockType ],
] )( 'by %s', ( label, nameOrType ) => {
it( 'should return false if not match', () => {
const result = isMatchingSearchTerm( state, nameOrType, 'Quote' );

expect( result ).toBe( false );
} );

it( 'should return true if match by title', () => {
const result = isMatchingSearchTerm( state, nameOrType, 'Paragraph' );

expect( result ).toBe( true );
} );

it( 'should return true if match ignoring case', () => {
const result = isMatchingSearchTerm( state, nameOrType, 'PARAGRAPH' );

expect( result ).toBe( true );
} );

it( 'should return true if match ignoring diacritics', () => {
const result = isMatchingSearchTerm( state, nameOrType, 'PÁRAGRAPH' );

expect( result ).toBe( true );
} );

it( 'should return true if match ignoring whitespace', () => {
const result = isMatchingSearchTerm( state, nameOrType, ' PARAGRAPH ' );

expect( result ).toBe( true );
} );

it( 'should return true if match using the keywords', () => {
const result = isMatchingSearchTerm( state, nameOrType, 'TEXT' );

expect( result ).toBe( true );
} );

it( 'should return true if match using the categories', () => {
const result = isMatchingSearchTerm( state, nameOrType, 'COMMON' );

expect( result ).toBe( true );
} );
} );
} );
} );
2 changes: 2 additions & 0 deletions packages/edit-post/src/components/layout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import TextEditor from '../text-editor';
import VisualEditor from '../visual-editor';
import EditorModeKeyboardShortcuts from '../keyboard-shortcuts';
import KeyboardShortcutHelpModal from '../keyboard-shortcut-help-modal';
import ManageBlocksModal from '../manage-blocks-modal';
import OptionsModal from '../options-modal';
import MetaBoxes from '../meta-boxes';
import SettingsSidebar from '../sidebar/settings-sidebar';
Expand Down Expand Up @@ -83,6 +84,7 @@ function Layout( {
<PreserveScrollInReorder />
<EditorModeKeyboardShortcuts />
<KeyboardShortcutHelpModal />
<ManageBlocksModal />
<OptionsModal />
{ ( mode === 'text' || ! isRichEditingEnabled ) && <TextEditor /> }
{ isRichEditingEnabled && mode === 'visual' && <VisualEditor /> }
Expand Down
103 changes: 103 additions & 0 deletions packages/edit-post/src/components/manage-blocks-modal/category.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* External dependencies
*/
import { without, map } from 'lodash';

/**
* WordPress dependencies
*/
import { withSelect, withDispatch } from '@wordpress/data';
import { compose, withInstanceId } from '@wordpress/compose';
import { CheckboxControl } from '@wordpress/components';

/**
* Internal dependencies
*/
import BlockTypesChecklist from './checklist';

function BlockManagerCategory( {
instanceId,
category,
blockTypes,
hiddenBlockTypes,
toggleVisible,
toggleAllVisible,
} ) {
if ( ! blockTypes.length ) {
return null;
}

const checkedBlockNames = without(
map( blockTypes, 'name' ),
...hiddenBlockTypes
);

const titleId = 'edit-post-manage-blocks-modal__category-title-' + instanceId;

const isAllChecked = checkedBlockNames.length === blockTypes.length;

let ariaChecked;
if ( isAllChecked ) {
ariaChecked = 'true';
} else if ( checkedBlockNames.length > 0 ) {
ariaChecked = 'mixed';
} else {
ariaChecked = 'false';
}

return (
<div
role="group"
aria-labelledby={ titleId }
className="edit-post-manage-blocks-modal__category"
>
<CheckboxControl
checked={ isAllChecked }
onChange={ toggleAllVisible }
className="edit-post-manage-blocks-modal__category-title"
aria-checked={ ariaChecked }
label={ <span id={ titleId }>{ category.title }</span> }
/>
<BlockTypesChecklist
blockTypes={ blockTypes }
value={ checkedBlockNames }
onItemChange={ toggleVisible }
/>
</div>
);
}

export default compose( [
withInstanceId,
withSelect( ( select ) => {
const { getPreference } = select( 'core/edit-post' );

return {
hiddenBlockTypes: getPreference( 'hiddenBlockTypes' ),
};
} ),
withDispatch( ( dispatch, ownProps ) => {
const {
showBlockTypes,
hideBlockTypes,
} = dispatch( 'core/edit-post' );

return {
toggleVisible( blockName, nextIsChecked ) {
if ( nextIsChecked ) {
showBlockTypes( blockName );
} else {
hideBlockTypes( blockName );
}
},
toggleAllVisible( nextIsChecked ) {
const blockNames = map( ownProps.blockTypes, 'name' );
if ( nextIsChecked ) {
showBlockTypes( blockNames );
} else {
hideBlockTypes( blockNames );
}
},
};
} ),
] )( BlockManagerCategory );
37 changes: 37 additions & 0 deletions packages/edit-post/src/components/manage-blocks-modal/checklist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* External dependencies
*/
import { partial } from 'lodash';

/**
* WordPress dependencies
*/
import { Fragment } from '@wordpress/element';
import { BlockIcon } from '@wordpress/block-editor';
import { CheckboxControl } from '@wordpress/components';

function BlockTypesChecklist( { blockTypes, value, onItemChange } ) {
return (
<ul className="edit-post-manage-blocks-modal__checklist">
{ blockTypes.map( ( blockType ) => (
<li
key={ blockType.name }
className="edit-post-manage-blocks-modal__checklist-item"
>
<CheckboxControl
label={ (
<Fragment>
{ blockType.title }
<BlockIcon icon={ blockType.icon } />
</Fragment>
) }
checked={ value.includes( blockType.name ) }
onChange={ partial( onItemChange, blockType.name ) }
/>
</li>
) ) }
</ul>
);
}

export default BlockTypesChecklist;
Loading

0 comments on commit 7417eb5

Please sign in to comment.