278 lines
7.6 KiB
JavaScript
278 lines
7.6 KiB
JavaScript
import { Scope, SignalArgs } from "./scope.js";
|
|
import browser from "../utils/browser.js";
|
|
import domObjects from "../utils/domObjects.js";
|
|
import { nodeContains } from "../utils/domUtils.js";
|
|
import * as pointerUtils from "../utils/pointerUtils.js";
|
|
import InteractionBase from "./Interaction.js";
|
|
import interactablePreventDefault from "./interactablePreventDefault.js";
|
|
import finder from "./interactionFinder.js";
|
|
const methodNames = ['pointerDown', 'pointerMove', 'pointerUp', 'updatePointer', 'removePointer', 'windowBlur'];
|
|
|
|
function install(scope) {
|
|
const listeners = {};
|
|
|
|
for (const method of methodNames) {
|
|
listeners[method] = doOnInteractions(method, scope);
|
|
}
|
|
|
|
const pEventTypes = browser.pEventTypes;
|
|
let docEvents;
|
|
|
|
if (domObjects.PointerEvent) {
|
|
docEvents = [{
|
|
type: pEventTypes.down,
|
|
listener: releasePointersOnRemovedEls
|
|
}, {
|
|
type: pEventTypes.down,
|
|
listener: listeners.pointerDown
|
|
}, {
|
|
type: pEventTypes.move,
|
|
listener: listeners.pointerMove
|
|
}, {
|
|
type: pEventTypes.up,
|
|
listener: listeners.pointerUp
|
|
}, {
|
|
type: pEventTypes.cancel,
|
|
listener: listeners.pointerUp
|
|
}];
|
|
} else {
|
|
docEvents = [{
|
|
type: 'mousedown',
|
|
listener: listeners.pointerDown
|
|
}, {
|
|
type: 'mousemove',
|
|
listener: listeners.pointerMove
|
|
}, {
|
|
type: 'mouseup',
|
|
listener: listeners.pointerUp
|
|
}, {
|
|
type: 'touchstart',
|
|
listener: releasePointersOnRemovedEls
|
|
}, {
|
|
type: 'touchstart',
|
|
listener: listeners.pointerDown
|
|
}, {
|
|
type: 'touchmove',
|
|
listener: listeners.pointerMove
|
|
}, {
|
|
type: 'touchend',
|
|
listener: listeners.pointerUp
|
|
}, {
|
|
type: 'touchcancel',
|
|
listener: listeners.pointerUp
|
|
}];
|
|
}
|
|
|
|
docEvents.push({
|
|
type: 'blur',
|
|
|
|
listener(event) {
|
|
for (const interaction of scope.interactions.list) {
|
|
interaction.documentBlur(event);
|
|
}
|
|
}
|
|
|
|
}); // for ignoring browser's simulated mouse events
|
|
|
|
scope.prevTouchTime = 0;
|
|
scope.Interaction = class extends InteractionBase {
|
|
get pointerMoveTolerance() {
|
|
return scope.interactions.pointerMoveTolerance;
|
|
}
|
|
|
|
set pointerMoveTolerance(value) {
|
|
scope.interactions.pointerMoveTolerance = value;
|
|
}
|
|
|
|
_now() {
|
|
return scope.now();
|
|
}
|
|
|
|
};
|
|
scope.interactions = {
|
|
// all active and idle interactions
|
|
list: [],
|
|
|
|
new(options) {
|
|
options.scopeFire = (name, arg) => scope.fire(name, arg);
|
|
|
|
const interaction = new scope.Interaction(options);
|
|
scope.interactions.list.push(interaction);
|
|
return interaction;
|
|
},
|
|
|
|
listeners,
|
|
docEvents,
|
|
pointerMoveTolerance: 1
|
|
};
|
|
|
|
function releasePointersOnRemovedEls() {
|
|
// for all inactive touch interactions with pointers down
|
|
for (const interaction of scope.interactions.list) {
|
|
if (!interaction.pointerIsDown || interaction.pointerType !== 'touch' || interaction._interacting) {
|
|
continue;
|
|
} // if a pointer is down on an element that is no longer in the DOM tree
|
|
|
|
|
|
for (const pointer of interaction.pointers) {
|
|
if (!scope.documents.some(({
|
|
doc
|
|
}) => nodeContains(doc, pointer.downTarget))) {
|
|
// remove the pointer from the interaction
|
|
interaction.removePointer(pointer.pointer, pointer.event);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
scope.usePlugin(interactablePreventDefault);
|
|
}
|
|
|
|
function doOnInteractions(method, scope) {
|
|
return function (event) {
|
|
const interactions = scope.interactions.list;
|
|
const pointerType = pointerUtils.getPointerType(event);
|
|
const [eventTarget, curEventTarget] = pointerUtils.getEventTargets(event);
|
|
const matches = []; // [ [pointer, interaction], ...]
|
|
|
|
if (/^touch/.test(event.type)) {
|
|
scope.prevTouchTime = scope.now(); // @ts-expect-error
|
|
|
|
for (const changedTouch of event.changedTouches) {
|
|
const pointer = changedTouch;
|
|
const pointerId = pointerUtils.getPointerId(pointer);
|
|
const searchDetails = {
|
|
pointer,
|
|
pointerId,
|
|
pointerType,
|
|
eventType: event.type,
|
|
eventTarget,
|
|
curEventTarget,
|
|
scope
|
|
};
|
|
const interaction = getInteraction(searchDetails);
|
|
matches.push([searchDetails.pointer, searchDetails.eventTarget, searchDetails.curEventTarget, interaction]);
|
|
}
|
|
} else {
|
|
let invalidPointer = false;
|
|
|
|
if (!browser.supportsPointerEvent && /mouse/.test(event.type)) {
|
|
// ignore mouse events while touch interactions are active
|
|
for (let i = 0; i < interactions.length && !invalidPointer; i++) {
|
|
invalidPointer = interactions[i].pointerType !== 'mouse' && interactions[i].pointerIsDown;
|
|
} // try to ignore mouse events that are simulated by the browser
|
|
// after a touch event
|
|
|
|
|
|
invalidPointer = invalidPointer || scope.now() - scope.prevTouchTime < 500 || // on iOS and Firefox Mobile, MouseEvent.timeStamp is zero if simulated
|
|
event.timeStamp === 0;
|
|
}
|
|
|
|
if (!invalidPointer) {
|
|
const searchDetails = {
|
|
pointer: event,
|
|
pointerId: pointerUtils.getPointerId(event),
|
|
pointerType,
|
|
eventType: event.type,
|
|
curEventTarget,
|
|
eventTarget,
|
|
scope
|
|
};
|
|
const interaction = getInteraction(searchDetails);
|
|
matches.push([searchDetails.pointer, searchDetails.eventTarget, searchDetails.curEventTarget, interaction]);
|
|
}
|
|
} // eslint-disable-next-line no-shadow
|
|
|
|
|
|
for (const [pointer, eventTarget, curEventTarget, interaction] of matches) {
|
|
interaction[method](pointer, event, eventTarget, curEventTarget);
|
|
}
|
|
};
|
|
}
|
|
|
|
function getInteraction(searchDetails) {
|
|
const {
|
|
pointerType,
|
|
scope
|
|
} = searchDetails;
|
|
const foundInteraction = finder.search(searchDetails);
|
|
const signalArg = {
|
|
interaction: foundInteraction,
|
|
searchDetails
|
|
};
|
|
scope.fire('interactions:find', signalArg);
|
|
return signalArg.interaction || scope.interactions.new({
|
|
pointerType
|
|
});
|
|
}
|
|
|
|
function onDocSignal({
|
|
doc,
|
|
scope,
|
|
options
|
|
}, eventMethodName) {
|
|
const {
|
|
interactions: {
|
|
docEvents
|
|
},
|
|
events
|
|
} = scope;
|
|
const eventMethod = events[eventMethodName];
|
|
|
|
if (scope.browser.isIOS && !options.events) {
|
|
options.events = {
|
|
passive: false
|
|
};
|
|
} // delegate event listener
|
|
|
|
|
|
for (const eventType in events.delegatedEvents) {
|
|
eventMethod(doc, eventType, events.delegateListener);
|
|
eventMethod(doc, eventType, events.delegateUseCapture, true);
|
|
}
|
|
|
|
const eventOptions = options && options.events;
|
|
|
|
for (const {
|
|
type,
|
|
listener
|
|
} of docEvents) {
|
|
eventMethod(doc, type, listener, eventOptions);
|
|
}
|
|
}
|
|
|
|
const interactions = {
|
|
id: 'core/interactions',
|
|
install,
|
|
listeners: {
|
|
'scope:add-document': arg => onDocSignal(arg, 'add'),
|
|
'scope:remove-document': arg => onDocSignal(arg, 'remove'),
|
|
'interactable:unset': ({
|
|
interactable
|
|
}, scope) => {
|
|
// Stop and destroy related interactions when an Interactable is unset
|
|
for (let i = scope.interactions.list.length - 1; i >= 0; i--) {
|
|
const interaction = scope.interactions.list[i];
|
|
|
|
if (interaction.interactable !== interactable) {
|
|
continue;
|
|
}
|
|
|
|
interaction.stop();
|
|
scope.fire('interactions:destroy', {
|
|
interaction
|
|
});
|
|
interaction.destroy();
|
|
|
|
if (scope.interactions.list.length > 2) {
|
|
scope.interactions.list.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
onDocSignal,
|
|
doOnInteractions,
|
|
methodNames
|
|
};
|
|
export default interactions;
|
|
//# sourceMappingURL=interactions.js.map
|