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 } ) => ( + openModal( 'edit-post/manage-blocks' ), + ] ) } + > + { __( 'Block Manager' ) } + + ); +} + +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,