diff options
Diffstat (limited to 'toolkit/jetpack/sdk/ui/sidebar.js')
-rw-r--r-- | toolkit/jetpack/sdk/ui/sidebar.js | 311 |
1 files changed, 311 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/ui/sidebar.js b/toolkit/jetpack/sdk/ui/sidebar.js new file mode 100644 index 000000000..59e35ea11 --- /dev/null +++ b/toolkit/jetpack/sdk/ui/sidebar.js @@ -0,0 +1,311 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +'use strict'; + +module.metadata = { + 'stability': 'experimental', + 'engines': { + 'Firefox': '*' + } +}; + +const { Class } = require('../core/heritage'); +const { merge } = require('../util/object'); +const { Disposable } = require('../core/disposable'); +const { off, emit, setListeners } = require('../event/core'); +const { EventTarget } = require('../event/target'); +const { URL } = require('../url'); +const { add, remove, has, clear, iterator } = require('../lang/weak-set'); +const { id: addonID, data } = require('../self'); +const { WindowTracker } = require('../deprecated/window-utils'); +const { isShowing } = require('./sidebar/utils'); +const { isBrowser, getMostRecentBrowserWindow, windows, isWindowPrivate } = require('../window/utils'); +const { ns } = require('../core/namespace'); +const { remove: removeFromArray } = require('../util/array'); +const { show, hide, toggle } = require('./sidebar/actions'); +const { Worker } = require('../deprecated/sync-worker'); +const { contract: sidebarContract } = require('./sidebar/contract'); +const { create, dispose, updateTitle, updateURL, isSidebarShowing, showSidebar, hideSidebar } = require('./sidebar/view'); +const { defer } = require('../core/promise'); +const { models, views, viewsFor, modelFor } = require('./sidebar/namespace'); +const { isLocalURL } = require('../url'); +const { ensure } = require('../system/unload'); +const { identify } = require('./id'); +const { uuid } = require('../util/uuid'); +const { viewFor } = require('../view/core'); + +const resolveURL = (url) => url ? data.url(url) : url; + +const sidebarNS = ns(); + +const WEB_PANEL_BROWSER_ID = 'web-panels-browser'; + +var sidebars = {}; + +const Sidebar = Class({ + implements: [ Disposable ], + extends: EventTarget, + setup: function(options) { + // inital validation for the model information + let model = sidebarContract(options); + + // save the model information + models.set(this, model); + + // generate an id if one was not provided + model.id = model.id || addonID + '-' + uuid(); + + // further validation for the title and url + validateTitleAndURLCombo({}, this.title, this.url); + + const self = this; + const internals = sidebarNS(self); + const windowNS = internals.windowNS = ns(); + + // see bug https://bugzilla.mozilla.org/show_bug.cgi?id=886148 + ensure(this, 'destroy'); + + setListeners(this, options); + + let bars = []; + internals.tracker = WindowTracker({ + onTrack: function(window) { + if (!isBrowser(window)) + return; + + let sidebar = window.document.getElementById('sidebar'); + let sidebarBox = window.document.getElementById('sidebar-box'); + + let bar = create(window, { + id: self.id, + title: self.title, + sidebarurl: self.url + }); + bars.push(bar); + windowNS(window).bar = bar; + + bar.addEventListener('command', function() { + if (isSidebarShowing(window, self)) { + hideSidebar(window, self).catch(() => {}); + return; + } + + showSidebar(window, self); + }, false); + + function onSidebarLoad() { + // check if the sidebar is ready + let isReady = sidebar.docShell && sidebar.contentDocument; + if (!isReady) + return; + + // check if it is a web panel + let panelBrowser = sidebar.contentDocument.getElementById(WEB_PANEL_BROWSER_ID); + if (!panelBrowser) { + bar.removeAttribute('checked'); + return; + } + + let sbTitle = window.document.getElementById('sidebar-title'); + function onWebPanelSidebarCreated() { + if (panelBrowser.contentWindow.location != resolveURL(model.url) || + sbTitle.value != model.title) { + return; + } + + let worker = windowNS(window).worker = Worker({ + window: panelBrowser.contentWindow, + injectInDocument: true + }); + + function onWebPanelSidebarUnload() { + windowNS(window).onWebPanelSidebarUnload = null; + + // uncheck the associated menuitem + bar.setAttribute('checked', 'false'); + + emit(self, 'hide', {}); + emit(self, 'detach', worker); + windowNS(window).worker = null; + } + windowNS(window).onWebPanelSidebarUnload = onWebPanelSidebarUnload; + panelBrowser.contentWindow.addEventListener('unload', onWebPanelSidebarUnload, true); + + // check the associated menuitem + bar.setAttribute('checked', 'true'); + + function onWebPanelSidebarReady() { + panelBrowser.contentWindow.removeEventListener('DOMContentLoaded', onWebPanelSidebarReady, false); + windowNS(window).onWebPanelSidebarReady = null; + + emit(self, 'ready', worker); + } + windowNS(window).onWebPanelSidebarReady = onWebPanelSidebarReady; + panelBrowser.contentWindow.addEventListener('DOMContentLoaded', onWebPanelSidebarReady, false); + + function onWebPanelSidebarLoad() { + panelBrowser.contentWindow.removeEventListener('load', onWebPanelSidebarLoad, true); + windowNS(window).onWebPanelSidebarLoad = null; + + // TODO: decide if returning worker is acceptable.. + //emit(self, 'show', { worker: worker }); + emit(self, 'show', {}); + } + windowNS(window).onWebPanelSidebarLoad = onWebPanelSidebarLoad; + panelBrowser.contentWindow.addEventListener('load', onWebPanelSidebarLoad, true); + + emit(self, 'attach', worker); + } + windowNS(window).onWebPanelSidebarCreated = onWebPanelSidebarCreated; + panelBrowser.addEventListener('DOMWindowCreated', onWebPanelSidebarCreated, true); + } + windowNS(window).onSidebarLoad = onSidebarLoad; + sidebar.addEventListener('load', onSidebarLoad, true); // removed properly + }, + onUntrack: function(window) { + if (!isBrowser(window)) + return; + + // hide the sidebar if it is showing + hideSidebar(window, self).catch(() => {}); + + // kill the menu item + let { bar } = windowNS(window); + if (bar) { + removeFromArray(viewsFor(self), bar); + dispose(bar); + } + + // kill listeners + let sidebar = window.document.getElementById('sidebar'); + + if (windowNS(window).onSidebarLoad) { + sidebar && sidebar.removeEventListener('load', windowNS(window).onSidebarLoad, true) + windowNS(window).onSidebarLoad = null; + } + + let panelBrowser = sidebar && sidebar.contentDocument.getElementById(WEB_PANEL_BROWSER_ID); + if (windowNS(window).onWebPanelSidebarCreated) { + panelBrowser && panelBrowser.removeEventListener('DOMWindowCreated', windowNS(window).onWebPanelSidebarCreated, true); + windowNS(window).onWebPanelSidebarCreated = null; + } + + if (windowNS(window).onWebPanelSidebarReady) { + panelBrowser && panelBrowser.contentWindow.removeEventListener('DOMContentLoaded', windowNS(window).onWebPanelSidebarReady, false); + windowNS(window).onWebPanelSidebarReady = null; + } + + if (windowNS(window).onWebPanelSidebarLoad) { + panelBrowser && panelBrowser.contentWindow.removeEventListener('load', windowNS(window).onWebPanelSidebarLoad, true); + windowNS(window).onWebPanelSidebarLoad = null; + } + + if (windowNS(window).onWebPanelSidebarUnload) { + panelBrowser && panelBrowser.contentWindow.removeEventListener('unload', windowNS(window).onWebPanelSidebarUnload, true); + windowNS(window).onWebPanelSidebarUnload(); + } + } + }); + + views.set(this, bars); + + add(sidebars, this); + }, + get id() { + return (modelFor(this) || {}).id; + }, + get title() { + return (modelFor(this) || {}).title; + }, + set title(v) { + // destroyed? + if (!modelFor(this)) + return; + // validation + if (typeof v != 'string') + throw Error('title must be a string'); + validateTitleAndURLCombo(this, v, this.url); + // do update + updateTitle(this, v); + return modelFor(this).title = v; + }, + get url() { + return (modelFor(this) || {}).url; + }, + set url(v) { + // destroyed? + if (!modelFor(this)) + return; + + // validation + if (!isLocalURL(v)) + throw Error('the url must be a valid local url'); + + validateTitleAndURLCombo(this, this.title, v); + + // do update + updateURL(this, v); + modelFor(this).url = v; + }, + show: function(window) { + return showSidebar(viewFor(window), this); + }, + hide: function(window) { + return hideSidebar(viewFor(window), this); + }, + dispose: function() { + const internals = sidebarNS(this); + + off(this); + + remove(sidebars, this); + + // stop tracking windows + if (internals.tracker) { + internals.tracker.unload(); + } + + internals.tracker = null; + internals.windowNS = null; + + views.delete(this); + models.delete(this); + } +}); +exports.Sidebar = Sidebar; + +function validateTitleAndURLCombo(sidebar, title, url) { + url = resolveURL(url); + + if (sidebar.title == title && sidebar.url == url) { + return false; + } + + for (let window of windows(null, { includePrivate: true })) { + let sidebar = window.document.querySelector('menuitem[sidebarurl="' + url + '"][label="' + title + '"]'); + if (sidebar) { + throw Error('The provided title and url combination is invalid (already used).'); + } + } + + return false; +} + +isShowing.define(Sidebar, isSidebarShowing.bind(null, null)); +show.define(Sidebar, showSidebar.bind(null, null)); +hide.define(Sidebar, hideSidebar.bind(null, null)); + +identify.define(Sidebar, function(sidebar) { + return sidebar.id; +}); + +function toggleSidebar(window, sidebar) { + // TODO: make sure this is not private + window = window || getMostRecentBrowserWindow(); + if (isSidebarShowing(window, sidebar)) { + return hideSidebar(window, sidebar); + } + return showSidebar(window, sidebar); +} +toggle.define(Sidebar, toggleSidebar.bind(null, null)); |