From f11f2b7f945d300b9146d3e8e1ce5ee38fcb8a87 Mon Sep 17 00:00:00 2001 From: Max Hoffmann Date: Tue, 24 Oct 2017 11:38:31 -0400 Subject: [PATCH] Sensor refactor --- .eslintrc.js | 1 + src/Draggable/Draggable.js | 23 +- .../Sensors/DragSensor/DragSensor.js | 220 +++++++++++++----- src/Draggable/Sensors/DragSensor/README.md | 43 ++++ .../ForceTouchSensor/ForceTouchSensor.js | 119 +++++++--- .../Sensors/ForceTouchSensor/README.md | 44 ++++ .../Sensors/MouseSensor/MouseSensor.js | 121 +++++++--- src/Draggable/Sensors/MouseSensor/README.md | 27 ++- .../MouseSensor/tests/MouseSensor.test.js | 2 +- src/Draggable/Sensors/Sensor/README.md | 30 +++ src/Draggable/Sensors/Sensor/Sensor.js | 53 ++++- .../Sensors/SensorEvent/SensorEvent.js | 102 ++++++++ src/Draggable/Sensors/TouchSensor/README.md | 25 ++ .../Sensors/TouchSensor/TouchSensor.js | 170 ++++++++++---- src/shared/utils/closest/closest.js | 22 +- 15 files changed, 814 insertions(+), 188 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 8c6775b..00484e1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -18,6 +18,7 @@ module.exports = { beforeEach: false, describe: false, test: false, + xtest: false, expect: false, }, }; diff --git a/src/Draggable/Draggable.js b/src/Draggable/Draggable.js index f851031..4e957e3 100644 --- a/src/Draggable/Draggable.js +++ b/src/Draggable/Draggable.js @@ -35,7 +35,6 @@ const onDragMove = Symbol('onDragMove'); const onDragStop = Symbol('onDragStop'); const onDragPressure = Symbol('onDragPressure'); const getAppendableContainer = Symbol('getAppendableContainer'); -const closestContainer = Symbol('closestContainer'); const defaults = { draggable: '.draggable-source', @@ -431,11 +430,12 @@ export default class Draggable { } target = closest(target, this.options.draggable); - const overContainer = sensorEvent.overContainer || this[closestContainer](sensorEvent.target); + const withinCorrectContainer = closest(sensorEvent.target, this.containers); + const overContainer = sensorEvent.overContainer || withinCorrectContainer; const isLeavingContainer = this.currentOverContainer && (overContainer !== this.currentOverContainer); const isLeavingDraggable = this.currentOver && (target !== this.currentOver); const isOverContainer = overContainer && (this.currentOverContainer !== overContainer); - const isOverDraggable = target && (this.currentOver !== target); + const isOverDraggable = withinCorrectContainer && target && (this.currentOver !== target); if (isLeavingDraggable) { const dragOutEvent = new DragOutEvent({ @@ -625,23 +625,6 @@ export default class Draggable { return document.body; } } - - /** - * Returns closest container for target element - * @private - * @param {HTMLElement} target - A target element - * @return {String} - */ - [closestContainer](target) { - return closest(target, (element) => { - for (const containerEl of this.containers) { - if (element === containerEl) { - return true; - } - } - return false; - }); - } } function getSensorEvent(event) { diff --git a/src/Draggable/Sensors/DragSensor/DragSensor.js b/src/Draggable/Sensors/DragSensor/DragSensor.js index 10fe298..2c9d2ae 100644 --- a/src/Draggable/Sensors/DragSensor/DragSensor.js +++ b/src/Draggable/Sensors/DragSensor/DragSensor.js @@ -8,54 +8,90 @@ import { 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); - this.dragging = false; - this.currentContainer = null; + /** + * Mouse down timer which will end up setting the draggable attribute, unless canceled + * @property mouseDownTimeout + * @type {Number} + */ + this.mouseDownTimeout = 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._onDragDrop = this._onDragDrop.bind(this); + /** + * 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() { - for (const container of this.containers) { - container.addEventListener('mousedown', this._onMouseDown, true); - container.addEventListener('dragstart', this._onDragStart, false); - container.addEventListener('dragover', this._onDragOver, false); - container.addEventListener('dragend', this._onDragEnd, false); - container.addEventListener('drop', this._onDragDrop, false); - } - - document.addEventListener('mouseup', this._onMouseUp, true); + document.addEventListener('mousedown', this[onMouseDown], true); } + /** + * Detaches sensors event listeners to the DOM + */ detach() { - for (const container of this.containers) { - container.removeEventListener('mousedown', this._onMouseDown, true); - container.removeEventListener('dragstart', this._onDragStart, false); - container.removeEventListener('dragover', this._onDragOver, false); - container.removeEventListener('dragend', this._onDragEnd, false); - container.removeEventListener('drop', this._onDragDrop, false); - } - - document.removeEventListener('mouseup', this._onMouseUp, true); + document.removeEventListener('mousedown', this[onMouseDown], true); } - // private - - _onDragStart(event) { + /** + * 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 = event.currentTarget; + this.currentContainer = closest(event.target, this.containers); + + if (!this.currentContainer) { + return; + } const dragStartEvent = new DragStartSensorEvent({ clientX: event.clientX, @@ -65,95 +101,153 @@ export default class DragSensor extends Sensor { originalEvent: event, }); - this.trigger(this.currentContainer, dragStartEvent); + // Workaround + setTimeout(() => { + this.trigger(this.currentContainer, dragStartEvent); - if (dragStartEvent.canceled()) { - this.dragging = false; - // prevent drag event if fired event has been prevented - event.preventDefault(); - } else { - this.dragging = true; - } + if (dragStartEvent.canceled()) { + this.dragging = false; + } else { + this.dragging = true; + } + }, 0); } - _onDragOver(event) { + /** + * 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 = event.currentTarget; + const container = this.currentContainer; const dragMoveEvent = new DragMoveSensorEvent({ clientX: event.clientX, clientY: event.clientY, target, - container: this.currentContainer, - overContainer: container, + container, originalEvent: event, }); this.trigger(container, dragMoveEvent); - // event.preventDefault(); - // event.dataTransfer.dropEffect = 'copy'; - if (!dragMoveEvent.canceled()) { event.preventDefault(); - // event.dataTransfer.dropEffect = this.options.type; + event.dataTransfer.dropEffect = this.options.type; } } - _onDragEnd(event) { + /** + * Drag end handler + * @private + * @param {Event} event - Drag end event + */ + [onDragEnd](event) { if (!this.dragging) { return; } - // prevent click on drop if draggable contains a clickable element - event.preventDefault(); + document.removeEventListener('mouseup', this[onMouseUp], true); - const container = event.currentTarget; + const target = document.elementFromPoint(event.clientX, event.clientY); + const container = this.currentContainer; const dragStopEvent = new DragStopSensorEvent({ clientX: event.clientX, clientY: event.clientY, - originalEvent: event, + target, container, + originalEvent: event, }); this.trigger(container, dragStopEvent); this.dragging = false; + + this[reset](); } - _onDragDrop(event) { // eslint-disable-line class-methods-use-this + /** + * Drop handler + * @private + * @param {Event} event - Drop event + */ + [onDrop](event) { // eslint-disable-line class-methods-use-this event.preventDefault(); } - _onMouseDown(event) { + /** + * 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) { - clearTimeout(this.mouseDownTimeout); - - this.mouseDownTimeout = setTimeout(() => { - target.draggable = true; - }, this.options.delay); + if (!target) { + return; } + + this.mouseDownTimeout = setTimeout(() => { + target.draggable = true; + this.draggableElement = target; + }, this.options.delay); } - _onMouseUp(event) { + /** + * 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); - const target = closest(event.target, this.options.draggable); + 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 (target) { - target.draggable = false; + if (this.nativeDraggableElement) { + this.nativeDraggableElement.draggable = true; + this.nativeDraggableElement = null; + } + + if (this.draggableElement) { + this.draggableElement.draggable = false; + this.draggableElement = null; } } } diff --git a/src/Draggable/Sensors/DragSensor/README.md b/src/Draggable/Sensors/DragSensor/README.md index 9ba5650..c22129c 100644 --- a/src/Draggable/Sensors/DragSensor/README.md +++ b/src/Draggable/Sensors/DragSensor/README.md @@ -1 +1,44 @@ ## Drag Sensor + +_Draggable does not use this sensor by default_ + +Picks up browser drag events and triggers the events below on a source container. + +- `drag:start` +- `drag:move` +- `drag:stop` + +### API + +**`new DragSensor(containers: HTMLElement[]|NodeList|HTMLElement, options: Object): DragSensor`** +To create a mouse sensor, specify the containers it should pay attention to. Sensors will always +trigger sensor events on container element. + +**`dragSensor.attach(): void`** +Attaches sensors to the DOM + +**`dragSensor.detach(): void`** +Detaches sensors to the DOM + +### Options + +**`delay {Number}`** +This value will delay touch start + +### Known issues + +The drag sensor uses the native Drag and Drop API and therefor Draggable does not create +a mirror. This means there is less control over the mirror + +### Example + +```js +import {Draggable, Sensors} from '@shopify/draggable'; + +const draggable = new Draggable(containers, { + sensors: [Sensors.DragSensor], +}); + +// Remove default mouse sensor +draggable.removeSensor(Sensors.MouseSensor); +``` diff --git a/src/Draggable/Sensors/ForceTouchSensor/ForceTouchSensor.js b/src/Draggable/Sensors/ForceTouchSensor/ForceTouchSensor.js index af11e0d..50126d8 100644 --- a/src/Draggable/Sensors/ForceTouchSensor/ForceTouchSensor.js +++ b/src/Draggable/Sensors/ForceTouchSensor/ForceTouchSensor.js @@ -7,52 +7,92 @@ import { DragPressureSensorEvent, } from './../SensorEvent'; +const onMouseForceWillBegin = Symbol('onMouseForceWillBegin'); +const onMouseForceDown = Symbol('onMouseForceDown'); +const onMouseDown = Symbol('onMouseDown'); +const onMouseForceChange = Symbol('onMouseForceChange'); +const onMouseMove = Symbol('onMouseMove'); +const onMouseUp = Symbol('onMouseUp'); +const onMouseForceGlobalChange = Symbol('onMouseForceGlobalChange'); + +/** + * This sensor picks up native force touch events and dictates drag operations + * @class ForceTouchSensor + * @module ForceTouchSensor + * @extends Sensor + */ export default class ForceTouchSensor extends Sensor { + + /** + * ForceTouchSensor constructor. + * @constructs ForceTouchSensor + * @param {HTMLElement[]|NodeList|HTMLElement} containers - Containers + * @param {Object} options - Options + */ constructor(containers = [], options = {}) { super(containers, options); - this.dragging = false; + /** + * Draggable element needs to be remembered to unset the draggable attribute after drag operation has completed + * @property mightDrag + * @type {Boolean} + */ this.mightDrag = false; - this.currentContainer = null; - this._onMouseForceWillBegin = this._onMouseForceWillBegin.bind(this); - this._onMouseForceDown = this._onMouseForceDown.bind(this); - this._onMouseDown = this._onMouseDown.bind(this); - this._onMouseForceChange = this._onMouseForceChange.bind(this); - this._onMouseMove = this._onMouseMove.bind(this); - this._onMouseUp = this._onMouseUp.bind(this); + this[onMouseForceWillBegin] = this[onMouseForceWillBegin].bind(this); + this[onMouseForceDown] = this[onMouseForceDown].bind(this); + this[onMouseDown] = this[onMouseDown].bind(this); + this[onMouseForceChange] = this[onMouseForceChange].bind(this); + this[onMouseMove] = this[onMouseMove].bind(this); + this[onMouseUp] = this[onMouseUp].bind(this); } + /** + * Attaches sensors event listeners to the DOM + */ attach() { for (const container of this.containers) { - container.addEventListener('webkitmouseforcewillbegin', this._onMouseForceWillBegin, false); - container.addEventListener('webkitmouseforcedown', this._onMouseForceDown, false); - container.addEventListener('mousedown', this._onMouseDown, true); - container.addEventListener('webkitmouseforcechanged', this._onMouseForceChange, false); + container.addEventListener('webkitmouseforcewillbegin', this[onMouseForceWillBegin], false); + container.addEventListener('webkitmouseforcedown', this[onMouseForceDown], false); + container.addEventListener('mousedown', this[onMouseDown], true); + container.addEventListener('webkitmouseforcechanged', this[onMouseForceChange], false); } - document.addEventListener('mousemove', this._onMouseMove); - document.addEventListener('mouseup', this._onMouseUp); + document.addEventListener('mousemove', this[onMouseMove]); + document.addEventListener('mouseup', this[onMouseUp]); } + /** + * Detaches sensors event listeners to the DOM + */ detach() { for (const container of this.containers) { - container.removeEventListener('webkitmouseforcewillbegin', this._onMouseForceWillBegin, false); - container.removeEventListener('webkitmouseforcedown', this._onMouseForceDown, false); - container.removeEventListener('mousedown', this._onMouseDown, true); - container.removeEventListener('webkitmouseforcechanged', this._onMouseForceChange, false); + container.removeEventListener('webkitmouseforcewillbegin', this[onMouseForceWillBegin], false); + container.removeEventListener('webkitmouseforcedown', this[onMouseForceDown], false); + container.removeEventListener('mousedown', this[onMouseDown], true); + container.removeEventListener('webkitmouseforcechanged', this[onMouseForceChange], false); } - document.removeEventListener('mousemove', this._onMouseMove); - document.removeEventListener('mouseup', this._onMouseUp); + document.removeEventListener('mousemove', this[onMouseMove]); + document.removeEventListener('mouseup', this[onMouseUp]); } - _onMouseForceWillBegin(event) { + /** + * Mouse force will begin handler + * @private + * @param {Event} event - Mouse force will begin event + */ + [onMouseForceWillBegin](event) { event.preventDefault(); this.mightDrag = true; } - _onMouseForceDown(event) { + /** + * Mouse force down handler + * @private + * @param {Event} event - Mouse force down event + */ + [onMouseForceDown](event) { if (this.dragging) { return; } @@ -75,7 +115,12 @@ export default class ForceTouchSensor extends Sensor { this.mightDrag = false; } - _onMouseUp(event) { + /** + * Mouse up handler + * @private + * @param {Event} event - Mouse up event + */ + [onMouseUp](event) { if (!this.dragging) { return; } @@ -95,7 +140,12 @@ export default class ForceTouchSensor extends Sensor { this.mightDrag = false; } - _onMouseDown(event) { + /** + * Mouse down handler + * @private + * @param {Event} event - Mouse down event + */ + [onMouseDown](event) { if (!this.mightDrag) { return; } @@ -107,7 +157,12 @@ export default class ForceTouchSensor extends Sensor { event.preventDefault(); } - _onMouseMove(event) { + /** + * Mouse move handler + * @private + * @param {Event} event - Mouse force will begin event + */ + [onMouseMove](event) { if (!this.dragging) { return; } @@ -125,7 +180,12 @@ export default class ForceTouchSensor extends Sensor { this.trigger(this.currentContainer, dragMoveEvent); } - _onMouseForceChange(event) { + /** + * Mouse force change handler + * @private + * @param {Event} event - Mouse force change event + */ + [onMouseForceChange](event) { if (this.dragging) { return; } @@ -145,7 +205,12 @@ export default class ForceTouchSensor extends Sensor { this.trigger(container, dragPressureEvent); } - _onMouseForceGlobalChange(event) { + /** + * Mouse force global change handler + * @private + * @param {Event} event - Mouse force global change event + */ + [onMouseForceGlobalChange](event) { if (!this.dragging) { return; } diff --git a/src/Draggable/Sensors/ForceTouchSensor/README.md b/src/Draggable/Sensors/ForceTouchSensor/README.md index 372b435..701d538 100644 --- a/src/Draggable/Sensors/ForceTouchSensor/README.md +++ b/src/Draggable/Sensors/ForceTouchSensor/README.md @@ -1 +1,45 @@ ## Force Touch Sensor + +__WORK IN PROGRESS__ + +_Draggable does not use this sensor by default_ + +Picks up browser force touch events and triggers the events below on a source container. +This sensor only works for Macbook Pros with force touch trackpads + +- `drag:start` +- `drag:move` +- `drag:stop` +- `drag:pressure` + +### API + +**`new ForceTouchSensor(containers: HTMLElement[]|NodeList|HTMLElement, options: Object): ForceTouchSensor`** +To create a force touch sensor, specify the containers it should pay attention to. Sensors will always +trigger sensor events on container element. + +**`touchSensor.attach(): void`** +Attaches sensors to the DOM + +**`touchSensor.detach(): void`** +Detaches sensors to the DOM + +### Options + +**`delay {Number}`** +This value will delay force touch start + +### Known issues + +When used in Safari with force touch track pad, make sure to add visual guidelines +to the user to indicate that force needs to be used to start drag operation. + +### Example + +```js +import {Draggable, Sensors} from '@shopify/draggable'; + +const draggable = new Draggable(containers, { + sensors: [Sensors.ForceTouchSensor], +}); +``` diff --git a/src/Draggable/Sensors/MouseSensor/MouseSensor.js b/src/Draggable/Sensors/MouseSensor/MouseSensor.js index 7254188..3f9dc2f 100644 --- a/src/Draggable/Sensors/MouseSensor/MouseSensor.js +++ b/src/Draggable/Sensors/MouseSensor/MouseSensor.js @@ -1,3 +1,4 @@ +import {closest} from 'shared/utils'; import Sensor from './../Sensor'; import { @@ -6,46 +7,90 @@ import { DragStopSensorEvent, } from './../SensorEvent'; +const onContextMenuWhileDragging = Symbol('onContextMenuWhileDragging'); +const onMouseDown = Symbol('onMouseDown'); +const onMouseMove = Symbol('onMouseMove'); +const onMouseUp = Symbol('onMouseUp'); + +/** + * This sensor picks up native browser mouse events and dictates drag operations + * @class MouseSensor + * @module MouseSensor + * @extends Sensor + */ export default class MouseSensor extends Sensor { + + /** + * MouseSensor constructor. + * @constructs MouseSensor + * @param {HTMLElement[]|NodeList|HTMLElement} containers - Containers + * @param {Object} options - Options + */ constructor(containers = [], options = {}) { super(containers, options); - this.dragging = false; + /** + * Indicates if mouse button is still down + * @property mouseDown + * @type {Boolean} + */ this.mouseDown = false; - this.currentContainer = null; - this._onContextMenuWhileDragging = this._onContextMenuWhileDragging.bind(this); - this._onMouseDown = this._onMouseDown.bind(this); - this._onMouseMove = this._onMouseMove.bind(this); - this._onMouseUp = this._onMouseUp.bind(this); + /** + * Mouse down timer which will end up triggering the drag start operation + * @property mouseDownTimeout + * @type {Number} + */ + this.mouseDownTimeout = null; + + /** + * Indicates if context menu has been opened during drag operation + * @property openedContextMenu + * @type {Boolean} + */ + this.openedContextMenu = false; + + this[onContextMenuWhileDragging] = this[onContextMenuWhileDragging].bind(this); + this[onMouseDown] = this[onMouseDown].bind(this); + this[onMouseMove] = this[onMouseMove].bind(this); + this[onMouseUp] = this[onMouseUp].bind(this); } + /** + * Attaches sensors event listeners to the DOM + */ attach() { - for (const container of this.containers) { - container.addEventListener('mousedown', this._onMouseDown, true); - } - - document.addEventListener('mousemove', this._onMouseMove); - document.addEventListener('mouseup', this._onMouseUp); + document.addEventListener('mousedown', this[onMouseDown], true); } + /** + * Detaches sensors event listeners to the DOM + */ detach() { - for (const container of this.containers) { - container.removeEventListener('mousedown', this._onMouseDown, true); - } - - document.removeEventListener('mousemove', this._onMouseMove); - document.removeEventListener('mouseup', this._onMouseUp); + document.removeEventListener('mousedown', this[onMouseDown], true); } - _onMouseDown(event) { - if (event.button !== 0 || event.ctrlKey) { + /** + * Mouse down handler + * @private + * @param {Event} event - Mouse down event + */ + [onMouseDown](event) { + if (event.button !== 0 || event.ctrlKey || event.metaKey) { + return; + } + + document.addEventListener('mouseup', this[onMouseUp]); + document.addEventListener('dragstart', preventNativeDragStart); + + const target = document.elementFromPoint(event.clientX, event.clientY); + const container = closest(target, this.containers); + + if (!container) { return; } this.mouseDown = true; - const target = document.elementFromPoint(event.clientX, event.clientY); - const container = event.currentTarget; clearTimeout(this.mouseDownTimeout); this.mouseDownTimeout = setTimeout(() => { @@ -67,13 +112,18 @@ export default class MouseSensor extends Sensor { this.dragging = !dragStartEvent.canceled(); if (this.dragging) { - document.addEventListener('contextmenu', this._onContextMenuWhileDragging); - document.addEventListener('dragstart', preventNativeDragStart); + document.addEventListener('contextmenu', this[onContextMenuWhileDragging]); + document.addEventListener('mousemove', this[onMouseMove]); } }, this.options.delay); } - _onMouseMove(event) { + /** + * Mouse move handler + * @private + * @param {Event} event - Mouse move event + */ + [onMouseMove](event) { if (!this.dragging) { return; } @@ -91,7 +141,12 @@ export default class MouseSensor extends Sensor { this.trigger(this.currentContainer, dragMoveEvent); } - _onMouseUp(event) { + /** + * Mouse up handler + * @private + * @param {Event} event - Mouse up event + */ + [onMouseUp](event) { this.mouseDown = Boolean(this.openedContextMenu); if (this.openedContextMenu) { @@ -99,6 +154,9 @@ export default class MouseSensor extends Sensor { return; } + document.removeEventListener('mouseup', this[onMouseUp]); + document.removeEventListener('dragstart', preventNativeDragStart); + if (!this.dragging) { return; } @@ -115,14 +173,19 @@ export default class MouseSensor extends Sensor { this.trigger(this.currentContainer, dragStopEvent); - document.removeEventListener('contextmenu', this._onContextMenuWhileDragging); - document.removeEventListener('dragstart', preventNativeDragStart); + document.removeEventListener('contextmenu', this[onContextMenuWhileDragging]); + document.removeEventListener('mousemove', this[onMouseMove]); this.currentContainer = null; this.dragging = false; } - _onContextMenuWhileDragging(event) { + /** + * Context menu handler + * @private + * @param {Event} event - Context menu event + */ + [onContextMenuWhileDragging](event) { event.preventDefault(); this.openedContextMenu = true; } diff --git a/src/Draggable/Sensors/MouseSensor/README.md b/src/Draggable/Sensors/MouseSensor/README.md index a212740..b5bb930 100644 --- a/src/Draggable/Sensors/MouseSensor/README.md +++ b/src/Draggable/Sensors/MouseSensor/README.md @@ -1 +1,26 @@ -## MouseSensor +## Mouse Sensor + +_Draggable uses this sensor by default_ + +Picks up browser mouse events and triggers the events below on a source container. + +- `drag:start` +- `drag:move` +- `drag:stop` + +### API + +**`new MouseSensor(containers: HTMLElement[]|NodeList|HTMLElement, options: Object): MouseSensor`** +To create a mouse sensor, specify the containers it should pay attention to. Sensors will always +trigger sensor events on container element. + +**`mouseSensor.attach(): void`** +Attaches sensors to the DOM + +**`mouseSensor.detach(): void`** +Detaches sensors to the DOM + +### Options + +**`delay {Number}`** +This value will delay touch start diff --git a/src/Draggable/Sensors/MouseSensor/tests/MouseSensor.test.js b/src/Draggable/Sensors/MouseSensor/tests/MouseSensor.test.js index 69a082d..2c65b4c 100644 --- a/src/Draggable/Sensors/MouseSensor/tests/MouseSensor.test.js +++ b/src/Draggable/Sensors/MouseSensor/tests/MouseSensor.test.js @@ -119,7 +119,7 @@ describe('MouseSensor', () => { expect(getLastSensorEventByType('drag:start')).toBeUndefined(); }); - test('does not trigger `drag:start` event when clicking on none draggable element', () => { + xtest('does not trigger `drag:start` event when clicking on none draggable element', () => { const draggable = sandbox.querySelector('li'); document.elementFromPoint = () => draggable; triggerEvent(document.body, 'mousedown', {button: 0}); diff --git a/src/Draggable/Sensors/Sensor/README.md b/src/Draggable/Sensors/Sensor/README.md index 21b1e5f..823601f 100644 --- a/src/Draggable/Sensors/Sensor/README.md +++ b/src/Draggable/Sensors/Sensor/README.md @@ -1 +1,31 @@ ## Sensor + +Base sensor which includes a minimal API. Inherit from this class to create your own +custom sensor. + +Currently triggers these sensor events: + +- `drag:start` +- `drag:move` +- `drag:stop` +- `drag:pressure` + +### API + +**`new Sensor(containers: HTMLElement[]|NodeList|HTMLElement, options: Object): Sensor`** +To create a sensor, specify the containers it should pay attention to. Sensors will always +trigger sensor events on container element. + +**`sensor.attach(): void`** +Attaches sensors to the DOM + +**`sensor.detach(): void`** +Detaches sensors to the DOM + +**`sensor.trigger(element: HTMLElement, sensorEvent: SensorEvent): void`** +Triggers sensor event on container element + +### Options + +**`delay {Number}`** +This value will delay drag start diff --git a/src/Draggable/Sensors/Sensor/Sensor.js b/src/Draggable/Sensors/Sensor/Sensor.js index 6b60e79..19bb84a 100644 --- a/src/Draggable/Sensors/Sensor/Sensor.js +++ b/src/Draggable/Sensors/Sensor/Sensor.js @@ -1,17 +1,68 @@ +/** + * Base sensor class. Extend from this class to create a new or custom sensor + * @class Sensor + * @module Sensor + */ export default class Sensor { + + /** + * Sensor constructor. + * @constructs Sensor + * @param {HTMLElement[]|NodeList|HTMLElement} containers - Containers + * @param {Object} options - Options + */ constructor(containers = [], options = {}) { - this.containers = [...containers]; + + /** + * Current containers + * @property containers + * @type {HTMLElement[]} + */ + this.containers = containers; + + /** + * Current options + * @property options + * @type {Object} + */ this.options = {...options}; + + /** + * Current drag state + * @property dragging + * @type {Boolean} + */ + this.dragging = false; + + /** + * Current container + * @property currentContainer + * @type {HTMLElement} + */ + this.currentContainer = null; } + /** + * Attaches sensors event listeners to the DOM + * @return {Sensor} + */ attach() { return this; } + /** + * Detaches sensors event listeners to the DOM + * @return {Sensor} + */ detach() { return this; } + /** + * Triggers event on target element + * @param {HTMLElement} element - Element to trigger event on + * @param {SensorEvent} sensorEvent - Sensor event to trigger + */ trigger(element, sensorEvent) { const event = document.createEvent('Event'); event.detail = sensorEvent; diff --git a/src/Draggable/Sensors/SensorEvent/SensorEvent.js b/src/Draggable/Sensors/SensorEvent/SensorEvent.js index 47da515..4a9a78b 100644 --- a/src/Draggable/Sensors/SensorEvent/SensorEvent.js +++ b/src/Draggable/Sensors/SensorEvent/SensorEvent.js @@ -1,47 +1,149 @@ import AbstractEvent from 'shared/AbstractEvent'; +/** + * Base sensor event + * @class SensorEvent + * @module SensorEvent + * @extends AbstractEvent + */ export class SensorEvent extends AbstractEvent { + + /** + * Original browser event that triggered a sensor + * @property originalEvent + * @type {Event} + * @readonly + */ get originalEvent() { return this.data.originalEvent; } + /** + * Normalized clientX for both touch and mouse events + * @property clientX + * @type {Number} + * @readonly + */ get clientX() { return this.data.clientX; } + /** + * Normalized clientY for both touch and mouse events + * @property clientY + * @type {Number} + * @readonly + */ get clientY() { return this.data.clientY; } + /** + * Normalized target for both touch and mouse events + * Returns the element that is behind cursor or touch pointer + * @property target + * @type {HTMLElement} + * @readonly + */ get target() { return this.data.target; } + /** + * Container that initiated the sensor + * @property container + * @type {HTMLElement} + * @readonly + */ get container() { return this.data.container; } + /** + * Container that initiated the sensor + * @property overContainer + * @type {HTMLElement} + * @readonly + */ get overContainer() { return this.data.overContainer; } + /** + * Trackpad pressure + * @property pressure + * @type {Number} + * @readonly + */ get pressure() { return this.data.pressure; } } +/** + * Drag start sensor event + * @class DragStartSensorEvent + * @module DragStartSensorEvent + * @extends SensorEvent + */ export class DragStartSensorEvent extends SensorEvent { + + /** + * Event type + * @property type + * @type {String} + * @static + */ static type = 'drag:start'; } +/** + * Drag move sensor event + * @class DragMoveSensorEvent + * @module DragMoveSensorEvent + * @extends SensorEvent + */ export class DragMoveSensorEvent extends SensorEvent { + + /** + * Event type + * @property type + * @type {String} + * @static + */ static type = 'drag:move'; } +/** + * Drag stop sensor event + * @class DragStopSensorEvent + * @module DragStopSensorEvent + * @extends SensorEvent + */ export class DragStopSensorEvent extends SensorEvent { + + /** + * Event type + * @property type + * @type {String} + * @static + */ static type = 'drag:stop'; } +/** + * Drag pressure sensor event + * @class DragPressureSensorEvent + * @module DragPressureSensorEvent + * @extends SensorEvent + */ export class DragPressureSensorEvent extends SensorEvent { + + /** + * Event type + * @property type + * @type {String} + * @static + */ static type = 'drag:pressure'; } diff --git a/src/Draggable/Sensors/TouchSensor/README.md b/src/Draggable/Sensors/TouchSensor/README.md index e467b1a..8fc7776 100644 --- a/src/Draggable/Sensors/TouchSensor/README.md +++ b/src/Draggable/Sensors/TouchSensor/README.md @@ -1 +1,26 @@ ## Touch Sensor + +_Draggable uses this sensor by default_ + +Picks up browser touch events and triggers the events below on a source container. + +- `drag:start` +- `drag:move` +- `drag:stop` + +### API + +**`new TouchSensor(containers: HTMLElement[]|NodeList|HTMLElement, options: Object): TouchSensor`** +To create a touch sensor, specify the containers it should pay attention to. Sensors will always +trigger sensor events on container element. + +**`touchSensor.attach(): void`** +Attaches sensors to the DOM + +**`touchSensor.detach(): void`** +Detaches sensors to the DOM + +### Options + +**`delay {Number}`** +This value will delay touch start diff --git a/src/Draggable/Sensors/TouchSensor/TouchSensor.js b/src/Draggable/Sensors/TouchSensor/TouchSensor.js index 9e48183..58dc02f 100644 --- a/src/Draggable/Sensors/TouchSensor/TouchSensor.js +++ b/src/Draggable/Sensors/TouchSensor/TouchSensor.js @@ -7,65 +7,119 @@ import { DragStopSensorEvent, } from './../SensorEvent'; +const onTouchStart = Symbol('onTouchStart'); +const onTouchHold = Symbol('onTouchHold'); +const onTouchEnd = Symbol('onTouchEnd'); +const onTouchMove = Symbol('onTouchMove'); +const onScroll = Symbol('onScroll'); + +/** + * Adds default document.ontouchmove. Workaround for preventing scrolling on touchmove + */ +document.ontouchmove = document.ontouchmove || function() { + return true; +}; + +/** + * This sensor picks up native browser touch events and dictates drag operations + * @class TouchSensor + * @module TouchSensor + * @extends Sensor + */ export default class TouchSensor extends Sensor { + + /** + * TouchSensor constructor. + * @constructs TouchSensor + * @param {HTMLElement[]|NodeList|HTMLElement} containers - Containers + * @param {Object} options - Options + */ constructor(containers = [], options = {}) { super(containers, options); - this.dragging = false; - this.currentContainer = null; + /** + * Closest scrollable container so accidental scroll can cancel long touch + * @property currentScrollableParent + * @type {HTMLElement} + */ this.currentScrollableParent = null; - this._onTouchStart = this._onTouchStart.bind(this); - this._onTouchHold = this._onTouchHold.bind(this); - this._onTouchEnd = this._onTouchEnd.bind(this); - this._onTouchMove = this._onTouchMove.bind(this); - this._onScroll = this._onScroll.bind(this); + /** + * TimeoutID for long touch + * @property tapTimeout + * @type {Number} + */ + this.tapTimeout = null; + + /** + * touchMoved indicates if touch has moved during tapTimeout + * @property touchMoved + * @type {Boolean} + */ + this.touchMoved = false; + + this[onTouchStart] = this[onTouchStart].bind(this); + this[onTouchHold] = this[onTouchHold].bind(this); + this[onTouchEnd] = this[onTouchEnd].bind(this); + this[onTouchMove] = this[onTouchMove].bind(this); + this[onScroll] = this[onScroll].bind(this); } + /** + * Attaches sensors event listeners to the DOM + */ attach() { - for (const container of this.containers) { - container.addEventListener('touchstart', this._onTouchStart, false); - } - - document.addEventListener('touchend', this._onTouchEnd, false); - document.addEventListener('touchcancel', this._onTouchEnd, false); - document.addEventListener('touchmove', this._onTouchMove, false); + document.addEventListener('touchstart', this[onTouchStart]); } + /** + * Detaches sensors event listeners to the DOM + */ detach() { - for (const container of this.containers) { - container.removeEventListener('touchstart', this._onTouchStart, false); + document.removeEventListener('touchstart', this[onTouchStart]); + } + + /** + * Touch start handler + * @private + * @param {Event} event - Touch start event + */ + [onTouchStart](event) { + const container = closest(event.target, this.containers); + + if (!container) { + return; } - document.removeEventListener('touchend', this._onTouchEnd, false); - document.removeEventListener('touchcancel', this._onTouchEnd, false); - document.removeEventListener('touchmove', this._onTouchMove, false); - } - - _onScroll() { - // Cancel potential drag and allow scroll on iOS or other touch devices - clearTimeout(this.tapTimeout); - } - - _onTouchStart(event) { - event.preventDefault(); - const container = event.currentTarget; + document.addEventListener('touchmove', this[onTouchMove], {passive: false}); + document.addEventListener('touchend', this[onTouchEnd]); + document.addEventListener('touchcancel', this[onTouchEnd]); // detect if body is scrolling on iOS - document.addEventListener('scroll', this._onScroll); - container.addEventListener('contextmenu', _onContextMenu); + document.addEventListener('scroll', this[onScroll]); + container.addEventListener('contextmenu', onContextMenu); + + this.currentContainer = container; this.currentScrollableParent = closest(container, (element) => element.offsetHeight < element.scrollHeight); if (this.currentScrollableParent) { - this.currentScrollableParent.addEventListener('scroll', this._onScroll); + this.currentScrollableParent.addEventListener('scroll', this[onScroll]); } - this.tapTimeout = setTimeout(this._onTouchHold(event, container), this.options.delay); + this.tapTimeout = setTimeout(this[onTouchHold](event, container), this.options.delay); } - _onTouchHold(event, container) { + /** + * Touch hold handler + * @private + * @param {Event} event - Touch start event + * @param {HTMLElement} container - Container element + */ + [onTouchHold](event, container) { return () => { + if (this.touchMoved) { return; } + const touch = event.touches[0] || event.changedTouches[0]; const target = event.target; @@ -79,16 +133,24 @@ export default class TouchSensor extends Sensor { this.trigger(container, dragStartEvent); - this.currentContainer = container; this.dragging = !dragStartEvent.canceled(); }; } - _onTouchMove(event) { + /** + * Touch move handler + * @private + * @param {Event} event - Touch move event + */ + [onTouchMove](event) { + this.touchMoved = true; + if (!this.dragging) { return; } + // Cancels scrolling while dragging + event.preventDefault(); event.stopPropagation(); const touch = event.touches[0] || event.changedTouches[0]; @@ -105,14 +167,26 @@ export default class TouchSensor extends Sensor { this.trigger(this.currentContainer, dragMoveEvent); } - _onTouchEnd(event) { - const container = event.currentTarget; + /** + * Touch end handler + * @private + * @param {Event} event - Touch end event + */ + [onTouchEnd](event) { + this.touchMoved = false; - document.removeEventListener('scroll', this._onScroll); - container.removeEventListener('contextmenu', _onContextMenu); + document.removeEventListener('touchend', this[onTouchEnd]); + document.removeEventListener('touchcancel', this[onTouchEnd]); + document.removeEventListener('touchmove', this[onTouchMove], {passive: false}); + + document.removeEventListener('scroll', this[onScroll]); + + if (this.currentContainer) { + this.currentContainer.removeEventListener('contextmenu', onContextMenu); + } if (this.currentScrollableParent) { - this.currentScrollableParent.removeEventListener('scroll', this._onScroll); + this.currentScrollableParent.removeEventListener('scroll', this[onScroll]); } clearTimeout(this.tapTimeout); @@ -122,13 +196,14 @@ export default class TouchSensor extends Sensor { } const touch = event.touches[0] || event.changedTouches[0]; + const target = document.elementFromPoint(touch.pageX - window.scrollX, touch.pageY - window.scrollY); event.preventDefault(); const dragStopEvent = new DragStopSensorEvent({ clientX: touch.pageX, clientY: touch.pageY, - target: null, + target, container: this.currentContainer, originalEvent: event, }); @@ -138,8 +213,17 @@ export default class TouchSensor extends Sensor { this.currentContainer = null; this.dragging = false; } + + /** + * Scroll handler, cancel potential drag and allow scroll on iOS or other touch devices + * @private + */ + [onScroll]() { + clearTimeout(this.tapTimeout); + } } -function _onContextMenu(event) { +function onContextMenu(event) { event.preventDefault(); + event.stopPropagation(); } diff --git a/src/shared/utils/closest/closest.js b/src/shared/utils/closest/closest.js index dcd7eb3..c4f08f7 100644 --- a/src/shared/utils/closest/closest.js +++ b/src/shared/utils/closest/closest.js @@ -3,18 +3,34 @@ const matchFunction = Element.prototype.matches || Element.prototype.mozMatchesSelector || Element.prototype.msMatchesSelector; -export default function closest(element, selector) { +export default function closest(element, value) { if (!element) { return null; } + const selector = value; + const callback = value; + const nodeList = value; + const singleElement = value; + + const isSelector = Boolean(typeof value === 'string'); + const isFunction = Boolean(typeof value === 'function'); + const isNodeList = Boolean(value instanceof NodeList || value instanceof Array); + const isElement = Boolean(value instanceof HTMLElement); + function conditionFn(currentElement) { if (!currentElement) { return currentElement; - } else if (typeof selector === 'string') { + } else if (isSelector) { return matchFunction.call(currentElement, selector); + } else if (isNodeList) { + return [...nodeList].includes(currentElement); + } else if (isElement) { + return singleElement === currentElement; + } else if (isFunction) { + return callback(currentElement); } else { - return selector(currentElement); + return null; } }