diff --git a/packages/editor/src/components/editor-canvas/edit-template-blocks-notification.js b/packages/editor/src/components/editor-canvas/edit-template-blocks-notification.js index 2000ae5727190..48542e58a8732 100644 --- a/packages/editor/src/components/editor-canvas/edit-template-blocks-notification.js +++ b/packages/editor/src/components/editor-canvas/edit-template-blocks-notification.js @@ -1,9 +1,8 @@ /** * WordPress dependencies */ -import { useSelect, useDispatch } from '@wordpress/data'; -import { useEffect, useState, useRef } from '@wordpress/element'; -import { store as noticesStore } from '@wordpress/notices'; +import { useSelect } from '@wordpress/data'; +import { useEffect, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { __experimentalConfirmDialog as ConfirmDialog } from '@wordpress/components'; @@ -38,73 +37,22 @@ export default function EditTemplateBlocksNotification( { contentRef } ) { }; }, [] ); - const { getNotices } = useSelect( noticesStore ); - - const { createInfoNotice, removeNotice } = useDispatch( noticesStore ); - const [ isDialogOpen, setIsDialogOpen ] = useState( false ); - const lastNoticeId = useRef( 0 ); - useEffect( () => { - const handleClick = async ( event ) => { - if ( ! event.target.classList.contains( 'is-root-container' ) ) { - return; - } - - const isNoticeAlreadyShowing = getNotices().some( - ( notice ) => notice.id === lastNoticeId.current - ); - if ( isNoticeAlreadyShowing ) { - return; - } - - const { notice } = await createInfoNotice( - __( 'Edit your template to edit this block.' ), - { - isDismissible: true, - type: 'snackbar', - actions: [ - { - label: __( 'Edit template' ), - onClick: () => - onNavigateToEntityRecord( { - postId: templateId, - postType: 'wp_template', - } ), - }, - ], - } - ); - lastNoticeId.current = notice.id; - }; - const handleDblClick = ( event ) => { if ( ! event.target.classList.contains( 'is-root-container' ) ) { return; } - if ( lastNoticeId.current ) { - removeNotice( lastNoticeId.current ); - } setIsDialogOpen( true ); }; const canvas = contentRef.current; - canvas?.addEventListener( 'click', handleClick ); canvas?.addEventListener( 'dblclick', handleDblClick ); return () => { - canvas?.removeEventListener( 'click', handleClick ); canvas?.removeEventListener( 'dblclick', handleDblClick ); }; - }, [ - lastNoticeId, - contentRef, - getNotices, - createInfoNotice, - onNavigateToEntityRecord, - templateId, - removeNotice, - ] ); + }, [ contentRef ] ); return ( setIsDialogOpen( false ) } > - { __( 'Edit your template to edit this block.' ) } + { __( + 'You’ve tried to select a block that is part of a template, which may be used on other posts and pages.' + ) } ); } diff --git a/packages/editor/src/components/editor-canvas/index.js b/packages/editor/src/components/editor-canvas/index.js index bc7d54583afbd..363d52b124aa0 100644 --- a/packages/editor/src/components/editor-canvas/index.js +++ b/packages/editor/src/components/editor-canvas/index.js @@ -29,6 +29,7 @@ import PostTitle from '../post-title'; import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; import EditTemplateBlocksNotification from './edit-template-blocks-notification'; +import useSelectNearestEditableBlock from '../../hooks/use-select-nearest-editable-block'; const { LayoutStyle, @@ -313,6 +314,9 @@ function EditorCanvas( { useFlashEditableBlocks( { isEnabled: renderingMode === 'template-locked', } ), + useSelectNearestEditableBlock( { + isEnabled: renderingMode === 'template-locked', + } ), ] ); return ( diff --git a/packages/editor/src/hooks/use-select-nearest-editable-block.js b/packages/editor/src/hooks/use-select-nearest-editable-block.js new file mode 100644 index 0000000000000..f6e621a25bf43 --- /dev/null +++ b/packages/editor/src/hooks/use-select-nearest-editable-block.js @@ -0,0 +1,95 @@ +/** + * WordPress dependencies + */ +import { useRefEffect } from '@wordpress/compose'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as blockEditorStore } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import { unlock } from '../lock-unlock'; + +const DISTANCE_THRESHOLD = 500; + +function clamp( value, min, max ) { + return Math.min( Math.max( value, min ), max ); +} + +function distanceFromRect( x, y, rect ) { + const dx = x - clamp( x, rect.left, rect.right ); + const dy = y - clamp( y, rect.top, rect.bottom ); + return Math.sqrt( dx * dx + dy * dy ); +} + +export default function useSelectNearestEditableBlock( { + isEnabled = true, +} = {} ) { + const { getEnabledClientIdsTree, getBlockName, getBlockOrder } = unlock( + useSelect( blockEditorStore ) + ); + const { selectBlock } = useDispatch( blockEditorStore ); + + return useRefEffect( + ( element ) => { + if ( ! isEnabled ) { + return; + } + + const selectNearestEditableBlock = ( x, y ) => { + const editableBlockClientIds = + getEnabledClientIdsTree().flatMap( ( { clientId } ) => { + const blockName = getBlockName( clientId ); + if ( blockName === 'core/template-part' ) { + return []; + } + if ( blockName === 'core/post-content' ) { + const innerBlocks = getBlockOrder( clientId ); + if ( innerBlocks.length ) { + return innerBlocks; + } + } + return [ clientId ]; + } ); + + let nearestDistance = Infinity, + nearestClientId = null; + + for ( const clientId of editableBlockClientIds ) { + const block = element.querySelector( + `[data-block="${ clientId }"]` + ); + if ( ! block ) { + continue; + } + const rect = block.getBoundingClientRect(); + const distance = distanceFromRect( x, y, rect ); + if ( + distance < nearestDistance && + distance < DISTANCE_THRESHOLD + ) { + nearestDistance = distance; + nearestClientId = clientId; + } + } + + if ( nearestClientId ) { + selectBlock( nearestClientId ); + } + }; + + const handleClick = ( event ) => { + const shouldSelect = + event.target === element || + event.target.classList.contains( 'is-root-container' ); + if ( shouldSelect ) { + selectNearestEditableBlock( event.clientX, event.clientY ); + } + }; + + element.addEventListener( 'click', handleClick ); + return () => element.removeEventListener( 'click', handleClick ); + }, + [ isEnabled ] + ); +}