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