@gechiui/block-editor
Version:
163 lines (137 loc) • 4.17 kB
JavaScript
/**
* External dependencies
*/
import { first, last } from 'lodash';
/**
* GeChiUI dependencies
*/
import { useRefEffect } from '@gechiui/compose';
import { useSelect } from '@gechiui/data';
/**
* Internal dependencies
*/
import { store as blockEditorStore } from '../../store';
import { __unstableUseBlockRef as useBlockRef } from '../block-list/use-block-props/use-block-refs';
function toggleRichText( container, toggle ) {
Array.from( container.querySelectorAll( '.rich-text' ) ).forEach(
( node ) => {
if ( toggle ) {
node.setAttribute( 'contenteditable', true );
} else {
node.removeAttribute( 'contenteditable' );
}
}
);
}
/**
* Returns for the deepest node at the start or end of a container node. Ignores
* any text nodes that only contain HTML formatting whitespace.
*
* @param {Element} node Container to search.
* @param {string} type 'start' or 'end'.
*/
function getDeepestNode( node, type ) {
const child = type === 'start' ? 'firstChild' : 'lastChild';
const sibling = type === 'start' ? 'nextSibling' : 'previousSibling';
while ( node[ child ] ) {
node = node[ child ];
while (
node.nodeType === node.TEXT_NODE &&
/^[ \t\n]*$/.test( node.data ) &&
node[ sibling ]
) {
node = node[ sibling ];
}
}
return node;
}
function selector( select ) {
const {
isMultiSelecting,
getMultiSelectedBlockClientIds,
hasMultiSelection,
getSelectedBlockClientId,
} = select( blockEditorStore );
return {
isMultiSelecting: isMultiSelecting(),
multiSelectedBlockClientIds: getMultiSelectedBlockClientIds(),
hasMultiSelection: hasMultiSelection(),
selectedBlockClientId: getSelectedBlockClientId(),
};
}
export default function useMultiSelection() {
const {
isMultiSelecting,
multiSelectedBlockClientIds,
hasMultiSelection,
selectedBlockClientId,
} = useSelect( selector, [] );
const selectedRef = useBlockRef( selectedBlockClientId );
// These must be in the right DOM order.
const startRef = useBlockRef( first( multiSelectedBlockClientIds ) );
const endRef = useBlockRef( last( multiSelectedBlockClientIds ) );
/**
* When the component updates, and there is multi selection, we need to
* select the entire block contents.
*/
return useRefEffect(
( node ) => {
const { ownerDocument } = node;
const { defaultView } = ownerDocument;
if ( ! hasMultiSelection || isMultiSelecting ) {
if ( ! selectedBlockClientId || isMultiSelecting ) {
return;
}
const selection = defaultView.getSelection();
if ( selection.rangeCount && ! selection.isCollapsed ) {
const blockNode = selectedRef.current;
const {
startContainer,
endContainer,
} = selection.getRangeAt( 0 );
if (
!! blockNode &&
( ! blockNode.contains( startContainer ) ||
! blockNode.contains( endContainer ) )
) {
selection.removeAllRanges();
}
}
return;
}
const { length } = multiSelectedBlockClientIds;
if ( length < 2 ) {
return;
}
// The block refs might not be immediately available
// when dragging blocks into another block.
if ( ! startRef.current || ! endRef.current ) {
return;
}
// For some browsers, like Safari, it is important that focus happens
// BEFORE selection.
node.focus();
const selection = defaultView.getSelection();
const range = ownerDocument.createRange();
// These must be in the right DOM order.
// The most stable way to select the whole block contents is to start
// and end at the deepest points.
const startNode = getDeepestNode( startRef.current, 'start' );
const endNode = getDeepestNode( endRef.current, 'end' );
// While rich text will be disabled with a delay when there is a multi
// selection, we must do it immediately because it's not possible to set
// selection across editable hosts.
toggleRichText( node, false );
range.setStartBefore( startNode );
range.setEndAfter( endNode );
selection.removeAllRanges();
selection.addRange( range );
},
[
hasMultiSelection,
isMultiSelecting,
multiSelectedBlockClientIds,
selectedBlockClientId,
]
);
}