UNPKG

@ionic/core

Version:
586 lines (579 loc) • 17.1 kB
import { h } from '../ionic.core.js'; class GestureController { constructor(doc) { this.doc = doc; this.gestureId = 0; this.requestedStart = new Map(); this.disabledGestures = new Map(); this.disabledScroll = new Set(); } createGesture(config) { return new GestureDelegate(this, this.newID(), config.name, config.priority || 0, !!config.disableScroll); } createBlocker(opts = {}) { return new BlockerDelegate(this, this.newID(), opts.disable, !!opts.disableScroll); } start(gestureName, id, priority) { if (!this.canStart(gestureName)) { this.requestedStart.delete(id); return false; } this.requestedStart.set(id, priority); return true; } capture(gestureName, id, priority) { if (!this.start(gestureName, id, priority)) { return false; } const requestedStart = this.requestedStart; let maxPriority = -10000; requestedStart.forEach(value => { maxPriority = Math.max(maxPriority, value); }); if (maxPriority === priority) { this.capturedId = id; requestedStart.clear(); const event = new CustomEvent('ionGestureCaptured', { detail: { gestureName } }); this.doc.dispatchEvent(event); return true; } requestedStart.delete(id); return false; } release(id) { this.requestedStart.delete(id); if (this.capturedId === id) { this.capturedId = undefined; } } disableGesture(gestureName, id) { let set = this.disabledGestures.get(gestureName); if (set === undefined) { set = new Set(); this.disabledGestures.set(gestureName, set); } set.add(id); } enableGesture(gestureName, id) { const set = this.disabledGestures.get(gestureName); if (set !== undefined) { set.delete(id); } } disableScroll(id) { this.disabledScroll.add(id); if (this.disabledScroll.size === 1) { this.doc.body.classList.add(BACKDROP_NO_SCROLL); } } enableScroll(id) { this.disabledScroll.delete(id); if (this.disabledScroll.size === 0) { this.doc.body.classList.remove(BACKDROP_NO_SCROLL); } } canStart(gestureName) { if (this.capturedId !== undefined) { return false; } if (this.isDisabled(gestureName)) { return false; } return true; } isCaptured() { return this.capturedId !== undefined; } isScrollDisabled() { return this.disabledScroll.size > 0; } isDisabled(gestureName) { const disabled = this.disabledGestures.get(gestureName); if (disabled && disabled.size > 0) { return true; } return false; } newID() { this.gestureId++; return this.gestureId; } } class GestureDelegate { constructor(ctrl, id, name, priority, disableScroll) { this.id = id; this.name = name; this.disableScroll = disableScroll; this.priority = priority * 1000000 + id; this.ctrl = ctrl; } canStart() { if (!this.ctrl) { return false; } return this.ctrl.canStart(this.name); } start() { if (!this.ctrl) { return false; } return this.ctrl.start(this.name, this.id, this.priority); } capture() { if (!this.ctrl) { return false; } const captured = this.ctrl.capture(this.name, this.id, this.priority); if (captured && this.disableScroll) { this.ctrl.disableScroll(this.id); } return captured; } release() { if (this.ctrl) { this.ctrl.release(this.id); if (this.disableScroll) { this.ctrl.enableScroll(this.id); } } } destroy() { this.release(); this.ctrl = undefined; } } class BlockerDelegate { constructor(ctrl, id, disable, disableScroll) { this.id = id; this.disable = disable; this.disableScroll = disableScroll; this.ctrl = ctrl; } block() { if (!this.ctrl) { return; } if (this.disable) { for (const gesture of this.disable) { this.ctrl.disableGesture(gesture, this.id); } } if (this.disableScroll) { this.ctrl.disableScroll(this.id); } } unblock() { if (!this.ctrl) { return; } if (this.disable) { for (const gesture of this.disable) { this.ctrl.enableGesture(gesture, this.id); } } if (this.disableScroll) { this.ctrl.enableScroll(this.id); } } destroy() { this.unblock(); this.ctrl = undefined; } } const BACKDROP_NO_SCROLL = 'backdrop-no-scroll'; const GESTURE_CONTROLLER = new GestureController(document); function addEventListener(el, eventName, callback, opts) { const listenerOpts = supportsPassive(el) ? { 'capture': !!opts.capture, 'passive': !!opts.passive, } : !!opts.capture; let add; let remove; if (el['__zone_symbol__addEventListener']) { add = '__zone_symbol__addEventListener'; remove = '__zone_symbol__removeEventListener'; } else { add = 'addEventListener'; remove = 'removeEventListener'; } el[add](eventName, callback, listenerOpts); return () => { el[remove](eventName, callback, listenerOpts); }; } let _sPassive; function supportsPassive(node) { if (_sPassive === undefined) { try { const opts = Object.defineProperty({}, 'passive', { get: () => { _sPassive = true; } }); node.addEventListener('optsTest', () => { return; }, opts); } catch (e) { _sPassive = false; } } return !!_sPassive; } const MOUSE_WAIT = 2000; function createPointerEvents(el, pointerDown, pointerMove, pointerUp, options) { let rmTouchStart; let rmTouchMove; let rmTouchEnd; let rmTouchCancel; let rmMouseStart; let rmMouseMove; let rmMouseUp; let lastTouchEvent = 0; function handleTouchStart(ev) { lastTouchEvent = Date.now() + MOUSE_WAIT; if (!pointerDown(ev)) { return; } if (!rmTouchMove && pointerMove) { rmTouchMove = addEventListener(el, 'touchmove', pointerMove, options); } if (!rmTouchEnd) { rmTouchEnd = addEventListener(el, 'touchend', handleTouchEnd, options); } if (!rmTouchCancel) { rmTouchCancel = addEventListener(el, 'touchcancel', handleTouchEnd, options); } } function handleMouseDown(ev) { if (lastTouchEvent > Date.now()) { console.debug('mousedown event dropped because of previous touch'); return; } if (!pointerDown(ev)) { return; } if (!rmMouseMove && pointerMove) { rmMouseMove = addEventListener(getDocument(el), 'mousemove', pointerMove, options); } if (!rmMouseUp) { rmMouseUp = addEventListener(getDocument(el), 'mouseup', handleMouseUp, options); } } function handleTouchEnd(ev) { stopTouch(); if (pointerUp) { pointerUp(ev); } } function handleMouseUp(ev) { stopMouse(); if (pointerUp) { pointerUp(ev); } } function stopTouch() { if (rmTouchMove) { rmTouchMove(); } if (rmTouchEnd) { rmTouchEnd(); } if (rmTouchCancel) { rmTouchCancel(); } rmTouchMove = rmTouchEnd = rmTouchCancel = undefined; } function stopMouse() { if (rmMouseMove) { rmMouseMove(); } if (rmMouseUp) { rmMouseUp(); } rmMouseMove = rmMouseUp = undefined; } function stop() { stopTouch(); stopMouse(); } function setDisabled(disabled) { if (disabled) { if (rmTouchStart) { rmTouchStart(); } if (rmMouseStart) { rmMouseStart(); } rmTouchStart = rmMouseStart = undefined; stop(); } else { if (!rmTouchStart) { rmTouchStart = addEventListener(el, 'touchstart', handleTouchStart, options); } if (!rmMouseStart) { rmMouseStart = addEventListener(el, 'mousedown', handleMouseDown, options); } } } function destroy() { setDisabled(true); pointerUp = pointerMove = pointerDown = undefined; } return { setDisabled, stop, destroy }; } function getDocument(node) { return node instanceof Document ? node : node.ownerDocument; } function createPanRecognizer(direction, thresh, maxAngle) { const radians = maxAngle * (Math.PI / 180); const isDirX = direction === 'x'; const maxCosine = Math.cos(radians); const threshold = thresh * thresh; let startX = 0; let startY = 0; let dirty = false; let isPan = 0; return { start(x, y) { startX = x; startY = y; isPan = 0; dirty = true; }, detect(x, y) { if (!dirty) { return false; } const deltaX = (x - startX); const deltaY = (y - startY); const distance = deltaX * deltaX + deltaY * deltaY; if (distance < threshold) { return false; } const hypotenuse = Math.sqrt(distance); const cosine = (isDirX ? deltaX : deltaY) / hypotenuse; if (cosine > maxCosine) { isPan = 1; } else if (cosine < -maxCosine) { isPan = -1; } else { isPan = 0; } dirty = false; return true; }, isGesture() { return isPan !== 0; }, getDirection() { return isPan; } }; } function createGesture(config) { const finalConfig = Object.assign({ disableScroll: false, direction: 'x', gesturePriority: 0, passive: true, maxAngle: 40, threshold: 10 }, config); const canStart = finalConfig.canStart; const onWillStart = finalConfig.onWillStart; const onStart = finalConfig.onStart; const onEnd = finalConfig.onEnd; const notCaptured = finalConfig.notCaptured; const onMove = finalConfig.onMove; const threshold = finalConfig.threshold; const queue = finalConfig.queue; const detail = { type: 'pan', startX: 0, startY: 0, startTimeStamp: 0, currentX: 0, currentY: 0, velocityX: 0, velocityY: 0, deltaX: 0, deltaY: 0, timeStamp: 0, event: undefined, data: undefined }; const pointerEvents = createPointerEvents(finalConfig.el, pointerDown, pointerMove, pointerUp, { capture: false, }); const pan = createPanRecognizer(finalConfig.direction, finalConfig.threshold, finalConfig.maxAngle); const gesture = GESTURE_CONTROLLER.createGesture({ name: config.gestureName, priority: config.gesturePriority, disableScroll: config.disableScroll }); let hasCapturedPan = false; let hasStartedPan = false; let hasFiredStart = true; let isMoveQueued = false; function pointerDown(ev) { const timeStamp = now(ev); if (hasStartedPan || !hasFiredStart) { return false; } updateDetail(ev, detail); detail.startX = detail.currentX; detail.startY = detail.currentY; detail.startTimeStamp = detail.timeStamp = timeStamp; detail.velocityX = detail.velocityY = detail.deltaX = detail.deltaY = 0; detail.event = ev; if (canStart && canStart(detail) === false) { return false; } gesture.release(); if (!gesture.start()) { return false; } hasStartedPan = true; if (threshold === 0) { return tryToCapturePan(); } pan.start(detail.startX, detail.startY); return true; } function pointerMove(ev) { if (hasCapturedPan) { if (!isMoveQueued && hasFiredStart) { isMoveQueued = true; calcGestureData(detail, ev); queue.write(fireOnMove); } return; } calcGestureData(detail, ev); if (pan.detect(detail.currentX, detail.currentY)) { if (!pan.isGesture() || !tryToCapturePan()) { abortGesture(); } } } function fireOnMove() { if (!hasCapturedPan) { return; } isMoveQueued = false; if (onMove) { onMove(detail); } } function tryToCapturePan() { if (gesture && !gesture.capture()) { return false; } hasCapturedPan = true; hasFiredStart = false; detail.startX = detail.currentX; detail.startY = detail.currentY; detail.startTimeStamp = detail.timeStamp; if (onWillStart) { onWillStart(detail).then(fireOnStart); } else { fireOnStart(); } return true; } function fireOnStart() { if (onStart) { onStart(detail); } hasFiredStart = true; } function abortGesture() { reset(); pointerEvents.stop(); if (notCaptured) { notCaptured(detail); } } function reset() { hasCapturedPan = false; hasStartedPan = false; isMoveQueued = false; hasFiredStart = true; gesture.release(); } function pointerUp(ev) { const tmpHasCaptured = hasCapturedPan; const tmpHasFiredStart = hasFiredStart; reset(); if (!tmpHasFiredStart) { return; } calcGestureData(detail, ev); if (tmpHasCaptured) { if (onEnd) { onEnd(detail); } return; } if (notCaptured) { notCaptured(detail); } } return { setDisabled(disabled) { if (disabled && hasCapturedPan) { pointerUp(undefined); } pointerEvents.setDisabled(disabled); }, destroy() { gesture.destroy(); pointerEvents.destroy(); } }; } function calcGestureData(detail, ev) { if (!ev) { return; } const prevX = detail.currentX; const prevY = detail.currentY; const prevT = detail.timeStamp; updateDetail(ev, detail); const currentX = detail.currentX; const currentY = detail.currentY; const timestamp = detail.timeStamp = now(ev); const timeDelta = timestamp - prevT; if (timeDelta > 0 && timeDelta < 100) { const velocityX = (currentX - prevX) / timeDelta; const velocityY = (currentY - prevY) / timeDelta; detail.velocityX = velocityX * 0.7 + detail.velocityX * 0.3; detail.velocityY = velocityY * 0.7 + detail.velocityY * 0.3; } detail.deltaX = currentX - detail.startX; detail.deltaY = currentY - detail.startY; detail.event = ev; } function updateDetail(ev, detail) { let x = 0; let y = 0; if (ev) { const changedTouches = ev.changedTouches; if (changedTouches && changedTouches.length > 0) { const touch = changedTouches[0]; x = touch.clientX; y = touch.clientY; } else if (ev.pageX !== undefined) { x = ev.pageX; y = ev.pageY; } } detail.currentX = x; detail.currentY = y; } function now(ev) { return ev.timeStamp || Date.now(); } export { createGesture, GESTURE_CONTROLLER };