sidebar.js 9.8 KB

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