diff --git a/devtools/client/inspector/components/InspectorTabPanel.js b/devtools/client/inspector/components/InspectorTabPanel.js index 677ddf96a74a..020e8d405bc5 100644 --- a/devtools/client/inspector/components/InspectorTabPanel.js +++ b/devtools/client/inspector/components/InspectorTabPanel.js @@ -25,8 +25,10 @@ class InspectorTabPanel extends Component { id: PropTypes.string.isRequired, // Optional prefix for panel IDs. idPrefix: PropTypes.string, - // Optional mount callback + // Optional mount callback. onMount: PropTypes.func, + // Optional unmount callback. + onUnmount: PropTypes.func, }; } @@ -52,6 +54,10 @@ class InspectorTabPanel extends Component { let doc = this.refs.content.ownerDocument; let panels = doc.getElementById("tabpanels"); + if (this.props.onUnmount) { + this.props.onUnmount(this.refs.content, this.props); + } + // Move panel's content node back into list of tab panels. panels.appendChild(this.refs.content.firstChild); } diff --git a/devtools/client/inspector/toolsidebar.js b/devtools/client/inspector/toolsidebar.js index 5afeaf2bad75..056af4a4fa87 100644 --- a/devtools/client/inspector/toolsidebar.js +++ b/devtools/client/inspector/toolsidebar.js @@ -137,6 +137,7 @@ ToolSidebar.prototype = { title: title, url: url, onMount: this.onSidePanelMounted.bind(this), + onUnmount: this.onSidePanelUnmounted.bind(this), }); this.addTab(id, title, panel, selected, index); @@ -163,6 +164,20 @@ ToolSidebar.prototype = { iframe.setAttribute("src", props.url); }, + onSidePanelUnmounted: function (content, props) { + let iframe = content.querySelector("iframe"); + if (!iframe || !iframe.hasAttribute("src")) { + return; + } + + let win = iframe.contentWindow; + if ("destroy" in win) { + win.destroy(this._toolPanel, iframe); + } + + iframe.removeAttribute("src"); + }, + /** * Remove an existing tab. * @param {String} tabId The ID of the tab that was used to register it, or diff --git a/devtools/client/jsonview/components/MainTabbedArea.js b/devtools/client/jsonview/components/MainTabbedArea.js index 431ed4ad80fd..888f2a093c32 100644 --- a/devtools/client/jsonview/components/MainTabbedArea.js +++ b/devtools/client/jsonview/components/MainTabbedArea.js @@ -61,6 +61,7 @@ define(function (require, exports, module) { tabActive: this.state.tabActive, onAfterChange: this.onTabChanged}, TabPanel({ + id: "json", className: "json", title: JSONView.Locale.$STR("jsonViewer.tab.JSON")}, JsonPanel({ @@ -71,6 +72,7 @@ define(function (require, exports, module) { }) ), TabPanel({ + id: "rawdata", className: "rawdata", title: JSONView.Locale.$STR("jsonViewer.tab.RawData")}, TextPanel({ @@ -81,6 +83,7 @@ define(function (require, exports, module) { }) ), TabPanel({ + id: "headers", className: "headers", title: JSONView.Locale.$STR("jsonViewer.tab.Headers")}, HeadersPanel({ diff --git a/devtools/client/shared/components/tabs/Tabs.js b/devtools/client/shared/components/tabs/Tabs.js index 164bfab77da4..03cef3fec673 100644 --- a/devtools/client/shared/components/tabs/Tabs.js +++ b/devtools/client/shared/components/tabs/Tabs.js @@ -72,14 +72,15 @@ define(function (require, exports, module) { this.state = { tabActive: props.tabActive, - // This array is used to store an information whether a tab - // at specific index has already been created (e.g. selected - // at least once). - // If yes, it's rendered even if not currently selected. - // This is because in some cases we don't want to re-create - // tab content when it's being unselected/selected. - // E.g. in case of an iframe being used as a tab-content - // we want the iframe to stay in the DOM. + // This array is used to store an object containing information on whether a tab + // at a specified index has already been created (e.g. selected at least once) and + // the tab id. An example of the object structure is the following: + // [{ isCreated: true, tabId: "ruleview" }, { isCreated: false, tabId: "foo" }]. + // If the tab at the specified index has already been created, it's rendered even + // if not currently selected. This is because in some cases we don't want + // to re-create tab content when it's being unselected/selected. + // E.g. in case of an iframe being used as a tab-content we want the iframe to + // stay in the DOM. created: [], // True if tabs can't fit into available horizontal space. @@ -116,24 +117,46 @@ define(function (require, exports, module) { componentWillReceiveProps(nextProps) { let { children, tabActive } = nextProps; + let panels = children.filter(panel => panel); + let created = [...this.state.created]; - // Check type of 'tabActive' props to see if it's valid - // (it's 0-based index). + // If the children props has changed due to an addition or removal of a tab, + // update the state's created array with the latest tab ids and whether or not + // the tab is already created. + if (this.state.created.length != panels.length) { + created = panels.map(panel => { + // Get whether or not the tab has already been created from the previous state. + let createdEntry = this.state.created.find(entry => { + return entry && entry.tabId === panel.props.id; + }); + let isCreated = !!createdEntry && createdEntry.isCreated; + let tabId = panel.props.id; + + return { + isCreated, + tabId, + }; + }); + } + + // Check type of 'tabActive' props to see if it's valid (it's 0-based index). if (typeof tabActive === "number") { - let panels = children.filter((panel) => panel); - // Reset to index 0 if index overflows the range of panel array tabActive = (tabActive < panels.length && tabActive >= 0) ? tabActive : 0; - let created = [...this.state.created]; - created[tabActive] = true; + created[tabActive] = Object.assign({}, created[tabActive], { + isCreated: true, + }); this.setState({ - created, tabActive, }); } + + this.setState({ + created, + }); } componentWillUnmount() { @@ -209,11 +232,13 @@ define(function (require, exports, module) { } let created = [...this.state.created]; - created[index] = true; + created[index] = Object.assign({}, created[index], { + isCreated: true, + }); let newState = Object.assign({}, this.state, { + created, tabActive: index, - created: created }); this.setState(newState, () => { @@ -335,6 +360,8 @@ define(function (require, exports, module) { } let id = tab.props.id; + let isCreated = this.state.created[index] && + this.state.created[index].isCreated; // Use 'visibility:hidden' + 'height:0' for hiding content of non-selected // tab. It's faster than 'display:none' because it avoids triggering frame @@ -354,13 +381,13 @@ define(function (require, exports, module) { return ( dom.div({ id: id ? id + "-panel" : "panel-" + index, - key: index, + key: id, style: style, className: selected ? "tab-panel-box" : "tab-panel-box hidden", role: "tabpanel", "aria-labelledby": id ? id + "-tab" : "tab-" + index, }, - (selected || this.state.created[index]) ? panel : null + (selected || isCreated) ? panel : null ) ); }); @@ -388,6 +415,8 @@ define(function (require, exports, module) { class Panel extends Component { static get propTypes() { return { + id: PropTypes.string.isRequired, + className: PropTypes.string, title: PropTypes.string.isRequired, children: PropTypes.oneOfType([ PropTypes.array, diff --git a/devtools/client/webconsole/net/components/net-info-body.js b/devtools/client/webconsole/net/components/net-info-body.js index a335b340abf7..d6137d4edfc1 100644 --- a/devtools/client/webconsole/net/components/net-info-body.js +++ b/devtools/client/webconsole/net/components/net-info-body.js @@ -98,6 +98,7 @@ class NetInfoBody extends Component { // Headers tab panels.push( TabPanel({ + id: "headers", className: "headers", key: "headers", title: Locale.$STR("netRequest.headers")}, @@ -109,6 +110,7 @@ class NetInfoBody extends Component { if (hasParams) { panels.push( TabPanel({ + id: "params", className: "params", key: "params", title: Locale.$STR("netRequest.params")}, @@ -121,6 +123,7 @@ class NetInfoBody extends Component { if (hasPostData) { panels.push( TabPanel({ + id: "post", className: "post", key: "post", title: Locale.$STR("netRequest.post")}, @@ -131,7 +134,10 @@ class NetInfoBody extends Component { // Response tab panels.push( - TabPanel({className: "response", key: "response", + TabPanel({ + id: "response", + className: "response", + key: "response", title: Locale.$STR("netRequest.response")}, ResponseTab({data: data, actions: actions}) ) @@ -141,6 +147,7 @@ class NetInfoBody extends Component { if (this.hasCookies()) { panels.push( TabPanel({ + id: "cookies", className: "cookies", key: "cookies", title: Locale.$STR("netRequest.cookies")}, @@ -156,6 +163,7 @@ class NetInfoBody extends Component { if (this.hasStackTrace()) { panels.push( TabPanel({ + id: "stacktrace-tab", className: "stacktrace-tab", key: "stacktrace", title: Locale.$STR("netRequest.callstack")},