From 071dae2671f54eb66cf3541b078d0a0154e70f7a Mon Sep 17 00:00:00 2001 From: John Godley Date: Fri, 17 Sep 2021 13:40:03 +0100 Subject: [PATCH 01/13] Remove block editor effects This now works by default without needing any middleware --- .../with-registry-provider/effects.js | 235 ------------------ .../with-registry-provider/index.js | 3 - .../with-registry-provider/middlewares.js | 43 ---- 3 files changed, 281 deletions(-) delete mode 100644 src/components/with-registry-provider/effects.js delete mode 100644 src/components/with-registry-provider/middlewares.js diff --git a/src/components/with-registry-provider/effects.js b/src/components/with-registry-provider/effects.js deleted file mode 100644 index 9d14f174b..000000000 --- a/src/components/with-registry-provider/effects.js +++ /dev/null @@ -1,235 +0,0 @@ -/** - * External dependencies - */ -import { findKey } from 'lodash'; - -/** - * WordPress dependencies - */ -import { speak } from '@wordpress/a11y'; -import { - getBlockType, - doBlocksMatchTemplate, - switchToBlockType, - synchronizeBlocksWithTemplate, - cloneBlock, -} from '@wordpress/blocks'; -import { _n, sprintf } from '@wordpress/i18n'; -import { create, toHTMLString, insert, remove } from '@wordpress/rich-text'; - -/** - * Internal dependencies - */ -import { storeConfig as blockEditorStoreConfig } from '@wordpress/block-editor'; -const { - replaceBlocks, - selectBlock, - setTemplateValidity, - resetBlocks, - selectionChange, -} = blockEditorStoreConfig.actions; -const { - getBlock, - getBlocks, - getSelectedBlockCount, - getTemplateLock, - getTemplate, - isValidTemplate, - getSelectionStart, -} = blockEditorStoreConfig.selectors; - -/** - * Block validity is a function of blocks state (at the point of a - * reset) and the template setting. As a compromise to its placement - * across distinct parts of state, it is implemented here as a side- - * effect of the block reset action. - * - * @param {Object} action RESET_BLOCKS action. - * @param {Object} store Store instance. - * - * @return {?Object} New validity set action if validity has changed. - */ -export function validateBlocksToTemplate( action, store ) { - const state = store.getState(); - const template = getTemplate( state ); - const templateLock = getTemplateLock( state ); - - // Unlocked templates are considered always valid because they act - // as default values only. - const isBlocksValidToTemplate = - ! template || templateLock !== 'all' || doBlocksMatchTemplate( action.blocks, template ); - - // Update if validity has changed. - if ( isBlocksValidToTemplate !== isValidTemplate( state ) ) { - return setTemplateValidity( isBlocksValidToTemplate ); - } -} - -export default { - MERGE_BLOCKS( action, store ) { - const { dispatch } = store; - const state = store.getState(); - const [ clientIdA, clientIdB ] = action.blocks; - const blockA = getBlock( state, clientIdA ); - const blockAType = getBlockType( blockA.name ); - - // Only focus the previous block if it's not mergeable - if ( ! blockAType.merge ) { - dispatch( selectBlock( blockA.clientId ) ); - return; - } - - const blockB = getBlock( state, clientIdB ); - const blockBType = getBlockType( blockB.name ); - const { clientId, attributeKey, offset } = getSelectionStart( state ); - const selectedBlockType = clientId === clientIdA ? blockAType : blockBType; - const attributeDefinition = selectedBlockType.attributes[ attributeKey ]; - const canRestoreTextSelection = - ( clientId === clientIdA || clientId === clientIdB ) && - attributeKey !== undefined && - offset !== undefined && - // We cannot restore text selection if the RichText identifier - // is not a defined block attribute key. This can be the case if the - // fallback intance ID is used to store selection (and no RichText - // identifier is set), or when the identifier is wrong. - !! attributeDefinition; - - if ( ! attributeDefinition ) { - if ( typeof attributeKey === 'number' ) { - window.console.error( - `RichText needs an identifier prop that is the block attribute key of the attribute it controls. Its type is expected to be a string, but was ${ typeof attributeKey }` - ); - } else { - window.console.error( - 'The RichText identifier prop does not match any attributes defined by the block.' - ); - } - } - - // A robust way to retain selection position through various transforms - // is to insert a special character at the position and then recover it. - const START_OF_SELECTED_AREA = '\u0086'; - - // Clone the blocks so we don't insert the character in a "live" block. - const cloneA = cloneBlock( blockA ); - const cloneB = cloneBlock( blockB ); - - if ( canRestoreTextSelection ) { - const selectedBlock = clientId === clientIdA ? cloneA : cloneB; - const html = selectedBlock.attributes[ attributeKey ]; - const { - multiline: multilineTag, - __unstableMultilineWrapperTags: multilineWrapperTags, - __unstablePreserveWhiteSpace: preserveWhiteSpace, - } = attributeDefinition; - const value = insert( - create( { - html, - multilineTag, - multilineWrapperTags, - preserveWhiteSpace, - } ), - START_OF_SELECTED_AREA, - offset, - offset - ); - - selectedBlock.attributes[ attributeKey ] = toHTMLString( { - value, - multilineTag, - preserveWhiteSpace, - } ); - } - - // We can only merge blocks with similar types - // thus, we transform the block to merge first - const blocksWithTheSameType = - blockA.name === blockB.name ? [ cloneB ] : switchToBlockType( cloneB, blockA.name ); - - // If the block types can not match, do nothing - if ( ! blocksWithTheSameType || ! blocksWithTheSameType.length ) { - return; - } - - // Calling the merge to update the attributes and remove the block to be merged - const updatedAttributes = blockAType.merge( cloneA.attributes, blocksWithTheSameType[ 0 ].attributes ); - - if ( canRestoreTextSelection ) { - const newAttributeKey = findKey( - updatedAttributes, - ( v ) => typeof v === 'string' && v.indexOf( START_OF_SELECTED_AREA ) !== -1 - ); - if ( newAttributeKey === undefined ) { - return; - } - const convertedHtml = updatedAttributes[ newAttributeKey ]; - const { - multiline: multilineTag, - __unstableMultilineWrapperTags: multilineWrapperTags, - __unstablePreserveWhiteSpace: preserveWhiteSpace, - } = blockAType.attributes[ newAttributeKey ]; - const convertedValue = create( { - html: convertedHtml, - multilineTag, - multilineWrapperTags, - preserveWhiteSpace, - } ); - const newOffset = convertedValue.text.indexOf( START_OF_SELECTED_AREA ); - const newValue = remove( convertedValue, newOffset, newOffset + 1 ); - const newHtml = toHTMLString( { - value: newValue, - multilineTag, - preserveWhiteSpace, - } ); - - updatedAttributes[ newAttributeKey ] = newHtml; - - dispatch( selectionChange( blockA.clientId, newAttributeKey, newOffset, newOffset ) ); - } - - dispatch( - replaceBlocks( - [ blockA.clientId, blockB.clientId ], - [ - { - ...blockA, - attributes: { - ...blockA.attributes, - ...updatedAttributes, - }, - }, - ...blocksWithTheSameType.slice( 1 ), - ] - ) - ); - }, - RESET_BLOCKS: [ validateBlocksToTemplate ], - MULTI_SELECT: ( action, { getState } ) => { - const blockCount = getSelectedBlockCount( getState() ); - - speak( - sprintf( - /* translators: %s: number of selected blocks */ - _n( '%s block selected.', '%s blocks selected.', blockCount ), - blockCount - ), - 'assertive' - ); - }, - SYNCHRONIZE_TEMPLATE( action, { getState } ) { - const state = getState(); - const blocks = getBlocks( state ); - const template = getTemplate( state ); - const updatedBlockList = synchronizeBlocksWithTemplate( blocks, template ); - - return resetBlocks( updatedBlockList ); - }, - MARK_AUTOMATIC_CHANGE( action, store ) { - // @ts-ignore */} - const { setTimeout, requestIdleCallback = ( callback ) => setTimeout( callback, 100 ) } = window; - - requestIdleCallback( () => { - store.dispatch( { type: 'MARK_AUTOMATIC_CHANGE_FINAL' } ); - } ); - }, -}; diff --git a/src/components/with-registry-provider/index.js b/src/components/with-registry-provider/index.js index 6baa9182e..4b62725b1 100644 --- a/src/components/with-registry-provider/index.js +++ b/src/components/with-registry-provider/index.js @@ -12,7 +12,6 @@ import { storeConfig as coreEditorStoreConfig } from '@wordpress/editor'; */ import storeConfig from '../../store'; import applyMiddlewares from '../../store/middlewares'; -import applyBlockEditorMiddlewares from './middlewares'; import reusableStore from './reusable-store'; import applyDefaultSettings from '../default-settings'; import decoratedEditor from '../../store/core-editor'; @@ -90,8 +89,6 @@ const withRegistryProvider = createHigherOrderComponent( applyMiddlewares( store ); setSubRegistry( newRegistry ); - applyBlockEditorMiddlewares( blockEditorStore ); - return function cleanup() { registries = registries.filter( ( item ) => item !== store ); }; diff --git a/src/components/with-registry-provider/middlewares.js b/src/components/with-registry-provider/middlewares.js deleted file mode 100644 index f23dbc631..000000000 --- a/src/components/with-registry-provider/middlewares.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * External dependencies - */ -import refx from 'refx'; -import multi from 'redux-multi'; -import { flowRight } from 'lodash'; - -/** - * Internal dependencies - */ -import effects from './effects'; - -/** - * Applies the custom middlewares used specifically in the editor module. - * - * @param {Object} store Store Object. - * - * @return {Object} Update Store Object. - */ -function applyMiddlewares( store ) { - const middlewares = [ refx( effects ), multi ]; - - let enhancedDispatch = () => { - throw new Error( - 'Dispatching while constructing your middleware is not allowed. ' + - 'Other middleware would not be applied to this dispatch.' - ); - }; - let chain = []; - - const middlewareAPI = { - getState: store.getState, - // @ts-ignore - dispatch: ( ...args ) => enhancedDispatch( ...args ), - }; - chain = middlewares.map( ( middleware ) => middleware( middlewareAPI ) ); - enhancedDispatch = flowRight( ...chain )( store.dispatch ); - - store.dispatch = enhancedDispatch; - return store; -} - -export default applyMiddlewares; From 0eb3d87d9ac893313fa9a3f4fe1fd93a40588e0f Mon Sep 17 00:00:00 2001 From: John Godley Date: Fri, 17 Sep 2021 13:41:24 +0100 Subject: [PATCH 02/13] Use BlockTools This updates the editor to use the latest core editing, and includes things such as multi-block deletion. It does force a two-line header. --- src/components/block-editor/visual-editor.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/block-editor/visual-editor.js b/src/components/block-editor/visual-editor.js index 10b357197..5c59da2a0 100644 --- a/src/components/block-editor/visual-editor.js +++ b/src/components/block-editor/visual-editor.js @@ -9,15 +9,16 @@ import React from 'react'; import { BlockList, WritingFlow, + ObserveTyping, __unstableUseBlockSelectionClearer as useBlockSelectionClearer, __unstableUseTypewriter as useTypewriter, __unstableUseClipboardHandler as useClipboardHandler, __unstableUseTypingObserver as useTypingObserver, __unstableUseCanvasClickRedirect as useCanvasClickRedirect, __experimentalBlockSettingsMenuFirstItem, + BlockTools, } from '@wordpress/block-editor'; import { useRef } from '@wordpress/element'; -import { Popover } from '@wordpress/components'; import { useMergeRefs } from '@wordpress/compose'; /** @@ -37,15 +38,15 @@ const VisualEditor = () => { ] ); return ( -
- - +
- + + +
-
+ ); }; From 0538fbd678c31ca4bf13e98e8426614c47e149f8 Mon Sep 17 00:00:00 2001 From: John Godley Date: Fri, 17 Sep 2021 13:42:54 +0100 Subject: [PATCH 03/13] Update toolbar to match core This includes changes needed for BlockTools, as well as adding the option to show the selector tool. --- .../header-toolbar/index.js | 80 ++++++++++++------- src/components/block-editor-toolbar/index.js | 2 - src/components/default-settings/index.js | 2 + src/index.js | 1 + src/store/editor/reducer.js | 1 + 5 files changed, 53 insertions(+), 33 deletions(-) diff --git a/src/components/block-editor-toolbar/header-toolbar/index.js b/src/components/block-editor-toolbar/header-toolbar/index.js index 76f9ca4cd..38f7f46ff 100644 --- a/src/components/block-editor-toolbar/header-toolbar/index.js +++ b/src/components/block-editor-toolbar/header-toolbar/index.js @@ -5,15 +5,10 @@ import { useViewportMatch } from '@wordpress/compose'; import { useSelect, useDispatch } from '@wordpress/data'; import { __, _x } from '@wordpress/i18n'; import { ToolbarItem, Button, Popover } from '@wordpress/components'; -import { - BlockToolbar, - NavigableToolbar, - BlockNavigationDropdown, - __experimentalLibrary as Library, -} from '@wordpress/block-editor'; +import { NavigableToolbar, BlockNavigationDropdown, __experimentalLibrary as Library, ToolSelector } from '@wordpress/block-editor'; import { TableOfContents } from '@wordpress/editor'; import { plus } from '@wordpress/icons'; -import { useRef } from '@wordpress/element'; +import { useRef, useCallback } from '@wordpress/element'; /** * Internal dependencies @@ -22,6 +17,10 @@ import EditorHistoryRedo from './redo'; import EditorHistoryUndo from './undo'; import './style.scss'; +const preventDefault = ( event ) => { + event.preventDefault(); +}; + function HeaderToolbar( props ) { const inserterButton = useRef(); const { setIsInserterOpened } = useDispatch( 'isolated/editor' ); @@ -31,6 +30,7 @@ function HeaderToolbar( props ) { hasPeers, isInserterEnabled, isTextModeEnabled, + showIconLabels, previewDeviceType, isInserterOpened, } = useSelect( ( select ) => { @@ -47,10 +47,12 @@ function HeaderToolbar( props ) { isTextModeEnabled: select( 'isolated/editor' ).getEditorMode() === 'text', previewDeviceType: 'Desktop', isInserterOpened: select( 'isolated/editor' ).isInserterOpened(), + showIconLabels: false, // Not implemented yet }; }, [] ); const isLargeViewport = useViewportMatch( 'medium' ); - const { inserter, toc, navigation, undo: undoSetting } = props.settings.iso.toolbar; + const isWideViewport = useViewportMatch( 'wide' ); + const { inserter, toc, navigation, undo: undoSetting, selectorTool } = props.settings.iso.toolbar; const undo = undoSetting && ! hasPeers; const displayBlockToolbar = ! isLargeViewport || previewDeviceType !== 'Desktop' || hasFixedToolbar; const toolbarAriaLabel = displayBlockToolbar @@ -58,6 +60,14 @@ function HeaderToolbar( props ) { __( 'Document and block tools' ) : /* translators: accessibility text for the editor toolbar when Top Toolbar is off */ __( 'Document tools' ); + const openInserter = useCallback( () => { + if ( isInserterOpened ) { + // Focusing the inserter button closes the inserter popover + inserterButton.current.focus(); + } else { + setIsInserterOpened( true ); + } + }, [ isInserterOpened, setIsInserterOpened ] ); return ( @@ -68,23 +78,16 @@ function HeaderToolbar( props ) { ref={ inserterButton } as={ Button } className="edit-post-header-toolbar__inserter-toggle" - isPrimary isPressed={ isInserterOpened } - onMouseDown={ ( event ) => { - event.preventDefault(); - } } - onClick={ () => { - if ( isInserterOpened ) { - // Focusing the inserter button closes the inserter popover - // @ts-ignore - inserterButton.current.focus(); - } else { - setIsInserterOpened( true ); - } - } } + onMouseDown={ preventDefault } + onClick={ openInserter } disabled={ ! isInserterEnabled } + isPrimary icon={ plus } - label={ _x( 'Add block', 'Generic label for block inserter button' ) } + /* translators: button label text should, if possible, be under 16 + characters. */ + label={ _x( 'Toggle block inserter', 'Generic label for block inserter button' ) } + showTooltip={ ! showIconLabels } /> ) } @@ -106,16 +109,31 @@ function HeaderToolbar( props ) { ) } - { undo && } - { undo && } + { selectorTool && } + { undo && ( + + ) } + { undo && ( + + ) } { navigation && } - { toc && } - - ) } - - { displayBlockToolbar && ! isTextModeEnabled && ( -
- + { toc && ( + + ) }
) }
diff --git a/src/components/block-editor-toolbar/index.js b/src/components/block-editor-toolbar/index.js index 76c71d175..09a19af0d 100644 --- a/src/components/block-editor-toolbar/index.js +++ b/src/components/block-editor-toolbar/index.js @@ -58,8 +58,6 @@ const BlockEditorToolbar = ( props ) => {
- - { inspector && (