interactjs
Version:
Drag and drop, resizing and multi-touch gestures with inertia and snapping for modern browsers (and also IE8+)
232 lines (189 loc) • 6.31 kB
JavaScript
const PointerEvent = require('./PointerEvent');
const Interaction = require('../Interaction');
const utils = require('../utils');
const browser = require('../utils/browser');
const defaults = require('../defaultOptions');
const signals = require('../utils/Signals').new();
const { filter } = require('../utils/arr');
const simpleSignals = [ 'down', 'up', 'cancel' ];
const simpleEvents = [ 'down', 'up', 'cancel' ];
const pointerEvents = {
PointerEvent,
fire,
collectEventTargets,
signals,
defaults: {
holdDuration: 600,
ignoreFrom : null,
allowFrom : null,
origin : { x: 0, y: 0 },
},
types: [
'down',
'move',
'up',
'cancel',
'tap',
'doubletap',
'hold',
],
};
function fire (arg) {
const {
interaction, pointer, event, eventTarget,
type = arg.pointerEvent.type,
targets = collectEventTargets(arg),
pointerEvent = new PointerEvent(type, pointer, event, eventTarget, interaction),
} = arg;
const signalArg = {
interaction,
pointer,
event,
eventTarget,
targets,
type,
pointerEvent,
};
for (let i = 0; i < targets.length; i++) {
const target = targets[i];
for (const prop in target.props || {}) {
pointerEvent[prop] = target.props[prop];
}
const origin = utils.getOriginXY(target.eventable, target.element);
pointerEvent.subtractOrigin(origin);
pointerEvent.eventable = target.eventable;
pointerEvent.currentTarget = target.element;
target.eventable.fire(pointerEvent);
pointerEvent.addOrigin(origin);
if (pointerEvent.immediatePropagationStopped
|| (pointerEvent.propagationStopped
&& (i + 1) < targets.length && targets[i + 1].element !== pointerEvent.currentTarget)) {
break;
}
}
signals.fire('fired', signalArg);
if (type === 'tap') {
// if pointerEvent should make a double tap, create and fire a doubletap
// PointerEvent and use that as the prevTap
const prevTap = pointerEvent.double
? fire({
interaction, pointer, event, eventTarget,
type: 'doubletap',
})
: pointerEvent;
interaction.prevTap = prevTap;
interaction.tapTime = prevTap.timeStamp;
}
return pointerEvent;
}
function collectEventTargets ({ interaction, pointer, event, eventTarget, type }) {
const pointerIndex = interaction.getPointerIndex(pointer);
// do not fire a tap event if the pointer was moved before being lifted
if (type === 'tap' && (interaction.pointerWasMoved
// or if the pointerup target is different to the pointerdown target
|| !(interaction.downTargets[pointerIndex] && interaction.downTargets[pointerIndex] === eventTarget))) {
return [];
}
const path = utils.getPath(eventTarget);
const signalArg = {
interaction,
pointer,
event,
eventTarget,
type,
path,
targets: [],
element: null,
};
for (const element of path) {
signalArg.element = element;
signals.fire('collect-targets', signalArg);
}
if (type === 'hold') {
signalArg.targets = filter(signalArg.targets, target =>
target.eventable.options.holdDuration === interaction.holdTimers[pointerIndex].duration);
}
return signalArg.targets;
}
Interaction.signals.on('update-pointer-down', function ({ interaction, pointerIndex }) {
interaction.holdTimers[pointerIndex] = { duration: Infinity, timeout: null };
});
Interaction.signals.on('remove-pointer', function ({ interaction, pointerIndex }) {
interaction.holdTimers.splice(pointerIndex, 1);
});
Interaction.signals.on('move', function ({ interaction, pointer, event, eventTarget, duplicateMove }) {
const pointerIndex = interaction.getPointerIndex(pointer);
if (!duplicateMove && (!interaction.pointerIsDown || interaction.pointerWasMoved)) {
if (interaction.pointerIsDown) {
clearTimeout(interaction.holdTimers[pointerIndex].timeout);
}
fire({
interaction, pointer, event, eventTarget,
type: 'move',
});
}
});
Interaction.signals.on('down', function ({ interaction, pointer, event, eventTarget, pointerIndex }) {
// copy event to be used in timeout for IE8
const eventCopy = browser.isIE8? utils.extend({}, event) : event;
const timer = interaction.holdTimers[pointerIndex];
const path = utils.getPath(eventTarget);
const signalArg = {
interaction,
pointer,
event,
eventTarget,
type: 'hold',
targets: [],
path,
element: null,
};
for (const element of path) {
signalArg.element = element;
signals.fire('collect-targets', signalArg);
}
if (!signalArg.targets.length) { return; }
let minDuration = Infinity;
for (let i = 0; i < signalArg.targets.length; i++) {
const target = signalArg.targets[i];
const holdDuration = target.eventable.options.holdDuration;
if (holdDuration < minDuration) {
minDuration = holdDuration;
}
}
timer.duration = minDuration;
timer.timeout = setTimeout(function () {
fire({
interaction, eventCopy, eventTarget,
pointer: browser.isIE8? eventCopy : pointer,
type: 'hold',
});
}, minDuration);
});
Interaction.signals.on('up', ({ interaction, pointer, event, eventTarget }) => {
if (!interaction.pointerWasMoved) {
fire({ interaction, eventTarget, pointer, event, type: 'tap' });
}
});
['up', 'cancel'].forEach(function (signalName) {
Interaction.signals.on(signalName, function ({ interaction, pointerIndex }) {
if (interaction.holdTimers[pointerIndex]) {
clearTimeout(interaction.holdTimers[pointerIndex].timeout);
}
});
});
function createSignalListener (type) {
return function ({ interaction, pointer, event, eventTarget }) {
fire({ interaction, eventTarget, pointer, event, type });
};
}
for (let i = 0; i < simpleSignals.length; i++) {
Interaction.signals.on(simpleSignals[i], createSignalListener(simpleEvents[i]));
}
Interaction.signals.on('new', function (interaction) {
interaction.prevTap = null; // the most recent tap event on this interaction
interaction.tapTime = 0; // time of the most recent tap event
interaction.holdTimers = []; // [{ duration, timeout }]
});
defaults.pointerEvents = pointerEvents.defaults;
module.exports = pointerEvents;