addon-sdk/source/lib/sdk/ui/sidebar.js

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:03ea77b183aa
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 'use strict';
5
6 module.metadata = {
7 'stability': 'experimental',
8 'engines': {
9 'Firefox': '*'
10 }
11 };
12
13 const { Class } = require('../core/heritage');
14 const { merge } = require('../util/object');
15 const { Disposable } = require('../core/disposable');
16 const { off, emit, setListeners } = require('../event/core');
17 const { EventTarget } = require('../event/target');
18 const { URL } = require('../url');
19 const { add, remove, has, clear, iterator } = require('../lang/weak-set');
20 const { id: addonID } = require('../self');
21 const { WindowTracker } = require('../deprecated/window-utils');
22 const { isShowing } = require('./sidebar/utils');
23 const { isBrowser, getMostRecentBrowserWindow, windows, isWindowPrivate } = require('../window/utils');
24 const { ns } = require('../core/namespace');
25 const { remove: removeFromArray } = require('../util/array');
26 const { show, hide, toggle } = require('./sidebar/actions');
27 const { Worker } = require('../content/worker');
28 const { contract: sidebarContract } = require('./sidebar/contract');
29 const { create, dispose, updateTitle, updateURL, isSidebarShowing, showSidebar, hideSidebar } = require('./sidebar/view');
30 const { defer } = require('../core/promise');
31 const { models, views, viewsFor, modelFor } = require('./sidebar/namespace');
32 const { isLocalURL } = require('../url');
33 const { ensure } = require('../system/unload');
34 const { identify } = require('./id');
35 const { uuid } = require('../util/uuid');
36
37 const sidebarNS = ns();
38
39 const WEB_PANEL_BROWSER_ID = 'web-panels-browser';
40
41 let sidebars = {};
42
43 const Sidebar = Class({
44 implements: [ Disposable ],
45 extends: EventTarget,
46 setup: function(options) {
47 // inital validation for the model information
48 let model = sidebarContract(options);
49
50 // save the model information
51 models.set(this, model);
52
53 // generate an id if one was not provided
54 model.id = model.id || addonID + '-' + uuid();
55
56 // further validation for the title and url
57 validateTitleAndURLCombo({}, this.title, this.url);
58
59 const self = this;
60 const internals = sidebarNS(self);
61 const windowNS = internals.windowNS = ns();
62
63 // see bug https://bugzilla.mozilla.org/show_bug.cgi?id=886148
64 ensure(this, 'destroy');
65
66 setListeners(this, options);
67
68 let bars = [];
69 internals.tracker = WindowTracker({
70 onTrack: function(window) {
71 if (!isBrowser(window))
72 return;
73
74 let sidebar = window.document.getElementById('sidebar');
75 let sidebarBox = window.document.getElementById('sidebar-box');
76
77 let bar = create(window, {
78 id: self.id,
79 title: self.title,
80 sidebarurl: self.url
81 });
82 bars.push(bar);
83 windowNS(window).bar = bar;
84
85 bar.addEventListener('command', function() {
86 if (isSidebarShowing(window, self)) {
87 hideSidebar(window, self);
88 return;
89 }
90
91 showSidebar(window, self);
92 }, false);
93
94 function onSidebarLoad() {
95 // check if the sidebar is ready
96 let isReady = sidebar.docShell && sidebar.contentDocument;
97 if (!isReady)
98 return;
99
100 // check if it is a web panel
101 let panelBrowser = sidebar.contentDocument.getElementById(WEB_PANEL_BROWSER_ID);
102 if (!panelBrowser) {
103 bar.removeAttribute('checked');
104 return;
105 }
106
107 let sbTitle = window.document.getElementById('sidebar-title');
108 function onWebPanelSidebarCreated() {
109 if (panelBrowser.contentWindow.location != model.url ||
110 sbTitle.value != model.title) {
111 return;
112 }
113
114 let worker = windowNS(window).worker = Worker({
115 window: panelBrowser.contentWindow,
116 injectInDocument: true
117 });
118
119 function onWebPanelSidebarUnload() {
120 windowNS(window).onWebPanelSidebarUnload = null;
121
122 // uncheck the associated menuitem
123 bar.setAttribute('checked', 'false');
124
125 emit(self, 'hide', {});
126 emit(self, 'detach', worker);
127 windowNS(window).worker = null;
128 }
129 windowNS(window).onWebPanelSidebarUnload = onWebPanelSidebarUnload;
130 panelBrowser.contentWindow.addEventListener('unload', onWebPanelSidebarUnload, true);
131
132 // check the associated menuitem
133 bar.setAttribute('checked', 'true');
134
135 function onWebPanelSidebarReady() {
136 panelBrowser.contentWindow.removeEventListener('DOMContentLoaded', onWebPanelSidebarReady, false);
137 windowNS(window).onWebPanelSidebarReady = null;
138
139 emit(self, 'ready', worker);
140 }
141 windowNS(window).onWebPanelSidebarReady = onWebPanelSidebarReady;
142 panelBrowser.contentWindow.addEventListener('DOMContentLoaded', onWebPanelSidebarReady, false);
143
144 function onWebPanelSidebarLoad() {
145 panelBrowser.contentWindow.removeEventListener('load', onWebPanelSidebarLoad, true);
146 windowNS(window).onWebPanelSidebarLoad = null;
147
148 // TODO: decide if returning worker is acceptable..
149 //emit(self, 'show', { worker: worker });
150 emit(self, 'show', {});
151 }
152 windowNS(window).onWebPanelSidebarLoad = onWebPanelSidebarLoad;
153 panelBrowser.contentWindow.addEventListener('load', onWebPanelSidebarLoad, true);
154
155 emit(self, 'attach', worker);
156 }
157 windowNS(window).onWebPanelSidebarCreated = onWebPanelSidebarCreated;
158 panelBrowser.addEventListener('DOMWindowCreated', onWebPanelSidebarCreated, true);
159 }
160 windowNS(window).onSidebarLoad = onSidebarLoad;
161 sidebar.addEventListener('load', onSidebarLoad, true); // removed properly
162 },
163 onUntrack: function(window) {
164 if (!isBrowser(window))
165 return;
166
167 // hide the sidebar if it is showing
168 hideSidebar(window, self);
169
170 // kill the menu item
171 let { bar } = windowNS(window);
172 if (bar) {
173 removeFromArray(viewsFor(self), bar);
174 dispose(bar);
175 }
176
177 // kill listeners
178 let sidebar = window.document.getElementById('sidebar');
179
180 if (windowNS(window).onSidebarLoad) {
181 sidebar && sidebar.removeEventListener('load', windowNS(window).onSidebarLoad, true)
182 windowNS(window).onSidebarLoad = null;
183 }
184
185 let panelBrowser = sidebar && sidebar.contentDocument.getElementById(WEB_PANEL_BROWSER_ID);
186 if (windowNS(window).onWebPanelSidebarCreated) {
187 panelBrowser && panelBrowser.removeEventListener('DOMWindowCreated', windowNS(window).onWebPanelSidebarCreated, true);
188 windowNS(window).onWebPanelSidebarCreated = null;
189 }
190
191 if (windowNS(window).onWebPanelSidebarReady) {
192 panelBrowser && panelBrowser.contentWindow.removeEventListener('DOMContentLoaded', windowNS(window).onWebPanelSidebarReady, false);
193 windowNS(window).onWebPanelSidebarReady = null;
194 }
195
196 if (windowNS(window).onWebPanelSidebarLoad) {
197 panelBrowser && panelBrowser.contentWindow.removeEventListener('load', windowNS(window).onWebPanelSidebarLoad, true);
198 windowNS(window).onWebPanelSidebarLoad = null;
199 }
200
201 if (windowNS(window).onWebPanelSidebarUnload) {
202 panelBrowser && panelBrowser.contentWindow.removeEventListener('unload', windowNS(window).onWebPanelSidebarUnload, true);
203 windowNS(window).onWebPanelSidebarUnload();
204 }
205 }
206 });
207
208 views.set(this, bars);
209
210 add(sidebars, this);
211 },
212 get id() (modelFor(this) || {}).id,
213 get title() (modelFor(this) || {}).title,
214 set title(v) {
215 // destroyed?
216 if (!modelFor(this))
217 return;
218 // validation
219 if (typeof v != 'string')
220 throw Error('title must be a string');
221 validateTitleAndURLCombo(this, v, this.url);
222 // do update
223 updateTitle(this, v);
224 return modelFor(this).title = v;
225 },
226 get url() (modelFor(this) || {}).url,
227 set url(v) {
228 // destroyed?
229 if (!modelFor(this))
230 return;
231
232 // validation
233 if (!isLocalURL(v))
234 throw Error('the url must be a valid local url');
235
236 validateTitleAndURLCombo(this, this.title, v);
237
238 // do update
239 updateURL(this, v);
240 modelFor(this).url = v;
241 },
242 show: function() {
243 return showSidebar(null, this);
244 },
245 hide: function() {
246 return hideSidebar(null, this);
247 },
248 dispose: function() {
249 const internals = sidebarNS(this);
250
251 off(this);
252
253 remove(sidebars, this);
254
255 // stop tracking windows
256 if (internals.tracker) {
257 internals.tracker.unload();
258 }
259
260 internals.tracker = null;
261 internals.windowNS = null;
262
263 views.delete(this);
264 models.delete(this);
265 }
266 });
267 exports.Sidebar = Sidebar;
268
269 function validateTitleAndURLCombo(sidebar, title, url) {
270 if (sidebar.title == title && sidebar.url == url) {
271 return false;
272 }
273
274 for (let window of windows(null, { includePrivate: true })) {
275 let sidebar = window.document.querySelector('menuitem[sidebarurl="' + url + '"][label="' + title + '"]');
276 if (sidebar) {
277 throw Error('The provided title and url combination is invalid (already used).');
278 }
279 }
280
281 return false;
282 }
283
284 isShowing.define(Sidebar, isSidebarShowing.bind(null, null));
285 show.define(Sidebar, showSidebar.bind(null, null));
286 hide.define(Sidebar, hideSidebar.bind(null, null));
287
288 identify.define(Sidebar, function(sidebar) {
289 return sidebar.id;
290 });
291
292 function toggleSidebar(window, sidebar) {
293 // TODO: make sure this is not private
294 window = window || getMostRecentBrowserWindow();
295 if (isSidebarShowing(window, sidebar)) {
296 return hideSidebar(window, sidebar);
297 }
298 return showSidebar(window, sidebar);
299 }
300 toggle.define(Sidebar, toggleSidebar.bind(null, null));

mercurial