Sensor refactor

This commit is contained in:
Max Hoffmann 2017-10-24 11:38:31 -04:00
parent f09f118930
commit f11f2b7f94
No known key found for this signature in database
GPG Key ID: 1DFA4D13DD27A676
15 changed files with 814 additions and 188 deletions

View File

@ -18,6 +18,7 @@ module.exports = {
beforeEach: false,
describe: false,
test: false,
xtest: false,
expect: false,
},
};

View File

@ -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) {

View File

@ -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;
}
}
}

View File

@ -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);
```

View File

@ -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;
}

View File

@ -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],
});
```

View File

@ -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;
}

View File

@ -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

View File

@ -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});

View File

@ -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

View File

@ -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;

View File

@ -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';
}

View File

@ -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

View File

@ -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();
}

View File

@ -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;
}
}