 |
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
5 /**
6 * SidebarUI controls showing and hiding the browser sidebar.
7 *
8 * @note
9 * Some of these methods take a commandID argument - we expect to find a
10 * xul:broadcaster element with the specified ID.
11 * The following attributes on that element may be used and/or modified:
12 * - id (required) the string to match commandID. The convention
13 * is to use this naming scheme: 'view<sidebar-name>Sidebar'.
14 * - sidebarurl (required) specifies the URL to load in this sidebar.
15 * - sidebartitle or label (in that order) specify the title to
16 * display on the sidebar.
17 * - checked indicates whether the sidebar is currently displayed.
18 * Note that toggleSidebar updates this attribute when
19 * it changes the sidebar's visibility.
20 * - group this attribute must be set to "sidebar".
21 */
22 let SidebarUI = {
23 browser: null,
24
25 _box: null,
26 _title: null,
27 _splitter: null,
28
29 init() {
30 this._box = document.getElementById("sidebar-box");
31 this.browser = document.getElementById("sidebar");
32 this._title = document.getElementById("sidebar-title");
33 this._splitter = document.getElementById("sidebar-splitter");
34
35 if (!this.adoptFromWindow(window.opener)) {
36 let commandID = this._box.getAttribute("sidebarcommand");
37 if (commandID) {
38 let command = document.getElementById(commandID);
39 if (command) {
40 this._delayedLoad = true;
41 this._box.hidden = false;
42 this._splitter.hidden = false;
43 command.setAttribute("checked", "true");
44 } else {
45 // Remove the |sidebarcommand| attribute, because the element it
46 // refers to no longer exists, so we should assume this sidebar
47 // panel has been uninstalled. (249883)
48 this._box.removeAttribute("sidebarcommand");
49 }
50 }
51 }
52 },
53
54 uninit() {
55 let enumerator = Services.wm.getEnumerator(null);
56 enumerator.getNext();
57 if (!enumerator.hasMoreElements()) {
58 document.persist("sidebar-box", "sidebarcommand");
59 document.persist("sidebar-box", "width");
60 document.persist("sidebar-box", "src");
61 document.persist("sidebar-title", "value");
62 }
63 },
64
65 /**
66 * Try and adopt the status of the sidebar from another window.
67 * @param {Window} sourceWindow - Window to use as a source for sidebar status.
68 * @return true if we adopted the state, or false if the caller should
69 * initialize the state itself.
70 */
71 adoptFromWindow(sourceWindow) {
72 // No source window, or it being closed, or not chrome, or in a different
73 // private-browsing context means we can't adopt.
74 if (!sourceWindow || sourceWindow.closed ||
75 !sourceWindow.document.documentURIObject.schemeIs("chrome") ||
76 PrivateBrowsingUtils.isWindowPrivate(window) != PrivateBrowsingUtils.isWindowPrivate(sourceWindow)) {
77 return false;
78 }
79
80 // If the opener had a sidebar, open the same sidebar in our window.
81 // The opener can be the hidden window too, if we're coming from the state
82 // where no windows are open, and the hidden window has no sidebar box.
83 let sourceUI = sourceWindow.SidebarUI;
84 if (!sourceUI || !sourceUI._box) {
85 // no source UI or no _box means we also can't adopt the state.
86 return false;
87 }
88 if (sourceUI._box.hidden) {
89 // just hidden means we have adopted the hidden state.
90 return true;
91 }
92
93 let commandID = sourceUI._box.getAttribute("sidebarcommand");
94 let commandElem = document.getElementById(commandID);
95
96 // dynamically generated sidebars will fail this check, but we still
97 // consider it adopted.
98 if (!commandElem) {
99 return true;
100 }
101
102 this._title.setAttribute("value",
103 sourceUI._title.getAttribute("value"));
104 this._box.setAttribute("width", sourceUI._box.boxObject.width);
105
106 this._box.setAttribute("sidebarcommand", commandID);
107 // Note: we're setting 'src' on this._box, which is a <vbox>, not on
108 // the <browser id="sidebar">. This lets us delay the actual load until
109 // delayedStartup().
110 this._box.setAttribute("src", sourceUI.browser.getAttribute("src"));
111 this._delayedLoad = true;
112
113 this._box.hidden = false;
114 this._splitter.hidden = false;
115 commandElem.setAttribute("checked", "true");
116 return true;
117 },
118
119 /**
120 * If loading a sidebar was delayed on startup, start the load now.
121 */
122 startDelayedLoad() {
123 if (!this._delayedLoad) {
124 return;
125 }
126
127 this.browser.setAttribute("src", this._box.getAttribute("src"));
128 },
129
130 /**
131 * Fire a "SidebarFocused" event on the sidebar's |window| to give the sidebar
132 * a chance to adjust focus as needed. An additional event is needed, because
133 * we don't want to focus the sidebar when it's opened on startup or in a new
134 * window, only when the user opens the sidebar.
135 */
136 _fireFocusedEvent() {
137 let event = new CustomEvent("SidebarFocused", {bubbles: true});
138 this.browser.contentWindow.dispatchEvent(event);
139
140 // Run the original function for backwards compatibility.
141 fireSidebarFocusedEvent();
142 },
143
144 /**
145 * True if the sidebar is currently open.
146 */
147 get isOpen() {
148 return !this._box.hidden;
149 },
150
151 /**
152 * The ID of the current sidebar (ie, the ID of the broadcaster being used).
153 * This can be set even if the sidebar is hidden.
154 */
155 get currentID() {
156 return this._box.getAttribute("sidebarcommand");
157 },
158
159 get title() {
160 return this._title.value;
161 },
162
163 set title(value) {
164 this._title.value = value;
165 },
166
167 /**
168 * Toggle the visibility of the sidebar. If the sidebar is hidden or is open
169 * with a different commandID, then the sidebar will be opened using the
170 * specified commandID. Otherwise the sidebar will be hidden.
171 *
172 * @param {string} commandID ID of the xul:broadcaster element to use.
173 * @return {Promise}
174 */
175 toggle(commandID = this.currentID) {
176 if (this.isOpen && commandID == this.currentID) {
177 this.hide();
178 return Promise.resolve();
179 } else {
180 return this.show(commandID);
181 }
182 },
183
184 /**
185 * Show the sidebar, using the parameters from the specified broadcaster.
186 * @see SidebarUI note.
187 *
188 * @param {string} commandID ID of the xul:broadcaster element to use.
189 */
190 show(commandID) {
191 return new Promise((resolve, reject) => {
192 let sidebarBroadcaster = document.getElementById(commandID);
193 if (!sidebarBroadcaster || sidebarBroadcaster.localName != "broadcaster") {
194 reject(new Error("Invalid sidebar broadcaster specified"));
195 return;
196 }
197
198 let broadcasters = document.getElementsByAttribute("group", "sidebar");
199 for (let broadcaster of broadcasters) {
200 // skip elements that observe sidebar broadcasters and random
201 // other elements
202 if (broadcaster.localName != "broadcaster") {
203 continue;
204 }
205
206 if (broadcaster != sidebarBroadcaster) {
207 broadcaster.removeAttribute("checked");
208 } else {
209 sidebarBroadcaster.setAttribute("checked", "true");
210 }
211 }
212
213 this._box.hidden = false;
214 this._splitter.hidden = false;
215
216 this._box.setAttribute("sidebarcommand", sidebarBroadcaster.id);
217
218 let title = sidebarBroadcaster.getAttribute("sidebartitle");
219 if (!title) {
220 title = sidebarBroadcaster.getAttribute("label");
221 }
222 this._title.value = title;
223
224 let url = sidebarBroadcaster.getAttribute("sidebarurl");
225 this.browser.setAttribute("src", url); // kick off async load
226
227 // We set this attribute here in addition to setting it on the <browser>
228 // element itself, because the code in SidebarUI.uninit() persists this
229 // attribute, not the "src" of the <browser id="sidebar">. The reason it
230 // does that is that we want to delay sidebar load a bit when a browser
231 // window opens. See delayedStartup() and SidebarUI.startDelayedLoad().
232 this._box.setAttribute("src", url);
233
234 if (this.browser.contentDocument.location.href != url) {
235 let onLoad = event => {
236 this.browser.removeEventListener("load", onLoad, true);
237
238 // We're handling the 'load' event before it bubbles up to the usual
239 // (non-capturing) event handlers. Let it bubble up before firing the
240 // SidebarFocused event.
241 setTimeout(() => this._fireFocusedEvent(), 0);
242
243 // Run the original function for backwards compatibility.
244 sidebarOnLoad(event);
245
246 resolve();
247 };
248
249 this.browser.addEventListener("load", onLoad, true);
250 } else {
251 // Older code handled this case, so we do it too.
252 this._fireFocusedEvent();
253 resolve();
254 }
255
256 let selBrowser = gBrowser.selectedBrowser;
257 selBrowser.messageManager.sendAsyncMessage("Sidebar:VisibilityChange",
258 {commandID: commandID, isOpen: true}
259 );
260 });
261 },
262
263 /**
264 * Hide the sidebar.
265 */
266 hide() {
267 if (!this.isOpen) {
268 return;
269 }
270
271 let commandID = this._box.getAttribute("sidebarcommand");
272 let sidebarBroadcaster = document.getElementById(commandID);
273
274 if (sidebarBroadcaster.getAttribute("checked") != "true") {
275 return;
276 }
277
278 // Replace the document currently displayed in the sidebar with about:blank
279 // so that we can free memory by unloading the page. We need to explicitly
280 // create a new content viewer because the old one doesn't get destroyed
281 // until about:blank has loaded (which does not happen as long as the
282 // element is hidden).
283 this.browser.setAttribute("src", "about:blank");
284 this.browser.docShell.createAboutBlankContentViewer(null);
285
286 sidebarBroadcaster.removeAttribute("checked");
287 this._box.setAttribute("sidebarcommand", "");
288 this._title.value = "";
289 this._box.hidden = true;
290 this._splitter.hidden = true;
291
292 let selBrowser = gBrowser.selectedBrowser;
293 selBrowser.focus();
294 selBrowser.messageManager.sendAsyncMessage("Sidebar:VisibilityChange",
295 {commandID: commandID, isOpen: false}
296 );
297 },
298 };
299
300 /**
301 * This exists for backards compatibility - it will be called once a sidebar is
302 * ready, following any request to show it.
303 *
304 * @deprecated
305 */
306 function fireSidebarFocusedEvent() {}
307
308 /**
309 * This exists for backards compatibility - it gets called when a sidebar has
310 * been loaded.
311 *
312 * @deprecated
313 */
314 function sidebarOnLoad(event) {}
315
316 /**
317 * This exists for backards compatibility, and is equivilent to
318 * SidebarUI.toggle() without the forceOpen param. With forceOpen set to true,
319 * it is equalivent to SidebarUI.show().
320 *
321 * @deprecated
322 */
323 function toggleSidebar(commandID, forceOpen = false) {
324 Deprecated.warning("toggleSidebar() is deprecated, please use SidebarUI.toggle() or SidebarUI.show() instead",
325 "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/Sidebar");
326
327 if (forceOpen) {
328 SidebarUI.show(commandID);
329 } else {
330 SidebarUI.toggle(commandID);
331 }
332 }
333