Mypal/devtools/client/webide/modules/tab-store.js

179 lines
5.6 KiB
JavaScript

/* 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;
},
};