@wordpress/block-editor
Version:
170 lines (144 loc) • 4.8 kB
JavaScript
/**
* WordPress dependencies
*/
import { useSelect, useDispatch } from '@wordpress/data';
import { useRefEffect } from '@wordpress/compose';
/**
* Internal dependencies
*/
import { store as blockEditorStore } from '../../store';
/**
* Sets the `contenteditable` wrapper element to `value`.
*
* @param {HTMLElement} node Block element.
* @param {boolean} value `contentEditable` value (true or false)
*/
function setContentEditableWrapper( node, value ) {
node.contentEditable = value;
// Firefox doesn't automatically move focus.
if ( value ) {
node.focus();
}
}
/**
* Sets a multi-selection based on the native selection across blocks.
*/
export default function useDragSelection() {
const { startMultiSelect, stopMultiSelect } =
useDispatch( blockEditorStore );
const {
isSelectionEnabled,
hasSelectedBlock,
isDraggingBlocks,
isMultiSelecting,
} = useSelect( blockEditorStore );
return useRefEffect(
( node ) => {
const { ownerDocument } = node;
const { defaultView } = ownerDocument;
let anchorElement;
let rafId;
function onMouseUp() {
stopMultiSelect();
// Equivalent to attaching the listener once.
defaultView.removeEventListener( 'mouseup', onMouseUp );
// The browser selection won't have updated yet at this point,
// so wait until the next animation frame to get the browser
// selection.
rafId = defaultView.requestAnimationFrame( () => {
if ( ! hasSelectedBlock() ) {
return;
}
// If the selection is complete (on mouse up), and no
// multiple blocks have been selected, set focus back to the
// anchor element. if the anchor element contains the
// selection. Additionally, the contentEditable wrapper can
// now be disabled again.
setContentEditableWrapper( node, false );
const selection = defaultView.getSelection();
if ( selection.rangeCount ) {
const range = selection.getRangeAt( 0 );
const { commonAncestorContainer } = range;
const clonedRange = range.cloneRange();
if (
anchorElement.contains( commonAncestorContainer )
) {
anchorElement.focus();
selection.removeAllRanges();
selection.addRange( clonedRange );
}
}
} );
}
let lastMouseDownTarget;
function onMouseDown( { target } ) {
lastMouseDownTarget = target;
}
function onMouseLeave( { buttons, target, relatedTarget } ) {
if ( ! target.contains( lastMouseDownTarget ) ) {
return;
}
// If we're moving into a child element, ignore. We're tracking
// the mouse leaving the element to a parent, no a child.
if ( target.contains( relatedTarget ) ) {
return;
}
// Avoid triggering a multi-selection if the user is already
// dragging blocks.
if ( isDraggingBlocks() ) {
return;
}
// The primary button must be pressed to initiate selection.
// See https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
if ( buttons !== 1 ) {
return;
}
// Abort if we are already multi-selecting.
if ( isMultiSelecting() ) {
return;
}
// Abort if selection is leaving writing flow.
if ( node === target ) {
return;
}
// Check the attribute, not the contentEditable attribute. All
// child elements of the content editable wrapper are editable
// and return true for this property. We only want to start
// multi selecting when the mouse leaves the wrapper.
if ( target.getAttribute( 'contenteditable' ) !== 'true' ) {
return;
}
if ( ! isSelectionEnabled() ) {
return;
}
// Do not rely on the active element because it may change after
// the mouse leaves for the first time. See
// https://github.com/WordPress/gutenberg/issues/48747.
anchorElement = target;
startMultiSelect();
// `onSelectionStart` is called after `mousedown` and
// `mouseleave` (from a block). The selection ends when
// `mouseup` happens anywhere in the window.
defaultView.addEventListener( 'mouseup', onMouseUp );
// Allow cross contentEditable selection by temporarily making
// all content editable. We can't rely on using the store and
// React because re-rending happens too slowly. We need to be
// able to select across instances immediately.
setContentEditableWrapper( node, true );
}
node.addEventListener( 'mouseout', onMouseLeave );
node.addEventListener( 'mousedown', onMouseDown );
return () => {
node.removeEventListener( 'mouseout', onMouseLeave );
defaultView.removeEventListener( 'mouseup', onMouseUp );
defaultView.cancelAnimationFrame( rafId );
};
},
[
startMultiSelect,
stopMultiSelect,
isSelectionEnabled,
hasSelectedBlock,
]
);
}