'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));