From f041a88551054f87506f3ac127cf179c3047c90c Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 28 May 2020 14:56:40 +0100 Subject: [PATCH 1/2] Extract inserter state into reusable hooks --- .../src/components/inserter/block-list.js | 68 ++------- .../src/components/inserter/block-patterns.js | 37 +---- .../inserter/hooks/use-block-types-state.js | 73 ++++++++++ .../inserter/hooks/use-insertion-point.js | 133 ++++++++++++++++++ .../inserter/hooks/use-patterns-state.js | 50 +++++++ .../src/components/inserter/menu.js | 108 +++----------- 6 files changed, 288 insertions(+), 181 deletions(-) create mode 100644 packages/block-editor/src/components/inserter/hooks/use-block-types-state.js create mode 100644 packages/block-editor/src/components/inserter/hooks/use-insertion-point.js create mode 100644 packages/block-editor/src/components/inserter/hooks/use-patterns-state.js diff --git a/packages/block-editor/src/components/inserter/block-list.js b/packages/block-editor/src/components/inserter/block-list.js index e9645bb469b20..0d8269b96666d 100644 --- a/packages/block-editor/src/components/inserter/block-list.js +++ b/packages/block-editor/src/components/inserter/block-list.js @@ -18,8 +18,6 @@ import { __, _x, _n, sprintf } from '@wordpress/i18n'; import { withSpokenMessages } from '@wordpress/components'; import { addQueryArgs } from '@wordpress/url'; import { controlsRepeat } from '@wordpress/icons'; -import { speak } from '@wordpress/a11y'; -import { createBlock } from '@wordpress/blocks'; import { useMemo, useEffect } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; @@ -33,19 +31,7 @@ import __experimentalInserterMenuExtension from '../inserter-menu-extension'; import { searchBlockItems } from './search-items'; import InserterPanel from './panel'; import InserterNoResults from './no-results'; - -// Copied over from the Columns block. It seems like it should become part of public API. -const createBlocksFromInnerBlocksTemplate = ( innerBlocksTemplate ) => { - return map( - innerBlocksTemplate, - ( [ name, attributes, innerBlocks = [] ] ) => - createBlock( - name, - attributes, - createBlocksFromInnerBlocksTemplate( innerBlocks ) - ) - ); -}; +import useBlockTypesState from './hooks/use-block-types-state'; const getBlockNamespace = ( item ) => item.name.split( '/' )[ 0 ]; @@ -55,64 +41,26 @@ export function InserterBlockList( { rootClientId, onInsert, onHover, - __experimentalSelectBlockOnInsert: selectBlockOnInsert, filterValue, debouncedSpeak, } ) { - const { - categories, - collections, - items, - rootChildBlocks, - fetchReusableBlocks, - } = useSelect( + const [ items, categories, collections, onSelectItem ] = useBlockTypesState( + rootClientId, + onInsert + ); + const { rootChildBlocks } = useSelect( ( select ) => { - const { getInserterItems, getBlockName, getSettings } = select( - 'core/block-editor' - ); - const { - getCategories, - getCollections, - getChildBlockNames, - } = select( 'core/blocks' ); + const { getBlockName } = select( 'core/block-editor' ); + const { getChildBlockNames } = select( 'core/blocks' ); const rootBlockName = getBlockName( rootClientId ); - const { __experimentalFetchReusableBlocks } = getSettings(); return { - categories: getCategories(), - collections: getCollections(), rootChildBlocks: getChildBlockNames( rootBlockName ), - items: getInserterItems( rootClientId ), - fetchReusableBlocks: __experimentalFetchReusableBlocks, }; }, [ rootClientId ] ); - // Fetch resuable blocks on mount - useEffect( () => { - if ( fetchReusableBlocks ) { - fetchReusableBlocks(); - } - }, [] ); - - const onSelectItem = ( item ) => { - const { name, title, initialAttributes, innerBlocks } = item; - const insertedBlock = createBlock( - name, - initialAttributes, - createBlocksFromInnerBlocksTemplate( innerBlocks ) - ); - - onInsert( insertedBlock ); - - if ( ! selectBlockOnInsert ) { - // translators: %s: the name of the block that has been added - const message = sprintf( __( '%s block added' ), title ); - speak( message ); - } - }; - const filteredItems = useMemo( () => { return searchBlockItems( items, categories, collections, filterValue ); }, [ filterValue, items, categories, collections ] ); diff --git a/packages/block-editor/src/components/inserter/block-patterns.js b/packages/block-editor/src/components/inserter/block-patterns.js index d92486522d12e..9fc7f67413469 100644 --- a/packages/block-editor/src/components/inserter/block-patterns.js +++ b/packages/block-editor/src/components/inserter/block-patterns.js @@ -1,16 +1,15 @@ /** * External dependencies */ -import { map, fromPairs } from 'lodash'; +import { fromPairs } from 'lodash'; /** * WordPress dependencies */ import { useMemo, useCallback } from '@wordpress/element'; -import { parse, cloneBlock } from '@wordpress/blocks'; -import { useDispatch, useSelect } from '@wordpress/data'; +import { parse } from '@wordpress/blocks'; import { ENTER, SPACE } from '@wordpress/keycodes'; -import { __, sprintf, _x } from '@wordpress/i18n'; +import { __, _x } from '@wordpress/i18n'; /** * Internal dependencies @@ -20,35 +19,7 @@ import useAsyncList from './use-async-list'; import InserterPanel from './panel'; import { searchItems } from './search-items'; import InserterNoResults from './no-results'; - -const usePatternsState = ( onInsert ) => { - const { patternCategories, patterns } = useSelect( ( select ) => { - const { - __experimentalBlockPatterns, - __experimentalBlockPatternCategories, - } = select( 'core/block-editor' ).getSettings(); - return { - patterns: __experimentalBlockPatterns, - patternCategories: __experimentalBlockPatternCategories, - }; - }, [] ); - const { createSuccessNotice } = useDispatch( 'core/notices' ); - const onClickPattern = useCallback( ( pattern, blocks ) => { - onInsert( map( blocks, ( block ) => cloneBlock( block ) ) ); - createSuccessNotice( - sprintf( - /* translators: %s: block pattern title. */ - __( 'Pattern "%s" inserted.' ), - pattern.title - ), - { - type: 'snackbar', - } - ); - }, [] ); - - return [ patterns, patternCategories, onClickPattern ]; -}; +import usePatternsState from './hooks/use-patterns-state'; function BlockPattern( { pattern, onClick } ) { const { content, viewportWidth } = pattern; diff --git a/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js b/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js new file mode 100644 index 0000000000000..26c7553997448 --- /dev/null +++ b/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js @@ -0,0 +1,73 @@ +/** + * External dependencies + */ +import { map } from 'lodash'; + +/** + * WordPress dependencies + */ +import { useEffect } from '@wordpress/element'; +import { createBlock } from '@wordpress/blocks'; +import { useSelect } from '@wordpress/data'; + +// Copied over from the Columns block. It seems like it should become part of public API. +const createBlocksFromInnerBlocksTemplate = ( innerBlocksTemplate ) => { + return map( + innerBlocksTemplate, + ( [ name, attributes, innerBlocks = [] ] ) => + createBlock( + name, + attributes, + createBlocksFromInnerBlocksTemplate( innerBlocks ) + ) + ); +}; + +/** + * Retrieves the block types inserter state. + * + * @param {string=} rootClientId Insertion's root client ID. + * @param {Function} onInsert function called when inserter a list of blocks. + * @return {Array} Returns the block types state. (block types, categories, collections, onSelect handler) + */ +const useBlockTypesState = ( rootClientId, onInsert ) => { + const { categories, collections, items, fetchReusableBlocks } = useSelect( + ( select ) => { + const { getInserterItems, getSettings } = select( + 'core/block-editor' + ); + const { getCategories, getCollections } = select( 'core/blocks' ); + const { __experimentalFetchReusableBlocks } = getSettings(); + + return { + categories: getCategories(), + collections: getCollections(), + items: getInserterItems( rootClientId ), + fetchReusableBlocks: __experimentalFetchReusableBlocks, + }; + }, + [ rootClientId ] + ); + + // Fetch resuable blocks on mount + useEffect( () => { + if ( fetchReusableBlocks ) { + fetchReusableBlocks(); + } + }, [] ); + + const onSelectItem = ( item ) => { + const { name, initialAttributes, innerBlocks } = item; + const insertedBlock = createBlock( + name, + initialAttributes, + createBlocksFromInnerBlocksTemplate( innerBlocks ) + ); + + onInsert( insertedBlock ); + }; + + return [ items, categories, collections, onSelectItem ]; +}; + +export default useBlockTypesState; diff --git a/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js b/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js new file mode 100644 index 0000000000000..60c19dd4f726d --- /dev/null +++ b/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js @@ -0,0 +1,133 @@ +/** + * External dependencies + */ +import { pick } from 'lodash'; + +/** + * WordPress dependencies + */ +import { useDispatch, useSelect } from '@wordpress/data'; +import { isUnmodifiedDefaultBlock } from '@wordpress/blocks'; +import { _n } from '@wordpress/i18n'; +import { speak } from '@wordpress/a11y'; + +/** + * @typedef WPInserterConfig + * + * @property {string=} rootClientId Inserter Root Client ID. + * @property {string=} clientId Inserter Client ID. + * @property {boolean} isAppender Whether the inserter is an appender or not. + * @property {boolean} selectBlockOnInsert Whether the block should be selected on insert. + */ + +/** + * Returns the insertion point state given the inserter config. + * + * @param {WPInserterConfig} config Inserter Config. + * @return {Array} Insertion Point State (rootClientID, onInsertBlocks and onToggle). + */ +function useInsertionPoint( { + rootClientId, + clientId, + isAppender, + selectBlockOnInsert, +} ) { + const { + destinationRootClientId, + getSelectedBlock, + getBlockIndex, + getBlockSelectionEnd, + getBlockOrder, + } = useSelect( + ( select ) => { + const { + getSettings, + getBlockRootClientId, + getBlockSelectionEnd: _getBlockSelectionEnd, + } = select( 'core/block-editor' ); + + let destRootClientId = rootClientId; + if ( ! destRootClientId && ! clientId && ! isAppender ) { + const end = _getBlockSelectionEnd(); + if ( end ) { + destRootClientId = getBlockRootClientId( end ) || undefined; + } + } + return { + hasPatterns: !! getSettings().__experimentalBlockPatterns + ?.length, + destinationRootClientId: destRootClientId, + ...pick( select( 'core/block-editor' ), [ + 'getSelectedBlock', + 'getBlockIndex', + 'getBlockSelectionEnd', + 'getBlockOrder', + ] ), + }; + }, + [ isAppender, clientId, rootClientId ] + ); + const { + replaceBlocks, + insertBlocks, + showInsertionPoint, + hideInsertionPoint, + } = useDispatch( 'core/block-editor' ); + + function getInsertionIndex() { + // If the clientId is defined, we insert at the position of the block. + if ( clientId ) { + return getBlockIndex( clientId, destinationRootClientId ); + } + + // If there a selected block, we insert after the selected block. + const end = getBlockSelectionEnd(); + if ( ! isAppender && end ) { + return getBlockIndex( end, destinationRootClientId ) + 1; + } + + // Otherwise, we insert at the end of the current rootClientId + return getBlockOrder( destinationRootClientId ).length; + } + + const onInsertBlocks = ( blocks ) => { + const selectedBlock = getSelectedBlock(); + if ( + ! isAppender && + selectedBlock && + isUnmodifiedDefaultBlock( selectedBlock ) + ) { + replaceBlocks( selectedBlock.clientId, blocks ); + } else { + insertBlocks( + blocks, + getInsertionIndex(), + destinationRootClientId, + selectBlockOnInsert + ); + } + + if ( ! selectBlockOnInsert ) { + // translators: %d: the name of the block that has been added + const message = _n( + '%d block added.', + '%d blocks added', + blocks.length + ); + speak( message ); + } + }; + + const onToggleInsertionPoint = ( show ) => { + if ( show ) { + const index = getInsertionIndex(); + showInsertionPoint( destinationRootClientId, index ); + } else { + hideInsertionPoint(); + } + }; + + return [ destinationRootClientId, onInsertBlocks, onToggleInsertionPoint ]; +} + +export default useInsertionPoint; diff --git a/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js b/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js new file mode 100644 index 0000000000000..8d9ff02f23141 --- /dev/null +++ b/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js @@ -0,0 +1,50 @@ +/** + * External dependencies + */ +import { map } from 'lodash'; + +/** + * WordPress dependencies + */ +import { useCallback } from '@wordpress/element'; +import { cloneBlock } from '@wordpress/blocks'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Retrieves the block patterns inserter state. + * + * @param {Function} onInsert function called when inserter a list of blocks. + * + * @return {Array} Returns the patterns state. (patterns, categories, onSelect handler) + */ +const usePatternsState = ( onInsert ) => { + const { patternCategories, patterns } = useSelect( ( select ) => { + const { + __experimentalBlockPatterns, + __experimentalBlockPatternCategories, + } = select( 'core/block-editor' ).getSettings(); + return { + patterns: __experimentalBlockPatterns, + patternCategories: __experimentalBlockPatternCategories, + }; + }, [] ); + const { createSuccessNotice } = useDispatch( 'core/notices' ); + const onClickPattern = useCallback( ( pattern, blocks ) => { + onInsert( map( blocks, ( block ) => cloneBlock( block ) ) ); + createSuccessNotice( + sprintf( + /* translators: %s: block pattern title. */ + __( 'Pattern "%s" inserted.' ), + pattern.title + ), + { + type: 'snackbar', + } + ); + }, [] ); + + return [ patterns, patternCategories, onClickPattern ]; +}; + +export default usePatternsState; diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index a1811e85dba6a..0393f7ec40716 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { includes, pick } from 'lodash'; +import { includes } from 'lodash'; /** * WordPress dependencies @@ -10,8 +10,7 @@ import { useState } from '@wordpress/element'; import { LEFT, RIGHT, UP, DOWN, BACKSPACE, ENTER } from '@wordpress/keycodes'; import { TabPanel } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { isUnmodifiedDefaultBlock } from '@wordpress/blocks'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -21,6 +20,7 @@ import InserterSearchForm from './search-form'; import InserterPreviewPanel from './preview-panel'; import InserterBlockList from './block-list'; import BlockPatterns from './block-patterns'; +import useInsertionPoint from './hooks/use-insertion-point'; const stopKeyPropagation = ( event ) => event.stopPropagation(); @@ -34,48 +34,27 @@ function InserterMenu( { } ) { const [ filterValue, setFilterValue ] = useState( '' ); const [ hoveredItem, setHoveredItem ] = useState( null ); - const { + const [ destinationRootClientId, - hasPatterns, - getSelectedBlock, - getBlockIndex, - getBlockSelectionEnd, - getBlockOrder, - } = useSelect( + onInsertBlocks, + onToggleInsertionPoint, + ] = useInsertionPoint( { + rootClientId, + clientId, + isAppender, + selectBlockOnInsert: __experimentalSelectBlockOnInsert, + } ); + const { hasPatterns } = useSelect( ( select ) => { - const { - getSettings, - getBlockRootClientId, - getBlockSelectionEnd: _getBlockSelectionEnd, - } = select( 'core/block-editor' ); - - let destRootClientId = rootClientId; - if ( ! destRootClientId && ! clientId && ! isAppender ) { - const end = _getBlockSelectionEnd(); - if ( end ) { - destRootClientId = getBlockRootClientId( end ) || undefined; - } - } + const { getSettings } = select( 'core/block-editor' ); return { hasPatterns: !! getSettings().__experimentalBlockPatterns ?.length, - destinationRootClientId: destRootClientId, - ...pick( select( 'core/block-editor' ), [ - 'getSelectedBlock', - 'getBlockIndex', - 'getBlockSelectionEnd', - 'getBlockOrder', - ] ), }; }, [ isAppender, clientId, rootClientId ] ); - const { - replaceBlocks, - insertBlocks, - showInsertionPoint, - hideInsertionPoint, - } = useDispatch( 'core/block-editor' ); + const showPatterns = ! destinationRootClientId && hasPatterns; const onKeyDown = ( event ) => { if ( @@ -89,55 +68,14 @@ function InserterMenu( { } }; - // To avoid duplication, getInsertionIndex is extracted and used in two event handlers - // This breaks the withDispatch not containing any logic rule. - // Since it's a function only called when the event handlers are called, - // it's fine to extract it. - // eslint-disable-next-line no-restricted-syntax - function getInsertionIndex() { - // If the clientId is defined, we insert at the position of the block. - if ( clientId ) { - return getBlockIndex( clientId, destinationRootClientId ); - } - - // If there a selected block, we insert after the selected block. - const end = getBlockSelectionEnd(); - if ( ! isAppender && end ) { - return getBlockIndex( end, destinationRootClientId ) + 1; - } - - // Otherwise, we insert at the end of the current rootClientId - return getBlockOrder( destinationRootClientId ).length; - } - - const onInsertBlocks = ( blocks ) => { - const selectedBlock = getSelectedBlock(); - if ( - ! isAppender && - selectedBlock && - isUnmodifiedDefaultBlock( selectedBlock ) - ) { - replaceBlocks( selectedBlock.clientId, blocks ); - } else { - insertBlocks( - blocks, - getInsertionIndex(), - destinationRootClientId, - __experimentalSelectBlockOnInsert - ); - } - + const onInsert = ( blocks ) => { + onInsertBlocks( blocks ); onSelect(); }; const onHover = ( item ) => { + onToggleInsertionPoint( !! item ); setHoveredItem( item ); - if ( item ) { - const index = getInsertionIndex(); - showInsertionPoint( destinationRootClientId, index ); - } else { - hideInsertionPoint(); - } }; const blocksTab = ( @@ -146,11 +84,8 @@ function InserterMenu( {
@@ -165,10 +100,7 @@ function InserterMenu( { const patternsTab = (
- +
); From 120daec29cb766ea88317dc2f52d21c37f0fc77b Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 29 May 2020 09:18:49 +0100 Subject: [PATCH 2/2] Review tweaks --- .../block-editor/src/components/inserter/block-list.js | 6 ++---- .../components/inserter/hooks/use-block-types-state.js | 3 +-- .../src/components/inserter/hooks/use-insertion-point.js | 8 ++++---- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/block-editor/src/components/inserter/block-list.js b/packages/block-editor/src/components/inserter/block-list.js index 0d8269b96666d..f6bd172add888 100644 --- a/packages/block-editor/src/components/inserter/block-list.js +++ b/packages/block-editor/src/components/inserter/block-list.js @@ -48,15 +48,13 @@ export function InserterBlockList( { rootClientId, onInsert ); - const { rootChildBlocks } = useSelect( + const rootChildBlocks = useSelect( ( select ) => { const { getBlockName } = select( 'core/block-editor' ); const { getChildBlockNames } = select( 'core/blocks' ); const rootBlockName = getBlockName( rootClientId ); - return { - rootChildBlocks: getChildBlockNames( rootBlockName ), - }; + return getChildBlockNames( rootBlockName ); }, [ rootClientId ] ); diff --git a/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js b/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js index 26c7553997448..80cd70ba3013d 100644 --- a/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js +++ b/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js @@ -56,8 +56,7 @@ const useBlockTypesState = ( rootClientId, onInsert ) => { } }, [] ); - const onSelectItem = ( item ) => { - const { name, initialAttributes, innerBlocks } = item; + const onSelectItem = ( { name, initialAttributes, innerBlocks } ) => { const insertedBlock = createBlock( name, initialAttributes, diff --git a/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js b/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js index 60c19dd4f726d..d8dbd5e012d2b 100644 --- a/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js +++ b/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js @@ -14,10 +14,10 @@ import { speak } from '@wordpress/a11y'; /** * @typedef WPInserterConfig * - * @property {string=} rootClientId Inserter Root Client ID. + * @property {string=} rootClientId Inserter Root Client ID. * @property {string=} clientId Inserter Client ID. - * @property {boolean} isAppender Whether the inserter is an appender or not. - * @property {boolean} selectBlockOnInsert Whether the block should be selected on insert. + * @property {boolean} isAppender Whether the inserter is an appender or not. + * @property {boolean} selectBlockOnInsert Whether the block should be selected on insert. */ /** @@ -50,7 +50,7 @@ function useInsertionPoint( { if ( ! destRootClientId && ! clientId && ! isAppender ) { const end = _getBlockSelectionEnd(); if ( end ) { - destRootClientId = getBlockRootClientId( end ) || undefined; + destRootClientId = getBlockRootClientId( end ); } } return {