Skip to content

Commit

Permalink
Block List: Extract copy, scroll-into-view logic to separate non-visu…
Browse files Browse the repository at this point in the history
…al components (#5021)

* Block List: Extract copy logic to separate non-visual component

* Block List: Restore multi-select scroll into view behavior

* Editor: Abort scrollIntoView if no scrollable contaienr
  • Loading branch information
aduth authored Feb 14, 2018
1 parent f1726d8 commit ad1fd15
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 91 deletions.
4 changes: 4 additions & 0 deletions edit-post/components/modes/visual-editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import { connect } from 'react-redux';
*/
import {
BlockList,
CopyHandler,
PostTitle,
WritingFlow,
EditorGlobalKeyboardShortcuts,
BlockSelectionClearer,
MultiSelectScrollIntoView,
} from '@wordpress/editor';
import { Fragment } from '@wordpress/element';

Expand All @@ -26,6 +28,8 @@ function VisualEditor( props ) {
return (
<BlockSelectionClearer className="edit-post-visual-editor">
<EditorGlobalKeyboardShortcuts />
<CopyHandler />
<MultiSelectScrollIntoView />
<WritingFlow>
<PostTitle />
<BlockList
Expand Down
26 changes: 1 addition & 25 deletions editor/components/block-list/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { __, sprintf } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { getScrollContainer } from '../../utils/dom';
import BlockMover from '../block-mover';
import BlockDropZone from '../block-drop-zone';
import BlockSettingsMenu from '../block-settings-menu';
Expand Down Expand Up @@ -71,31 +72,6 @@ import {

const { BACKSPACE, ESCAPE, DELETE, ENTER, UP, RIGHT, DOWN, LEFT } = keycodes;

/**
* Given a DOM node, finds the closest scrollable container node.
*
* @param {Element} node Node from which to start.
*
* @return {?Element} Scrollable container node, if found.
*/
function getScrollContainer( node ) {
if ( ! node ) {
return;
}

// Scrollable if scrollable height exceeds displayed...
if ( node.scrollHeight > node.clientHeight ) {
// ...except when overflow is defined to be hidden or visible
const { overflowY } = window.getComputedStyle( node );
if ( /(auto|scroll)/.test( overflowY ) ) {
return node;
}
}

// Continue traversing
return getScrollContainer( node.parentNode );
}

export class BlockListBlock extends Component {
constructor() {
super( ...arguments );
Expand Down
67 changes: 1 addition & 66 deletions editor/components/block-list/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,19 @@ import {
findLast,
map,
invert,
isEqual,
mapValues,
sortBy,
throttle,
get,
last,
} from 'lodash';
import scrollIntoView from 'dom-scroll-into-view';
import 'element-closest';

/**
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { serialize, getDefaultBlockName } from '@wordpress/blocks';
import { getDefaultBlockName } from '@wordpress/blocks';

/**
* Internal dependencies
Expand All @@ -30,16 +28,11 @@ import BlockListBlock from './block';
import BlockSelectionClearer from '../block-selection-clearer';
import DefaultBlockAppender from '../default-block-appender';
import {
getMultiSelectedBlocksStartUid,
getMultiSelectedBlocksEndUid,
getMultiSelectedBlocks,
getMultiSelectedBlockUids,
getSelectedBlock,
isSelectionEnabled,
isMultiSelecting,
} from '../../store/selectors';
import { startMultiSelect, stopMultiSelect, multiSelect, selectBlock } from '../../store/actions';
import { documentHasSelection } from '../../utils/dom';

class BlockListLayout extends Component {
constructor( props ) {
Expand All @@ -48,8 +41,6 @@ class BlockListLayout extends Component {
this.onSelectionStart = this.onSelectionStart.bind( this );
this.onSelectionEnd = this.onSelectionEnd.bind( this );
this.onShiftSelection = this.onShiftSelection.bind( this );
this.onCopy = this.onCopy.bind( this );
this.onCut = this.onCut.bind( this );
this.setBlockRef = this.setBlockRef.bind( this );
this.setLastClientY = this.setLastClientY.bind( this );
this.onPointerMove = throttle( this.onPointerMove.bind( this ), 100 );
Expand All @@ -62,32 +53,13 @@ class BlockListLayout extends Component {
}

componentDidMount() {
document.addEventListener( 'copy', this.onCopy );
document.addEventListener( 'cut', this.onCut );
window.addEventListener( 'mousemove', this.setLastClientY );
}

componentWillUnmount() {
document.removeEventListener( 'copy', this.onCopy );
document.removeEventListener( 'cut', this.onCut );
window.removeEventListener( 'mousemove', this.setLastClientY );
}

componentWillReceiveProps( nextProps ) {
if ( isEqual( this.props.multiSelectedBlockUids, nextProps.multiSelectedBlockUids ) ) {
return;
}

if ( nextProps.multiSelectedBlockUids && nextProps.multiSelectedBlockUids.length > 0 ) {
const extent = this.nodes[ nextProps.selectionEnd ];
if ( extent ) {
scrollIntoView( extent, extent.closest( '.edit-post-layout__content' ), {
onlyScrollIfNeeded: true,
} );
}
}
}

setLastClientY( { clientY } ) {
this.lastClientY = clientY;
}
Expand Down Expand Up @@ -125,36 +97,6 @@ class BlockListLayout extends Component {
this.onSelectionChange( this.coordMap[ key ] );
}

onCopy( event ) {
const { multiSelectedBlocks, selectedBlock } = this.props;

if ( ! multiSelectedBlocks.length && ! selectedBlock ) {
return;
}

// Let native copy behaviour take over in input fields.
if ( selectedBlock && documentHasSelection() ) {
return;
}

const serialized = serialize( selectedBlock || multiSelectedBlocks );

event.clipboardData.setData( 'text/plain', serialized );
event.clipboardData.setData( 'text/html', serialized );

event.preventDefault();
}

onCut( event ) {
const { multiSelectedBlockUids } = this.props;

this.onCopy( event );

if ( multiSelectedBlockUids.length ) {
this.props.onRemove( multiSelectedBlockUids );
}
}

/**
* Binds event handlers to the document for tracking a pending multi-select
* in response to a mousedown event occurring in a rendered block.
Expand Down Expand Up @@ -295,10 +237,6 @@ class BlockListLayout extends Component {

export default connect(
( state ) => ( {
selectionStart: getMultiSelectedBlocksStartUid( state ),
selectionEnd: getMultiSelectedBlocksEndUid( state ),
multiSelectedBlocks: getMultiSelectedBlocks( state ),
multiSelectedBlockUids: getMultiSelectedBlockUids( state ),
selectedBlock: getSelectedBlock( state ),
isSelectionEnabled: isSelectionEnabled( state ),
isMultiSelecting: isMultiSelecting( state ),
Expand All @@ -316,8 +254,5 @@ export default connect(
onSelect( uid ) {
dispatch( selectBlock( uid ) );
},
onRemove( uids ) {
dispatch( { type: 'REMOVE_BLOCKS', uids } );
},
} )
)( BlockListLayout );
85 changes: 85 additions & 0 deletions editor/components/copy-handler/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* External dependencies
*/
import { connect } from 'react-redux';

/**
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { serialize } from '@wordpress/blocks';

/**
* Internal dependencies
*/
import { documentHasSelection } from '../../utils/dom';
import { removeBlocks } from '../../store/actions';
import {
getMultiSelectedBlocks,
getMultiSelectedBlockUids,
getSelectedBlock,
} from '../../store/selectors';

class CopyHandler extends Component {
constructor() {
super( ...arguments );

this.onCopy = this.onCopy.bind( this );
this.onCut = this.onCut.bind( this );
}

componentDidMount() {
document.addEventListener( 'copy', this.onCopy );
document.addEventListener( 'cut', this.onCut );
}

componentWillUnmount() {
document.removeEventListener( 'copy', this.onCopy );
document.removeEventListener( 'cut', this.onCut );
}

onCopy( event ) {
const { multiSelectedBlocks, selectedBlock } = this.props;

if ( ! multiSelectedBlocks.length && ! selectedBlock ) {
return;
}

// Let native copy behaviour take over in input fields.
if ( selectedBlock && documentHasSelection() ) {
return;
}

const serialized = serialize( selectedBlock || multiSelectedBlocks );

event.clipboardData.setData( 'text/plain', serialized );
event.clipboardData.setData( 'text/html', serialized );

event.preventDefault();
}

onCut( event ) {
const { multiSelectedBlockUids } = this.props;

this.onCopy( event );

if ( multiSelectedBlockUids.length ) {
this.props.onRemove( multiSelectedBlockUids );
}
}

render() {
return null;
}
}

export default connect(
( state ) => {
return {
multiSelectedBlocks: getMultiSelectedBlocks( state ),
multiSelectedBlockUids: getMultiSelectedBlockUids( state ),
selectedBlock: getSelectedBlock( state ),
};
},
{ onRemove: removeBlocks },
)( CopyHandler );
2 changes: 2 additions & 0 deletions editor/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,12 @@ export { default as BlockMover } from './block-mover';
export { default as BlockSelectionClearer } from './block-selection-clearer';
export { default as BlockSettingsMenu } from './block-settings-menu';
export { default as BlockToolbar } from './block-toolbar';
export { default as CopyHandler } from './copy-handler';
export { default as DefaultBlockAppender } from './default-block-appender';
export { default as ErrorBoundary } from './error-boundary';
export { default as Inserter } from './inserter';
export { default as MultiBlocksSwitcher } from './block-switcher/multi-blocks-switcher';
export { default as MultiSelectScrollIntoView } from './multi-select-scroll-into-view';
export { default as NavigableToolbar } from './navigable-toolbar';
export { default as Warning } from './warning';
export { default as WritingFlow } from './writing-flow';
Expand Down
63 changes: 63 additions & 0 deletions editor/components/multi-select-scroll-into-view/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* External dependencies
*/
import scrollIntoView from 'dom-scroll-into-view';

/**
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { query } from '@wordpress/data';

/**
* Internal dependencies
*/
import { getScrollContainer } from '../../utils/dom';

class MultiSelectScrollIntoView extends Component {
componentDidUpdate() {
// Relies on expectation that `componentDidUpdate` will only be called
// if value of `extentUID` changes.
this.scrollIntoView();
}

/**
* Ensures that if a multi-selection exists, the extent of the selection is
* visible within the nearest scrollable container.
*
* @return {void}
*/
scrollIntoView() {
const { extentUID } = this.props;
if ( ! extentUID ) {
return;
}

const extentNode = document.querySelector( '[data-block="' + extentUID + '"]' );
if ( ! extentNode ) {
return;
}

const scrollContainer = getScrollContainer( extentNode );

// If there's no scroll container, it follows that there's no scrollbar
// and thus there's no need to try to scroll into view.
if ( ! scrollContainer ) {
return;
}

scrollIntoView( extentNode, scrollContainer, {
onlyScrollIfNeeded: true,
} );
}

render() {
return null;
}
}

export default query( ( select ) => {
return {
extentUID: select( 'core/editor' ).getLastMultiSelectedBlockUid(),
};
} )( MultiSelectScrollIntoView );
2 changes: 2 additions & 0 deletions editor/store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import reducer from './reducer';
import applyMiddlewares from './middlewares';
import {
getEditedPostAttribute,
getLastMultiSelectedBlockUid,
getSelectedBlockCount,
} from './selectors';

Expand All @@ -26,6 +27,7 @@ loadAndPersist( store, reducer, 'preferences', STORAGE_KEY );

registerSelectors( MODULE_KEY, {
getEditedPostAttribute,
getLastMultiSelectedBlockUid,
getSelectedBlockCount,
} );

Expand Down
Loading

0 comments on commit ad1fd15

Please sign in to comment.