Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
734 lines
20 KiB
JavaScript
Executable file
734 lines
20 KiB
JavaScript
Executable file
'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();
|
|
}
|
|
|
|
const input = this.input.value;
|
|
this.loadSearchApp().then(() => {
|
|
this._port.postMessage({action: 'submit', input: input});
|
|
}).catch(error => {
|
|
delete this.pendingInitConnection;
|
|
console.error('Search connection failed', error);
|
|
});
|
|
|
|
// 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._port) { return Promise.resolve(); }
|
|
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));
|