'use strict'; /* global SettingsListener */ /* global Service */ /* global SheetsTransition */ /* global StackManager */ /* global TouchForwarder */ (function(exports) { const kEdgeIntertia = 250; const kEdgeThreshold = 0.3; const kEdgeAngleThreshold = Math.PI / 6; const kSignificant = 10; /** * Detects user gestures for moving between apps using edge gestures. * Gestures are listened for on the previous/next elements, which is * above the app container. Forwards events when they are not relevant to * the app iframe. * @class EdgeSwipeDetector */ function EdgeSwipeDetector() {} EdgeSwipeDetector.prototype = { leftPanel: document.getElementById('left-panel'), rightPanel: document.getElementById('right-panel'), screen: document.getElementById('screen'), _touchForwarder: null, /** * Starts EdgeSwipeDetector and begins listening for events. * @memberof EdgeSwipeDetector.prototype */ start: function esd_init() { window.addEventListener('homescreenopened', this); window.addEventListener('appopened', this); window.addEventListener('cardviewclosed', this); window.addEventListener('mozChromeEvent', this); window.addEventListener('updatepromptshown', this); window.addEventListener('updateprompthidden', this); window.addEventListener('installpromptshown', this); window.addEventListener('installprompthidden', this); window.addEventListener('shrinking-start', this); window.addEventListener('shrinking-stop', this); window.addEventListener('hierarchychanged', this); window.addEventListener('hierarchytopmostwindowchanged', this); ['touchstart', 'touchmove', 'touchend', 'mousedown', 'mousemove', 'mouseup'].forEach(function(e) { this.leftPanel.addEventListener(e, this); this.rightPanel.addEventListener(e, this); }, this); this._touchForwarder = new TouchForwarder(); SettingsListener.observe('edgesgesture.enabled', false, this.settingUpdate.bind(this)); SettingsListener.observe('edgesgesture.debug', false, this.debugUpdate.bind(this)); }, /** * Whether or not the setting is enabled. * @memberof EdgeSwipeDetector.prototype */ _settingEnabled: false, /** * Called when the edgesgesture.enabled setting is changed. * @memberof EdgeSwipeDetector.prototype */ settingUpdate: function esd_settingUpdate(enabled) { this._settingEnabled = enabled; this._updateEnabled(); }, /** * Called when the edgesgesture.debug setting is changed. * @memberof EdgeSwipeDetector.prototype */ debugUpdate: function esd_debugUpdate(enabled) { this.screen.classList.toggle('edges-debug', enabled); }, _lifecycleEnabled: false, get lifecycleEnabled() { return this._lifecycleEnabled; }, set lifecycleEnabled(enable) { this._lifecycleEnabled = enable; this._updateEnabled(); }, /** * General event handler. * @param {Object} e * @memberof EdgeSwipeDetector.prototype */ handleEvent: function esd_handleEvent(e) { switch (e.type) { case 'hierarchychanged': case 'hierarchytopmostwindowchanged': // XXX: Use this.appWindowManager instead if // we become part of appWindowManager submodules. // i.e., Service.query('getTopMostUI') === this.parent var topMostUI = Service.query('getTopMostUI'); var topMostWindow = Service.query('getTopMostWindow'); if (topMostUI && topMostUI.name === 'AppWindowManager') { if (topMostWindow && !topMostWindow.getBottomMostWindow().isHomescreen && !Service.query('isFtuRunning')) { this.lifecycleEnabled = true; break; } } this.lifecycleEnabled = false; break; case 'mousedown': case 'mousemove': case 'mouseup': // Preventing gecko reflows until // https://bugzilla.mozilla.org/show_bug.cgi?id=1005815 lands e.preventDefault(); break; case 'touchstart': e.preventDefault(); this._touchStart(e); break; case 'touchmove': e.preventDefault(); this._touchMove(e); break; case 'touchend': e.preventDefault(); this._touchEnd(e); break; case 'appopened': var app = e.detail; if (!app.stayBackground) { this.lifecycleEnabled = (app.origin !== Service.query('getFtuOrigin')); } break; case 'shrinking-start': this.lifecycleEnabled = false; break; case 'cardviewclosed': var targetApp = e.detail; // Enable the edge gestures if we switched back into any app except // the homescreen. if (!targetApp.isHomescreen) { this.lifecycleEnabled = true; } break; case 'mozChromeEvent': if (e.detail.type !== 'accessibility-control') { break; } var details = JSON.parse(e.detail.details); switch (details.eventType) { case 'edge-swipe-right': this.autoSwipe('ltr'); break; case 'edge-swipe-left': this.autoSwipe('rtl'); break; } break; case 'updatepromptshown': case 'installpromptshown': this.lifecycleEnabled = false; break; // XXX: Move install/update dialog into system dialog // and then we could remove this. case 'updateprompthidden': case 'installprompthidden': case 'shrinking-stop': if (Service.query('getTopMostWindow') && !Service.query('getTopMostWindow').isHomescreen) { this.lifecycleEnabled = true; } break; } }, /** * Enables or disables the previous/next triggers. * @memberof EdgeSwipeDetector.prototype */ _updateEnabled: function esd_updateEnabled() { var enabled = this._lifecycleEnabled && this._settingEnabled; this.leftPanel.classList.toggle('disabled', !enabled); this.rightPanel.classList.toggle('disabled', !enabled); if (!enabled && this._touchStartEvt) { this._touchStartEvt = null; // we ignore the rest of the gesture SheetsTransition.snapInPlace(); } }, _touchStartEvt: null, _startDate: null, _startX: null, _deltaX: null, _startY: null, _deltaY: null, _forwardTimeout: null, _progress: null, _winWidth: null, _beganTransition: null, _moved: null, _direction: null, _forwarding: null, _redispatching: null, _touchStart: function esd_touchStart(e) { this._winWidth = window.innerWidth; this._direction = (e.target == this.rightPanel) ? 'rtl' : 'ltr'; this._touchStartEvt = e; this._startDate = Date.now(); var iframe = StackManager.getCurrent().getTopMostWindow().iframe; this._touchForwarder.destination = iframe; var touch = e.changedTouches[0]; this._startX = touch.clientX; this._startY = touch.clientY; this._deltaX = 0; this._deltaY = 0; this._beganTransition = false; this._moved = false; this._forwarding = false; this._redispatching = false; this._clearForwardTimeout(); this._forwardTimeout = setTimeout((function longTouch() { // Didn't move for a while after the touchstart, // this isn't a swipe this._forwardTimeout = null; this._forwarding = true; this._forward(this._touchStartEvt); }).bind(this), 300); }, _touchMove: function esd_touchMove(e) { if (!this._touchStartEvt) { return; } var touch = e.touches[0]; this._updateProgress(touch); var delta = Math.max(Math.abs(this._deltaX), Math.abs(this._deltaY)); // If we already started forwarding we just continue if (this._forwarding) { this._forward(e); return; } // If it's a pinch gesture we start forwarding if (e.touches.length > 1) { this._startForwarding(e); return; } // If the gesture isn't horizontal we start forwarding if (delta > kSignificant && !this._horizontalGesture()) { this._startForwarding(e); return; } // After a small threshold... if ((this._deltaX < kSignificant || this._outsideApp(e)) && !this._moved) { return; } // preparing to move the sheets... if (!this._beganTransition) { SheetsTransition.begin(this._direction); this._clearForwardTimeout(); this._beganTransition = true; } SheetsTransition.moveInDirection(this._direction, this._progress); this._moved = true; }, _touchEnd: function esd_touchEnd(e) { if (!this._touchStartEvt) { return; } // Edge gestures are never multi-touch var touches = e.touches.length + e.changedTouches.length; if (touches > 1 && !this._forwarding) { this._touchStartEvt = null; SheetsTransition.snapInPlace(); return; } var touch = e.changedTouches[0]; this._updateProgress(touch); if (this._forwarding) { this._forward(e); } else if ((this._deltaX < kSignificant) && (this._deltaY < kSignificant)) { setTimeout(function(self, touchstart, touchend) { self._forward(touchstart); setTimeout(function() { self._forward(touchend); }, 100); }, 0, this, this._touchStartEvt, e); this._forwarding = true; } this._clearForwardTimeout(); var deltaT = Date.now() - this._startDate; var speed = this._progress / deltaT; // progress / ms var inertia = speed * kEdgeIntertia; // ms of intertia var adjustedProgress = (this._progress + inertia); if (adjustedProgress < kEdgeThreshold || this._forwarding) { SheetsTransition.snapInPlace(); return; } var direction = this._direction; if (direction === 'ltr') { SheetsTransition.snapLeft(speed); } else { SheetsTransition.snapRight(speed); } if (direction === document.documentElement.dir) { StackManager.goPrev(); } else { StackManager.goNext(); } }, _updateProgress: function esd_updateProgress(touch) { this._deltaX = touch.clientX - this._startX; this._deltaY = touch.clientY - this._startY; if (this._direction == 'rtl') { this._deltaX *= -1; this._deltaY *= -1; } this._progress = this._deltaX / this._winWidth; }, _horizontalGesture: function esd_horizontalGesture() { var angle = Math.atan2(this._deltaX, this._deltaY); var horizontalAngle = Math.PI / 2; return Math.abs(angle - horizontalAngle) < kEdgeAngleThreshold; }, _clearForwardTimeout: function esd_clearForwardTimeout() { if (this._forwardTimeout) { clearTimeout(this._forwardTimeout); this._forwardTimeout = null; } }, _startForwarding: function esd_startForwarding(e) { this._clearForwardTimeout(); this._forwarding = true; this._forward(this._touchStartEvt); this._forward(e); if (this._beganTransition) { SheetsTransition.snapInPlace(); } }, /** * Once we identify the gesture as something other than an horizontal * swipe we replay and start forwarding the touch events: * * * either sending them to the current app through the sendTouchEvent * mozbrowser API * * or redispatching the events to the system app if they were outside * the app frame * * When redispatching, since we don't know on which element they should be * dispatched (the target is always the gesture-panel) we send a custom * event instead. * @memberof EdgeSwipeDetector.prototype */ _forward: function esd_forward(e) { if (this._moved) { return; } // We continue dispatching/forwarding where we started. if (this._outsideApp(e) || this._redispatching) { window.dispatchEvent(new CustomEvent('edge-touch-redispatch', { bubbles: true, detail: e })); this._redispatching = true; } else { this._touchForwarder.forward(e); } }, /** * Detects whether or not a touch event is outside of an app container. * @param {Object} e The relevant touch event. * @memberof EdgeSwipeDetector.prototype */ _outsideApp: function esd_outsideApp(e) { var touch = (e.type === 'touchend') ? e.changedTouches[0] : e.touches[0]; var x = touch.pageX; var y = touch.pageY; // In fullscreen_layout mode the app frame will always be fullscreen // but we still want to redispatch touch events to the "overlayed" // software home button var softwareButtonOverlayed = Service.query('getTopMostWindow') && Service.query('getTopMostWindow').isFullScreenLayout(); var width = Service.query('LayoutManager.width'); var height = Service.query('LayoutManager.height'); if (softwareButtonOverlayed) { var sbWidth = Service.query('SoftwareButtonManager.width'); var sbHeight = Service.query('SoftwareButtonManager.height'); return x > (width - sbWidth) || y > (height - sbHeight); } return (x > width || y > height); }, /** * Swipes a page when we receive an event from gecko. * @param {String} direction * @memberof EdgeSwipeDetector.prototype */ autoSwipe: function esd_autoSwipe(direction) { if (!this._lifecycleEnabled) { return; } SheetsTransition.begin(direction); if (direction === 'ltr') { SheetsTransition.snapLeft(1); } else { SheetsTransition.snapRight(1); } if (direction === document.documentElement.dir) { StackManager.goPrev(); } else { StackManager.goNext(); } } }; exports.EdgeSwipeDetector = EdgeSwipeDetector; }(window));