UNPKG

joicomponents

Version:

Design patterns to build native web components

182 lines (168 loc) 7.72 kB
const startListener = Symbol("touchStartListener"); const moveListener = Symbol("touchMoveListener"); const endListener = Symbol("touchEndListener"); const start = Symbol("touchStart"); const move = Symbol("touchMove"); const end = Symbol("touchEnd"); const spin = Symbol("spin"); const recordedEventDetails = Symbol("recordedEventDetails"); const cachedTouchAction = Symbol("cachedTouchAction"); const oneHit = Symbol("firstTouchIsAHit"); function calcAngle(x, y) { return ((Math.atan2(y, -x) * 180 / Math.PI) + 270) % 360; } function findLastEventOlderThan(events, timeTest) { for (let i = events.length - 1; i >= 0; i--) { if (events[i].touchevent.timeStamp < timeTest) return events[i]; } return null; } function makeDetail(touchevent) { const f1 = touchevent.targetTouches[0]; const f2 = touchevent.targetTouches[1]; const x1 = f1.pageX; const y1 = f1.pageY; const x2 = f2.pageX; const y2 = f2.pageY; const width = Math.abs(x2 - x1); const height = Math.abs(y2 - y1); const diagonal = Math.sqrt(width * width + height * height); const angle = calcAngle(x1 - x2, y1 - y2); return {touchevent, x1, y1, x2, y2, diagonal, width, height, angle}; } /** * PinchGesture mixin. * PinchGesture records a sequence of two-finger touchstart, touchmove * and touchend events into a series of pinch and spin events. * The PinchGesture mixin only reacts when only two fingers are used. * If three fingers touches the screen, the EventRecording is cancelled. * * PinchGesture can be used for two finger gestures such as: * - pinch * - expand * - rotate * - two-finger drag * * If both the two fingers are removed from the screen while in motion, * a spin-event is triggered. The spin event resembles the fling event. * * PinchGesture has the following *optional* reactive callback methods: * - pinchstartCallback({touchevent, x1, y1, x2, y2, diagonal, width, height, angle}) * - pinchCallback({touchevent, x1, y1, x2, y2, diagonal, width, height, angle}) * - pinchendCallback({touchevent, x1, y1, x2, y2, diagonal, width, height, angle}) [1] * - spinCallback({touchevent, diagonal, width, height, angle, duration}) * * PinchGesture has the following StaticSettings: * - pinchEvent: true => mixin will also dispatch the following events * - pinchstart: {touchevent, x1, y1, x2, y2, diagonal, width, height, angle} * - pinch: {touchevent, x1, y1, x2, y2, diagonal, width, height, angle} * - pinchend: {touchevent, x1, y1, x2, y2, diagonal, width, height, angle} [1] * - spin: {touchevent, diagonal, width, height, angle, duration} * * [1] pinchend coordinates are copied from the last successful pinch. * * `PinchGesture` implement an extensive [InvadeAndRetreat!] strategy * to block default actions in the browsers such as "pinch-to-zoom". * * @param Base * @returns {PinchGesture} */ export const PinchGesture = function (Base) { return class extends Base { constructor() { super(); this[recordedEventDetails] = undefined; this[cachedTouchAction] = undefined; //block touchAction this[oneHit] = false; this[startListener] = (e) => this[start](e); this[moveListener] = (e) => this[move](e); this[endListener] = (e) => this[end](e); } /** * By default it is only callback, if this staticSetting is true, dispatch event too * @returns {boolean} true => di */ // static get pinchEvent() { // return false; // } static get spinSettings() { return {spinMotion: 50, spinDuration: 100}; } connectedCallback() { if (super.connectedCallback) super.connectedCallback(); this.style.touchAction = "none"; //block touchAction this.addEventListener("touchstart", this[startListener]); } disconnectedCallback() { if (super.disconnectedCallback) super.disconnectedCallback(); this.removeEventListener("touchstart", this[startListener]); } [start](e) { const length = e.targetTouches.length; if (length > 2) return this[end](e); if (length === 1) { this[oneHit] = true; return; } if (length !== 2) throw new Error("omg?! how many fingers??"); if (!this[oneHit]) //first finger was not pressed on the element, so this second touch is part of something bigger. return; e.preventDefault(); //block defaultAction const body = document.querySelector("body"); //block touchAction this[cachedTouchAction] = body.style.touchAction; //block touchAction body.style.touchAction = "none"; //block touchAction window.addEventListener("touchmove", this[moveListener]); window.addEventListener("touchend", this[endListener]); window.addEventListener("touchcancel", this[endListener]); const detail = makeDetail(e); this[recordedEventDetails] = [detail]; this.pinchstartCallback && this.pinchstartCallback(detail); this.constructor.pinchEvent && this.dispatchEvent(new CustomEvent("pinchstart", {bubbles: true, detail})); } [move](e) { e.preventDefault(); const detail = makeDetail(e); //block defaultAction this[recordedEventDetails].push(detail); this.pinchCallback && this.pinchCallback(detail); this.constructor.pinchEvent && this.dispatchEvent(new CustomEvent("pinch", {bubbles: true, detail})); } [end](e) { e.preventDefault(); //block defaultAction window.removeEventListener("touchmove", this[moveListener]); window.removeEventListener("touchend", this[endListener]); window.removeEventListener("touchcancel", this[endListener]); this[oneHit] = false; const body = document.querySelector("body"); //retreat touchAction body.style.touchAction = this[cachedTouchAction]; //retreat touchAction this[cachedTouchAction] = undefined; //retreat touchAction const detail = Object.assign({}, this[recordedEventDetails][this[recordedEventDetails].length - 1]); detail.touchevent = e; this[spin](e); this[recordedEventDetails] = undefined; this.pinchendCallback && this.pinchendCallback(detail); this.constructor.pinchEvent && this.dispatchEvent(new CustomEvent("pinchend", {bubbles: true, detail})); } [spin](event) { const settings = this.constructor.spinSettings; const spinTime = event.timeStamp - settings.spinDuration; const spinStart = findLastEventOlderThan(this[recordedEventDetails], spinTime); if (!spinStart) return; const detail = Object.assign({}, this[recordedEventDetails][this[recordedEventDetails].length - 1]); detail.touchevent = event; detail.duration = settings.spinDuration; detail.xFactor = Math.abs(spinStart.width / detail.width); detail.yFactor = Math.abs(spinStart.height / detail.height); detail.diagonalFactor = Math.abs(spinStart.diagonal / detail.diagonal); detail.rotation = Math.abs(spinStart.angle - detail.angle); let lastspinMotion = Math.abs(detail.x1 - spinStart.x1) + (detail.y1 - spinStart.y1); //the sum of the distance of the start and end positions of finger 1 and 2 if (lastspinMotion < settings.spinMotion) return; this.spinCallback && this.spinCallback(detail); this.constructor.pinchEvent && this.dispatchEvent(new CustomEvent("spin", {bubbles: true, detail})); } } };