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

Editor: Track elements for more reliable drag detection #550

Merged
merged 3 commits into from
Dec 8, 2015
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
72 changes: 61 additions & 11 deletions client/components/drop-zone/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* External dependencies
*/
var React = require( 'react/addons' ),
without = require( 'lodash/array/without' ),
includes = require( 'lodash/collection/includes' ),
classNames = require( 'classnames' ),
noop = require( 'lodash/utility/noop' );

Expand Down Expand Up @@ -39,29 +41,78 @@ module.exports = React.createClass( {
},

componentDidMount: function() {
this.dragEnteredCounter = 0;
this.dragEnterNodes = [];

window.addEventListener( 'dragover', this.preventDefault );
window.addEventListener( 'drop', this.onDrop );
window.addEventListener( 'dragenter', this.toggleDraggingOverDocument );
window.addEventListener( 'dragleave', this.toggleDraggingOverDocument );
window.addEventListener( 'mouseup', this.resetDragState );
},

componentWillUnmount: function() {
delete this.dragEnteredCounter;
componentDidUpdate: function( prevProps, prevState ) {
if ( prevState.isDraggingOverDocument !== this.state.isDraggingOverDocument ) {
this.toggleMutationObserver();
}
},

componentWillUnmount: function() {
window.removeEventListener( 'dragover', this.preventDefault );
window.removeEventListener( 'drop', this.onDrop );
window.removeEventListener( 'dragenter', this.toggleDraggingOverDocument );
window.removeEventListener( 'dragleave', this.toggleDraggingOverDocument );
window.removeEventListener( 'mouseup', this.resetDragState );
this.disconnectMutationObserver();
},

resetDragState: function() {
if ( ! ( this.state.isDraggingOverDocument || this.state.isDraggingOverElement ) ) {
return;
}

this.setState( this.getInitialState() );
},

toggleMutationObserver: function() {
this.disconnectMutationObserver();

if ( this.state.isDraggingOverDocument ) {
this.observer = new window.MutationObserver( this.detectNodeRemoval );
this.observer.observe( document.body, {
childList: true,
subtree: true
} );
}
},

disconnectMutationObserver: function() {
if ( ! this.observer ) {
return;
}

this.observer.disconnect();
delete this.observer;
},

detectNodeRemoval: function( mutations ) {
mutations.forEach( ( mutation ) => {
if ( ! mutation.removedNodes.length ) {
return;
}

this.dragEnterNodes = without( this.dragEnterNodes, Array.from( mutation.removedNodes ) );
} );
},

toggleDraggingOverDocument: function( event ) {
var isDraggingOverDocument, detail, isValidDrag;

switch ( event.type ) {
case 'dragenter': this.dragEnteredCounter++; break;
case 'dragleave': this.dragEnteredCounter--; break;
// Track nodes that have received a drag event. So long as nodes exist
// in the set, we can assume that an item is being dragged on the page.
if ( 'dragenter' === event.type && ! includes( this.dragEnterNodes, event.target ) ) {
this.dragEnterNodes.push( event.target );
} else if ( 'dragleave' === event.type ) {
this.dragEnterNodes = without( this.dragEnterNodes, event.target );
}

// In some contexts, it may be necessary to capture and redirect the
Expand All @@ -73,7 +124,7 @@ module.exports = React.createClass( {
detail = window.CustomEvent && event instanceof window.CustomEvent ? event.detail : event;

isValidDrag = this.props.onVerifyValidTransfer( detail.dataTransfer );
isDraggingOverDocument = isValidDrag && 0 !== this.dragEnteredCounter;
isDraggingOverDocument = isValidDrag && this.dragEnterNodes.length;

this.setState( {
isDraggingOverDocument: isDraggingOverDocument,
Expand All @@ -82,10 +133,9 @@ module.exports = React.createClass( {
} );

if ( window.CustomEvent && event instanceof window.CustomEvent ) {
// For redirected CustomEvent instances, immediately decrement the
// counter following the state update, since another "real" event
// will be triggered on the `window` object immediately following.
this.dragEnteredCounter--;
// For redirected CustomEvent instances, immediately remove window
// from tracked nodes since another "real" event will be triggered.
this.dragEnterNodes = without( this.dragEnterNodes, window );
}
},

Expand Down
35 changes: 35 additions & 0 deletions client/components/drop-zone/test/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@ describe( 'DropZone', function() {
before( function() {
DropZone.type.prototype.__reactAutoBindMap.translate = sinon.stub().returnsArg( 0 );
container = document.getElementById( 'container' );
window.MutationObserver = sinon.stub().returns( {
observe: sinon.stub(),
disconnect: sinon.stub()
} );
} );

after( function() {
delete window.MutationObserver;
delete DropZone.type.prototype.__reactAutoBindMap.translate;
} );

Expand Down Expand Up @@ -87,6 +92,36 @@ describe( 'DropZone', function() {
expect( tree.state.isDraggingOverElement ).to.not.be.ok;
} );

it( 'should start observing the body for mutations when dragging over', function( done ) {
var tree = React.render( React.createElement( DropZone ), container ),
dragEnterEvent = new window.MouseEvent();

dragEnterEvent.initMouseEvent( 'dragenter', true, true );
window.dispatchEvent( dragEnterEvent );

process.nextTick( function() {
expect( tree.observer ).to.be.ok;
done();
} );
} );

it( 'should stop observing the body for mutations upon drag ending', function( done ) {
var tree = React.render( React.createElement( DropZone ), container ),
dragEnterEvent = new window.MouseEvent(),
dragLeaveEvent = new window.MouseEvent();

dragEnterEvent.initMouseEvent( 'dragenter', true, true );
window.dispatchEvent( dragEnterEvent );

dragLeaveEvent.initMouseEvent( 'dragleave', true, true );
window.dispatchEvent( dragLeaveEvent );

process.nextTick( function() {
expect( tree.observer ).to.be.undefined;
done();
} );
} );

it( 'should not highlight if onVerifyValidTransfer returns false', function() {
var dragEnterEvent = new window.MouseEvent(),
tree;
Expand Down