Skip to content

Commit

Permalink
Merge pull request #550 from Automattic/fix/508-editor-media-drag-tra…
Browse files Browse the repository at this point in the history
…cking

Editor: Track elements for more reliable drag detection
  • Loading branch information
aduth committed Dec 8, 2015
2 parents 0feeaf6 + f8091ad commit 2487e30
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 11 deletions.
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

0 comments on commit 2487e30

Please sign in to comment.