Skip to content

Commit

Permalink
Smooth selection
Browse files Browse the repository at this point in the history
  • Loading branch information
ellatrix committed Oct 4, 2019
1 parent 3fb633a commit f1b08ac
Show file tree
Hide file tree
Showing 6 changed files with 47 additions and 148 deletions.
29 changes: 22 additions & 7 deletions packages/block-editor/src/components/block-list/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ const preventDrag = ( event ) => {
};

function BlockListBlock( {
blockRef,
mode,
isFocusMode,
hasFixedToolbar,
Expand Down Expand Up @@ -97,6 +96,7 @@ function BlockListBlock( {
toggleSelection,
onShiftSelection,
onSelectionStart,
onSelectionEnd,
animateOnChange,
enableAnimation,
isNavigationMode,
Expand All @@ -108,9 +108,6 @@ function BlockListBlock( {

// Reference of the wrapper
const wrapper = useRef( null );
useEffect( () => {
blockRef( wrapper.current, clientId );
}, [] );

// Reference to the block edit node
const blockNodeRef = useRef();
Expand Down Expand Up @@ -332,12 +329,14 @@ function BlockListBlock( {
}
};

const isPointerDown = useRef( false );

/**
* Begins tracking cursor multi-selection when clicking down within block.
*
* @param {MouseEvent} event A mousedown event.
*/
const onPointerDown = ( event ) => {
const onMouseDown = ( event ) => {
// Not the main button.
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
if ( event.button !== 0 ) {
Expand All @@ -353,7 +352,7 @@ function BlockListBlock( {
// Avoid triggering multi-selection if we click toolbars/inspectors
// and all elements that are outside the Block Edit DOM tree.
} else if ( blockNodeRef.current.contains( event.target ) ) {
onSelectionStart( clientId );
isPointerDown.current = true;

// Allow user to escape out of a multi-selection to a singular
// selection of a block via click. This is handled here since
Expand All @@ -366,6 +365,19 @@ function BlockListBlock( {
}
};

const onMouseUp = () => {
onSelectionEnd( clientId );
isPointerDown.current = false;
};

const onMouseLeave = () => {
if ( isPointerDown.current ) {
onSelectionStart( clientId );
}

isPointerDown.current = false;
};

const selectOnOpen = ( open ) => {
if ( open && ! isSelected ) {
onSelect();
Expand Down Expand Up @@ -431,6 +443,7 @@ function BlockListBlock( {
'is-selected': shouldAppearSelected,
'is-navigate-mode': isNavigationMode,
'is-multi-selected': isPartOfMultiSelection,
'is-multi-selected-first': isFirstMultiSelected,
'is-hovered': shouldAppearHovered,
'is-reusable': isReusableBlock( blockType ),
'is-dragging': isDragging,
Expand Down Expand Up @@ -558,7 +571,9 @@ function BlockListBlock( {
<IgnoreNestedEvents
ref={ blockNodeRef }
onDragStart={ preventDrag }
onMouseDown={ onPointerDown }
onMouseDown={ onMouseDown }
onMouseUp={ onMouseUp }
onMouseLeave={ onMouseLeave }
data-block={ clientId }
>
<BlockCrashBoundary onError={ onBlockError }>
Expand Down
136 changes: 9 additions & 127 deletions packages/block-editor/src/components/block-list/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
/**
* External dependencies
*/
import {
findLast,
invert,
mapValues,
sortBy,
throttle,
} from 'lodash';
import classnames from 'classnames';

/**
Expand All @@ -27,7 +20,6 @@ import { compose } from '@wordpress/compose';
import BlockAsyncModeProvider from './block-async-mode-provider';
import BlockListBlock from './block';
import BlockListAppender from '../block-list-appender';
import { getBlockDOMNode } from '../../utils/dom';

/**
* If the block count exceeds the threshold, we disable the reordering animation
Expand All @@ -49,23 +41,6 @@ class BlockList extends Component {

this.onSelectionStart = this.onSelectionStart.bind( this );
this.onSelectionEnd = this.onSelectionEnd.bind( this );
this.setBlockRef = this.setBlockRef.bind( this );
this.setLastClientY = this.setLastClientY.bind( this );
this.onPointerMove = throttle( this.onPointerMove.bind( this ), 100 );
// Browser does not fire `*move` event when the pointer position changes
// relative to the document, so fire it with the last known position.
this.onScroll = () => this.onPointerMove( { clientY: this.lastClientY } );

this.lastClientY = 0;
this.nodes = {};
}

componentDidMount() {
window.addEventListener( 'mousemove', this.setLastClientY );
}

componentWillUnmount() {
window.removeEventListener( 'mousemove', this.setLastClientY );
}

componentDidUpdate() {
Expand Down Expand Up @@ -99,48 +74,6 @@ class BlockList extends Component {
selection.addRange( range );
}

setLastClientY( { clientY } ) {
this.lastClientY = clientY;
}

setBlockRef( node, clientId ) {
if ( node === null ) {
delete this.nodes[ clientId ];
} else {
this.nodes = {
...this.nodes,
[ clientId ]: node,
};
}
}

/**
* Handles a pointer move event to update the extent of the current cursor
* multi-selection.
*
* @param {MouseEvent} event A mousemove event object.
*/
onPointerMove( { clientY } ) {
// We don't start multi-selection until the mouse starts moving, so as
// to avoid dispatching multi-selection actions on an in-place click.
if ( ! this.props.isMultiSelecting ) {
this.props.onStartMultiSelect();
}

const blockContentBoundaries = getBlockDOMNode( this.selectionAtStart ).getBoundingClientRect();

// prevent multi-selection from triggering when the selected block is a float
// and the cursor is still between the top and the bottom of the block.
if ( clientY >= blockContentBoundaries.top && clientY <= blockContentBoundaries.bottom ) {
return;
}

const y = clientY - blockContentBoundaries.top;
const key = findLast( this.coordMapKeys, ( coordY ) => coordY < y );

this.onSelectionChange( this.coordMap[ key ] );
}

/**
* Binds event handlers to the document for tracking a pending multi-select
* in response to a mousedown event occurring in a rendered block.
Expand All @@ -152,73 +85,22 @@ class BlockList extends Component {
return;
}

const boundaries = this.nodes[ clientId ].getBoundingClientRect();

// Create a clientId to Y coördinate map.
const clientIdToCoordMap = mapValues( this.nodes, ( node ) =>
node.getBoundingClientRect().top - boundaries.top );

// Cache a Y coördinate to clientId map for use in `onPointerMove`.
this.coordMap = invert( clientIdToCoordMap );
// Cache an array of the Y coördinates for use in `onPointerMove`.
// Sort the coördinates, as `this.nodes` will not necessarily reflect
// the current block sequence.
this.coordMapKeys = sortBy( Object.values( clientIdToCoordMap ) );
this.selectionAtStart = clientId;

window.addEventListener( 'mousemove', this.onPointerMove );
// Capture scroll on all elements.
window.addEventListener( 'scroll', this.onScroll, true );
window.addEventListener( 'mouseup', this.onSelectionEnd );
this.onSelectionStart.clientId = clientId;
this.props.onStartMultiSelect();
}

/**
* Handles multi-selection changes in response to pointer move.
* Handles a mouseup event to end the current cursor multi-selection.
*
* @param {string} clientId Client ID of block under cursor in multi-select
* drag.
* @param {string} clientId Client ID of block where mouseup occurred.
*/
onSelectionChange( clientId ) {
const { onMultiSelect, selectionStart, selectionEnd } = this.props;
const { selectionAtStart } = this;
const isAtStart = selectionAtStart === clientId;

if ( ! selectionAtStart || ! this.props.isSelectionEnabled ) {
onSelectionEnd( clientId ) {
if ( ! this.props.isMultiSelecting ) {
return;
}

// If multi-selecting and cursor extent returns to the start of
// selection, cancel multi-select.
if ( isAtStart && selectionStart ) {
onMultiSelect( null, null );
}

// Expand multi-selection to block under cursor.
if ( ! isAtStart && selectionEnd !== clientId ) {
onMultiSelect( selectionAtStart, clientId );
}
}

/**
* Handles a mouseup event to end the current cursor multi-selection.
*/
onSelectionEnd() {
// Cancel throttled calls.
this.onPointerMove.cancel();

delete this.coordMap;
delete this.coordMapKeys;
delete this.selectionAtStart;

window.removeEventListener( 'mousemove', this.onPointerMove );
window.removeEventListener( 'scroll', this.onScroll, true );
window.removeEventListener( 'mouseup', this.onSelectionEnd );

// We may or may not be in a multi-selection when mouseup occurs (e.g.
// an in-place mouse click), so only trigger stop if multi-selecting.
if ( this.props.isMultiSelecting ) {
this.props.onStopMultiSelect();
}
this.props.onMultiSelect( this.onSelectionStart.clientId, clientId );
this.props.onStopMultiSelect();
}

render() {
Expand Down Expand Up @@ -255,8 +137,8 @@ class BlockList extends Component {
<BlockListBlock
rootClientId={ rootClientId }
clientId={ clientId }
blockRef={ this.setBlockRef }
onSelectionStart={ this.onSelectionStart }
onSelectionEnd={ this.onSelectionEnd }
isDraggable={ isDraggable }

// This prop is explicitely computed and passed down
Expand Down
13 changes: 12 additions & 1 deletion packages/block-editor/src/components/block-list/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@

// Selected style.
&.is-selected {

> .block-editor-block-list__block-edit::before {
// Use opacity to work in various editor styles.
border-color: $dark-opacity-light-800;
Expand Down Expand Up @@ -157,6 +156,18 @@
}
}

// Selected style.
&.is-multi-selected {
> .block-editor-block-list__block-edit::before {
border-left-color: $dark-opacity-light-800;
box-shadow: inset $block-left-border-width 0 0 0 $dark-gray-500;
}

&.is-multi-selected-first > .block-editor-block-list__block-edit::before {
border-top-color: $dark-opacity-light-800;
}
}

// Hover style.
&.is-hovered:not(.is-navigate-mode) > .block-editor-block-list__block-edit::before {
box-shadow: -$block-left-border-width 0 0 0 $dark-opacity-light-500;
Expand Down
8 changes: 4 additions & 4 deletions packages/block-editor/src/components/rich-text/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ class RichTextWrapper extends Component {
undo,
placeholder,
keepPlaceholderOnFocus,
hasMultiSelection,
isMultiSelecting,
// eslint-disable-next-line no-unused-vars
allowedFormats,
withoutInteractiveFormatting,
Expand Down Expand Up @@ -421,7 +421,7 @@ class RichTextWrapper extends Component {
__unstableMarkAutomaticChange={ markAutomaticChange }
__unstableDidAutomaticChange={ didAutomaticChange }
__unstableUndo={ undo }
__unstableContentEditable={ ! hasMultiSelection }
__unstableContentEditable={ ! isMultiSelecting }
>
{ ( { isSelected, value, onChange, Editable } ) =>
<>
Expand Down Expand Up @@ -493,7 +493,7 @@ const RichTextContainer = compose( [
getSelectionEnd,
getSettings,
didAutomaticChange,
hasMultiSelection,
isMultiSelecting,
} = select( 'core/block-editor' );

const selectionStart = getSelectionStart();
Expand All @@ -515,7 +515,7 @@ const RichTextContainer = compose( [
selectionEnd: isSelected ? selectionEnd.offset : undefined,
isSelected,
didAutomaticChange: didAutomaticChange(),
hasMultiSelection: hasMultiSelection(),
isMultiSelecting: isMultiSelecting(),
};
} ),
withDispatch( ( dispatch, {
Expand Down
4 changes: 0 additions & 4 deletions packages/block-editor/src/components/rich-text/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@
background: $light-gray-200;
font-family: $editor-html-font;
font-size: inherit; // This is necessary to override upstream CSS.

.is-multi-selected & {
background: darken($blue-medium-highlight, 15%);
}
}

&:focus {
Expand Down
5 changes: 0 additions & 5 deletions packages/block-editor/src/components/warning/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@
text-align: left;
padding: 10px $block-padding $block-padding;

// Avoid conflict with the multi-selection highlight color.
.has-warning.is-multi-selected & {
background-color: transparent;
}

.is-selected & {
// Use opacity to work in various editor styles.
border-color: $dark-opacity-light-800;
Expand Down

0 comments on commit f1b08ac

Please sign in to comment.