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