/* global LazyLoader, InitialPanelHandler, AppStarter */
/* exported InitialPanelHandler, AppStarter */
/**
* InitialPanelHandler provides basic interaction including navigation and
* toggling settings before requirejs and other settings service get loaded.
* This could increase the responsiveness of the settings app.
*
* @module InitialPanelHandler
*/
(function(exports) {
'use strict';
/**
* @class InitialPanelHandler
* @param {HTMLElement} rootElement
* Root element of the panel.
* @returns {InitialPanelHandler}
*/
function InitialPanelHandler(rootElement) {
this._rootElement = rootElement;
this._pendingTargetPanel = null;
this._anchors = [];
this._inputs = [];
this._inputListenerInfos = [];
this._settings = navigator.mozSettings;
if (this._settings) {
this._init();
}
}
InitialPanelHandler.prototype = {
/**
* The panel that a user click on before the core modules are loaded.
*
* @access public
* @memberOf InitialPanelHandler.prototype
* @type {HTMLElement}
*/
get pendingTargetPanel() {
return this._pendingTargetPanel;
},
/**
* Add click listeners to all list items with "href" attribute.
*
* @access private
* @memberOf InitialPanelHandler.prototype
*/
_addClickListeners: function iph_addClickListeners() {
this._anchors = Array.prototype.slice.call(
this._rootElement.querySelectorAll('li a[href]'));
this._anchors.forEach(function(anchor) {
anchor.onclick = (event) => {
event.preventDefault();
this._pendingTargetPanel = anchor.getAttribute('href');
this._pendingTargetPanel = this._pendingTargetPanel.replace('#', '');
};
}, this);
},
/**
* Remove click listeners added in _addClickListeners.
*
* @access private
* @memberOf InitialPanelHandler.prototype
*/
_removeClickListeners: function iph_removeClickListeners() {
this._anchors.forEach(function(anchor) {
anchor.onclick = null;
});
},
/**
* Add listeners added to all input elements bound to the settings database.
*
* @access private
* @memberOf InitialPanelHandler.prototype
*/
_addInputListeners: function iph_addInputListeners() {
this._inputs = Array.prototype.slice.call(
this._rootElement.querySelectorAll('input[name]'));
this._inputs.forEach(function(input) {
var name = input.getAttribute('name');
var boundOnSettingChange = this._onSettingChange.bind(this, input);
input.onchange = this._onInputChange.bind(this);
this._settings.createLock().get(name).then((result) => {
this._updateInput(input, result[name]);
});
this._settings.addObserver(name, boundOnSettingChange);
this._inputListenerInfos.push({
name: name,
handler: boundOnSettingChange
});
}, this);
},
/**
* Remove the listeners added in _addInputListeners.
*
* @access private
* @memberOf InitialPanelHandler.prototype
*/
_removeInputListeners: function iph_removeInputListeners() {
this._inputs.forEach(function(input) {
input.onchange = null;
});
this._inputListenerInfos.forEach(function(inputListenerInfo) {
this._settings.removeObserver(inputListenerInfo.name,
inputListenerInfo.handler);
}, this);
},
/**
* Handler for settings field changes.
*
* @access private
* @memberOf InitialPanelHandler.prototype
* @param {HTMLInputElement} input
* @param {Event} event
*/
_onSettingChange: function iph_onSettingChange(input, event) {
this._updateInput(input, event.settingValue);
},
/**
* Handler for input changes. It updates the chage to the settings database.
*
* @access private
* @memberOf InitialPanelHandler.prototype
* @param {Event} event
*/
_onInputChange: function iph_onInputChange(event) {
var input = event.target;
var type = input.type;
var key = input.name;
if (!key || event.type !== 'change') {
return;
}
// Not touching with data-setting attribute here
// because they would have to be committed with a explicit "submit"
// of their own dialog.
if (input.dataset.setting) {
return;
}
var value;
switch (type) {
case 'checkbox':
case 'switch':
value = input.checked; // boolean
break;
case 'range':
// Bug 906296:
// We parseFloat() once to be able to round to 1 digit, then
// we parseFloat() again to make sure to store a Number and
// not a String, otherwise this will make Gecko unable to
// apply new settings.
value = parseFloat(parseFloat(input.value).toFixed(1)); // float
break;
case 'select-one':
case 'radio':
case 'text':
case 'password':
value = input.value; // default as text
if (input.dataset.valueType === 'integer') { // integer
value = parseInt(value, 10);
}
break;
}
var cset = {};
cset[key] = value;
this._settings.createLock().set(cset);
},
/**
* Update the input element with a value.
*
* @access private
* @memberOf InitialPanelHandler.prototype
* @param {HTMLInputElement} input
* @param {Object} value
*/
_updateInput: function iph_updateInput(input, value) {
var i;
var count;
switch (input.type) {
case 'checkbox':
case 'switch':
if (input.checked === value) {
return;
}
input.checked = value;
break;
case 'range':
if (input.value === value) {
return;
}
input.value = value;
break;
case 'select':
for (i = 0, count = input.options.length; i < count; i++) {
if (input.options[i].value === value) {
input.options[i].selected = true;
break;
}
}
break;
}
},
/**
* Add listeners for click and input changes.
*
* @access private
* @memberOf InitialPanelHandler.prototype
*/
_init: function iph_init() {
this._addClickListeners();
this._addInputListeners();
},
/**
* Release the control from the handler to the panel.
*
* @access public
* @memberOf InitialPanelHandler.prototype
*/
release: function iph_release() {
this._removeClickListeners();
this._removeInputListeners();
}
};
exports.InitialPanelHandler =
function ctor_InitialPanelHandler(rootElement) {
return new InitialPanelHandler(rootElement);
};
})(window);
/**
* AppStarter determines the initial panel to be displayed for this launch. It
* is also reponsible for attaching basic panel handlers for enabling the
* ability of interacting with users.
*
* @module AppStarter
*/
(function(exports) {
'use strict';
/**
* @class AppStarter
* @returns {AppStarter}
*/
function AppStarter() {
this._started = false;
this._launchContext = null;
}
AppStarter.prototype = {
/**
* Returns the initial panel id based on the pending system message. If
* there is no system message available, it returns 'root'.
*
* @access private
* @memberOf AppStarter.prototype
* @returns {Promise.}
*/
_getInitialPanelId: function as_getInitialPanelId() {
return new Promise(function(resolve) {
if (navigator.mozHasPendingMessage('activity')) {
// Load activity handler only when we need to handle it.
LazyLoader.load(['js/activity_handler.js'], function ah_loaded() {
window.ActivityHandler.ready().then(function ah_ready() {
resolve(window.ActivityHandler.targetPanelId);
});
});
} else {
resolve('root');
}
});
},
/**
* Insert the elements of the initial panel.
*
* @access private
* @memberOf AppStarter.prototype
*/
_showInitialPanel: function as_showInitialPanel(initialPanelId) {
var initialPanel = document.getElementById(initialPanelId);
// Use lazy loader because it handles the case in DEBUG mode.
return LazyLoader.load([initialPanel]).then(() => {
initialPanel.classList.add('current');
});
},
/**
* The function defines a launch context storing the information regarding
* the launch to be used by the AMD modules.
*
* @access private
* @memberOf AppStarter.prototype
*/
_createLaunchContext: function as_createLaunchContext(initialPanelId,
initialPanelHandler, activityHandler) {
this._launchContext = {
get initialPanelId() {
return initialPanelId;
},
get initialPanelHandler() {
return initialPanelHandler;
},
get activityHandler() {
return activityHandler;
}
};
var that = this;
Object.defineProperty(exports, 'LaunchContext', {
configurable: true,
get: function() {
return that._launchContext;
}
});
},
/**
* Load alameda and the required modules defined in main.js.
*
* @access private
* @memberOf AppStarter.prototype
*/
_loadAlameda: function as_loadAlameda() {
var scriptNode = document.createElement('script');
scriptNode.setAttribute('data-main', 'js/main.js');
scriptNode.src = 'js/vendor/alameda.js';
document.head.appendChild(scriptNode);
},
/**
* The function determines the first panel to be displayed and loads the
* minimal set of modules for basic interaction. It also exposes the launch
* context for the delay loaded AMD modules.
*
* @access public
* @memberOf AppStarter.prototype
*/
start: function as_start() {
if (this._started) {
return Promise.resolve();
} else {
this._started = true;
}
document.l10n.ready.then(function l10nDone() {
// Since the settings app contains its chrome already existing in the
// DOM, we can fire that it's loaded as soon as the DOM is localized
window.performance.mark('navigationLoaded');
// Since the settings app has no functional chrome, we can fire the
// interactive event now because there are no events to bind
window.performance.mark('navigationInteractive');
});
var initialPanelId;
return this._getInitialPanelId().then((panelId) => {
initialPanelId = panelId;
return this._showInitialPanel(panelId);
}).then(() => {
var initialPanelHandler =
InitialPanelHandler(document.getElementById(initialPanelId));
// Initial panel handler registers basic events for interaction so we
// can fire the content interactive evnet here.
window.performance.mark('contentInteractive');
this._createLaunchContext(initialPanelId, initialPanelHandler,
window.ActivityHandler);
}).then(() => {
// Add timeout as loading the modules could block scrolling.
return new Promise((resolve) => {
setTimeout(() => {
this._loadAlameda();
resolve();
}, 100);
});
});
}
};
exports.AppStarter = function ctor_appStarter() {
return new AppStarter();
};
})(window);
(function() {
'use strict';
var appStarter = AppStarter();
// Deferred Gaia components contain UMD wrappers. Finish loading them before
// Alameda starts, otherwise an anonymous component can become a panel module.
if (document.readyState === 'complete') {
appStarter.start();
} else {
document.addEventListener('DOMContentLoaded', function() {
appStarter.start();
}, { once: true });
}
})();