const scope = require('./scope');
const utils = require('./utils');
const events = require('./utils/events');
const browser = require('./utils/browser');
const domObjects = require('./utils/domObjects');
const finder = require('./utils/interactionFinder');
const signals = require('./utils/Signals').new();
const listeners = {};
const methodNames = [
'pointerDown', 'pointerMove', 'pointerUp',
'updatePointer', 'removePointer',
];
// for ignoring browser's simulated mouse events
let prevTouchTime = 0;
// all active and idle interactions
scope.interactions = [];
class Interaction {
/** */
constructor ({ pointerType }) {
this.target = null; // current interactable being interacted with
this.element = null; // the target element of the interactable
this.prepared = { // action that's ready to be fired on next move event
name : null,
axis : null,
edges: null,
};
// keep track of added pointers
this.pointers = [];
this.pointerIds = [];
this.downTargets = [];
this.downTimes = [];
// Previous native pointer move event coordinates
this.prevCoords = {
page : { x: 0, y: 0 },
client : { x: 0, y: 0 },
timeStamp: 0,
};
// current native pointer move event coordinates
this.curCoords = {
page : { x: 0, y: 0 },
client : { x: 0, y: 0 },
timeStamp: 0,
};
// Starting InteractEvent pointer coordinates
this.startCoords = {
page : { x: 0, y: 0 },
client : { x: 0, y: 0 },
timeStamp: 0,
};
// Change in coordinates and time of the pointer
this.pointerDelta = {
page : { x: 0, y: 0, vx: 0, vy: 0, speed: 0 },
client : { x: 0, y: 0, vx: 0, vy: 0, speed: 0 },
timeStamp: 0,
};
this.downEvent = null; // pointerdown/mousedown/touchstart event
this.downPointer = {};
this._eventTarget = null;
this._curEventTarget = null;
this.prevEvent = null; // previous action event
this.pointerIsDown = false;
this.pointerWasMoved = false;
this._interacting = false;
this._ending = false;
this.pointerType = pointerType;
signals.fire('new', this);
scope.interactions.push(this);
}
pointerDown (pointer, event, eventTarget) {
const pointerIndex = this.updatePointer(pointer, event, true);
signals.fire('down', {
pointer,
event,
eventTarget,
pointerIndex,
interaction: this,
});
}
/**
* ```js
* interact(target)
* .draggable({
* // disable the default drag start by down->move
* manualStart: true
* })
* // start dragging after the user holds the pointer down
* .on('hold', function (event) {
* var interaction = event.interaction;
*
* if (!interaction.interacting()) {
* interaction.start({ name: 'drag' },
* event.interactable,
* event.currentTarget);
* }
* });
* ```
*
* Start an action with the given Interactable and Element as tartgets. The
* action must be enabled for the target Interactable and an appropriate
* number of pointers must be held down - 1 for drag/resize, 2 for gesture.
*
* Use it with `interactable.<action>able({ manualStart: false })` to always
* [start actions manually](https://github.com/taye/interact.js/issues/114)
*
* @param {object} action The action to be performed - drag, resize, etc.
* @param {Interactable} target The Interactable to target
* @param {Element} element The DOM Element to target
* @return {object} interact
*/
start (action, target, element) {
if (this.interacting()
|| !this.pointerIsDown
|| this.pointerIds.length < (action.name === 'gesture'? 2 : 1)) {
return;
}
// if this interaction had been removed after stopping
// add it back
if (scope.interactions.indexOf(this) === -1) {
scope.interactions.push(this);
}
utils.copyAction(this.prepared, action);
this.target = target;
this.element = element;
signals.fire('action-start', {
interaction: this,
event: this.downEvent,
});
}
pointerMove (pointer, event, eventTarget) {
if (!this.simulation) {
this.updatePointer(pointer);
utils.setCoords(this.curCoords, this.pointers);
}
const duplicateMove = (this.curCoords.page.x === this.prevCoords.page.x
&& this.curCoords.page.y === this.prevCoords.page.y
&& this.curCoords.client.x === this.prevCoords.client.x
&& this.curCoords.client.y === this.prevCoords.client.y);
let dx;
let dy;
// register movement greater than pointerMoveTolerance
if (this.pointerIsDown && !this.pointerWasMoved) {
dx = this.curCoords.client.x - this.startCoords.client.x;
dy = this.curCoords.client.y - this.startCoords.client.y;
this.pointerWasMoved = utils.hypot(dx, dy) > Interaction.pointerMoveTolerance;
}
const signalArg = {
pointer,
pointerIndex: this.getPointerIndex(pointer),
event,
eventTarget,
dx,
dy,
duplicate: duplicateMove,
interaction: this,
interactingBeforeMove: this.interacting(),
};
if (!duplicateMove) {
// set pointer coordinate, time changes and speeds
utils.setCoordDeltas(this.pointerDelta, this.prevCoords, this.curCoords);
}
signals.fire('move', signalArg);
if (!duplicateMove) {
// if interacting, fire an 'action-move' signal etc
if (this.interacting()) {
this.doMove(signalArg);
}
if (this.pointerWasMoved) {
utils.copyCoords(this.prevCoords, this.curCoords);
}
}
}
/**
* ```js
* interact(target)
* .draggable(true)
* .on('dragmove', function (event) {
* if (someCondition) {
* // change the snap settings
* event.interactable.draggable({ snap: { targets: [] }});
* // fire another move event with re-calculated snap
* event.interaction.doMove();
* }
* });
* ```
*
* Force a move of the current action at the same coordinates. Useful if
* snap/restrict has been changed and you want a movement with the new
* settings.
*/
doMove (signalArg) {
signalArg = utils.extend({
pointer: this.pointers[0],
event: this.prevEvent,
eventTarget: this._eventTarget,
interaction: this,
}, signalArg || {});
signals.fire('before-action-move', signalArg);
if (!this._dontFireMove) {
signals.fire('action-move', signalArg);
}
this._dontFireMove = false;
}
// End interact move events and stop auto-scroll unless simulation is running
pointerUp (pointer, event, eventTarget, curEventTarget) {
const pointerIndex = this.getPointerIndex(pointer);
signals.fire(/cancel$/i.test(event.type)? 'cancel' : 'up', {
pointer,
pointerIndex,
event,
eventTarget,
curEventTarget,
interaction: this,
});
if (!this.simulation) {
this.end(event);
}
this.pointerIsDown = false;
this.removePointer(pointer, event);
}
/**
* ```js
* interact(target)
* .draggable(true)
* .on('move', function (event) {
* if (event.pageX > 1000) {
* // end the current action
* event.interaction.end();
* // stop all further listeners from being called
* event.stopImmediatePropagation();
* }
* });
* ```
*
* Stop the current action and fire an end event. Inertial movement does
* not happen.
*
* @param {PointerEvent} [event]
*/
end (event) {
this._ending = true;
event = event || this.prevEvent;
if (this.interacting()) {
signals.fire('action-end', {
event,
interaction: this,
});
}
this.stop();
this._ending = false;
}
currentAction () {
return this._interacting? this.prepared.name: null;
}
interacting () {
return this._interacting;
}
/** */
stop () {
signals.fire('stop', { interaction: this });
if (this._interacting) {
signals.fire('stop-active', { interaction: this });
signals.fire('stop-' + this.prepared.name, { interaction: this });
}
this.target = this.element = null;
this._interacting = false;
this.prepared.name = this.prevEvent = null;
}
getPointerIndex (pointer) {
// mouse and pen interactions may have only one pointer
if (this.pointerType === 'mouse' || this.pointerType === 'pen') {
return 0;
}
return this.pointerIds.indexOf(utils.getPointerId(pointer));
}
updatePointer (pointer, event, down = event && /(down|start)$/i.test(event.type)) {
const id = utils.getPointerId(pointer);
let index = this.getPointerIndex(pointer);
if (index === -1) {
index = this.pointerIds.length;
this.pointerIds[index] = id;
}
if (down) {
signals.fire('update-pointer-down', {
pointer,
event,
down,
pointerId: id,
pointerIndex: index,
interaction: this,
});
}
this.pointers[index] = pointer;
return index;
}
removePointer (pointer, event) {
const index = this.getPointerIndex(pointer);
if (index === -1) { return; }
signals.fire('remove-pointer', {
pointer,
event,
pointerIndex: index,
interaction: this,
});
this.pointers .splice(index, 1);
this.pointerIds .splice(index, 1);
this.downTargets.splice(index, 1);
this.downTimes .splice(index, 1);
}
_updateEventTargets (target, currentTarget) {
this._eventTarget = target;
this._curEventTarget = currentTarget;
}
}
for (const method of methodNames) {
listeners[method] = doOnInteractions(method);
}
function doOnInteractions (method) {
return (function (event) {
const pointerType = utils.getPointerType(event);
const [eventTarget, curEventTarget] = utils.getEventTargets(event);
const matches = []; // [ [pointer, interaction], ...]
if (browser.supportsTouch && /touch/.test(event.type)) {
prevTouchTime = new Date().getTime();
for (const changedTouch of event.changedTouches) {
const pointer = changedTouch;
const interaction = finder.search(pointer, event.type, eventTarget);
matches.push([pointer, interaction || new Interaction({ pointerType })]);
}
}
else {
let invalidPointer = false;
if (!browser.supportsPointerEvent && /mouse/.test(event.type)) {
// ignore mouse events while touch interactions are active
for (let i = 0; i < scope.interactions.length && !invalidPointer; i++) {
invalidPointer = scope.interactions[i].pointerType !== 'mouse' && scope.interactions[i].pointerIsDown;
}
// try to ignore mouse events that are simulated by the browser
// after a touch event
invalidPointer = invalidPointer
|| (new Date().getTime() - prevTouchTime < 500)
// on iOS and Firefox Mobile, MouseEvent.timeStamp is zero if simulated
|| event.timeStamp === 0;
}
if (!invalidPointer) {
let interaction = finder.search(event, event.type, eventTarget);
if (!interaction) {
interaction = new Interaction({ pointerType });
}
matches.push([event, interaction]);
}
}
for (const [pointer, interaction] of matches) {
interaction._updateEventTargets(eventTarget, curEventTarget);
interaction[method](pointer, event, eventTarget, curEventTarget);
}
});
}
function endAll (event) {
for (const interaction of scope.interactions) {
interaction.end(event);
signals.fire('endall', { event, interaction });
}
}
const docEvents = { /* 'eventType': listenerFunc */ };
const pEventTypes = browser.pEventTypes;
if (domObjects.PointerEvent) {
docEvents[pEventTypes.down ] = listeners.pointerDown;
docEvents[pEventTypes.move ] = listeners.pointerMove;
docEvents[pEventTypes.up ] = listeners.pointerUp;
docEvents[pEventTypes.cancel] = listeners.pointerUp;
}
else {
docEvents.mousedown = listeners.pointerDown;
docEvents.mousemove = listeners.pointerMove;
docEvents.mouseup = listeners.pointerUp;
docEvents.touchstart = listeners.pointerDown;
docEvents.touchmove = listeners.pointerMove;
docEvents.touchend = listeners.pointerUp;
docEvents.touchcancel = listeners.pointerUp;
}
docEvents.blur = endAll;
function onDocSignal ({ doc }, signalName) {
const eventMethod = signalName.indexOf('add') === 0
? events.add : events.remove;
// delegate event listener
for (const eventType in scope.delegatedEvents) {
eventMethod(doc, eventType, events.delegateListener);
eventMethod(doc, eventType, events.delegateUseCapture, true);
}
for (const eventType in docEvents) {
eventMethod(doc, eventType, docEvents[eventType], browser.isIOS ? { passive: false } : undefined);
}
}
signals.on('update-pointer-down', ({ interaction, pointer, pointerId, pointerIndex, event, eventTarget, down }) => {
interaction.pointerIds[pointerIndex] = pointerId;
interaction.pointers[pointerIndex] = pointer;
if (down) {
interaction.pointerIsDown = true;
}
if (!interaction.interacting()) {
utils.setCoords(interaction.startCoords, interaction.pointers);
utils.copyCoords(interaction.curCoords , interaction.startCoords);
utils.copyCoords(interaction.prevCoords, interaction.startCoords);
interaction.downEvent = event;
interaction.downTimes[pointerIndex] = interaction.curCoords.timeStamp;
interaction.downTargets[pointerIndex] = eventTarget || event && utils.getEventTargets(event)[0];
interaction.pointerWasMoved = false;
utils.pointerExtend(interaction.downPointer, pointer);
}
});
scope.signals.on('add-document' , onDocSignal);
scope.signals.on('remove-document', onDocSignal);
Interaction.pointerMoveTolerance = 1;
Interaction.doOnInteractions = doOnInteractions;
Interaction.endAll = endAll;
Interaction.signals = signals;
Interaction.docEvents = docEvents;
scope.endAllInteractions = endAll;
module.exports = Interaction;