Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi-select: move handlers to block ref callback #31334

Merged
merged 1 commit into from
Apr 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { useEventHandlers } from './use-selected-block-event-handlers';
import { useNavModeExit } from './use-nav-mode-exit';
import { useScrollIntoView } from './use-scroll-into-view';
import { useBlockRefProvider } from './use-block-refs';
import { useMultiSelection } from './use-multi-selection';
import { store as blockEditorStore } from '../../../store';

/**
Expand Down Expand Up @@ -108,6 +109,7 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) {
useScrollIntoView( clientId ),
useBlockRefProvider( clientId ),
useFocusHandler( clientId ),
useMultiSelection( clientId ),
useEventHandlers( clientId ),
useNavModeExit( clientId ),
useIsHovered(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/**
* WordPress dependencies
*/
import { useSelect, useDispatch } from '@wordpress/data';
import { useRefEffect } from '@wordpress/compose';

/**
* Internal dependencies
*/
import { store as blockEditorStore } from '../../../store';
import { getBlockClientId } from '../../../utils/dom';

function toggleRichText( container, toggle ) {
Array.from(
container
.closest( '.is-root-container' )
.querySelectorAll( '.rich-text' )
).forEach( ( node ) => {
if ( toggle ) {
node.setAttribute( 'contenteditable', true );
} else {
node.removeAttribute( 'contenteditable' );
}
} );
}

/**
* Sets a multi-selection based on the native selection across blocks.
*
* @param {string} clientId Block client ID.
*/
export function useMultiSelection( clientId ) {
const {
startMultiSelect,
stopMultiSelect,
multiSelect,
selectBlock,
} = useDispatch( blockEditorStore );
const { isSelectionEnabled, isBlockSelected, getBlockParents } = useSelect(
blockEditorStore
);
return useRefEffect(
( node ) => {
const { ownerDocument } = node;
const { defaultView } = ownerDocument;

let anchorElement;
let rafId;

function onSelectionChange( { isSelectionEnd } ) {
const selection = defaultView.getSelection();

// If no selection is found, end multi selection and enable all rich
// text areas.
if ( ! selection.rangeCount || selection.isCollapsed ) {
toggleRichText( node, true );
return;
}

const endClientId = getBlockClientId( selection.focusNode );
const isSingularSelection = clientId === endClientId;

if ( isSingularSelection ) {
selectBlock( clientId );

// 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, rich text elements that were
// previously disabled can now be enabled again.
if ( isSelectionEnd ) {
toggleRichText( node, true );

if ( selection.rangeCount ) {
const {
commonAncestorContainer,
} = selection.getRangeAt( 0 );

if (
anchorElement.contains(
commonAncestorContainer
)
) {
anchorElement.focus();
}
}
}
} else {
const startPath = [
...getBlockParents( clientId ),
clientId,
];
const endPath = [
...getBlockParents( endClientId ),
endClientId,
];
const depth =
Math.min( startPath.length, endPath.length ) - 1;

multiSelect( startPath[ depth ], endPath[ depth ] );
}
}

function onSelectionEnd() {
ownerDocument.removeEventListener(
'selectionchange',
onSelectionChange
);
// Equivalent to attaching the listener once.
defaultView.removeEventListener( 'mouseup', onSelectionEnd );
// 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( () => {
onSelectionChange( { isSelectionEnd: true } );
stopMultiSelect();
} );
}

function onMouseLeave( { buttons } ) {
// 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;
}

if ( ! isSelectionEnabled() || ! isBlockSelected( clientId ) ) {
return;
}

anchorElement = ownerDocument.activeElement;
startMultiSelect();

// `onSelectionStart` is called after `mousedown` and
// `mouseleave` (from a block). The selection ends when
// `mouseup` happens anywhere in the window.
ownerDocument.addEventListener(
'selectionchange',
onSelectionChange
);
defaultView.addEventListener( 'mouseup', onSelectionEnd );

// Removing the contenteditable attributes within the block
// editor is essential for selection to work across editable
// areas. The edible hosts are removed, allowing selection to be
// extended outside the DOM element. `startMultiSelect` sets a
// flag in the store so the rich text components are updated,
// but the rerender may happen very slowly, especially in Safari
// for the blocks that are asynchonously rendered. To ensure the
// browser instantly removes the selection boundaries, we remove
// the contenteditable attributes manually.
toggleRichText( node, false );
}

node.addEventListener( 'mouseleave', onMouseLeave );

return () => {
node.removeEventListener( 'mouseleave', onMouseLeave );
ownerDocument.removeEventListener(
'selectionchange',
onSelectionChange
);
defaultView.removeEventListener( 'mouseup', onSelectionEnd );
defaultView.cancelAnimationFrame( rafId );
};
},
[
clientId,
startMultiSelect,
stopMultiSelect,
multiSelect,
selectBlock,
isSelectionEnabled,
isBlockSelected,
getBlockParents,
]
);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/**
* WordPress dependencies
*/
import { useContext } from '@wordpress/element';
import { isTextField } from '@wordpress/dom';
import { ENTER, BACKSPACE, DELETE } from '@wordpress/keycodes';
import { useSelect, useDispatch } from '@wordpress/data';
Expand All @@ -10,20 +9,17 @@ import { useRefEffect } from '@wordpress/compose';
/**
* Internal dependencies
*/
import { SelectionStart } from '../../writing-flow';
import { store as blockEditorStore } from '../../../store';

/**
* Adds block behaviour:
* - Removes the block on BACKSPACE.
* - Inserts a default block on ENTER.
* - Initiates selection start for multi-selection.
* - Disables dragging of block contents.
*
* @param {string} clientId Block client ID.
*/
export function useEventHandlers( clientId ) {
const onSelectionStart = useContext( SelectionStart );
const isSelected = useSelect(
( select ) => select( blockEditorStore ).isBlockSelected( clientId ),
[ clientId ]
Expand Down Expand Up @@ -76,14 +72,6 @@ export function useEventHandlers( clientId ) {
}
}

function onMouseLeave( { buttons } ) {
// The primary button must be pressed to initiate selection.
// See https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
if ( buttons === 1 ) {
onSelectionStart( clientId );
}
}

/**
* Prevents default dragging behavior within a block. To do: we must
* handle this in the future and clean up the drag target.
Expand All @@ -95,11 +83,9 @@ export function useEventHandlers( clientId ) {
}

node.addEventListener( 'keydown', onKeyDown );
node.addEventListener( 'mouseleave', onMouseLeave );
node.addEventListener( 'dragstart', onDragStart );

return () => {
node.removeEventListener( 'mouseleave', onMouseLeave );
node.removeEventListener( 'keydown', onKeyDown );
node.removeEventListener( 'dragstart', onDragStart );
};
Expand All @@ -109,7 +95,6 @@ export function useEventHandlers( clientId ) {
isSelected,
getBlockRootClientId,
getBlockIndex,
onSelectionStart,
insertDefaultBlock,
removeBlock,
]
Expand Down
10 changes: 4 additions & 6 deletions packages/block-editor/src/components/writing-flow/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { find, reverse, first, last } from 'lodash';
/**
* WordPress dependencies
*/
import { useRef, useEffect, createContext } from '@wordpress/element';
import { useRef, useEffect } from '@wordpress/element';
import {
computeCaretRect,
focus,
Expand Down Expand Up @@ -36,8 +36,6 @@ import { isInSameBlock, getBlockClientId } from '../../utils/dom';
import useMultiSelection from './use-multi-selection';
import { store as blockEditorStore } from '../../store';

export const SelectionStart = createContext();

/**
* Useful for positioning an element within the viewport so focussing the
* element does not scroll the page.
Expand Down Expand Up @@ -523,7 +521,7 @@ export default function WritingFlow( { children } ) {
// This hook sets the selection after the user makes a multi-selection. For
// some browsers, like Safari, it is important that this happens AFTER
// setting focus on the multi-selection container above.
const onSelectionStart = useMultiSelection( container );
useMultiSelection( container );

const lastFocus = useRef();

Expand Down Expand Up @@ -566,7 +564,7 @@ export default function WritingFlow( { children } ) {
// bubbling events from children to determine focus transition intents.
/* eslint-disable jsx-a11y/no-static-element-interactions */
return (
<SelectionStart.Provider value={ onSelectionStart }>
<>
<div
ref={ focusCaptureBeforeRef }
tabIndex={ focusCaptureTabIndex }
Expand Down Expand Up @@ -598,7 +596,7 @@ export default function WritingFlow( { children } ) {
onFocus={ onFocusCapture }
style={ PREVENT_SCROLL_ON_FOCUS }
/>
</SelectionStart.Provider>
</>
);
/* eslint-enable jsx-a11y/no-static-element-interactions */
}
Loading