diff options
Diffstat (limited to 'devtools/client/webide/modules/tab-store.js')
-rw-r--r-- | devtools/client/webide/modules/tab-store.js | 178 |
1 files changed, 178 insertions, 0 deletions
diff --git a/devtools/client/webide/modules/tab-store.js b/devtools/client/webide/modules/tab-store.js new file mode 100644 index 000000000..0fed366cc --- /dev/null +++ b/devtools/client/webide/modules/tab-store.js @@ -0,0 +1,178 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { Cu } = require("chrome"); + +const { TargetFactory } = require("devtools/client/framework/target"); +const EventEmitter = require("devtools/shared/event-emitter"); +const { Connection } = require("devtools/shared/client/connection-manager"); +const promise = require("promise"); +const { Task } = require("devtools/shared/task"); + +const _knownTabStores = new WeakMap(); + +var TabStore; + +module.exports = TabStore = function (connection) { + // If we already know about this connection, + // let's re-use the existing store. + if (_knownTabStores.has(connection)) { + return _knownTabStores.get(connection); + } + + _knownTabStores.set(connection, this); + + EventEmitter.decorate(this); + + this._resetStore(); + + this.destroy = this.destroy.bind(this); + this._onStatusChanged = this._onStatusChanged.bind(this); + + this._connection = connection; + this._connection.once(Connection.Events.DESTROYED, this.destroy); + this._connection.on(Connection.Events.STATUS_CHANGED, this._onStatusChanged); + this._onTabListChanged = this._onTabListChanged.bind(this); + this._onTabNavigated = this._onTabNavigated.bind(this); + this._onStatusChanged(); + return this; +}; + +TabStore.prototype = { + + destroy: function () { + if (this._connection) { + // While this.destroy is bound using .once() above, that event may not + // have occurred when the TabStore client calls destroy, so we + // manually remove it here. + this._connection.off(Connection.Events.DESTROYED, this.destroy); + this._connection.off(Connection.Events.STATUS_CHANGED, this._onStatusChanged); + _knownTabStores.delete(this._connection); + this._connection = null; + } + }, + + _resetStore: function () { + this.response = null; + this.tabs = []; + this._selectedTab = null; + this._selectedTabTargetPromise = null; + }, + + _onStatusChanged: function () { + if (this._connection.status == Connection.Status.CONNECTED) { + // Watch for changes to remote browser tabs + this._connection.client.addListener("tabListChanged", + this._onTabListChanged); + this._connection.client.addListener("tabNavigated", + this._onTabNavigated); + this.listTabs(); + } else { + if (this._connection.client) { + this._connection.client.removeListener("tabListChanged", + this._onTabListChanged); + this._connection.client.removeListener("tabNavigated", + this._onTabNavigated); + } + this._resetStore(); + } + }, + + _onTabListChanged: function () { + this.listTabs().then(() => this.emit("tab-list")) + .catch(console.error); + }, + + _onTabNavigated: function (e, { from, title, url }) { + if (!this._selectedTab || from !== this._selectedTab.actor) { + return; + } + this._selectedTab.url = url; + this._selectedTab.title = title; + this.emit("navigate"); + }, + + listTabs: function () { + if (!this._connection || !this._connection.client) { + return promise.reject(new Error("Can't listTabs, not connected.")); + } + let deferred = promise.defer(); + this._connection.client.listTabs(response => { + if (response.error) { + this._connection.disconnect(); + deferred.reject(response.error); + return; + } + let tabsChanged = JSON.stringify(this.tabs) !== JSON.stringify(response.tabs); + this.response = response; + this.tabs = response.tabs; + this._checkSelectedTab(); + if (tabsChanged) { + this.emit("tab-list"); + } + deferred.resolve(response); + }); + return deferred.promise; + }, + + // TODO: Tab "selection" should really take place by creating a TabProject + // which is the selected project. This should be done as part of the + // project-agnostic work. + _selectedTab: null, + _selectedTabTargetPromise: null, + get selectedTab() { + return this._selectedTab; + }, + set selectedTab(tab) { + if (this._selectedTab === tab) { + return; + } + this._selectedTab = tab; + this._selectedTabTargetPromise = null; + // Attach to the tab to follow navigation events + if (this._selectedTab) { + this.getTargetForTab(); + } + }, + + _checkSelectedTab: function () { + if (!this._selectedTab) { + return; + } + let alive = this.tabs.some(tab => { + return tab.actor === this._selectedTab.actor; + }); + if (!alive) { + this._selectedTab = null; + this._selectedTabTargetPromise = null; + this.emit("closed"); + } + }, + + getTargetForTab: function () { + if (this._selectedTabTargetPromise) { + return this._selectedTabTargetPromise; + } + let store = this; + this._selectedTabTargetPromise = Task.spawn(function* () { + // If you connect to a tab, then detach from it, the root actor may have + // de-listed the actors that belong to the tab. This breaks the toolbox + // if you try to connect to the same tab again. To work around this + // issue, we force a "listTabs" request before connecting to a tab. + yield store.listTabs(); + return TargetFactory.forRemoteTab({ + form: store._selectedTab, + client: store._connection.client, + chrome: false + }); + }); + this._selectedTabTargetPromise.then(target => { + target.once("close", () => { + this._selectedTabTargetPromise = null; + }); + }); + return this._selectedTabTargetPromise; + }, + +}; |