src/Draggable/Sensors/DragSensor/DragSensor.js
import {closest} from 'shared/utils';
import Sensor from '../Sensor';
import {DragStartSensorEvent, DragMoveSensorEvent, DragStopSensorEvent} from '../SensorEvent';
const onMouseDown = Symbol('onMouseDown');
const onMouseUp = Symbol('onMouseUp');
const onDragStart = Symbol('onDragStart');
const onDragOver = Symbol('onDragOver');
const onDragEnd = Symbol('onDragEnd');
const onDrop = Symbol('onDrop');
const reset = Symbol('reset');
/**
* This sensor picks up native browser drag events and dictates drag operations
* @class DragSensor
* @module DragSensor
* @extends Sensor
*/
export default class DragSensor extends Sensor {
/**
* DragSensor constructor.
* @constructs DragSensor
* @param {HTMLElement[]|NodeList|HTMLElement} containers - Containers
* @param {Object} options - Options
*/
constructor(containers = [], options = {}) {
super(containers, options);
/**
* Mouse down timer which will end up setting the draggable attribute, unless canceled
* @property mouseDownTimeout
* @type {Number}
*/
this.mouseDownTimeout = null;
/**
* Draggable element needs to be remembered to unset the draggable attribute after drag operation has completed
* @property draggableElement
* @type {HTMLElement}
*/
this.draggableElement = null;
/**
* Native draggable element could be links or images, their draggable state will be disabled during drag operation
* @property nativeDraggableElement
* @type {HTMLElement}
*/
this.nativeDraggableElement = null;
this[onMouseDown] = this[onMouseDown].bind(this);
this[onMouseUp] = this[onMouseUp].bind(this);
this[onDragStart] = this[onDragStart].bind(this);
this[onDragOver] = this[onDragOver].bind(this);
this[onDragEnd] = this[onDragEnd].bind(this);
this[onDrop] = this[onDrop].bind(this);
}
/**
* Attaches sensors event listeners to the DOM
*/
attach() {
document.addEventListener('mousedown', this[onMouseDown], true);
}
/**
* Detaches sensors event listeners to the DOM
*/
detach() {
document.removeEventListener('mousedown', this[onMouseDown], true);
}
/**
* Drag start handler
* @private
* @param {Event} event - Drag start event
*/
[onDragStart](event) {
// Need for firefox. "text" key is needed for IE
event.dataTransfer.setData('text', '');
event.dataTransfer.effectAllowed = this.options.type;
const target = document.elementFromPoint(event.clientX, event.clientY);
this.currentContainer = closest(event.target, this.containers);
if (!this.currentContainer) {
return;
}
const dragStartEvent = new DragStartSensorEvent({
clientX: event.clientX,
clientY: event.clientY,
target,
container: this.currentContainer,
originalEvent: event,
});
// Workaround
setTimeout(() => {
this.trigger(this.currentContainer, dragStartEvent);
if (dragStartEvent.canceled()) {
this.dragging = false;
} else {
this.dragging = true;
}
}, 0);
}
/**
* Drag over handler
* @private
* @param {Event} event - Drag over event
*/
[onDragOver](event) {
if (!this.dragging) {
return;
}
const target = document.elementFromPoint(event.clientX, event.clientY);
const container = this.currentContainer;
const dragMoveEvent = new DragMoveSensorEvent({
clientX: event.clientX,
clientY: event.clientY,
target,
container,
originalEvent: event,
});
this.trigger(container, dragMoveEvent);
if (!dragMoveEvent.canceled()) {
event.preventDefault();
event.dataTransfer.dropEffect = this.options.type;
}
}
/**
* Drag end handler
* @private
* @param {Event} event - Drag end event
*/
[onDragEnd](event) {
if (!this.dragging) {
return;
}
document.removeEventListener('mouseup', this[onMouseUp], true);
const target = document.elementFromPoint(event.clientX, event.clientY);
const container = this.currentContainer;
const dragStopEvent = new DragStopSensorEvent({
clientX: event.clientX,
clientY: event.clientY,
target,
container,
originalEvent: event,
});
this.trigger(container, dragStopEvent);
this.dragging = false;
this.startEvent = null;
this[reset]();
}
/**
* Drop handler
* @private
* @param {Event} event - Drop event
*/
[onDrop](event) {
// eslint-disable-line class-methods-use-this
event.preventDefault();
}
/**
* Mouse down handler
* @private
* @param {Event} event - Mouse down event
*/
[onMouseDown](event) {
// Firefox bug for inputs within draggables https://bugzilla.mozilla.org/show_bug.cgi?id=739071
if (event.target && (event.target.form || event.target.contenteditable)) {
return;
}
const nativeDraggableElement = closest(event.target, (element) => element.draggable);
if (nativeDraggableElement) {
nativeDraggableElement.draggable = false;
this.nativeDraggableElement = nativeDraggableElement;
}
document.addEventListener('mouseup', this[onMouseUp], true);
document.addEventListener('dragstart', this[onDragStart], false);
document.addEventListener('dragover', this[onDragOver], false);
document.addEventListener('dragend', this[onDragEnd], false);
document.addEventListener('drop', this[onDrop], false);
const target = closest(event.target, this.options.draggable);
if (!target) {
return;
}
this.startEvent = event;
this.mouseDownTimeout = setTimeout(() => {
target.draggable = true;
this.draggableElement = target;
}, this.delay.drag);
}
/**
* Mouse up handler
* @private
* @param {Event} event - Mouse up event
*/
[onMouseUp]() {
this[reset]();
}
/**
* Mouse up handler
* @private
* @param {Event} event - Mouse up event
*/
[reset]() {
clearTimeout(this.mouseDownTimeout);
document.removeEventListener('mouseup', this[onMouseUp], true);
document.removeEventListener('dragstart', this[onDragStart], false);
document.removeEventListener('dragover', this[onDragOver], false);
document.removeEventListener('dragend', this[onDragEnd], false);
document.removeEventListener('drop', this[onDrop], false);
if (this.nativeDraggableElement) {
this.nativeDraggableElement.draggable = true;
this.nativeDraggableElement = null;
}
if (this.draggableElement) {
this.draggableElement.draggable = false;
this.draggableElement = null;
}
}
}