diff --git a/docs/designers-developers/developers/data/data-core-blocks.md b/docs/designers-developers/developers/data/data-core-blocks.md
index abdb30b903773c..c7a63e769f350f 100644
--- a/docs/designers-developers/developers/data/data-core-blocks.md
+++ b/docs/designers-developers/developers/data/data-core-blocks.md
@@ -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.
diff --git a/docs/designers-developers/developers/data/data-core-edit-post.md b/docs/designers-developers/developers/data/data-core-edit-post.md
index c12cf62a83d633..f36cf5bc47068f 100644
--- a/docs/designers-developers/developers/data/data-core-edit-post.md
+++ b/docs/designers-developers/developers/data/data-core-edit-post.md
@@ -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
diff --git a/packages/blocks/src/store/selectors.js b/packages/blocks/src/store/selectors.js
index 9ae3373cbd5dc1..97d323c65fb168 100644
--- a/packages/blocks/src/store/selectors.js
+++ b/packages/blocks/src/store/selectors.js
@@ -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.
@@ -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',
@@ -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.
*
diff --git a/packages/blocks/src/store/test/selectors.js b/packages/blocks/src/store/test/selectors.js
index ea24fcd7d855b3..2f37ed006042c0 100644
--- a/packages/blocks/src/store/test/selectors.js
+++ b/packages/blocks/src/store/test/selectors.js
@@ -1,7 +1,10 @@
/**
* Internal dependencies
*/
-import { getChildBlockNames } from '../selectors';
+import {
+ getChildBlockNames,
+ isMatchingSearchTerm,
+} from '../selectors';
describe( 'selectors', () => {
describe( 'getChildBlockNames', () => {
@@ -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 );
+ } );
+ } );
+ } );
} );
diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js
index 2274ff9ffa3ecf..d033b993a00b28 100644
--- a/packages/edit-post/src/components/layout/index.js
+++ b/packages/edit-post/src/components/layout/index.js
@@ -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';
@@ -83,6 +84,7 @@ function Layout( {
+
{ ( mode === 'text' || ! isRichEditingEnabled ) && }
{ isRichEditingEnabled && mode === 'visual' && }
diff --git a/packages/edit-post/src/components/manage-blocks-modal/category.js b/packages/edit-post/src/components/manage-blocks-modal/category.js
new file mode 100644
index 00000000000000..00a2ff15ab7937
--- /dev/null
+++ b/packages/edit-post/src/components/manage-blocks-modal/category.js
@@ -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 (
+
+ { category.title } }
+ />
+
+
+ );
+}
+
+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 );
diff --git a/packages/edit-post/src/components/manage-blocks-modal/checklist.js b/packages/edit-post/src/components/manage-blocks-modal/checklist.js
new file mode 100644
index 00000000000000..b65042929d647f
--- /dev/null
+++ b/packages/edit-post/src/components/manage-blocks-modal/checklist.js
@@ -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 (
+
+ { blockTypes.map( ( blockType ) => (
+ -
+
+ { blockType.title }
+
+
+ ) }
+ checked={ value.includes( blockType.name ) }
+ onChange={ partial( onItemChange, blockType.name ) }
+ />
+
+ ) ) }
+
+ );
+}
+
+export default BlockTypesChecklist;
diff --git a/packages/edit-post/src/components/manage-blocks-modal/index.js b/packages/edit-post/src/components/manage-blocks-modal/index.js
new file mode 100644
index 00000000000000..ee88068ad6d6e1
--- /dev/null
+++ b/packages/edit-post/src/components/manage-blocks-modal/index.js
@@ -0,0 +1,53 @@
+/**
+ * WordPress dependencies
+ */
+import { Modal } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { withSelect, withDispatch } from '@wordpress/data';
+import { compose } from '@wordpress/compose';
+
+/**
+ * Internal dependencies
+ */
+import BlockManager from './manager';
+
+/**
+ * Unique identifier for Manage Blocks modal.
+ *
+ * @type {string}
+ */
+const MODAL_NAME = 'edit-post/manage-blocks';
+
+export function ManageBlocksModal( { isActive, closeModal } ) {
+ if ( ! isActive ) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+}
+
+export default compose( [
+ withSelect( ( select ) => {
+ const { isModalActive } = select( 'core/edit-post' );
+
+ return {
+ isActive: isModalActive( MODAL_NAME ),
+ };
+ } ),
+ withDispatch( ( dispatch ) => {
+ const { closeModal } = dispatch( 'core/edit-post' );
+
+ return {
+ closeModal,
+ };
+ } ),
+] )( ManageBlocksModal );
diff --git a/packages/edit-post/src/components/manage-blocks-modal/manager.js b/packages/edit-post/src/components/manage-blocks-modal/manager.js
new file mode 100644
index 00000000000000..7cf25463e8a4d7
--- /dev/null
+++ b/packages/edit-post/src/components/manage-blocks-modal/manager.js
@@ -0,0 +1,88 @@
+/**
+ * External dependencies
+ */
+import { filter } from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+import { withSelect } from '@wordpress/data';
+import { compose, withState } from '@wordpress/compose';
+import { TextControl } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import BlockManagerCategory from './category';
+
+function BlockManager( {
+ search,
+ setState,
+ blockTypes,
+ categories,
+ hasBlockSupport,
+ isMatchingSearchTerm,
+} ) {
+ // Filtering occurs here (as opposed to `withSelect`) to avoid wasted
+ // wasted renders by consequence of `Array#filter` producing a new
+ // value reference on each call.
+ blockTypes = blockTypes.filter( ( blockType ) => (
+ hasBlockSupport( blockType, 'inserter', true ) &&
+ ( ! search || isMatchingSearchTerm( blockType, search ) )
+ ) );
+
+ return (
+
+
setState( {
+ search: nextSearch,
+ } ) }
+ className="edit-post-manage-blocks-modal__search"
+ />
+
+ { blockTypes.length === 0 && (
+
+ { __( 'No blocks found.' ) }
+
+ ) }
+ { categories.map( ( category ) => (
+
+ ) ) }
+
+
+ );
+}
+
+export default compose( [
+ withState( { search: '' } ),
+ withSelect( ( select ) => {
+ const {
+ getBlockTypes,
+ getCategories,
+ hasBlockSupport,
+ isMatchingSearchTerm,
+ } = select( 'core/blocks' );
+
+ return {
+ blockTypes: getBlockTypes(),
+ categories: getCategories(),
+ hasBlockSupport,
+ isMatchingSearchTerm,
+ };
+ } ),
+] )( BlockManager );
diff --git a/packages/edit-post/src/components/manage-blocks-modal/show-all.js b/packages/edit-post/src/components/manage-blocks-modal/show-all.js
new file mode 100644
index 00000000000000..7aaa206bc626ac
--- /dev/null
+++ b/packages/edit-post/src/components/manage-blocks-modal/show-all.js
@@ -0,0 +1,31 @@
+/**
+ * WordPress dependencies
+ */
+import { withInstanceId } from '@wordpress/compose';
+import { FormToggle } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+
+function BlockManagerShowAll( { instanceId, checked, onChange } ) {
+ const id = 'edit-post-manage-blocks-modal__show-all-' + instanceId;
+
+ return (
+
+
+ onChange( event.target.checked ) }
+ />
+
+ );
+}
+
+export default withInstanceId( BlockManagerShowAll );
diff --git a/packages/edit-post/src/components/manage-blocks-modal/style.scss b/packages/edit-post/src/components/manage-blocks-modal/style.scss
new file mode 100644
index 00000000000000..45569b28c6083f
--- /dev/null
+++ b/packages/edit-post/src/components/manage-blocks-modal/style.scss
@@ -0,0 +1,117 @@
+.edit-post-manage-blocks-modal {
+ @include break-small() {
+ height: calc(100% - #{ $header-height } - #{ $header-height });
+ }
+}
+
+.edit-post-manage-blocks-modal .components-modal__content {
+ padding-bottom: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.edit-post-manage-blocks-modal .components-modal__header {
+ flex-shrink: 0;
+ margin-bottom: 0;
+}
+
+.edit-post-manage-blocks-modal__content {
+ display: flex;
+ flex-direction: column;
+ flex: 0 1 100%;
+ min-height: 0;
+}
+
+.edit-post-manage-blocks-modal__no-results {
+ font-style: italic;
+ padding: 24px 0;
+ text-align: center;
+}
+
+.edit-post-manage-blocks-modal__search {
+ margin: $grid-size-large 0;
+
+ .components-base-control__field {
+ margin-bottom: 0;
+ }
+
+ .components-base-control__label {
+ margin-top: -0.25 * $grid-size-large;
+ }
+
+ input[type="search"].components-text-control__input {
+ padding: 0.75 * $grid-size-large;
+ border-radius: $radius-round-rectangle;
+ }
+}
+
+.edit-post-manage-blocks-modal__category {
+ margin: 0 0 2rem 0;
+}
+
+.edit-post-manage-blocks-modal__category-title {
+ position: sticky;
+ top: 0;
+ padding: $panel-padding 0;
+ background-color: $white;
+
+ .components-base-control__field {
+ margin-bottom: 0;
+ }
+
+ .components-checkbox-control__label {
+ font-size: 0.9rem;
+ font-weight: 600;
+ }
+}
+
+.edit-post-manage-blocks-modal__show-all {
+ margin-right: $grid-size;
+}
+
+.edit-post-manage-blocks-modal__checklist {
+ margin-top: 0;
+}
+
+.edit-post-manage-blocks-modal__checklist-item {
+ margin-bottom: 0;
+ padding-left: $grid-size-large;
+ border-top: 1px solid $light-gray-500;
+
+ &:last-child {
+ border-bottom: 1px solid $light-gray-500;
+ }
+
+ .components-base-control__field {
+ align-items: center;
+ display: flex;
+ margin: 0;
+ }
+
+ .components-modal__content & input[type="checkbox"] {
+ margin: 0 $grid-size;
+ }
+
+ .components-checkbox-control__label {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-grow: 1;
+ padding: 0.6rem 0 0.6rem 10px;
+ }
+
+ .editor-block-icon {
+ margin-right: 10px;
+ fill: $dark-gray-500;
+ }
+}
+
+.edit-post-manage-blocks-modal__results {
+ height: 100%;
+ overflow: auto;
+ margin-left: -1 * $grid-size-large;
+ margin-right: -1 * $grid-size-large;
+ padding-left: $grid-size-large;
+ padding-right: $grid-size-large;
+ border-top: $border-width solid $light-gray-500;
+}
diff --git a/packages/edit-post/src/components/options-modal/style.scss b/packages/edit-post/src/components/options-modal/style.scss
index 62b827d104ecdc..01b5f816c0ca0e 100644
--- a/packages/edit-post/src/components/options-modal/style.scss
+++ b/packages/edit-post/src/components/options-modal/style.scss
@@ -21,6 +21,10 @@
margin: 0;
}
+ &.components-base-control + &.components-base-control {
+ margin-bottom: 0;
+ }
+
.components-checkbox-control__label {
flex-grow: 1;
padding: 0.6rem 0 0.6rem 10px;
diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js
index 3e51956a0e074c..28e55b08d6cbf6 100644
--- a/packages/edit-post/src/editor.js
+++ b/packages/edit-post/src/editor.js
@@ -2,6 +2,7 @@
* External dependencies
*/
import memize from 'memize';
+import { size, map, without } from 'lodash';
/**
* WordPress dependencies
@@ -26,12 +27,37 @@ class Editor extends Component {
} );
}
- getEditorSettings( settings, hasFixedToolbar, focusMode ) {
- return {
+ getEditorSettings(
+ settings,
+ hasFixedToolbar,
+ focusMode,
+ hiddenBlockTypes,
+ blockTypes,
+ ) {
+ settings = {
...settings,
hasFixedToolbar,
focusMode,
};
+
+ // Omit hidden block types if exists and non-empty.
+ if ( size( hiddenBlockTypes ) > 0 ) {
+ // Defer to passed setting for `allowedBlockTypes` if provided as
+ // anything other than `true` (where `true` is equivalent to allow
+ // all block types).
+ const defaultAllowedBlockTypes = (
+ true === settings.allowedBlockTypes ?
+ map( blockTypes, 'name' ) :
+ ( settings.allowedBlockTypes || [] )
+ );
+
+ settings.allowedBlockTypes = without(
+ defaultAllowedBlockTypes,
+ ...hiddenBlockTypes,
+ );
+ }
+
+ return settings;
}
render() {
@@ -42,6 +68,8 @@ class Editor extends Component {
post,
initialEdits,
onError,
+ hiddenBlockTypes,
+ blockTypes,
...props
} = this.props;
@@ -49,11 +77,13 @@ class Editor extends Component {
return null;
}
- const editorSettings = {
- ...settings,
+ const editorSettings = this.getEditorSettings(
+ settings,
hasFixedToolbar,
focusMode,
- };
+ hiddenBlockTypes,
+ blockTypes,
+ );
return (
@@ -74,8 +104,16 @@ class Editor extends Component {
}
}
-export default withSelect( ( select, { postId, postType } ) => ( {
- hasFixedToolbar: select( 'core/edit-post' ).isFeatureActive( 'fixedToolbar' ),
- focusMode: select( 'core/edit-post' ).isFeatureActive( 'focusMode' ),
- post: select( 'core' ).getEntityRecord( 'postType', postType, postId ),
-} ) )( Editor );
+export default withSelect( ( select, { postId, postType } ) => {
+ const { isFeatureActive, getPreference } = select( 'core/edit-post' );
+ const { getEntityRecord } = select( 'core' );
+ const { getBlockTypes } = select( 'core/blocks' );
+
+ return {
+ hasFixedToolbar: isFeatureActive( 'fixedToolbar' ),
+ focusMode: isFeatureActive( 'focusMode' ),
+ post: getEntityRecord( 'postType', postType, postId ),
+ hiddenBlockTypes: getPreference( 'hiddenBlockTypes' ),
+ blockTypes: getBlockTypes(),
+ };
+} )( Editor );
diff --git a/packages/edit-post/src/plugins/index.js b/packages/edit-post/src/plugins/index.js
index 05a079db4738f2..47f09e6bd547e6 100644
--- a/packages/edit-post/src/plugins/index.js
+++ b/packages/edit-post/src/plugins/index.js
@@ -11,6 +11,7 @@ import { addQueryArgs } from '@wordpress/url';
* Internal dependencies
*/
import CopyContentMenuItem from './copy-content-menu-item';
+import ManageBlocksMenuItem from './manage-blocks-menu-item';
import KeyboardShortcutsHelpMenuItem from './keyboard-shortcuts-help-menu-item';
import ToolsMoreMenuGroup from '../components/header/tools-more-menu-group';
@@ -21,6 +22,7 @@ registerPlugin( 'edit-post', {
{ ( { onClose } ) => (
+
+ );
+}
+
+export default withDispatch( ( dispatch ) => {
+ const {
+ openModal,
+ } = dispatch( 'core/edit-post' );
+
+ return {
+ openModal,
+ };
+} )( ManageBlocksMenuItem );
diff --git a/packages/edit-post/src/store/actions.js b/packages/edit-post/src/store/actions.js
index 2e2af7a2295f45..c12eb674837eb4 100644
--- a/packages/edit-post/src/store/actions.js
+++ b/packages/edit-post/src/store/actions.js
@@ -1,3 +1,8 @@
+/**
+ * External dependencies
+ */
+import { castArray } from 'lodash';
+
/**
* Returns an action object used in signalling that the user opened an editor sidebar.
*
@@ -160,6 +165,36 @@ export function togglePinnedPluginItem( pluginName ) {
};
}
+/**
+ * Returns an action object used in signalling that block types by the given
+ * name(s) should be hidden.
+ *
+ * @param {string[]} blockNames Names of block types to hide.
+ *
+ * @return {Object} Action object.
+ */
+export function hideBlockTypes( blockNames ) {
+ return {
+ type: 'HIDE_BLOCK_TYPES',
+ blockNames: castArray( blockNames ),
+ };
+}
+
+/**
+ * Returns an action object used in signalling that block types by the given
+ * name(s) should be shown.
+ *
+ * @param {string[]} blockNames Names of block types to show.
+ *
+ * @return {Object} Action object.
+ */
+export function showBlockTypes( blockNames ) {
+ return {
+ type: 'SHOW_BLOCK_TYPES',
+ blockNames: castArray( blockNames ),
+ };
+}
+
/**
* Returns an action object used in signaling
* what Meta boxes are available in which location.
diff --git a/packages/edit-post/src/store/defaults.js b/packages/edit-post/src/store/defaults.js
index b88b2c24a6dc03..9cfc6506fece04 100644
--- a/packages/edit-post/src/store/defaults.js
+++ b/packages/edit-post/src/store/defaults.js
@@ -10,4 +10,5 @@ export const PREFERENCES_DEFAULTS = {
fixedToolbar: false,
},
pinnedPluginItems: {},
+ hiddenBlockTypes: [],
};
diff --git a/packages/edit-post/src/store/reducer.js b/packages/edit-post/src/store/reducer.js
index a016c701582a90..ab64ba7c5ae62c 100644
--- a/packages/edit-post/src/store/reducer.js
+++ b/packages/edit-post/src/store/reducer.js
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { get, includes } from 'lodash';
+import { get, includes, flow, without, union } from 'lodash';
/**
* WordPress dependencies
@@ -20,6 +20,18 @@ import { PREFERENCES_DEFAULTS } from './defaults';
*/
export const DEFAULT_ACTIVE_GENERAL_SIDEBAR = 'edit-post/document';
+/**
+ * Higher-order reducer creator which provides the given initial state for the
+ * original reducer.
+ *
+ * @param {*} initialState Initial state to provide to reducer.
+ *
+ * @return {Function} Higher-order reducer.
+ */
+const createWithInitialState = ( initialState ) => ( reducer ) => {
+ return ( state = initialState, action ) => reducer( state, action );
+};
+
/**
* Reducer returning the user preferences.
*
@@ -39,8 +51,11 @@ export const DEFAULT_ACTIVE_GENERAL_SIDEBAR = 'edit-post/document';
*
* @return {Object} Updated state.
*/
-export const preferences = combineReducers( {
- isGeneralSidebarDismissed( state = false, action ) {
+export const preferences = flow( [
+ combineReducers,
+ createWithInitialState( PREFERENCES_DEFAULTS ),
+] )( {
+ isGeneralSidebarDismissed( state, action ) {
switch ( action.type ) {
case 'OPEN_GENERAL_SIDEBAR':
case 'CLOSE_GENERAL_SIDEBAR':
@@ -49,7 +64,7 @@ export const preferences = combineReducers( {
return state;
},
- panels( state = PREFERENCES_DEFAULTS.panels, action ) {
+ panels( state, action ) {
switch ( action.type ) {
case 'TOGGLE_PANEL_ENABLED': {
const { panelName } = action;
@@ -77,7 +92,7 @@ export const preferences = combineReducers( {
return state;
},
- features( state = PREFERENCES_DEFAULTS.features, action ) {
+ features( state, action ) {
if ( action.type === 'TOGGLE_FEATURE' ) {
return {
...state,
@@ -87,14 +102,14 @@ export const preferences = combineReducers( {
return state;
},
- editorMode( state = PREFERENCES_DEFAULTS.editorMode, action ) {
+ editorMode( state, action ) {
if ( action.type === 'SWITCH_MODE' ) {
return action.mode;
}
return state;
},
- pinnedPluginItems( state = PREFERENCES_DEFAULTS.pinnedPluginItems, action ) {
+ pinnedPluginItems( state, action ) {
if ( action.type === 'TOGGLE_PINNED_PLUGIN_ITEM' ) {
return {
...state,
@@ -103,6 +118,17 @@ export const preferences = combineReducers( {
}
return state;
},
+ hiddenBlockTypes( state, action ) {
+ switch ( action.type ) {
+ case 'SHOW_BLOCK_TYPES':
+ return without( state, ...action.blockNames );
+
+ case 'HIDE_BLOCK_TYPES':
+ return union( state, action.blockNames );
+ }
+
+ return state;
+ },
} );
/**
diff --git a/packages/edit-post/src/store/test/reducer.js b/packages/edit-post/src/store/test/reducer.js
index 66027cd21dc443..4241de25d5b951 100644
--- a/packages/edit-post/src/store/test/reducer.js
+++ b/packages/edit-post/src/store/test/reducer.js
@@ -15,21 +15,14 @@ import {
metaBoxLocations,
removedPanels,
} from '../reducer';
+import { PREFERENCES_DEFAULTS } from '../defaults';
describe( 'state', () => {
describe( 'preferences()', () => {
it( 'should apply all defaults', () => {
const state = preferences( undefined, {} );
- expect( state ).toEqual( {
- editorMode: 'visual',
- isGeneralSidebarDismissed: false,
- panels: {
- 'post-status': { opened: true },
- },
- features: { fixedToolbar: false },
- pinnedPluginItems: {},
- } );
+ expect( state ).toEqual( PREFERENCES_DEFAULTS );
} );
it( 'should set the general sidebar dismissed', () => {
@@ -223,6 +216,40 @@ describe( 'state', () => {
expect( state.pinnedPluginItems[ 'foo/disabled' ] ).toBe( true );
} );
} );
+
+ describe( 'hiddenBlockTypes', () => {
+ it( 'concatenates unique names on disable', () => {
+ const original = deepFreeze( {
+ hiddenBlockTypes: [ 'a', 'b' ],
+ } );
+
+ const state = preferences( original, {
+ type: 'HIDE_BLOCK_TYPES',
+ blockNames: [ 'b', 'c' ],
+ } );
+
+ expect( state.hiddenBlockTypes ).toEqual( [
+ 'a',
+ 'b',
+ 'c',
+ ] );
+ } );
+
+ it( 'omits present names by enable', () => {
+ const original = deepFreeze( {
+ hiddenBlockTypes: [ 'a', 'b' ],
+ } );
+
+ const state = preferences( original, {
+ type: 'SHOW_BLOCK_TYPES',
+ blockNames: [ 'b', 'c' ],
+ } );
+
+ expect( state.hiddenBlockTypes ).toEqual( [
+ 'a',
+ ] );
+ } );
+ } );
} );
describe( 'activeGeneralSidebar', () => {
diff --git a/packages/edit-post/src/style.scss b/packages/edit-post/src/style.scss
index 44595911436094..4b8b9e39491402 100644
--- a/packages/edit-post/src/style.scss
+++ b/packages/edit-post/src/style.scss
@@ -6,6 +6,7 @@
@import "./components/header/pinned-plugins/style.scss";
@import "./components/keyboard-shortcut-help-modal/style.scss";
@import "./components/layout/style.scss";
+@import "./components/manage-blocks-modal/style.scss";
@import "./components/meta-boxes/meta-boxes-area/style.scss";
@import "./components/sidebar/style.scss";
@import "./components/sidebar/last-revision/style.scss";
@@ -228,9 +229,44 @@ body.block-editor-page {
input[type="checkbox"] {
border-radius: $radius-round-rectangle / 2;
- &:checked::before {
- margin: -4px 0 0 -5px;
+ &:checked::before,
+ &[aria-checked="mixed"]::before {
+ margin: -3px -5px;
color: $white;
+
+ @include break-medium() {
+ margin: -4px 0 0 -5px;
+ }
+ }
+
+ &[aria-checked="mixed"] {
+ background: theme(toggle);
+ border-color: theme(toggle);
+
+ &::before {
+ // Inherited from `forms.css`.
+ // See: https://github.com/WordPress/wordpress-develop/tree/5.1.1/src/wp-admin/css/forms.css#L122-L132
+ content: "\f460";
+ float: left;
+ display: inline-block;
+ vertical-align: middle;
+ width: 16px;
+ /* stylelint-disable */
+ font: normal 30px/1 dashicons;
+ /* stylelint-enable */
+ speak: none;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ @include break-medium() {
+ float: none;
+ font-size: 21px;
+ }
+ }
+
+ &:focus {
+ box-shadow: 0 0 0 2px $dark-gray-500;
+ }
}
}
diff --git a/packages/editor/src/store/defaults.js b/packages/editor/src/store/defaults.js
index 53178b43cb5aa4..d22d6e7f67de8c 100644
--- a/packages/editor/src/store/defaults.js
+++ b/packages/editor/src/store/defaults.js
@@ -17,13 +17,14 @@ export const INITIAL_EDITS_DEFAULTS = {};
/**
* The default post editor settings
*
- * richEditingEnabled boolean Whether rich editing is enabled or not
- * enableCustomFields boolean Whether the WordPress custom fields are enabled or not
- * autosaveInterval number Autosave Interval
- * availableTemplates array? The available post templates
- * disablePostFormats boolean Whether or not the post formats are disabled
- * allowedMimeTypes array? List of allowed mime types and file extensions
- * maxUploadFileSize number Maximum upload file size
+ * allowedBlockTypes boolean|Array Allowed block types
+ * richEditingEnabled boolean Whether rich editing is enabled or not
+ * enableCustomFields boolean Whether the WordPress custom fields are enabled or not
+ * autosaveInterval number Autosave Interval
+ * availableTemplates array? The available post templates
+ * disablePostFormats boolean Whether or not the post formats are disabled
+ * allowedMimeTypes array? List of allowed mime types and file extensions
+ * maxUploadFileSize number Maximum upload file size
*/
export const EDITOR_SETTINGS_DEFAULTS = {
...SETTINGS_DEFAULTS,