class GestureRecognizer { constructor(target = null, delegate = null) { this._targetTouches = []; this.modifierKeys = { alt : false, ctrl : false, meta : false, shift : false }; this._state = GestureRecognizer.States.Possible; this._enabled = true; this.target = target; this.delegate = delegate; } // Public get state() { return this._state; } set state(state) { if (this._state === state && state !== GestureRecognizer.States.Changed) return; this._state = state; if (this.delegate && typeof this.delegate.gestureRecognizerStateDidChange === "function") this.delegate.gestureRecognizerStateDidChange(this); } get target() { return this._target; } set target(target) { if (!target || this._target === target) return; this._target = target; this._initRecognizer(); } get numberOfTouches() { return this._targetTouches.length; } get enabled() { return this._enabled; } set enabled(enabled) { if (this._enabled === enabled) return; this._enabled = enabled; if (!enabled) { if (this.numberOfTouches === 0) { this._removeTrackingListeners(); this.reset(); } else this.enterCancelledState(); } this._updateBaseListeners(); } reset() { // Implemented by subclasses. } locationInElement(element) { const p = new DOMPoint; const touches = this._targetTouches; const count = touches.length; for (let i = 0; i < count; ++i) { const touch = touches[i]; p.x += touch.pageX; p.y += touch.pageY; } p.x /= count; p.y /= count; if (!element) return p; // FIXME: are WebKitPoint and DOMPoint interchangeable? const wkPoint = window.webkitConvertPointFromPageToNode(element, new WebKitPoint(p.x, p.y)); return new DOMPoint(wkPoint.x, wkPoint.y); } locationInClient() { const p = new DOMPoint; const touches = this._targetTouches; const count = touches.length; for (let i = 0; i < count; ++i) { const touch = touches[i]; p.x += touch.clientX; p.y += touch.clientY; } p.x /= count; p.y /= count; return p; } locationOfTouchInElement(touchIndex, element) { const touch = this._targetTouches[touchIndex]; if (!touch) return new DOMPoint; const touchLocation = new DOMPoint(touch.pageX, touch.pageY); if (!element) return touchLocation; // FIXME: are WebKitPoint and DOMPoint interchangeable? const wkPoint = window.webkitConvertPointFromPageToNode(element, new WebKitPoint(touchLocation.x, touchLocation.y)); return new DOMPoint(wkPoint.x, wkPoint.y); } touchesBegan(event) { if (event.currentTarget !== this._target) return; window.addEventListener(GestureRecognizer.Events.TouchMove, this, true); window.addEventListener(GestureRecognizer.Events.TouchEnd, this, true); window.addEventListener(GestureRecognizer.Events.TouchCancel, this, true); this.enterPossibleState(); } touchesMoved(event) { // Implemented by subclasses. } touchesEnded(event) { // Implemented by subclasses. } touchesCancelled(event) { // Implemented by subclasses. } gestureBegan(event) { if (event.currentTarget !== this._target) return; window.addEventListener(GestureRecognizer.Events.GestureChange, this, true); window.addEventListener(GestureRecognizer.Events.GestureEnd, this, true); this.enterPossibleState(); } gestureChanged(event) { // Implemented by subclasses. } gestureEnded(event) { // Implemented by subclasses. } enterPossibleState() { this.state = GestureRecognizer.States.Possible; } enterBeganState() { if (this.delegate && typeof this.delegate.gestureRecognizerShouldBegin === "function" && !this.delegate.gestureRecognizerShouldBegin(this)) { this.enterFailedState(); return; } this.state = GestureRecognizer.States.Began; } enterEndedState() { this.state = GestureRecognizer.States.Ended; this._removeTrackingListeners(); this.reset(); } enterCancelledState() { this.state = GestureRecognizer.States.Cancelled; this._removeTrackingListeners(); this.reset(); } enterFailedState() { this.state = GestureRecognizer.States.Failed; this._removeTrackingListeners(); this.reset(); } enterChangedState() { this.state = GestureRecognizer.States.Changed; } enterRecognizedState() { this.state = GestureRecognizer.States.Recognized; } // Protected handleEvent(event) { this._updateTargetTouches(event); this._updateKeyboardModifiers(event); switch (event.type) { case GestureRecognizer.Events.TouchStart: this.touchesBegan(event); break; case GestureRecognizer.Events.TouchMove: this.touchesMoved(event); break; case GestureRecognizer.Events.TouchEnd: this.touchesEnded(event); break; case GestureRecognizer.Events.TouchCancel: this.touchesCancelled(event); break; case GestureRecognizer.Events.GestureStart: this.gestureBegan(event); break; case GestureRecognizer.Events.GestureChange: this.gestureChanged(event); break; case GestureRecognizer.Events.GestureEnd: this.gestureEnded(event); break; } } // Private _initRecognizer() { this.reset(); this.state = GestureRecognizer.States.Possible; this._updateBaseListeners(); } _updateBaseListeners() { if (!this._target) return; if (this._enabled) { this._target.addEventListener(GestureRecognizer.Events.TouchStart, this); if (GestureRecognizer.SupportsGestures) this._target.addEventListener(GestureRecognizer.Events.GestureStart, this); } else { this._target.removeEventListener(GestureRecognizer.Events.TouchStart, this); if (GestureRecognizer.SupportsGestures) this._target.removeEventListener(GestureRecognizer.Events.GestureStart, this); } } _removeTrackingListeners() { window.removeEventListener(GestureRecognizer.Events.TouchMove, this, true); window.removeEventListener(GestureRecognizer.Events.TouchEnd, this, true); window.removeEventListener(GestureRecognizer.Events.GestureChange, this, true); window.removeEventListener(GestureRecognizer.Events.GestureEnd, this, true); } _updateTargetTouches(event) { if (!GestureRecognizer.SupportsTouches) { if (event.type === GestureRecognizer.Events.TouchEnd) this._targetTouches = []; else this._targetTouches = [event]; return; } if (!(event instanceof TouchEvent)) return; // With a touchstart event, event.targetTouches is accurate so // we simply add all of those. if (event.type === GestureRecognizer.Events.TouchStart) { this._targetTouches = []; let touches = event.targetTouches; for (let i = 0, count = touches.length; i < count; ++i) this._targetTouches.push(touches[i]); return; } // With a touchmove event, the target is window so event.targetTouches is // inaccurate so we add all touches that we knew about previously. if (event.type === GestureRecognizer.Events.TouchMove) { let targetIdentifiers = this._targetTouches.map(function(touch) { return touch.identifier; }); this._targetTouches = []; let touches = event.touches; for (let i = 0, count = touches.length; i < count; ++i) { let touch = touches[i]; if (targetIdentifiers.indexOf(touch.identifier) !== -1) this._targetTouches.push(touch); } return; } // With a touchend or touchcancel event, we only keep the existing touches // that are also found in event.touches. let allTouches = event.touches; let existingIdentifiers = []; for (let i = 0, count = allTouches.length; i < count; ++i) existingIdentifiers.push(allTouches[i].identifier); this._targetTouches = this._targetTouches.filter(function(touch) { return existingIdentifiers.indexOf(touch.identifier) !== -1; }); } _updateKeyboardModifiers(event) { this.modifierKeys.alt = event.altKey; this.modifierKeys.ctrl = event.ctrlKey; this.modifierKeys.meta = event.metaKey; this.modifierKeys.shift = event.shiftKey; } } GestureRecognizer.SupportsTouches = "createTouch" in document; GestureRecognizer.SupportsGestures = !!window.GestureEvent; GestureRecognizer.States = { Possible : "possible", Began : "began", Changed : "changed", Ended : "ended", Cancelled : "cancelled", Failed : "failed", Recognized : "ended" }; GestureRecognizer.Events = { TouchStart : GestureRecognizer.SupportsTouches ? "touchstart" : "mousedown", TouchMove : GestureRecognizer.SupportsTouches ? "touchmove" : "mousemove", TouchEnd : GestureRecognizer.SupportsTouches ? "touchend" : "mouseup", TouchCancel : "touchcancel", GestureStart : "gesturestart", GestureChange : "gesturechange", GestureEnd : "gestureend" };