Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
730 lines
19 KiB
JavaScript
730 lines
19 KiB
JavaScript
'use strict';
|
|
/* global eventSafety */
|
|
/* global Service, SearchWindow, places, Promise, UtilityTray */
|
|
|
|
(function(exports) {
|
|
|
|
/**
|
|
* The Rocketbar is a system-wide URL/search/title bar.
|
|
* @requires SearchWindow
|
|
* @requires SettingsListener
|
|
* @class Rocketbar
|
|
*/
|
|
function Rocketbar() {
|
|
|
|
// States
|
|
this.enabled = false;
|
|
this.focused = false;
|
|
this.active = false;
|
|
|
|
// Properties
|
|
this._port = null; // Inter-app communications port
|
|
this._wasClicked = false; // Remember when transition triggered by a click
|
|
this._pendingMessage = null;
|
|
|
|
// Get DOM elements
|
|
this.body = document.body;
|
|
this.screen = document.getElementById('screen');
|
|
this.rocketbar = document.getElementById('rocketbar');
|
|
this.form = document.getElementById('rocketbar-form');
|
|
this.input = document.getElementById('rocketbar-input');
|
|
this.cancel = document.getElementById('rocketbar-cancel');
|
|
this.clearBtn = document.getElementById('rocketbar-clear');
|
|
this.results = document.getElementById('rocketbar-results');
|
|
this.backdrop = document.getElementById('rocketbar-backdrop');
|
|
}
|
|
|
|
Rocketbar.prototype = {
|
|
EVENT_PREFIX: 'rocketbar',
|
|
name: 'Rocketbar',
|
|
|
|
/**
|
|
* True during the rocketbar closing animation.
|
|
*/
|
|
isClosing: false,
|
|
|
|
publish: function(name) {
|
|
window.dispatchEvent(new CustomEvent(this.EVENT_PREFIX + name, {
|
|
detail: this
|
|
}));
|
|
},
|
|
|
|
isActive: function() {
|
|
return this.active;
|
|
},
|
|
|
|
getActiveWindow: function() {
|
|
return this.isActive() ? this.searchWindow : null;
|
|
},
|
|
|
|
setHierarchy: function(active) {
|
|
this.searchWindow && this.searchWindow.setVisibleForScreenReader(active);
|
|
},
|
|
|
|
setFocus: function(active) {
|
|
if (active) {
|
|
this.focus();
|
|
}
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Starts Rocketbar.
|
|
* @memberof Rocketbar.prototype
|
|
*/
|
|
start: function() {
|
|
this.addEventListeners();
|
|
this.enabled = true;
|
|
Service.request('registerHierarchy', this);
|
|
Service.registerState('enabled', this);
|
|
Service.register('handleInput', this);
|
|
},
|
|
|
|
/**
|
|
* Put Rocketbar in the active state.
|
|
*
|
|
* Input is displayed, title is hidden and search app is loaded, input
|
|
* not always focused.
|
|
* @return {Promise}
|
|
* @memberof Rocketbar.prototype
|
|
*/
|
|
activate: function() {
|
|
if (this.isClosing) {
|
|
return Promise.reject();
|
|
}
|
|
|
|
this._activateCall = new Promise(resolve => {
|
|
if (this.active) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
this.active = true;
|
|
this.rocketbar.classList.add('active');
|
|
this.form.classList.remove('hidden');
|
|
this.screen.classList.add('rocketbar-focused');
|
|
|
|
// We wait for the transition do be over and the search app to be loaded
|
|
// before moving on (and resolving the promise).
|
|
var searchLoaded = false;
|
|
var transitionEnded = false;
|
|
var waitOver = () => {
|
|
if (searchLoaded && transitionEnded) {
|
|
resolve();
|
|
this._activateCall = null;
|
|
this.publish('-activated');
|
|
}
|
|
};
|
|
|
|
var backdrop = this.backdrop;
|
|
var finishTransition = () => {
|
|
window.dispatchEvent(new CustomEvent('rocketbar-overlayopened'));
|
|
transitionEnded = true;
|
|
waitOver();
|
|
};
|
|
backdrop.classList.remove('hidden');
|
|
eventSafety(backdrop, 'transitionend', finishTransition, 200);
|
|
|
|
this.loadSearchApp().then(() => {
|
|
if (this.input.value.length) {
|
|
this.handleInput();
|
|
}
|
|
searchLoaded = true;
|
|
waitOver();
|
|
});
|
|
this.publish('-activating');
|
|
});
|
|
|
|
// Immediately hide if the utility tray is active.
|
|
// In the future we might be able to streamline this flow, but for now we
|
|
// need to ensure that all events are properly fired so that the chrome
|
|
// collapses. If for example we early exit, currently the chrome will
|
|
// not collapse.
|
|
if (UtilityTray.shown) {
|
|
this._activateCall
|
|
.then(this._closeSearch.bind(this));
|
|
}
|
|
|
|
return this._activateCall;
|
|
},
|
|
|
|
/**
|
|
* Take Rocketbar out of active state.
|
|
*
|
|
* Title is displayed, input is hidden, input is always blurred.
|
|
* @memberof Rocketbar.prototype
|
|
*/
|
|
deactivate: function() {
|
|
if (!this.active) {
|
|
return;
|
|
}
|
|
this.active = false;
|
|
this.isClosing = true;
|
|
|
|
var backdrop = this.backdrop;
|
|
var self = this;
|
|
var finish = () => {
|
|
this.form.classList.add('hidden');
|
|
this.rocketbar.classList.remove('active');
|
|
this.screen.classList.remove('rocketbar-focused');
|
|
|
|
backdrop.classList.add('hidden');
|
|
|
|
eventSafety(backdrop, 'transitionend', () => {
|
|
window.dispatchEvent(new CustomEvent('rocketbar-overlayclosed'));
|
|
self.publish('-deactivated');
|
|
self.isClosing = false;
|
|
}, 200);
|
|
};
|
|
|
|
if (this.focused) {
|
|
eventSafety(window, 'keyboardhidden', finish, 1000);
|
|
this.blur();
|
|
} else {
|
|
finish();
|
|
}
|
|
this.publish('-deactivating');
|
|
},
|
|
|
|
/**
|
|
* Add event listeners. Only called when Rocketbar is turned on.
|
|
* @memberof Rocketbar.prototype
|
|
*/
|
|
addEventListeners: function() {
|
|
// Listen for events from window manager
|
|
window.addEventListener('apploading', this);
|
|
window.addEventListener('appforeground', this);
|
|
window.addEventListener('apptitlechange', this);
|
|
window.addEventListener('lockscreen-appopened', this);
|
|
window.addEventListener('appopened', this);
|
|
window.addEventListener('launchapp', this);
|
|
window.addEventListener('searchterminated', this);
|
|
window.addEventListener('permissiondialoghide', this);
|
|
window.addEventListener('global-search-request', this);
|
|
window.addEventListener('attentionopening', this);
|
|
window.addEventListener('attentionopened', this);
|
|
window.addEventListener('activityrequesting', this);
|
|
window.addEventListener('searchopened', this);
|
|
window.addEventListener('searchclosed', this);
|
|
window.addEventListener('utilitytray-overlayopening', this);
|
|
window.addEventListener('utility-tray-overlayopened', this);
|
|
window.addEventListener('simlockrequestfocus', this);
|
|
window.addEventListener('cardviewbeforeshow', this);
|
|
|
|
// Listen for events from Rocketbar
|
|
this.input.addEventListener('focus', this);
|
|
this.input.addEventListener('blur', this);
|
|
this.input.addEventListener('input', this);
|
|
this.cancel.addEventListener('click', this);
|
|
this.clearBtn.addEventListener('click', this);
|
|
this.form.addEventListener('submit', this);
|
|
this.backdrop.addEventListener('click', this);
|
|
|
|
// Listen for messages from search app
|
|
window.addEventListener('iac-search-results', this);
|
|
},
|
|
|
|
'_handle_system-resize': function(evt) {
|
|
if (this.isActive()) {
|
|
var p = this.searchWindow &&
|
|
this.searchWindow.frontWindow &&
|
|
this.searchWindow.frontWindow.resize();
|
|
|
|
if (evt.detail && typeof evt.detail.waitUntil === 'function') {
|
|
evt.detail.waitUntil(p);
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
respondToHierarchyEvent: function(evt) {
|
|
if (this['_handle_' + evt.type]) {
|
|
return this['_handle_' + evt.type](evt);
|
|
}
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Dispatch events to correct event handlers.
|
|
*
|
|
* @param {Event} e Event.
|
|
* @memberof Rocketbar.prototype
|
|
*/
|
|
handleEvent: function(e) {
|
|
switch(e.type) {
|
|
case 'searchopened':
|
|
window.addEventListener('open-app', this);
|
|
break;
|
|
case 'searchclosed':
|
|
window.removeEventListener('open-app', this);
|
|
break;
|
|
case 'apploading':
|
|
case 'launchapp':
|
|
// Do not close the search app if something opened in the background.
|
|
var detail = e.detail;
|
|
if (detail && detail.stayBackground) {
|
|
return;
|
|
}
|
|
this._closeSearch();
|
|
break;
|
|
case 'open-app':
|
|
// Do not hide the searchWindow if we have a frontWindow.
|
|
if (this.searchWindow && this.searchWindow.frontWindow) {
|
|
return;
|
|
}
|
|
if (e.detail && !e.detail.showApp) {
|
|
return;
|
|
}
|
|
/* falls through */
|
|
case 'attentionopening':
|
|
case 'attentionopened':
|
|
case 'appforeground':
|
|
case 'appopened':
|
|
case 'activityrequesting':
|
|
case 'simlockrequestfocus':
|
|
case 'cardviewbeforeshow':
|
|
|
|
// Hide rocketbar if the user opens the utility tray.
|
|
// The utility tray and rocketbar share the same space in the mental
|
|
// model - only one can be active at any given time. For consistency,
|
|
// any activities are also closed along with rocketbar when the
|
|
// utility tray is opened.
|
|
case 'utilitytray-overlayopening':
|
|
case 'utility-tray-overlayopened':
|
|
this._closeSearch();
|
|
break;
|
|
case 'lockscreen-appopened':
|
|
this.handleLock(e);
|
|
break;
|
|
case 'focus':
|
|
this.handleFocus(e);
|
|
break;
|
|
case 'blur':
|
|
this.handleBlur(e);
|
|
break;
|
|
case 'input':
|
|
this.handleInput(e);
|
|
break;
|
|
case 'click':
|
|
if (e.target == this.cancel) {
|
|
this.handleCancel(e);
|
|
} else if (e.target == this.clearBtn) {
|
|
this.clear();
|
|
this.focus();
|
|
} else if (e.target == this.backdrop) {
|
|
this._closeSearch();
|
|
}
|
|
break;
|
|
case 'searchterminated':
|
|
this.handleSearchTerminated(e);
|
|
break;
|
|
case 'submit':
|
|
this.handleSubmit(e);
|
|
break;
|
|
case 'iac-search-results':
|
|
this.handleSearchMessage(e);
|
|
break;
|
|
case 'permissiondialoghide':
|
|
if (this.active) {
|
|
this.focus();
|
|
}
|
|
break;
|
|
case 'global-search-request':
|
|
var app = Service.query('AppWindowManager.getActiveWindow');
|
|
var afterActivate;
|
|
|
|
if (app && !app.isActive()) {
|
|
return;
|
|
}
|
|
|
|
// If the app is not a browser, retain the search value and activate.
|
|
if (app && !app.isBrowser()) {
|
|
afterActivate = this.focus.bind(this);
|
|
} else {
|
|
this.setInput(app.config.url);
|
|
|
|
afterActivate = () => {
|
|
this.hideResults();
|
|
setTimeout(() => {
|
|
this.focus();
|
|
this.selectAll();
|
|
});
|
|
};
|
|
}
|
|
|
|
if (app && app.appChrome && !app.appChrome.isMaximized()) {
|
|
app.appChrome.maximize(() => {
|
|
this.activate().then(afterActivate);
|
|
});
|
|
} else {
|
|
this.activate().then(afterActivate);
|
|
}
|
|
break;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Set URL and generate manifest URL of search app.
|
|
* @param {String} url The search app URL.
|
|
* @memberof Rocketbar.prototype
|
|
*/
|
|
setSearchAppURL: function(url) {
|
|
this._searchAppURL = url;
|
|
this._searchManifestURL = url ? url.match(/(^.*?:\/\/.*?\/)/)[1] +
|
|
'manifest.webapp' : '';
|
|
},
|
|
|
|
setInput: function(input) {
|
|
this.input.value = input;
|
|
this.rocketbar.classList.toggle('has-text', input.length);
|
|
},
|
|
|
|
/**
|
|
* Show the Rocketbar results pane.
|
|
* @memberof Rocketbar.prototype
|
|
*/
|
|
showResults: function() {
|
|
if (this.searchWindow && !this.searchWindow.isDead()) {
|
|
this.searchWindow.open();
|
|
}
|
|
this.results.classList.remove('hidden');
|
|
this.backdrop.classList.add('results-shown');
|
|
},
|
|
|
|
/**
|
|
* Hide the Rocketbar results pane.
|
|
* @memberof Rocketbar.prototype
|
|
*/
|
|
hideResults: function() {
|
|
if (this.searchWindow) {
|
|
this.searchWindow.close();
|
|
this.searchWindow.hideContextMenu();
|
|
}
|
|
|
|
this.results.classList.add('hidden');
|
|
this.backdrop.classList.remove('results-shown');
|
|
|
|
// Send a message to the search app to clear results
|
|
if (this._port) {
|
|
this._port.postMessage({
|
|
action: 'clear'
|
|
});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Reset the Rocketbar to its initial empty state.
|
|
*/
|
|
clear: function() {
|
|
this.setInput('');
|
|
this.hideResults();
|
|
},
|
|
|
|
/**
|
|
* Enable back button.
|
|
*/
|
|
enableNavigation: function() {
|
|
this.rocketbar.classList.add('navigation');
|
|
},
|
|
|
|
/**
|
|
* Disable back button.
|
|
*/
|
|
disableNavigation: function() {
|
|
this.rocketbar.classList.remove('navigation');
|
|
},
|
|
|
|
/**
|
|
* Focus Rocketbar input.
|
|
* @memberof Rocketbar.prototype
|
|
*/
|
|
focus: function() {
|
|
if (this.active) {
|
|
this.input.focus();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* SelectAll text content from Rocketbar input.
|
|
* @memberof Rocketbar.prototype
|
|
*/
|
|
selectAll: function() {
|
|
this.input.setSelectionRange(0, this.input.value.length, 'forward');
|
|
},
|
|
|
|
/**
|
|
* Handle a focus event.
|
|
* @memberof Rocketbar.prototype
|
|
*/
|
|
handleFocus: function() {
|
|
this.focused = true;
|
|
},
|
|
|
|
/**
|
|
* Handle press of hardware home button.
|
|
* @memberof Rocketbar.prototype
|
|
*/
|
|
_handle_home: function() {
|
|
this._closeSearch();
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Blur Rocketbar input.
|
|
* @memberof Rocketbar.prototype
|
|
*/
|
|
blur: function() {
|
|
this.input.blur();
|
|
},
|
|
|
|
/**
|
|
* Handle a blur event.
|
|
* @memberof Rocketbar.prototype
|
|
*/
|
|
handleBlur: function() {
|
|
this.focused = false;
|
|
},
|
|
|
|
/**
|
|
* Handle a lock event.
|
|
* @memberof Rocketbar.prototype
|
|
*/
|
|
handleLock: function() {
|
|
this._closeSearch();
|
|
},
|
|
|
|
/**
|
|
* This function is called in respondToHierarchyEvent()
|
|
* when there is a value selector event and rocketbar
|
|
* is the current top most UI by HierarchyManager.
|
|
* @param {Object} evt Event object
|
|
*/
|
|
'_handle_inputfocus': function(evt) {
|
|
if (this.searchWindow) {
|
|
this.searchWindow.getTopMostWindow()
|
|
.broadcast('inputfocus', evt.detail);
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
'_handle_inputblur': function(evt) {
|
|
if (this.searchWindow) {
|
|
this.searchWindow.getTopMostWindow().broadcast('inputblur');
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Handles activities for the search app.
|
|
* @memberof Rocketbar.prototype
|
|
*/
|
|
_handle_launchactivity: function(e) {
|
|
if (e.detail.isActivity && e.detail.inline && this.searchWindow &&
|
|
this.searchWindow.manifestURL === e.detail.parentApp) {
|
|
this.searchWindow.broadcast('launchactivity', e.detail);
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
_closeSearch: function() {
|
|
var hideAndDeactivate = () => {
|
|
this.hideResults();
|
|
this.deactivate();
|
|
};
|
|
|
|
if (this._activateCall) {
|
|
this._activateCall
|
|
.then(hideAndDeactivate);
|
|
} else {
|
|
hideAndDeactivate();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Handle text input in Rocketbar.
|
|
* @memberof Rocketbar.prototype
|
|
*/
|
|
handleInput: function() {
|
|
var input = this.input.value;
|
|
|
|
this.rocketbar.classList.toggle('has-text', input.length);
|
|
|
|
if (UtilityTray.shown) {
|
|
this._closeSearch();
|
|
return;
|
|
}
|
|
|
|
if (!input && !this.results.classList.contains('hidden')) {
|
|
this.hideResults();
|
|
return;
|
|
}
|
|
|
|
if (this.results.classList.contains('hidden')) {
|
|
this.showResults();
|
|
}
|
|
|
|
if (this._port) {
|
|
this._port.postMessage({
|
|
action: 'change',
|
|
input: input,
|
|
isPrivateBrowser:
|
|
Service.query('AppWindowManager.getActiveWindow').isPrivateBrowser()
|
|
});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Handle click of cancel button.
|
|
* @memberof Rocketbar.prototype
|
|
*/
|
|
handleCancel: function(e) {
|
|
this.setInput('');
|
|
this._closeSearch();
|
|
},
|
|
|
|
/**
|
|
* Handle submission of the Rocketbar.
|
|
*
|
|
* @param {Event} e Submit event.
|
|
* @memberof Rocketbar.prototype
|
|
*/
|
|
handleSubmit: function(e) {
|
|
e.preventDefault();
|
|
|
|
this.input.blur();
|
|
|
|
if (this.results.classList.contains('hidden')) {
|
|
this.showResults();
|
|
}
|
|
|
|
this._port.postMessage({
|
|
action: 'submit',
|
|
input: this.input.value
|
|
});
|
|
|
|
// Do not persist search submissions from private windows.
|
|
if (Service.query('AppWindowManager.getActiveWindow')
|
|
.isPrivateBrowser()) {
|
|
this.setInput('');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Instantiates a new SearchWindow.
|
|
* @return {Promise}
|
|
* @memberof Rocketbar.prototype
|
|
*/
|
|
loadSearchApp: function() {
|
|
if (!this.searchWindow) {
|
|
this.searchWindow = new SearchWindow();
|
|
}
|
|
|
|
return this.initSearchConnection();
|
|
},
|
|
|
|
/**
|
|
* Handles when the search app terminates.
|
|
* @memberof Rocketbar.prototype
|
|
*/
|
|
handleSearchTerminated: function(e) {
|
|
if (!this.searchWindow) {
|
|
return;
|
|
}
|
|
|
|
this._closeSearch();
|
|
|
|
this.searchWindow = null;
|
|
this._port = null;
|
|
},
|
|
|
|
/**
|
|
* Initialise inter-app connection with search app.
|
|
* @return {Promise}
|
|
* @memberof Rocketbar.prototype
|
|
*/
|
|
initSearchConnection: function() {
|
|
if (this.pendingInitConnection) {
|
|
return this.pendingInitConnection;
|
|
}
|
|
|
|
this.pendingInitConnection = new Promise((resolve, reject) => {
|
|
navigator.mozApps.getSelf().onsuccess = (event) => {
|
|
var app = event.target.result;
|
|
if (!app) {
|
|
reject();
|
|
return;
|
|
}
|
|
|
|
app.connect('search').then(ports => {
|
|
ports.forEach(port => {
|
|
this._port = port;
|
|
});
|
|
if (this._pendingMessage) {
|
|
this.handleSearchMessage(this._pendingMessage);
|
|
delete this._pendingMessage;
|
|
}
|
|
delete this.pendingInitConnection;
|
|
resolve();
|
|
}, reject);
|
|
};
|
|
});
|
|
return this.pendingInitConnection;
|
|
},
|
|
|
|
/**
|
|
* Handle messages from the search app.
|
|
*
|
|
* @param {Event} e Message event.
|
|
* @memberof Rocketbar.prototype
|
|
*/
|
|
handleSearchMessage: function(e) {
|
|
// Open the search connection if we receive a message before it's open
|
|
if (!this._port) {
|
|
this._pendingMessage = e;
|
|
this.initSearchConnection();
|
|
return;
|
|
}
|
|
|
|
switch (e.detail.action) {
|
|
case 'private-window':
|
|
window.dispatchEvent(new CustomEvent('new-private-window'));
|
|
break;
|
|
case 'render':
|
|
this.activate().then(this.focus.bind(this));
|
|
break;
|
|
case 'focus':
|
|
this.focus();
|
|
break;
|
|
case 'input':
|
|
this.setInput(e.detail.input);
|
|
this.focus();
|
|
this.handleInput();
|
|
break;
|
|
case 'request-screenshot':
|
|
places.screenshotRequested(e.detail.url);
|
|
break;
|
|
case 'hide':
|
|
this._closeSearch();
|
|
break;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Tell the search app to update its index.
|
|
* @memberof Rocketbar.prototype
|
|
*/
|
|
updateSearchIndex: function() {
|
|
if (this._port) {
|
|
this._port.postMessage({
|
|
action: 'syncPlaces'
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
exports.Rocketbar = Rocketbar;
|
|
|
|
}(window));
|