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