toolsidebar.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
  2. /* This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  5. "use strict";
  6. var EventEmitter = require("devtools/shared/event-emitter");
  7. var Telemetry = require("devtools/client/shared/telemetry");
  8. var { Task } = require("devtools/shared/task");
  9. /**
  10. * This object represents replacement for ToolSidebar
  11. * implemented in devtools/client/framework/sidebar.js module
  12. *
  13. * This new component is part of devtools.html aimed at
  14. * removing XUL and use HTML for entire DevTools UI.
  15. * There are currently two implementation of the side bar since
  16. * the `sidebar.js` module (mentioned above) is still used by
  17. * other panels.
  18. * As soon as all panels are using this HTML based
  19. * implementation it can be removed.
  20. */
  21. function ToolSidebar(tabbox, panel, uid, options = {}) {
  22. EventEmitter.decorate(this);
  23. this._tabbox = tabbox;
  24. this._uid = uid;
  25. this._panelDoc = this._tabbox.ownerDocument;
  26. this._toolPanel = panel;
  27. this._options = options;
  28. if (!options.disableTelemetry) {
  29. this._telemetry = new Telemetry();
  30. }
  31. this._tabs = [];
  32. if (this._options.hideTabstripe) {
  33. this._tabbox.setAttribute("hidetabs", "true");
  34. }
  35. this.render();
  36. this._toolPanel.emit("sidebar-created", this);
  37. }
  38. exports.ToolSidebar = ToolSidebar;
  39. ToolSidebar.prototype = {
  40. TABPANEL_ID_PREFIX: "sidebar-panel-",
  41. // React
  42. get React() {
  43. return this._toolPanel.React;
  44. },
  45. get ReactDOM() {
  46. return this._toolPanel.ReactDOM;
  47. },
  48. get browserRequire() {
  49. return this._toolPanel.browserRequire;
  50. },
  51. get InspectorTabPanel() {
  52. return this._toolPanel.InspectorTabPanel;
  53. },
  54. // Rendering
  55. render: function () {
  56. let Tabbar = this.React.createFactory(this.browserRequire(
  57. "devtools/client/shared/components/tabs/tabbar"));
  58. let sidebar = Tabbar({
  59. toolbox: this._toolPanel._toolbox,
  60. showAllTabsMenu: true,
  61. onSelect: this.handleSelectionChange.bind(this),
  62. });
  63. this._tabbar = this.ReactDOM.render(sidebar, this._tabbox);
  64. },
  65. /**
  66. * Register a side-panel tab.
  67. *
  68. * @param {string} tab uniq id
  69. * @param {string} title tab title
  70. * @param {React.Component} panel component. See `InspectorPanelTab` as an example.
  71. * @param {boolean} selected true if the panel should be selected
  72. */
  73. addTab: function (id, title, panel, selected) {
  74. this._tabbar.addTab(id, title, selected, panel);
  75. this.emit("new-tab-registered", id);
  76. },
  77. /**
  78. * Helper API for adding side-panels that use existing DOM nodes
  79. * (defined within inspector.xhtml) as the content.
  80. *
  81. * @param {string} tab uniq id
  82. * @param {string} title tab title
  83. * @param {boolean} selected true if the panel should be selected
  84. */
  85. addExistingTab: function (id, title, selected) {
  86. let panel = this.InspectorTabPanel({
  87. id: id,
  88. idPrefix: this.TABPANEL_ID_PREFIX,
  89. key: id,
  90. title: title,
  91. });
  92. this.addTab(id, title, panel, selected);
  93. },
  94. /**
  95. * Helper API for adding side-panels that use existing <iframe> nodes
  96. * (defined within inspector.xhtml) as the content.
  97. * The document must have a title, which will be used as the name of the tab.
  98. *
  99. * @param {string} tab uniq id
  100. * @param {string} title tab title
  101. * @param {string} url
  102. * @param {boolean} selected true if the panel should be selected
  103. */
  104. addFrameTab: function (id, title, url, selected) {
  105. let panel = this.InspectorTabPanel({
  106. id: id,
  107. idPrefix: this.TABPANEL_ID_PREFIX,
  108. key: id,
  109. title: title,
  110. url: url,
  111. onMount: this.onSidePanelMounted.bind(this),
  112. });
  113. this.addTab(id, title, panel, selected);
  114. },
  115. onSidePanelMounted: function (content, props) {
  116. let iframe = content.querySelector("iframe");
  117. if (!iframe || iframe.getAttribute("src")) {
  118. return;
  119. }
  120. let onIFrameLoaded = (event) => {
  121. iframe.removeEventListener("load", onIFrameLoaded, true);
  122. let doc = event.target;
  123. let win = doc.defaultView;
  124. if ("setPanel" in win) {
  125. win.setPanel(this._toolPanel, iframe);
  126. }
  127. this.emit(props.id + "-ready");
  128. };
  129. iframe.addEventListener("load", onIFrameLoaded, true);
  130. iframe.setAttribute("src", props.url);
  131. },
  132. /**
  133. * Remove an existing tab.
  134. * @param {String} tabId The ID of the tab that was used to register it, or
  135. * the tab id attribute value if the tab existed before the sidebar
  136. * got created.
  137. * @param {String} tabPanelId Optional. If provided, this ID will be used
  138. * instead of the tabId to retrieve and remove the corresponding <tabpanel>
  139. */
  140. removeTab: Task.async(function* (tabId, tabPanelId) {
  141. this._tabbar.removeTab(tabId);
  142. let win = this.getWindowForTab(tabId);
  143. if (win && ("destroy" in win)) {
  144. yield win.destroy();
  145. }
  146. this.emit("tab-unregistered", tabId);
  147. }),
  148. /**
  149. * Show or hide a specific tab.
  150. * @param {Boolean} isVisible True to show the tab/tabpanel, False to hide it.
  151. * @param {String} id The ID of the tab to be hidden.
  152. */
  153. toggleTab: function (isVisible, id) {
  154. this._tabbar.toggleTab(id, isVisible);
  155. },
  156. /**
  157. * Select a specific tab.
  158. */
  159. select: function (id) {
  160. this._tabbar.select(id);
  161. },
  162. /**
  163. * Return the id of the selected tab.
  164. */
  165. getCurrentTabID: function () {
  166. return this._currentTool;
  167. },
  168. /**
  169. * Returns the requested tab panel based on the id.
  170. * @param {String} id
  171. * @return {DOMNode}
  172. */
  173. getTabPanel: function (id) {
  174. // Search with and without the ID prefix as there might have been existing
  175. // tabpanels by the time the sidebar got created
  176. return this._panelDoc.querySelector("#" +
  177. this.TABPANEL_ID_PREFIX + id + ", #" + id);
  178. },
  179. /**
  180. * Event handler.
  181. */
  182. handleSelectionChange: function (id) {
  183. if (this._destroyed) {
  184. return;
  185. }
  186. let previousTool = this._currentTool;
  187. if (previousTool) {
  188. if (this._telemetry) {
  189. this._telemetry.toolClosed(previousTool);
  190. }
  191. this.emit(previousTool + "-unselected");
  192. }
  193. this._currentTool = id;
  194. if (this._telemetry) {
  195. this._telemetry.toolOpened(this._currentTool);
  196. }
  197. this.emit(this._currentTool + "-selected");
  198. this.emit("select", this._currentTool);
  199. },
  200. /**
  201. * Show the sidebar.
  202. *
  203. * @param {String} id
  204. * The sidebar tab id to select.
  205. */
  206. show: function (id) {
  207. this._tabbox.removeAttribute("hidden");
  208. // If an id is given, select the corresponding sidebar tab and record the
  209. // tool opened.
  210. if (id) {
  211. this._currentTool = id;
  212. if (this._telemetry) {
  213. this._telemetry.toolOpened(this._currentTool);
  214. }
  215. }
  216. this.emit("show");
  217. },
  218. /**
  219. * Show the sidebar.
  220. */
  221. hide: function () {
  222. this._tabbox.setAttribute("hidden", "true");
  223. this.emit("hide");
  224. },
  225. /**
  226. * Return the window containing the tab content.
  227. */
  228. getWindowForTab: function (id) {
  229. // Get the tabpanel and make sure it contains an iframe
  230. let panel = this.getTabPanel(id);
  231. if (!panel || !panel.firstElementChild || !panel.firstElementChild.contentWindow) {
  232. return null;
  233. }
  234. return panel.firstElementChild.contentWindow;
  235. },
  236. /**
  237. * Clean-up.
  238. */
  239. destroy: Task.async(function* () {
  240. if (this._destroyed) {
  241. return;
  242. }
  243. this._destroyed = true;
  244. this.emit("destroy");
  245. // Note that we check for the existence of this._tabbox.tabpanels at each
  246. // step as the container window may have been closed by the time one of the
  247. // panel's destroy promise resolves.
  248. let tabpanels = [...this._tabbox.querySelectorAll(".tab-panel-box")];
  249. for (let panel of tabpanels) {
  250. let iframe = panel.querySelector("iframe");
  251. if (!iframe) {
  252. continue;
  253. }
  254. let win = iframe.contentWindow;
  255. if (win && ("destroy" in win)) {
  256. yield win.destroy();
  257. }
  258. panel.remove();
  259. }
  260. if (this._currentTool && this._telemetry) {
  261. this._telemetry.toolClosed(this._currentTool);
  262. }
  263. this._toolPanel.emit("sidebar-destroyed", this);
  264. this._tabs = null;
  265. this._tabbox = null;
  266. this._panelDoc = null;
  267. this._toolPanel = null;
  268. })
  269. };