Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
418 lines
14 KiB
JavaScript
418 lines
14 KiB
JavaScript
/* global Service */
|
|
'use strict';
|
|
|
|
(function(exports) {
|
|
var TransitionEvents =
|
|
['open', 'close', 'complete', 'timeout',
|
|
'immediate-open', 'immediate-close'];
|
|
|
|
var TransitionStateTable = {
|
|
'closed': ['opening', null, null, null, 'opened', null],
|
|
'opened': [null, 'closing', null, null, null, 'closed'],
|
|
'opening': [null, 'closing', 'opened', 'opened', 'opened', 'closed'],
|
|
'closing': ['opened', null, 'closed', 'closed', 'opened', 'closed']
|
|
};
|
|
|
|
/**
|
|
* AppTransitionController controlls the opening and closing animation
|
|
* of the given appWindow.
|
|
*
|
|
* ##### Flow chart #####
|
|
* 
|
|
*
|
|
* ##### State machine #####
|
|
* 
|
|
*
|
|
* @param {AppWindow} app The app window instance which this controller
|
|
* belongs to.
|
|
*
|
|
* @class AppTransitionController
|
|
*/
|
|
var AppTransitionController =
|
|
function AppTransitionController(app) {
|
|
if (!app || !app.element) {
|
|
return;
|
|
}
|
|
|
|
this.app = app;
|
|
this.app.debug('default animation:',
|
|
this.app.openAnimation, this.app.closeAnimation);
|
|
if (this.app.openAnimation) {
|
|
this.openAnimation = this.app.openAnimation;
|
|
}
|
|
|
|
if (this.app.closeAnimation) {
|
|
this.closeAnimation = this.app.closeAnimation;
|
|
}
|
|
|
|
if (this.app.CLASS_NAME == 'AppWindow') {
|
|
this.OPENING_TRANSITION_TIMEOUT = 2500;
|
|
}
|
|
|
|
this.app.element.addEventListener('_opening', this);
|
|
this.app.element.addEventListener('_closing', this);
|
|
this.app.element.addEventListener('_opened', this);
|
|
this.app.element.addEventListener('_closed', this);
|
|
this.app.element.addEventListener('_opentransitionstart', this);
|
|
this.app.element.addEventListener('_closetransitionstart', this);
|
|
this.app.element.addEventListener('_loaded', this);
|
|
this.app.element.addEventListener('_openingtimeout', this);
|
|
this.app.element.addEventListener('_closingtimeout', this);
|
|
this.app.element.addEventListener('animationend', this);
|
|
};
|
|
|
|
AppTransitionController.prototype.destroy = function() {
|
|
if (!this.app || !this.app.element) {
|
|
return;
|
|
}
|
|
|
|
this.app.element.removeEventListener('_opening', this);
|
|
this.app.element.removeEventListener('_closing', this);
|
|
this.app.element.removeEventListener('_opened', this);
|
|
this.app.element.removeEventListener('_closed', this);
|
|
this.app.element.removeEventListener('_opentransitionstart', this);
|
|
this.app.element.removeEventListener('_closetransitionstart', this);
|
|
this.app.element.removeEventListener('_loaded', this);
|
|
this.app.element.removeEventListener('_openingtimeout', this);
|
|
this.app.element.removeEventListener('_closingtimeout', this);
|
|
this.app.element.removeEventListener('animationend', this);
|
|
this.app = null;
|
|
};
|
|
|
|
AppTransitionController.prototype._transitionState = 'closed';
|
|
AppTransitionController.prototype._waitingForLoad = false;
|
|
AppTransitionController.prototype.openAnimation = 'enlarge';
|
|
AppTransitionController.prototype.closeAnimation = 'reduce';
|
|
AppTransitionController.prototype.OPENING_TRANSITION_TIMEOUT = 1000;
|
|
AppTransitionController.prototype.CLOSING_TRANSITION_TIMEOUT = 1000;
|
|
AppTransitionController.prototype.SLOW_TRANSITION_TIMEOUT = 3500;
|
|
AppTransitionController.prototype._firstTransition = true;
|
|
AppTransitionController.prototype.changeTransitionState =
|
|
function atc_changeTransitionState(evt) {
|
|
var currentState = this._transitionState;
|
|
var evtIndex = TransitionEvents.indexOf(evt);
|
|
var state = TransitionStateTable[currentState][evtIndex];
|
|
if (!state) {
|
|
return;
|
|
}
|
|
|
|
this.app.debug(currentState, state, '::', evt);
|
|
|
|
this.switchTransitionState(state);
|
|
this.resetTransition();
|
|
this['_do_' + state]();
|
|
this.app.publish(state);
|
|
|
|
//backward compatibility
|
|
if (!this.app) {
|
|
return;
|
|
}
|
|
if (state == 'opening') {
|
|
/**
|
|
* Fired when the app is doing opening animation.
|
|
* @event AppWindow#appopening
|
|
*/
|
|
this.app.publish('willopen');
|
|
} else if (state == 'closing') {
|
|
/**
|
|
* Fired when the app is doing closing animation.
|
|
* @event AppWindow#appclosing
|
|
*/
|
|
this.app.publish('willclose');
|
|
} else if (state == 'opened') {
|
|
/**
|
|
* Fired when the app's opening animation is ended.
|
|
* @event AppWindow#appopen
|
|
*/
|
|
this.app.publish('open');
|
|
} else if (state == 'closed') {
|
|
/**
|
|
* Fired when the app's closing animation is ended.
|
|
* @event AppWindow#appclose
|
|
*/
|
|
this.app.publish('close');
|
|
}
|
|
};
|
|
|
|
AppTransitionController.prototype._do_closing =
|
|
function atc_do_closing() {
|
|
var slow = Service.query('slowTransition');
|
|
var timeout = slow ? this.SLOW_TRANSITION_TIMEOUT :
|
|
this.CLOSING_TRANSITION_TIMEOUT;
|
|
this.app.debug('timer to ensure closed does occur.');
|
|
this._closingTimeout = window.setTimeout(() => {
|
|
if (!this.app) {
|
|
return;
|
|
}
|
|
this.app.broadcast('closingtimeout');
|
|
}, timeout);
|
|
|
|
if (!this.app || !this.app.element) {
|
|
return;
|
|
}
|
|
this.app.element.classList.add('transition-closing');
|
|
this.app.element.classList.add(this.getAnimationName('close'));
|
|
};
|
|
|
|
AppTransitionController.prototype._do_closed =
|
|
function atc_do_closed() {
|
|
};
|
|
|
|
AppTransitionController.prototype.getAnimationName = function(type) {
|
|
return this.currentAnimation || this[type + 'Animation'] || type;
|
|
};
|
|
|
|
AppTransitionController.prototype._do_opening =
|
|
function atc_do_opening() {
|
|
var slow = Service.query('slowTransition');
|
|
var timeout = slow ? this.SLOW_TRANSITION_TIMEOUT :
|
|
this.OPENING_TRANSITION_TIMEOUT;
|
|
this.app.debug('timer to ensure opened does occur.');
|
|
this._openingTimeout = window.setTimeout(() => {
|
|
this.app && this.app.broadcast('openingtimeout');
|
|
}, timeout);
|
|
this._waitingForLoad = false;
|
|
this.app.element.classList.add('transition-opening');
|
|
this.app.element.classList.add(this.getAnimationName('open'));
|
|
this.app.debug(this.app.element.classList);
|
|
};
|
|
|
|
AppTransitionController.prototype._do_opened =
|
|
function atc_do_opened() {
|
|
};
|
|
|
|
AppTransitionController.prototype.switchTransitionState =
|
|
function atc_switchTransitionState(state) {
|
|
this._transitionState = state;
|
|
if (!this.app) {
|
|
return;
|
|
}
|
|
this.app._changeState('transition', this._transitionState);
|
|
};
|
|
|
|
// TODO: move general transition handlers into another object.
|
|
AppTransitionController.prototype.handle_closing =
|
|
function atc_handle_closing() {
|
|
if (!this.app || !this.app.element) {
|
|
return;
|
|
}
|
|
/* The AttentionToaster will take care of that for AttentionWindows */
|
|
/* InputWindow & InputWindowManager will take care of visibility of IM */
|
|
if (!this.app.isAttentionWindow && !this.app.isCallscreenWindow &&
|
|
!this.app.isInputMethod) {
|
|
this.app.setVisible(false);
|
|
}
|
|
this.switchTransitionState('closing');
|
|
};
|
|
|
|
AppTransitionController.prototype.handle_closed =
|
|
function atc_handle_closed() {
|
|
if (!this.app || !this.app.element) {
|
|
return;
|
|
}
|
|
|
|
this.resetTransition();
|
|
this.app.setNFCFocus(false);
|
|
|
|
this.app.element.classList.remove('active');
|
|
};
|
|
|
|
AppTransitionController.prototype.handle_opening =
|
|
function atc_handle_opening() {
|
|
if (!this.app || !this.app.element) {
|
|
return;
|
|
}
|
|
if (this.app.loaded) {
|
|
var self = this;
|
|
this.app.element.addEventListener('_opened', function onopen() {
|
|
// Perf test needs.
|
|
self.app.element.removeEventListener('_opened', onopen);
|
|
self.app.publish('loadtime', {
|
|
time: parseInt(Date.now() - self.app.launchTime),
|
|
type: 'w',
|
|
src: self.app.config.url
|
|
});
|
|
});
|
|
}
|
|
this.app.reviveBrowser();
|
|
this.app.launchTime = Date.now();
|
|
this.app.fadeIn();
|
|
|
|
// TODO:
|
|
// May have orientation manager to deal with lock orientation request.
|
|
if (this.app.isHomescreen ||
|
|
this.app.isCallscreenWindow) {
|
|
this.app.setOrientation();
|
|
}
|
|
};
|
|
|
|
AppTransitionController.prototype.handle_opened =
|
|
function atc_handle_opened() {
|
|
if (!this.app || !this.app.element) {
|
|
return;
|
|
}
|
|
|
|
this.app.reviveBrowser();
|
|
this.resetTransition();
|
|
this.app.element.removeAttribute('aria-hidden');
|
|
this.app.show();
|
|
this.app.element.classList.add('active');
|
|
|
|
// If we set the orientation in handle_opening we want to let the resize
|
|
// happen before requesting foreground.
|
|
if (this.currentAnimation == 'immediate') {
|
|
setTimeout(this.app.requestForeground.bind(this.app));
|
|
} else {
|
|
this.app.requestForeground();
|
|
}
|
|
|
|
// TODO:
|
|
// May have orientation manager to deal with lock orientation request.
|
|
if (!this.app.isCallscreenWindow) {
|
|
this.app.setOrientation();
|
|
}
|
|
this.focusApp();
|
|
};
|
|
|
|
AppTransitionController.prototype.focusApp = function() {
|
|
if (!this.app) {
|
|
return;
|
|
}
|
|
|
|
if (this._shouldFocusApp()) {
|
|
this.app.debug('focusing this app.');
|
|
this.app.focus();
|
|
this.app.setNFCFocus(true);
|
|
}
|
|
};
|
|
|
|
AppTransitionController.prototype._shouldFocusApp = function() {
|
|
// SearchWindow should not focus itself,
|
|
// because the input is inside system app.
|
|
var bottomWindow = this.app.getBottomMostWindow();
|
|
var topmostui = Service.query('getTopMostUI');
|
|
var stayBackground = this.app.config &&
|
|
this.app.config.stayBackground;
|
|
return (this.app.CLASS_NAME !== 'SearchWindow' &&
|
|
this._transitionState == 'opened' &&
|
|
Service.query('getTopMostWindow') === this.app &&
|
|
!stayBackground &&
|
|
topmostui &&
|
|
topmostui.name === bottomWindow.HIERARCHY_MANAGER);
|
|
};
|
|
|
|
AppTransitionController.prototype.requireOpen = function(animation) {
|
|
this.currentAnimation = animation || this.openAnimation;
|
|
this.app.debug('open with ' + this.currentAnimation);
|
|
if (this.currentAnimation == 'immediate') {
|
|
this.app.publish('opening');
|
|
this.changeTransitionState('immediate-open');
|
|
} else {
|
|
this.changeTransitionState('open');
|
|
}
|
|
};
|
|
|
|
AppTransitionController.prototype.requireClose = function(animation) {
|
|
this.currentAnimation = animation || this.closeAnimation;
|
|
this.app.debug('close with ' + this.currentAnimation);
|
|
if (this.currentAnimation == 'immediate') {
|
|
this.app.publish('closing');
|
|
this.changeTransitionState('immediate-close');
|
|
} else {
|
|
this.changeTransitionState('close');
|
|
}
|
|
};
|
|
|
|
AppTransitionController.prototype.resetTransition =
|
|
function atc_resetTransition() {
|
|
if (this._openingTimeout) {
|
|
window.clearTimeout(this._openingTimeout);
|
|
this._openingTimeout = null;
|
|
}
|
|
|
|
if (this._closingTimeout) {
|
|
window.clearTimeout(this._closingTimeout);
|
|
this._closingTimeout = null;
|
|
}
|
|
this.clearTransitionClasses();
|
|
};
|
|
|
|
AppTransitionController.prototype.clearTransitionClasses =
|
|
function atc_removeTransitionClasses() {
|
|
if (!this.app || !this.app.element) {
|
|
return;
|
|
}
|
|
|
|
var classes = ['enlarge', 'reduce', 'to-cardview', 'from-cardview',
|
|
'invoking', 'invoked', 'zoom-in', 'zoom-out', 'fade-in', 'fade-out',
|
|
'transition-opening', 'transition-closing', 'immediate', 'fadeout',
|
|
'slideleft', 'slideright', 'in-from-left', 'out-to-right',
|
|
'will-become-active', 'will-become-inactive',
|
|
'slide-to-top', 'slide-from-top',
|
|
'slide-to-bottom', 'slide-from-bottom',
|
|
'home-from-cardview', 'home-to-cardview', 'from-new-card'];
|
|
|
|
classes.forEach(function iterator(cls) {
|
|
this.app.element.classList.remove(cls);
|
|
}, this);
|
|
};
|
|
|
|
AppTransitionController.prototype.handleEvent =
|
|
function atc_handleEvent(evt) {
|
|
switch (evt.type) {
|
|
case '_opening':
|
|
this.handle_opening();
|
|
break;
|
|
case '_opened':
|
|
this.handle_opened();
|
|
break;
|
|
case '_closed':
|
|
this.handle_closed();
|
|
break;
|
|
case '_closing':
|
|
this.handle_closing();
|
|
break;
|
|
case '_closingtimeout':
|
|
case '_openingtimeout':
|
|
this.changeTransitionState('timeout', evt.type);
|
|
break;
|
|
case '_loaded':
|
|
if (this._waitingForLoad) {
|
|
this._waitingForLoad = false;
|
|
this.changeTransitionState('complete');
|
|
}
|
|
break;
|
|
case 'animationend':
|
|
evt.stopPropagation();
|
|
|
|
if (this._transitionState == 'closing') {
|
|
this.changeTransitionState('complete', evt.type);
|
|
break;
|
|
}
|
|
|
|
// Hide touch-blocker when launching animation is ended.
|
|
this.app.element.classList.remove('transition-opening');
|
|
|
|
// We decide to drop this event if system is busy loading
|
|
// the active app or doing some other more important task.
|
|
// In case of the browser, we don't want to wait for the page
|
|
// being fully loaded to trigger the 'opened' event
|
|
if (!this.app.isBrowser() && Service.query('isBusyLoading')) {
|
|
this._waitingForLoad = true;
|
|
if (this.app.isHomescreen && this._transitionState == 'opening') {
|
|
/**
|
|
* focusing the app will have some side effect,
|
|
* but we don't care if we are opening the homescreen.
|
|
*/
|
|
this.app.focus();
|
|
}
|
|
break;
|
|
}
|
|
|
|
this.app.debug(evt.animationName + ' has been ENDED!');
|
|
this.changeTransitionState('complete', evt.type);
|
|
break;
|
|
}
|
|
};
|
|
exports.AppTransitionController = AppTransitionController;
|
|
}(window));
|