Skip to content

Commit

Permalink
Reinstate changes lost during complex rebase
Browse files Browse the repository at this point in the history
  • Loading branch information
getdave committed Dec 13, 2019
1 parent 1f90c6f commit 841b4a8
Show file tree
Hide file tree
Showing 2 changed files with 4 additions and 214 deletions.
7 changes: 4 additions & 3 deletions packages/block-editor/src/components/block-list/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ function BlockListBlock( {
isTypingWithinBlock,
isCaretWithinFormattedText,
isEmptyDefaultBlock,
isParentOfSelectedBlock,
isAncestorOfSelectedBlock,
isCapturingDescendantToolbars,
hasAncestorCapturingToolbars,
isSelectionEnabled,
Expand All @@ -104,7 +104,6 @@ function BlockListBlock( {
setNavigationMode,
isMultiSelecting,
isLargeViewport,
__experimentalCaptureChildToolbar: captureChildToolbar,
} ) {
// In addition to withSelect, we should favor using useSelect in this component going forward
// to avoid leaking new props to the public API (editor.BlockListBlock filter)
Expand Down Expand Up @@ -291,7 +290,7 @@ function BlockListBlock( {
* (via `setFocus`), typically if there is no focusable input in the block.
*/
const onFocus = () => {
if ( ! isSelected && ! isParentOfSelectedBlock && ! isPartOfMultiSelection ) {
if ( ! isSelected && ! isAncestorOfSelectedBlock && ! isPartOfMultiSelection ) {
onSelect();
}
};
Expand Down Expand Up @@ -697,6 +696,8 @@ const applyWithSelect = withSelect(
const { hasFixedToolbar, focusMode, isRTL } = getSettings();
const templateLock = getTemplateLock( rootClientId );
const checkDeep = true;

// "ancestor" is the more appropriate label due to "deep" check
const isAncestorOfSelectedBlock = hasSelectedInnerBlock( clientId, checkDeep );
const index = getBlockIndex( clientId, rootClientId );
const blockOrder = getBlockOrder( rootClientId );
Expand Down
211 changes: 0 additions & 211 deletions packages/block-editor/src/components/block-list/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,221 +32,10 @@ const forceSyncUpdates = ( WrappedComponent ) => ( props ) => {
);
};

/**
* 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;
}

class BlockList extends Component {
constructor( props ) {
super( props );

this.onSelectionStart = this.onSelectionStart.bind( this );
this.onSelectionEnd = this.onSelectionEnd.bind( this );
this.setSelection = this.setSelection.bind( this );
this.updateNativeSelection = this.updateNativeSelection.bind( this );

this.ref = createRef();
}

componentDidUpdate() {
this.updateNativeSelection();
}

componentWillUnmount() {
window.removeEventListener( 'mouseup', this.onSelectionEnd );
window.cancelAnimationFrame( this.rafId );
}

/**
* When the component updates, and there is multi selection, we need to
* select the entire block contents.
*/
updateNativeSelection() {
const {
hasMultiSelection,
blockClientIds,
// These must be in the right DOM order.
multiSelectedBlockClientIds,
} = this.props;

if ( ! hasMultiSelection ) {
return;
}

const { length } = multiSelectedBlockClientIds;
const start = multiSelectedBlockClientIds[ 0 ];
const end = multiSelectedBlockClientIds[ length - 1 ];
const startIndex = blockClientIds.indexOf( start );

// The selected block is not in this block list.
if ( startIndex === -1 ) {
return;
}

let startNode = this.ref.current.querySelector(
`[data-block="${ start }"]`
);
let endNode = this.ref.current.querySelector(
`[data-block="${ end }"]`
);

const selection = window.getSelection();
const range = document.createRange();

// The most stable way to select the whole block contents is to start
// and end at the deepest points.
startNode = getDeepestNode( startNode, 'start' );
endNode = getDeepestNode( endNode, 'end' );

range.setStartBefore( startNode );
range.setEndAfter( endNode );

selection.removeAllRanges();
selection.addRange( range );
}

/**
* Binds event handlers to the document for tracking a pending multi-select
* in response to a mousedown event occurring in a rendered block.
*
* @param {string} clientId Client ID of block where mousedown occurred.
*/
onSelectionStart( clientId ) {
if ( ! this.props.isSelectionEnabled ) {
return;
}

this.startClientId = clientId;
this.props.onStartMultiSelect();

// `onSelectionStart` is called after `mousedown` and `mouseleave`
// (from a block). The selection ends when `mouseup` happens anywhere
// in the window.
window.addEventListener( 'mouseup', this.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. `onStartMultiSelect` 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.
Array.from(
this.ref.current.querySelectorAll( '.rich-text' )
).forEach( ( node ) => {
node.removeAttribute( 'contenteditable' );
} );
}

/**
* Handles a mouseup event to end the current mouse multi-selection.
*/
onSelectionEnd() {
// Equivalent to attaching the listener once.
window.removeEventListener( 'mouseup', this.onSelectionEnd );

if ( ! this.props.isMultiSelecting ) {
return;
}

// The browser selection won't have updated yet at this point, so wait
// until the next animation frame to get the browser selection.
this.rafId = window.requestAnimationFrame( this.setSelection );
}

setSelection() {
const selection = window.getSelection();

// If no selection is found, end multi selection.
if ( ! selection.rangeCount || selection.isCollapsed ) {
this.props.onStopMultiSelect();
return;
}

let { focusNode } = selection;
let clientId;

// Find the client ID of the block where the selection ends.
do {
focusNode = focusNode.parentElement;
} while (
focusNode &&
! ( clientId = focusNode.getAttribute( 'data-block' ) )
);

// If the final selection doesn't leave the block, there is no multi
// selection.
if ( this.startClientId === clientId ) {
this.props.onStopMultiSelect();
return;
}

this.props.onMultiSelect( this.startClientId, clientId );
this.props.onStopMultiSelect();
}

render() {
const {
className,
blockClientIds,
rootClientId,
__experimentalMoverDirection: moverDirection = 'vertical',
isDraggable,
selectedBlockClientId,
multiSelectedBlockClientIds,
hasMultiSelection,
renderAppender,
enableAnimation,
isMultiSelecting,
} = this.props;

return (
<div
ref={ this.ref }
className={ classnames(
'editor-block-list__layout block-editor-block-list__layout',
className
) }
>
{ blockClientIds.map( ( clientId, index ) => {
const isBlockInSelection = hasMultiSelection ?
multiSelectedBlockClientIds.includes( clientId ) :
selectedBlockClientId === clientId;

function BlockList( {
className,
rootClientId,
__experimentalMoverDirection: moverDirection = 'vertical',
isMultiSelecting={ isMultiSelecting }
// This prop is explicitely computed and passed down
// to avoid being impacted by the async mode
// otherwise there might be a small delay to trigger the animation.
animateOnChange={ index }
enableAnimation={ enableAnimation }
/>
isDraggable,
renderAppender,
} ) {
Expand Down

0 comments on commit 841b4a8

Please sign in to comment.