1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/addon-sdk/source/lib/sdk/ui/sidebar.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,300 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 +* License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 +'use strict'; 1.8 + 1.9 +module.metadata = { 1.10 + 'stability': 'experimental', 1.11 + 'engines': { 1.12 + 'Firefox': '*' 1.13 + } 1.14 +}; 1.15 + 1.16 +const { Class } = require('../core/heritage'); 1.17 +const { merge } = require('../util/object'); 1.18 +const { Disposable } = require('../core/disposable'); 1.19 +const { off, emit, setListeners } = require('../event/core'); 1.20 +const { EventTarget } = require('../event/target'); 1.21 +const { URL } = require('../url'); 1.22 +const { add, remove, has, clear, iterator } = require('../lang/weak-set'); 1.23 +const { id: addonID } = require('../self'); 1.24 +const { WindowTracker } = require('../deprecated/window-utils'); 1.25 +const { isShowing } = require('./sidebar/utils'); 1.26 +const { isBrowser, getMostRecentBrowserWindow, windows, isWindowPrivate } = require('../window/utils'); 1.27 +const { ns } = require('../core/namespace'); 1.28 +const { remove: removeFromArray } = require('../util/array'); 1.29 +const { show, hide, toggle } = require('./sidebar/actions'); 1.30 +const { Worker } = require('../content/worker'); 1.31 +const { contract: sidebarContract } = require('./sidebar/contract'); 1.32 +const { create, dispose, updateTitle, updateURL, isSidebarShowing, showSidebar, hideSidebar } = require('./sidebar/view'); 1.33 +const { defer } = require('../core/promise'); 1.34 +const { models, views, viewsFor, modelFor } = require('./sidebar/namespace'); 1.35 +const { isLocalURL } = require('../url'); 1.36 +const { ensure } = require('../system/unload'); 1.37 +const { identify } = require('./id'); 1.38 +const { uuid } = require('../util/uuid'); 1.39 + 1.40 +const sidebarNS = ns(); 1.41 + 1.42 +const WEB_PANEL_BROWSER_ID = 'web-panels-browser'; 1.43 + 1.44 +let sidebars = {}; 1.45 + 1.46 +const Sidebar = Class({ 1.47 + implements: [ Disposable ], 1.48 + extends: EventTarget, 1.49 + setup: function(options) { 1.50 + // inital validation for the model information 1.51 + let model = sidebarContract(options); 1.52 + 1.53 + // save the model information 1.54 + models.set(this, model); 1.55 + 1.56 + // generate an id if one was not provided 1.57 + model.id = model.id || addonID + '-' + uuid(); 1.58 + 1.59 + // further validation for the title and url 1.60 + validateTitleAndURLCombo({}, this.title, this.url); 1.61 + 1.62 + const self = this; 1.63 + const internals = sidebarNS(self); 1.64 + const windowNS = internals.windowNS = ns(); 1.65 + 1.66 + // see bug https://bugzilla.mozilla.org/show_bug.cgi?id=886148 1.67 + ensure(this, 'destroy'); 1.68 + 1.69 + setListeners(this, options); 1.70 + 1.71 + let bars = []; 1.72 + internals.tracker = WindowTracker({ 1.73 + onTrack: function(window) { 1.74 + if (!isBrowser(window)) 1.75 + return; 1.76 + 1.77 + let sidebar = window.document.getElementById('sidebar'); 1.78 + let sidebarBox = window.document.getElementById('sidebar-box'); 1.79 + 1.80 + let bar = create(window, { 1.81 + id: self.id, 1.82 + title: self.title, 1.83 + sidebarurl: self.url 1.84 + }); 1.85 + bars.push(bar); 1.86 + windowNS(window).bar = bar; 1.87 + 1.88 + bar.addEventListener('command', function() { 1.89 + if (isSidebarShowing(window, self)) { 1.90 + hideSidebar(window, self); 1.91 + return; 1.92 + } 1.93 + 1.94 + showSidebar(window, self); 1.95 + }, false); 1.96 + 1.97 + function onSidebarLoad() { 1.98 + // check if the sidebar is ready 1.99 + let isReady = sidebar.docShell && sidebar.contentDocument; 1.100 + if (!isReady) 1.101 + return; 1.102 + 1.103 + // check if it is a web panel 1.104 + let panelBrowser = sidebar.contentDocument.getElementById(WEB_PANEL_BROWSER_ID); 1.105 + if (!panelBrowser) { 1.106 + bar.removeAttribute('checked'); 1.107 + return; 1.108 + } 1.109 + 1.110 + let sbTitle = window.document.getElementById('sidebar-title'); 1.111 + function onWebPanelSidebarCreated() { 1.112 + if (panelBrowser.contentWindow.location != model.url || 1.113 + sbTitle.value != model.title) { 1.114 + return; 1.115 + } 1.116 + 1.117 + let worker = windowNS(window).worker = Worker({ 1.118 + window: panelBrowser.contentWindow, 1.119 + injectInDocument: true 1.120 + }); 1.121 + 1.122 + function onWebPanelSidebarUnload() { 1.123 + windowNS(window).onWebPanelSidebarUnload = null; 1.124 + 1.125 + // uncheck the associated menuitem 1.126 + bar.setAttribute('checked', 'false'); 1.127 + 1.128 + emit(self, 'hide', {}); 1.129 + emit(self, 'detach', worker); 1.130 + windowNS(window).worker = null; 1.131 + } 1.132 + windowNS(window).onWebPanelSidebarUnload = onWebPanelSidebarUnload; 1.133 + panelBrowser.contentWindow.addEventListener('unload', onWebPanelSidebarUnload, true); 1.134 + 1.135 + // check the associated menuitem 1.136 + bar.setAttribute('checked', 'true'); 1.137 + 1.138 + function onWebPanelSidebarReady() { 1.139 + panelBrowser.contentWindow.removeEventListener('DOMContentLoaded', onWebPanelSidebarReady, false); 1.140 + windowNS(window).onWebPanelSidebarReady = null; 1.141 + 1.142 + emit(self, 'ready', worker); 1.143 + } 1.144 + windowNS(window).onWebPanelSidebarReady = onWebPanelSidebarReady; 1.145 + panelBrowser.contentWindow.addEventListener('DOMContentLoaded', onWebPanelSidebarReady, false); 1.146 + 1.147 + function onWebPanelSidebarLoad() { 1.148 + panelBrowser.contentWindow.removeEventListener('load', onWebPanelSidebarLoad, true); 1.149 + windowNS(window).onWebPanelSidebarLoad = null; 1.150 + 1.151 + // TODO: decide if returning worker is acceptable.. 1.152 + //emit(self, 'show', { worker: worker }); 1.153 + emit(self, 'show', {}); 1.154 + } 1.155 + windowNS(window).onWebPanelSidebarLoad = onWebPanelSidebarLoad; 1.156 + panelBrowser.contentWindow.addEventListener('load', onWebPanelSidebarLoad, true); 1.157 + 1.158 + emit(self, 'attach', worker); 1.159 + } 1.160 + windowNS(window).onWebPanelSidebarCreated = onWebPanelSidebarCreated; 1.161 + panelBrowser.addEventListener('DOMWindowCreated', onWebPanelSidebarCreated, true); 1.162 + } 1.163 + windowNS(window).onSidebarLoad = onSidebarLoad; 1.164 + sidebar.addEventListener('load', onSidebarLoad, true); // removed properly 1.165 + }, 1.166 + onUntrack: function(window) { 1.167 + if (!isBrowser(window)) 1.168 + return; 1.169 + 1.170 + // hide the sidebar if it is showing 1.171 + hideSidebar(window, self); 1.172 + 1.173 + // kill the menu item 1.174 + let { bar } = windowNS(window); 1.175 + if (bar) { 1.176 + removeFromArray(viewsFor(self), bar); 1.177 + dispose(bar); 1.178 + } 1.179 + 1.180 + // kill listeners 1.181 + let sidebar = window.document.getElementById('sidebar'); 1.182 + 1.183 + if (windowNS(window).onSidebarLoad) { 1.184 + sidebar && sidebar.removeEventListener('load', windowNS(window).onSidebarLoad, true) 1.185 + windowNS(window).onSidebarLoad = null; 1.186 + } 1.187 + 1.188 + let panelBrowser = sidebar && sidebar.contentDocument.getElementById(WEB_PANEL_BROWSER_ID); 1.189 + if (windowNS(window).onWebPanelSidebarCreated) { 1.190 + panelBrowser && panelBrowser.removeEventListener('DOMWindowCreated', windowNS(window).onWebPanelSidebarCreated, true); 1.191 + windowNS(window).onWebPanelSidebarCreated = null; 1.192 + } 1.193 + 1.194 + if (windowNS(window).onWebPanelSidebarReady) { 1.195 + panelBrowser && panelBrowser.contentWindow.removeEventListener('DOMContentLoaded', windowNS(window).onWebPanelSidebarReady, false); 1.196 + windowNS(window).onWebPanelSidebarReady = null; 1.197 + } 1.198 + 1.199 + if (windowNS(window).onWebPanelSidebarLoad) { 1.200 + panelBrowser && panelBrowser.contentWindow.removeEventListener('load', windowNS(window).onWebPanelSidebarLoad, true); 1.201 + windowNS(window).onWebPanelSidebarLoad = null; 1.202 + } 1.203 + 1.204 + if (windowNS(window).onWebPanelSidebarUnload) { 1.205 + panelBrowser && panelBrowser.contentWindow.removeEventListener('unload', windowNS(window).onWebPanelSidebarUnload, true); 1.206 + windowNS(window).onWebPanelSidebarUnload(); 1.207 + } 1.208 + } 1.209 + }); 1.210 + 1.211 + views.set(this, bars); 1.212 + 1.213 + add(sidebars, this); 1.214 + }, 1.215 + get id() (modelFor(this) || {}).id, 1.216 + get title() (modelFor(this) || {}).title, 1.217 + set title(v) { 1.218 + // destroyed? 1.219 + if (!modelFor(this)) 1.220 + return; 1.221 + // validation 1.222 + if (typeof v != 'string') 1.223 + throw Error('title must be a string'); 1.224 + validateTitleAndURLCombo(this, v, this.url); 1.225 + // do update 1.226 + updateTitle(this, v); 1.227 + return modelFor(this).title = v; 1.228 + }, 1.229 + get url() (modelFor(this) || {}).url, 1.230 + set url(v) { 1.231 + // destroyed? 1.232 + if (!modelFor(this)) 1.233 + return; 1.234 + 1.235 + // validation 1.236 + if (!isLocalURL(v)) 1.237 + throw Error('the url must be a valid local url'); 1.238 + 1.239 + validateTitleAndURLCombo(this, this.title, v); 1.240 + 1.241 + // do update 1.242 + updateURL(this, v); 1.243 + modelFor(this).url = v; 1.244 + }, 1.245 + show: function() { 1.246 + return showSidebar(null, this); 1.247 + }, 1.248 + hide: function() { 1.249 + return hideSidebar(null, this); 1.250 + }, 1.251 + dispose: function() { 1.252 + const internals = sidebarNS(this); 1.253 + 1.254 + off(this); 1.255 + 1.256 + remove(sidebars, this); 1.257 + 1.258 + // stop tracking windows 1.259 + if (internals.tracker) { 1.260 + internals.tracker.unload(); 1.261 + } 1.262 + 1.263 + internals.tracker = null; 1.264 + internals.windowNS = null; 1.265 + 1.266 + views.delete(this); 1.267 + models.delete(this); 1.268 + } 1.269 +}); 1.270 +exports.Sidebar = Sidebar; 1.271 + 1.272 +function validateTitleAndURLCombo(sidebar, title, url) { 1.273 + if (sidebar.title == title && sidebar.url == url) { 1.274 + return false; 1.275 + } 1.276 + 1.277 + for (let window of windows(null, { includePrivate: true })) { 1.278 + let sidebar = window.document.querySelector('menuitem[sidebarurl="' + url + '"][label="' + title + '"]'); 1.279 + if (sidebar) { 1.280 + throw Error('The provided title and url combination is invalid (already used).'); 1.281 + } 1.282 + } 1.283 + 1.284 + return false; 1.285 +} 1.286 + 1.287 +isShowing.define(Sidebar, isSidebarShowing.bind(null, null)); 1.288 +show.define(Sidebar, showSidebar.bind(null, null)); 1.289 +hide.define(Sidebar, hideSidebar.bind(null, null)); 1.290 + 1.291 +identify.define(Sidebar, function(sidebar) { 1.292 + return sidebar.id; 1.293 +}); 1.294 + 1.295 +function toggleSidebar(window, sidebar) { 1.296 + // TODO: make sure this is not private 1.297 + window = window || getMostRecentBrowserWindow(); 1.298 + if (isSidebarShowing(window, sidebar)) { 1.299 + return hideSidebar(window, sidebar); 1.300 + } 1.301 + return showSidebar(window, sidebar); 1.302 +} 1.303 +toggle.define(Sidebar, toggleSidebar.bind(null, null));