commit
63847f4d2d
10 changed files with 646 additions and 437 deletions
25
.eslintrc.js
25
.eslintrc.js
|
@ -1,13 +1,13 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 8
|
||||||
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"es6": true,
|
"es6": true,
|
||||||
"node": true,
|
"node": true,
|
||||||
"webextensions": true
|
"webextensions": true
|
||||||
},
|
},
|
||||||
"extends": [
|
|
||||||
"eslint:recommended"
|
|
||||||
],
|
|
||||||
"globals": {
|
"globals": {
|
||||||
"CustomizableUI": true,
|
"CustomizableUI": true,
|
||||||
"CustomizableWidgets": true,
|
"CustomizableWidgets": true,
|
||||||
|
@ -16,7 +16,10 @@ module.exports = {
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"promise",
|
"promise",
|
||||||
"no-unescaped"
|
"no-unsanitized"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended"
|
||||||
],
|
],
|
||||||
"root": true,
|
"root": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
|
@ -29,8 +32,18 @@ module.exports = {
|
||||||
"promise/no-promise-in-callback": "warn",
|
"promise/no-promise-in-callback": "warn",
|
||||||
"promise/no-return-wrap": "error",
|
"promise/no-return-wrap": "error",
|
||||||
"promise/param-names": "error",
|
"promise/param-names": "error",
|
||||||
"no-unescaped/no-key-assignment": "error",
|
|
||||||
"no-unescaped/enforce": "error",
|
"no-unsanitized/method": [
|
||||||
|
"error"
|
||||||
|
],
|
||||||
|
"no-unsanitized/property": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"escape": {
|
||||||
|
"taggedTemplates": ["escaped"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
"eqeqeq": "error",
|
"eqeqeq": "error",
|
||||||
"indent": ["error", 2],
|
"indent": ["error", 2],
|
||||||
|
|
526
index.js
526
index.js
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||||
const DEFAULT_TAB = "about:newtab";
|
const DEFAULT_TAB = "about:newtab";
|
||||||
|
const LOOKUP_KEY = "$ref";
|
||||||
|
|
||||||
const SHOW_MENU_TIMEOUT = 100;
|
const SHOW_MENU_TIMEOUT = 100;
|
||||||
const HIDE_MENU_TIMEOUT = 300;
|
const HIDE_MENU_TIMEOUT = 300;
|
||||||
|
@ -73,41 +74,43 @@ Cu.import("resource:///modules/CustomizableWidgets.jsm");
|
||||||
Cu.import("resource:///modules/sessionstore/SessionStore.jsm");
|
Cu.import("resource:///modules/sessionstore/SessionStore.jsm");
|
||||||
Cu.import("resource://gre/modules/Services.jsm");
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// ContextualIdentityProxy
|
// ContextualIdentityProxy
|
||||||
|
|
||||||
const ContextualIdentityProxy = {
|
const ContextualIdentityProxy = {
|
||||||
getIdentities() {
|
getIdentities() {
|
||||||
|
let response;
|
||||||
if ("getPublicIdentities" in ContextualIdentityService) {
|
if ("getPublicIdentities" in ContextualIdentityService) {
|
||||||
return ContextualIdentityService.getPublicIdentities();
|
response = ContextualIdentityService.getPublicIdentities();
|
||||||
|
} else {
|
||||||
|
response = ContextualIdentityService.getIdentities();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ContextualIdentityService.getIdentities();
|
return response.map((identity) => {
|
||||||
},
|
return this._convert(identity);
|
||||||
|
});
|
||||||
getUserContextLabel(userContextId) {
|
|
||||||
return ContextualIdentityService.getUserContextLabel(userContextId);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getIdentityFromId(userContextId) {
|
getIdentityFromId(userContextId) {
|
||||||
|
let response;
|
||||||
if ("getPublicIdentityFromId" in ContextualIdentityService) {
|
if ("getPublicIdentityFromId" in ContextualIdentityService) {
|
||||||
return ContextualIdentityService.getPublicIdentityFromId(userContextId);
|
response = ContextualIdentityService.getPublicIdentityFromId(userContextId);
|
||||||
|
} else {
|
||||||
|
response = ContextualIdentityService.getIdentityFromId(userContextId);
|
||||||
}
|
}
|
||||||
|
if (response) {
|
||||||
return ContextualIdentityService.getIdentityFromId(userContextId);
|
return this._convert(response);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
create(name, icon, color) {
|
_convert(identity) {
|
||||||
return ContextualIdentityService.create(name, icon, color);
|
return {
|
||||||
|
name: ContextualIdentityService.getUserContextLabel(identity.userContextId),
|
||||||
|
icon: identity.icon,
|
||||||
|
color: identity.color,
|
||||||
|
userContextId: identity.userContextId,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
update(userContextId, name, icon, color) {
|
|
||||||
return ContextualIdentityService.update(userContextId, name, icon, color);
|
|
||||||
},
|
|
||||||
|
|
||||||
remove(userContextId) {
|
|
||||||
return ContextualIdentityService.remove(userContextId);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
@ -119,12 +122,14 @@ const ContainerService = {
|
||||||
_containerWasEnabled: false,
|
_containerWasEnabled: false,
|
||||||
_onBackgroundConnectCallback: null,
|
_onBackgroundConnectCallback: null,
|
||||||
|
|
||||||
init(installation, reason) {
|
async init(installation, reason) {
|
||||||
// If we are just been installed, we must store some information for the
|
// If we are just been installed, we must store some information for the
|
||||||
// uninstallation. This object contains also a version number, in case we
|
// uninstallation. This object contains also a version number, in case we
|
||||||
// need to implement a migration in the future.
|
// need to implement a migration in the future.
|
||||||
// In 1.1.1 and less we deleted savedConfiguration on upgrade so we need to rebuild
|
// In 1.1.1 and less we deleted savedConfiguration on upgrade so we need to rebuild
|
||||||
if (installation && (reason !== "upgrade" || !ss.storage.savedConfiguration)) {
|
if (!("savedConfiguration" in ss.storage) ||
|
||||||
|
!("prefs" in ss.storage.savedConfiguration) ||
|
||||||
|
(installation && reason !== "upgrade")) {
|
||||||
let preInstalledIdentities = []; // eslint-disable-line prefer-const
|
let preInstalledIdentities = []; // eslint-disable-line prefer-const
|
||||||
ContextualIdentityProxy.getIdentities().forEach(identity => {
|
ContextualIdentityProxy.getIdentities().forEach(identity => {
|
||||||
preInstalledIdentities.push(identity.userContextId);
|
preInstalledIdentities.push(identity.userContextId);
|
||||||
|
@ -143,29 +148,40 @@ const ContainerService = {
|
||||||
|
|
||||||
ss.storage.savedConfiguration = object;
|
ss.storage.savedConfiguration = object;
|
||||||
|
|
||||||
// Maybe rename the Banking container.
|
|
||||||
if (prefService.get("privacy.userContext.enabled") !== true) {
|
if (prefService.get("privacy.userContext.enabled") !== true) {
|
||||||
|
// Maybe rename the Banking container.
|
||||||
const identity = ContextualIdentityProxy.getIdentityFromId(3);
|
const identity = ContextualIdentityProxy.getIdentityFromId(3);
|
||||||
if (identity && identity.l10nID === "userContextBanking.label") {
|
if (identity && identity.l10nID === "userContextBanking.label") {
|
||||||
ContextualIdentityProxy.update(identity.userContextId,
|
ContextualIdentityService.update(identity.userContextId,
|
||||||
"Finance",
|
"Finance",
|
||||||
identity.icon,
|
identity.icon,
|
||||||
identity.color);
|
identity.color);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Let's create the default containers in case there are none.
|
// Let's create the default containers in case there are none.
|
||||||
if (prefService.get("privacy.userContext.enabled") !== true &&
|
if (ss.storage.savedConfiguration.preInstalledIdentities.length === 0) {
|
||||||
ss.storage.savedConfiguration.preInstalledIdentities.length === 0) {
|
// Note: we have to create them in this way because there is no way to
|
||||||
// Note: we have to create them in this way because there is no way to
|
// reuse the same ID and the localized strings.
|
||||||
// reuse the same ID and the localized strings.
|
ContextualIdentityService.create("Personal", "fingerprint", "blue");
|
||||||
ContextualIdentityService.create("Personal", "fingerprint", "blue");
|
ContextualIdentityService.create("Work", "briefcase", "orange");
|
||||||
ContextualIdentityService.create("Work", "briefcase", "orange");
|
ContextualIdentityService.create("Finance", "dollar", "green");
|
||||||
ContextualIdentityService.create("Finance", "dollar", "green");
|
ContextualIdentityService.create("Shopping", "cart", "pink");
|
||||||
ContextualIdentityService.create("Shopping", "cart", "pink");
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TOCHECK should this run on all code
|
||||||
|
ContextualIdentityProxy.getIdentities().forEach(identity => {
|
||||||
|
const newIcon = this._fromIconToName(identity.icon);
|
||||||
|
const newColor = this._fromColorToName(identity.color);
|
||||||
|
if (newIcon !== identity.icon || newColor !== identity.color) {
|
||||||
|
ContextualIdentityService.update(identity.userContextId,
|
||||||
|
ContextualIdentityService.getUserContextLabel(identity.userContextId),
|
||||||
|
newIcon,
|
||||||
|
newColor);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Let's see if containers were enabled before this addon.
|
// Let's see if containers were enabled before this addon.
|
||||||
this._containerWasEnabled =
|
this._containerWasEnabled =
|
||||||
ss.storage.savedConfiguration.prefs["privacy.userContext.enabled"];
|
ss.storage.savedConfiguration.prefs["privacy.userContext.enabled"];
|
||||||
|
@ -191,16 +207,14 @@ const ContainerService = {
|
||||||
"sortTabs",
|
"sortTabs",
|
||||||
"getTabs",
|
"getTabs",
|
||||||
"showTab",
|
"showTab",
|
||||||
"openTab",
|
|
||||||
"moveTabsToWindow",
|
"moveTabsToWindow",
|
||||||
"queryIdentities",
|
"queryIdentitiesState",
|
||||||
"getIdentity",
|
"getIdentity",
|
||||||
"createIdentity",
|
|
||||||
"removeIdentity",
|
|
||||||
"updateIdentity",
|
|
||||||
"getPreference",
|
"getPreference",
|
||||||
"sendTelemetryPayload",
|
"sendTelemetryPayload",
|
||||||
"getTheme",
|
"getTheme",
|
||||||
|
"refreshNeeded",
|
||||||
|
"forgetIdentityAndRefresh",
|
||||||
"checkIncompatibleAddons"
|
"checkIncompatibleAddons"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -253,7 +267,8 @@ const ContainerService = {
|
||||||
|
|
||||||
// WebExtension startup
|
// WebExtension startup
|
||||||
|
|
||||||
webExtension.startup().then(api => {
|
try {
|
||||||
|
const api = await webExtension.startup();
|
||||||
api.browser.runtime.onMessage.addListener((message, sender, sendReply) => {
|
api.browser.runtime.onMessage.addListener((message, sender, sendReply) => {
|
||||||
if ("method" in message && methods.indexOf(message.method) !== -1) {
|
if ("method" in message && methods.indexOf(message.method) !== -1) {
|
||||||
sendReply(this[message.method](message));
|
sendReply(this[message.method](message));
|
||||||
|
@ -261,9 +276,9 @@ const ContainerService = {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.registerBackgroundConnection(api);
|
this.registerBackgroundConnection(api);
|
||||||
}).catch(() => {
|
} catch (e) {
|
||||||
throw new Error("WebExtension startup failed. Unable to continue.");
|
throw new Error("WebExtension startup failed. Unable to continue.");
|
||||||
});
|
}
|
||||||
|
|
||||||
this._sendEvent = new Metrics({
|
this._sendEvent = new Metrics({
|
||||||
type: "sdk",
|
type: "sdk",
|
||||||
|
@ -308,7 +323,7 @@ const ContainerService = {
|
||||||
},
|
},
|
||||||
|
|
||||||
registerBackgroundConnection(api) {
|
registerBackgroundConnection(api) {
|
||||||
// This is only used for theme and container deletion notifications
|
// This is only used for theme notifications and new tab
|
||||||
api.browser.runtime.onConnect.addListener((port) => {
|
api.browser.runtime.onConnect.addListener((port) => {
|
||||||
this._onBackgroundConnectCallback = (message, topic) => {
|
this._onBackgroundConnectCallback = (message, topic) => {
|
||||||
port.postMessage({
|
port.postMessage({
|
||||||
|
@ -325,13 +340,14 @@ const ContainerService = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
observe(subject, topic) {
|
async observe(subject, topic) {
|
||||||
if (topic === "lightweight-theme-changed") {
|
if (topic === "lightweight-theme-changed") {
|
||||||
this.getTheme().then((theme) => {
|
try {
|
||||||
|
const theme = await this.getTheme();
|
||||||
this.triggerBackgroundCallback(theme, topic);
|
this.triggerBackgroundCallback(theme, topic);
|
||||||
}).catch(() => {
|
} catch (e) {
|
||||||
throw new Error("Unable to get theme");
|
throw new Error("Unable to get theme");
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -396,18 +412,6 @@ const ContainerService = {
|
||||||
return containersCounts;
|
return containersCounts;
|
||||||
},
|
},
|
||||||
|
|
||||||
_convert(identity) {
|
|
||||||
// Let's convert the known colors to their color names.
|
|
||||||
return {
|
|
||||||
name: ContextualIdentityProxy.getUserContextLabel(identity.userContextId),
|
|
||||||
image: this._fromIconToName(identity.icon),
|
|
||||||
color: this._fromColorToName(identity.color),
|
|
||||||
userContextId: identity.userContextId,
|
|
||||||
hasHiddenTabs: !!this._identitiesState[identity.userContextId].hiddenTabs.length,
|
|
||||||
hasOpenTabs: !!this._identitiesState[identity.userContextId].openTabs
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
// In FF 50-51, the icon is the full path, in 52 and following
|
// In FF 50-51, the icon is the full path, in 52 and following
|
||||||
// releases, we have IDs to be used with a svg file. In this function
|
// releases, we have IDs to be used with a svg file. In this function
|
||||||
// we map URLs to svg IDs.
|
// we map URLs to svg IDs.
|
||||||
|
@ -433,10 +437,6 @@ const ContainerService = {
|
||||||
|
|
||||||
// Helper methods for converting icons to names and names to icons.
|
// Helper methods for converting icons to names and names to icons.
|
||||||
|
|
||||||
_fromNameToIcon(name) {
|
|
||||||
return this._fromNameOrIcon(name, "image", "");
|
|
||||||
},
|
|
||||||
|
|
||||||
_fromIconToName(icon) {
|
_fromIconToName(icon) {
|
||||||
return this._fromNameOrIcon(icon, "name", "circle");
|
return this._fromNameOrIcon(icon, "name", "circle");
|
||||||
},
|
},
|
||||||
|
@ -456,16 +456,31 @@ const ContainerService = {
|
||||||
return parseInt(viewFor(tab).getAttribute("usercontextid") || 0, 10);
|
return parseInt(viewFor(tab).getAttribute("usercontextid") || 0, 10);
|
||||||
},
|
},
|
||||||
|
|
||||||
_createTabObject(tab) {
|
async _createTabObject(tab) {
|
||||||
return { title: tab.title, url: tab.url, id: tab.id, active: true };
|
let url;
|
||||||
|
try {
|
||||||
|
url = await getFavicon(tab.url);
|
||||||
|
} catch (e) {
|
||||||
|
url = "";
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
title: tab.title,
|
||||||
|
url: tab.url,
|
||||||
|
favicon: url,
|
||||||
|
id: tab.id,
|
||||||
|
active: true,
|
||||||
|
pinned: tabsUtils.isPinned(viewFor(tab))
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
_containerTabIterator(userContextId, cb) {
|
_matchTabsByContainer(userContextId) {
|
||||||
for (let tab of tabs) { // eslint-disable-line prefer-const
|
const matchedTabs = [];
|
||||||
|
for (const tab of tabs) {
|
||||||
if (userContextId === this._getUserContextIdFromTab(tab)) {
|
if (userContextId === this._getUserContextIdFromTab(tab)) {
|
||||||
cb(tab);
|
matchedTabs.push(tab);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return matchedTabs;
|
||||||
},
|
},
|
||||||
|
|
||||||
_createIdentityState() {
|
_createIdentityState() {
|
||||||
|
@ -486,10 +501,7 @@ const ContainerService = {
|
||||||
},
|
},
|
||||||
|
|
||||||
_remapTabsFromUserContextId(userContextId) {
|
_remapTabsFromUserContextId(userContextId) {
|
||||||
this._identitiesState[userContextId].openTabs = 0;
|
this._identitiesState[userContextId].openTabs = this._matchTabsByContainer(userContextId).length;
|
||||||
this._containerTabIterator(userContextId, () => {
|
|
||||||
++this._identitiesState[userContextId].openTabs;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_remapTab(tab) {
|
_remapTab(tab) {
|
||||||
|
@ -503,30 +515,25 @@ const ContainerService = {
|
||||||
return userContextId in this._identitiesState;
|
return userContextId in this._identitiesState;
|
||||||
},
|
},
|
||||||
|
|
||||||
_closeTabs(tabsToClose) {
|
async _closeTabs(tabsToClose) {
|
||||||
// We create a new tab only if the current operation closes all the
|
// We create a new tab only if the current operation closes all the
|
||||||
// existing ones.
|
// existing ones.
|
||||||
let promise;
|
if (tabs.length === tabsToClose.length) {
|
||||||
if (tabs.length !== tabsToClose.length) {
|
await this.openTab({});
|
||||||
promise = Promise.resolve(null);
|
|
||||||
} else {
|
|
||||||
promise = this.openTab({});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return promise.then(() => {
|
for (const tab of tabsToClose) {
|
||||||
for (let tab of tabsToClose) { // eslint-disable-line prefer-const
|
// after .close() window is null. Let's take it now.
|
||||||
// after .close() window is null. Let's take it now.
|
const window = viewFor(tab.window);
|
||||||
const window = viewFor(tab.window);
|
|
||||||
|
|
||||||
tab.close();
|
tab.close();
|
||||||
|
|
||||||
// forget about this tab. 0 is the index of the forgotten tab and 0
|
// forget about this tab. 0 is the index of the forgotten tab and 0
|
||||||
// means the last one.
|
// means the last one.
|
||||||
try {
|
try {
|
||||||
SessionStore.forgetClosedTab(window, 0);
|
SessionStore.forgetClosedTab(window, 0);
|
||||||
} catch(e) {} // eslint-disable-line no-empty
|
} catch (e) {} // eslint-disable-line no-empty
|
||||||
}
|
}
|
||||||
}).catch(() => null);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_recentBrowserWindow() {
|
_recentBrowserWindow() {
|
||||||
|
@ -553,6 +560,29 @@ const ContainerService = {
|
||||||
};
|
};
|
||||||
Object.assign(payload, args);
|
Object.assign(payload, args);
|
||||||
|
|
||||||
|
/* This is to masage the data whilst it is still active in the SDK side */
|
||||||
|
const containersCounts = this._containersCounts();
|
||||||
|
Object.keys(payload).forEach((keyName) => {
|
||||||
|
let value = payload[keyName];
|
||||||
|
if (value === LOOKUP_KEY) {
|
||||||
|
switch (keyName) {
|
||||||
|
case "clickedContainerTabCount":
|
||||||
|
value = this._containerTabCount(payload.userContextId);
|
||||||
|
break;
|
||||||
|
case "shownContainersCount":
|
||||||
|
value = containersCounts.shown;
|
||||||
|
break;
|
||||||
|
case "hiddenContainersCount":
|
||||||
|
value = containersCounts.hidden;
|
||||||
|
break;
|
||||||
|
case "totalContainersCount":
|
||||||
|
value = containersCounts.total;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payload[keyName] = value;
|
||||||
|
});
|
||||||
|
|
||||||
this._sendEvent(payload);
|
this._sendEvent(payload);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -573,51 +603,46 @@ const ContainerService = {
|
||||||
|
|
||||||
// Tabs management
|
// Tabs management
|
||||||
|
|
||||||
hideTabs(args) {
|
async hideTabs(args) {
|
||||||
if (!("userContextId" in args)) {
|
if (!("userContextId" in args)) {
|
||||||
return Promise.reject("hideTabs must be called with userContextId argument.");
|
return new Error("hideTabs must be called with userContextId argument.");
|
||||||
}
|
}
|
||||||
|
|
||||||
this._remapTabsIfMissing(args.userContextId);
|
this._remapTabsIfMissing(args.userContextId);
|
||||||
if (!this._isKnownContainer(args.userContextId)) {
|
if (!this._isKnownContainer(args.userContextId)) {
|
||||||
return Promise.resolve(null);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const containersCounts = this._containersCounts();
|
|
||||||
this.sendTelemetryPayload({
|
this.sendTelemetryPayload({
|
||||||
"event": "hide-tabs",
|
"event": "hide-tabs",
|
||||||
"userContextId": args.userContextId,
|
"userContextId": args.userContextId,
|
||||||
"clickedContainerTabCount": this._containerTabCount(args.userContextId),
|
"clickedContainerTabCount": LOOKUP_KEY,
|
||||||
"shownContainersCount": containersCounts.shown,
|
"shownContainersCount": LOOKUP_KEY,
|
||||||
"hiddenContainersCount": containersCounts.hidden,
|
"hiddenContainersCount": LOOKUP_KEY,
|
||||||
"totalContainersCount": containersCounts.total
|
"totalContainersCount": LOOKUP_KEY
|
||||||
});
|
});
|
||||||
|
|
||||||
const tabsToClose = [];
|
const tabsToClose = [];
|
||||||
|
|
||||||
this._containerTabIterator(args.userContextId, tab => {
|
const tabObjects = await Promise.all(this._matchTabsByContainer(args.userContextId).map((tab) => {
|
||||||
const object = this._createTabObject(tab);
|
tabsToClose.push(tab);
|
||||||
|
return this._createTabObject(tab);
|
||||||
|
}));
|
||||||
|
|
||||||
|
tabObjects.forEach((object) => {
|
||||||
// This tab is going to be closed. Let's mark this tabObject as
|
// This tab is going to be closed. Let's mark this tabObject as
|
||||||
// non-active.
|
// non-active.
|
||||||
object.active = false;
|
object.active = false;
|
||||||
|
|
||||||
getFavicon(object.url).then(url => {
|
|
||||||
object.favicon = url;
|
|
||||||
}).catch(() => {
|
|
||||||
object.favicon = "";
|
|
||||||
});
|
|
||||||
|
|
||||||
this._identitiesState[args.userContextId].hiddenTabs.push(object);
|
this._identitiesState[args.userContextId].hiddenTabs.push(object);
|
||||||
tabsToClose.push(tab);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return this._closeTabs(tabsToClose).then(() => {
|
await this._closeTabs(tabsToClose);
|
||||||
return this._syncTabs();
|
|
||||||
});
|
return this._syncTabs();
|
||||||
},
|
},
|
||||||
|
|
||||||
showTabs(args) {
|
async showTabs(args) {
|
||||||
if (!("userContextId" in args)) {
|
if (!("userContextId" in args)) {
|
||||||
return Promise.reject("showTabs must be called with userContextId argument.");
|
return Promise.reject("showTabs must be called with userContextId argument.");
|
||||||
}
|
}
|
||||||
|
@ -627,14 +652,13 @@ const ContainerService = {
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const containersCounts = this._containersCounts();
|
|
||||||
this.sendTelemetryPayload({
|
this.sendTelemetryPayload({
|
||||||
"event": "show-tabs",
|
"event": "show-tabs",
|
||||||
"userContextId": args.userContextId,
|
"userContextId": args.userContextId,
|
||||||
"clickedContainerTabCount": this._containerTabCount(args.userContextId),
|
"clickedContainerTabCount": LOOKUP_KEY,
|
||||||
"shownContainersCount": containersCounts.shown,
|
"shownContainersCount": LOOKUP_KEY,
|
||||||
"hiddenContainersCount": containersCounts.hidden,
|
"hiddenContainersCount": LOOKUP_KEY,
|
||||||
"totalContainersCount": containersCounts.total
|
"totalContainersCount": LOOKUP_KEY
|
||||||
});
|
});
|
||||||
|
|
||||||
const promises = [];
|
const promises = [];
|
||||||
|
@ -647,15 +671,14 @@ const ContainerService = {
|
||||||
userContextId: args.userContextId,
|
userContextId: args.userContextId,
|
||||||
url: object.url,
|
url: object.url,
|
||||||
nofocus: args.nofocus || false,
|
nofocus: args.nofocus || false,
|
||||||
window: args.window || null,
|
pinned: object.pinned,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
this._identitiesState[args.userContextId].hiddenTabs = [];
|
this._identitiesState[args.userContextId].hiddenTabs = [];
|
||||||
|
|
||||||
return Promise.all(promises).then(() => {
|
await Promise.all(promises);
|
||||||
return this._syncTabs();
|
return this._syncTabs();
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
sortTabs() {
|
sortTabs() {
|
||||||
|
@ -685,7 +708,7 @@ const ContainerService = {
|
||||||
|
|
||||||
// Let's collect UCIs/tabs for this window.
|
// Let's collect UCIs/tabs for this window.
|
||||||
const map = new Map;
|
const map = new Map;
|
||||||
for (let tab of tabs) { // eslint-disable-line prefer-const
|
for (const tab of tabs) {
|
||||||
if (pinnedTabs && !tabsUtils.isPinned(tab)) {
|
if (pinnedTabs && !tabsUtils.isPinned(tab)) {
|
||||||
// We don't have, or we already handled all the pinned tabs.
|
// We don't have, or we already handled all the pinned tabs.
|
||||||
break;
|
break;
|
||||||
|
@ -709,44 +732,29 @@ const ContainerService = {
|
||||||
|
|
||||||
// Let's move tabs.
|
// Let's move tabs.
|
||||||
sortMap.forEach(tabs => {
|
sortMap.forEach(tabs => {
|
||||||
for (let tab of tabs) { // eslint-disable-line prefer-const
|
for (const tab of tabs) {
|
||||||
xulWindow.gBrowser.moveTabTo(tab, pos++);
|
xulWindow.gBrowser.moveTabTo(tab, pos++);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getTabs(args) {
|
async getTabs(args) {
|
||||||
if (!("userContextId" in args)) {
|
if (!("userContextId" in args)) {
|
||||||
return Promise.reject("getTabs must be called with userContextId argument.");
|
return new Error("getTabs must be called with userContextId argument.");
|
||||||
}
|
}
|
||||||
|
|
||||||
this._remapTabsIfMissing(args.userContextId);
|
this._remapTabsIfMissing(args.userContextId);
|
||||||
if (!this._isKnownContainer(args.userContextId)) {
|
if (!this._isKnownContainer(args.userContextId)) {
|
||||||
return Promise.resolve([]);
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
const promises = [];
|
||||||
const list = [];
|
this._matchTabsByContainer(args.userContextId).forEach((tab) => {
|
||||||
this._containerTabIterator(args.userContextId, tab => {
|
promises.push(this._createTabObject(tab));
|
||||||
list.push(this._createTabObject(tab));
|
|
||||||
});
|
|
||||||
|
|
||||||
const promises = [];
|
|
||||||
|
|
||||||
for (let object of list) { // eslint-disable-line prefer-const
|
|
||||||
promises.push(getFavicon(object.url).then(url => {
|
|
||||||
object.favicon = url;
|
|
||||||
}).catch(() => {
|
|
||||||
object.favicon = "";
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.all(promises).then(() => {
|
|
||||||
resolve(list.concat(this._identitiesState[args.userContextId].hiddenTabs));
|
|
||||||
}).catch((e) => {
|
|
||||||
reject(e);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const list = await Promise.all(promises);
|
||||||
|
return list.concat(this._identitiesState[args.userContextId].hiddenTabs);
|
||||||
},
|
},
|
||||||
|
|
||||||
showTab(args) {
|
showTab(args) {
|
||||||
|
@ -756,7 +764,7 @@ const ContainerService = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let tab of tabs) { // eslint-disable-line prefer-const
|
for (const tab of tabs) {
|
||||||
if (tab.id === args.tabId) {
|
if (tab.id === args.tabId) {
|
||||||
tab.window.activate();
|
tab.window.activate();
|
||||||
tab.activate();
|
tab.activate();
|
||||||
|
@ -786,11 +794,7 @@ const ContainerService = {
|
||||||
"clickedContainerTabCount": this._containerTabCount(args.userContextId),
|
"clickedContainerTabCount": this._containerTabCount(args.userContextId),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Let's create a list of the tabs.
|
const list = this._matchTabsByContainer(args.userContextId);
|
||||||
const list = [];
|
|
||||||
this._containerTabIterator(args.userContextId, tab => {
|
|
||||||
list.push(tab);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Nothing to do
|
// Nothing to do
|
||||||
if (list.length === 0 &&
|
if (list.length === 0 &&
|
||||||
|
@ -833,149 +837,36 @@ const ContainerService = {
|
||||||
},
|
},
|
||||||
|
|
||||||
openTab(args) {
|
openTab(args) {
|
||||||
return new Promise(resolve => {
|
return this.triggerBackgroundCallback(args, "open-tab");
|
||||||
if ("window" in args && args.window) {
|
|
||||||
resolve(args.window);
|
|
||||||
} else {
|
|
||||||
this._recentBrowserWindow().then(browserWin => {
|
|
||||||
resolve(browserWin);
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
}).then(browserWin => {
|
|
||||||
const userContextId = ("userContextId" in args) ? args.userContextId : 0;
|
|
||||||
const source = ("source" in args) ? args.source : null;
|
|
||||||
const nofocus = ("nofocus" in args) ? args.nofocus : false;
|
|
||||||
|
|
||||||
// Only send telemetry for tabs opened by UI - i.e., not via showTabs
|
|
||||||
if (source && userContextId) {
|
|
||||||
this.sendTelemetryPayload({
|
|
||||||
"event": "open-tab",
|
|
||||||
"eventSource": source,
|
|
||||||
"userContextId": userContextId,
|
|
||||||
"clickedContainerTabCount": this._containerTabCount(userContextId)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let promise;
|
|
||||||
if (userContextId) {
|
|
||||||
promise = this.showTabs(args);
|
|
||||||
} else {
|
|
||||||
promise = Promise.resolve(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return promise.then(() => {
|
|
||||||
const tab = browserWin.gBrowser.addTab(args.url || DEFAULT_TAB, { userContextId });
|
|
||||||
if (!nofocus) {
|
|
||||||
browserWin.gBrowser.selectedTab = tab;
|
|
||||||
browserWin.focusAndSelectUrlBar();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}).catch(() => false);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Identities management
|
// Identities management
|
||||||
|
queryIdentitiesState() {
|
||||||
queryIdentities() {
|
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const identities = [];
|
const identities = {};
|
||||||
|
|
||||||
ContextualIdentityProxy.getIdentities().forEach(identity => {
|
ContextualIdentityProxy.getIdentities().forEach(identity => {
|
||||||
this._remapTabsIfMissing(identity.userContextId);
|
this._remapTabsIfMissing(identity.userContextId);
|
||||||
const convertedIdentity = this._convert(identity);
|
const convertedIdentity = {
|
||||||
identities.push(convertedIdentity);
|
hasHiddenTabs: !!this._identitiesState[identity.userContextId].hiddenTabs.length,
|
||||||
|
hasOpenTabs: !!this._identitiesState[identity.userContextId].openTabs
|
||||||
|
};
|
||||||
|
|
||||||
|
identities[identity.userContextId] = convertedIdentity;
|
||||||
});
|
});
|
||||||
|
|
||||||
resolve(identities);
|
resolve(identities);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getIdentity(args) {
|
queryIdentities() {
|
||||||
if (!("userContextId" in args)) {
|
return new Promise(resolve => {
|
||||||
return Promise.reject("getIdentity must be called with userContextId argument.");
|
const identities = ContextualIdentityProxy.getIdentities();
|
||||||
}
|
identities.forEach(identity => {
|
||||||
|
this._remapTabsIfMissing(identity.userContextId);
|
||||||
|
});
|
||||||
|
|
||||||
const identity = ContextualIdentityProxy.getIdentityFromId(args.userContextId);
|
resolve(identities);
|
||||||
return Promise.resolve(identity ? this._convert(identity) : null);
|
|
||||||
},
|
|
||||||
|
|
||||||
createIdentity(args) {
|
|
||||||
this.sendTelemetryPayload({
|
|
||||||
"event": "add-container",
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let arg of [ "name", "color", "icon"]) { // eslint-disable-line prefer-const
|
|
||||||
if (!(arg in args)) {
|
|
||||||
return Promise.reject("createIdentity must be called with " + arg + " argument.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const color = this._fromNameToColor(args.color);
|
|
||||||
const icon = this._fromNameToIcon(args.icon);
|
|
||||||
|
|
||||||
const identity = ContextualIdentityProxy.create(args.name, icon, color);
|
|
||||||
|
|
||||||
this._identitiesState[identity.userContextId] = this._createIdentityState();
|
|
||||||
|
|
||||||
this._refreshNeeded().then(() => {
|
|
||||||
return this._convert(identity);
|
|
||||||
}).catch(() => {
|
|
||||||
return this._convert(identity);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
updateIdentity(args) {
|
|
||||||
if (!("userContextId" in args)) {
|
|
||||||
return Promise.reject("updateIdentity must be called with userContextId argument.");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sendTelemetryPayload({
|
|
||||||
"event": "edit-container",
|
|
||||||
"userContextId": args.userContextId
|
|
||||||
});
|
|
||||||
|
|
||||||
const identity = ContextualIdentityProxy.getIdentityFromId(args.userContextId);
|
|
||||||
for (let arg of [ "name", "color", "icon"]) { // eslint-disable-line prefer-const
|
|
||||||
if ((arg in args)) {
|
|
||||||
identity[arg] = args[arg];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const color = this._fromNameToColor(identity.color);
|
|
||||||
const icon = this._fromNameToIcon(identity.icon);
|
|
||||||
|
|
||||||
const updated = ContextualIdentityProxy.update(args.userContextId,
|
|
||||||
identity.name,
|
|
||||||
icon, color);
|
|
||||||
|
|
||||||
this._refreshNeeded().then(() => {
|
|
||||||
return updated;
|
|
||||||
}).catch(() => {
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
removeIdentity(args) {
|
|
||||||
const eventName = "delete-container";
|
|
||||||
if (!("userContextId" in args)) {
|
|
||||||
return Promise.reject("removeIdentity must be called with userContextId argument.");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sendTelemetryPayload({
|
|
||||||
"event": eventName,
|
|
||||||
"userContextId": args.userContextId
|
|
||||||
});
|
|
||||||
|
|
||||||
const tabsToClose = [];
|
|
||||||
this._containerTabIterator(args.userContextId, tab => {
|
|
||||||
tabsToClose.push(tab);
|
|
||||||
});
|
|
||||||
|
|
||||||
return this._closeTabs(tabsToClose).then(() => {
|
|
||||||
const removed = ContextualIdentityProxy.remove(args.userContextId);
|
|
||||||
this.triggerBackgroundCallback({userContextId: args.userContextId}, eventName);
|
|
||||||
this._forgetIdentity(args.userContextId);
|
|
||||||
return this._refreshNeeded().then(() => removed );
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1027,7 +918,7 @@ const ContainerService = {
|
||||||
return this._windowMap.get(window);
|
return this._windowMap.get(window);
|
||||||
},
|
},
|
||||||
|
|
||||||
_refreshNeeded() {
|
refreshNeeded() {
|
||||||
return this._configureWindows();
|
return this._configureWindows();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1043,26 +934,25 @@ const ContainerService = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const userContextId = ContainerService._getUserContextIdFromTab(tab);
|
const userContextId = ContainerService._getUserContextIdFromTab(tab);
|
||||||
return ContainerService.getIdentity({userContextId}).then(identity => {
|
const identity = ContextualIdentityProxy.getIdentityFromId(userContextId);
|
||||||
const hbox = viewFor(tab.window).document.getElementById("userContext-icons");
|
const hbox = viewFor(tab.window).document.getElementById("userContext-icons");
|
||||||
|
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
hbox.setAttribute("data-identity-color", "");
|
hbox.setAttribute("data-identity-color", "");
|
||||||
return;
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
hbox.setAttribute("data-identity-color", identity.color);
|
hbox.setAttribute("data-identity-color", identity.color);
|
||||||
|
|
||||||
const label = viewFor(tab.window).document.getElementById("userContext-label");
|
const label = viewFor(tab.window).document.getElementById("userContext-label");
|
||||||
label.setAttribute("value", identity.name);
|
label.setAttribute("value", identity.name);
|
||||||
label.style.color = ContainerService._fromNameToColor(identity.color);
|
label.style.color = ContainerService._fromNameToColor(identity.color);
|
||||||
|
|
||||||
const indicator = viewFor(tab.window).document.getElementById("userContext-indicator");
|
const indicator = viewFor(tab.window).document.getElementById("userContext-indicator");
|
||||||
indicator.setAttribute("data-identity-icon", identity.image);
|
indicator.setAttribute("data-identity-icon", identity.icon);
|
||||||
indicator.style.listStyleImage = "";
|
indicator.style.listStyleImage = "";
|
||||||
}).then(() => {
|
|
||||||
return this._restyleTab(tab);
|
return this._restyleTab(tab);
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_restyleTab(tab) {
|
_restyleTab(tab) {
|
||||||
|
@ -1070,12 +960,11 @@ const ContainerService = {
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
const userContextId = ContainerService._getUserContextIdFromTab(tab);
|
const userContextId = ContainerService._getUserContextIdFromTab(tab);
|
||||||
return ContainerService.getIdentity({userContextId}).then(identity => {
|
const identity = ContextualIdentityProxy.getIdentityFromId(userContextId);
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
return;
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
viewFor(tab).setAttribute("data-identity-color", identity.color);
|
return Promise.resolve(viewFor(tab).setAttribute("data-identity-color", identity.color));
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Uninstallation
|
// Uninstallation
|
||||||
|
@ -1134,7 +1023,7 @@ const ContainerService = {
|
||||||
const preInstalledIdentities = data.preInstalledIdentities;
|
const preInstalledIdentities = data.preInstalledIdentities;
|
||||||
ContextualIdentityProxy.getIdentities().forEach(identity => {
|
ContextualIdentityProxy.getIdentities().forEach(identity => {
|
||||||
if (!preInstalledIdentities.includes(identity.userContextId)) {
|
if (!preInstalledIdentities.includes(identity.userContextId)) {
|
||||||
ContextualIdentityProxy.remove(identity.userContextId);
|
ContextualIdentityService.remove(identity.userContextId);
|
||||||
} else {
|
} else {
|
||||||
// Let's cleanup all the cookies for this container.
|
// Let's cleanup all the cookies for this container.
|
||||||
Services.obs.notifyObservers(null, "clear-origin-attributes-data",
|
Services.obs.notifyObservers(null, "clear-origin-attributes-data",
|
||||||
|
@ -1157,6 +1046,11 @@ const ContainerService = {
|
||||||
// End-Of-Hack
|
// End-Of-Hack
|
||||||
},
|
},
|
||||||
|
|
||||||
|
forgetIdentityAndRefresh(args) {
|
||||||
|
this._forgetIdentity(args.userContextId);
|
||||||
|
return this.refreshNeeded();
|
||||||
|
},
|
||||||
|
|
||||||
_forgetIdentity(userContextId = 0) {
|
_forgetIdentity(userContextId = 0) {
|
||||||
for (let window of windows.browserWindows) { // eslint-disable-line prefer-const
|
for (let window of windows.browserWindows) { // eslint-disable-line prefer-const
|
||||||
window = viewFor(window);
|
window = viewFor(window);
|
||||||
|
@ -1267,7 +1161,7 @@ ContainerWindow.prototype = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_configurePlusButtonMenu() {
|
async _configurePlusButtonMenu() {
|
||||||
const mainPopupSetElement = this._window.document.getElementById("mainPopupSet");
|
const mainPopupSetElement = this._window.document.getElementById("mainPopupSet");
|
||||||
|
|
||||||
// Let's remove all the previous panels.
|
// Let's remove all the previous panels.
|
||||||
|
@ -1294,21 +1188,21 @@ ContainerWindow.prototype = {
|
||||||
this._cleanAllTimeouts();
|
this._cleanAllTimeouts();
|
||||||
});
|
});
|
||||||
|
|
||||||
return ContainerService.queryIdentities().then(identities => {
|
try {
|
||||||
|
const identities = await ContainerService.queryIdentities();
|
||||||
identities.forEach(identity => {
|
identities.forEach(identity => {
|
||||||
const menuItemElement = this._window.document.createElementNS(XUL_NS, "menuitem");
|
const menuItemElement = this._window.document.createElementNS(XUL_NS, "menuitem");
|
||||||
this._panelElement.appendChild(menuItemElement);
|
this._panelElement.appendChild(menuItemElement);
|
||||||
menuItemElement.className = "menuitem-iconic";
|
menuItemElement.className = "menuitem-iconic";
|
||||||
menuItemElement.setAttribute("label", identity.name);
|
menuItemElement.setAttribute("label", identity.name);
|
||||||
menuItemElement.setAttribute("data-usercontextid", identity.userContextId);
|
menuItemElement.setAttribute("data-usercontextid", identity.userContextId);
|
||||||
menuItemElement.setAttribute("data-identity-icon", identity.image);
|
menuItemElement.setAttribute("data-identity-icon", identity.icon);
|
||||||
menuItemElement.setAttribute("data-identity-color", identity.color);
|
menuItemElement.setAttribute("data-identity-color", identity.color);
|
||||||
|
|
||||||
menuItemElement.addEventListener("command", (e) => {
|
menuItemElement.addEventListener("command", (e) => {
|
||||||
ContainerService.openTab({
|
ContainerService.openTab({
|
||||||
userContextId: identity.userContextId,
|
userContextId: identity.userContextId,
|
||||||
source: "tab-bar",
|
source: "tab-bar"
|
||||||
window: this._window,
|
|
||||||
});
|
});
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
});
|
});
|
||||||
|
@ -1321,9 +1215,9 @@ ContainerWindow.prototype = {
|
||||||
|
|
||||||
this._panelElement.appendChild(menuItemElement);
|
this._panelElement.appendChild(menuItemElement);
|
||||||
});
|
});
|
||||||
}).catch(() => {
|
} catch (e) {
|
||||||
this.hidePanel();
|
this.hidePanel();
|
||||||
});
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_configureTabStyle() {
|
_configureTabStyle() {
|
||||||
|
@ -1344,8 +1238,7 @@ ContainerWindow.prototype = {
|
||||||
const userContextId = parseInt(e.target.getAttribute("data-usercontextid"), 10);
|
const userContextId = parseInt(e.target.getAttribute("data-usercontextid"), 10);
|
||||||
ContainerService.openTab({
|
ContainerService.openTab({
|
||||||
userContextId: userContextId,
|
userContextId: userContextId,
|
||||||
source: "file-menu",
|
source: "file-menu"
|
||||||
window: this._window,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -1360,8 +1253,7 @@ ContainerWindow.prototype = {
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
return ContainerService.openTab({
|
return ContainerService.openTab({
|
||||||
userContextId,
|
userContextId,
|
||||||
source: "alltabs-menu",
|
source: "alltabs-menu"
|
||||||
window: this._window,
|
|
||||||
});
|
});
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
});
|
});
|
||||||
|
@ -1463,7 +1355,7 @@ ContainerWindow.prototype = {
|
||||||
menuitem.classList.add("menuitem-iconic");
|
menuitem.classList.add("menuitem-iconic");
|
||||||
menuitem.setAttribute("data-usercontextid", identity.userContextId);
|
menuitem.setAttribute("data-usercontextid", identity.userContextId);
|
||||||
menuitem.setAttribute("data-identity-color", identity.color);
|
menuitem.setAttribute("data-identity-color", identity.color);
|
||||||
menuitem.setAttribute("data-identity-icon", identity.image);
|
menuitem.setAttribute("data-identity-icon", identity.icon);
|
||||||
fragment.appendChild(menuitem);
|
fragment.appendChild(menuitem);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "testpilot-containers",
|
"name": "testpilot-containers",
|
||||||
"title": "Containers Experiment",
|
"title": "Containers Experiment",
|
||||||
"description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.",
|
"description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.",
|
||||||
"version": "2.2.0",
|
"version": "2.3.0",
|
||||||
"author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston",
|
"author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/mozilla/testpilot-containers/issues"
|
"url": "https://github.com/mozilla/testpilot-containers/issues"
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
"addons-linter": "^0.15.14",
|
"addons-linter": "^0.15.14",
|
||||||
"deploy-txp": "^1.0.7",
|
"deploy-txp": "^1.0.7",
|
||||||
"eslint": "^3.17.1",
|
"eslint": "^3.17.1",
|
||||||
"eslint-plugin-no-unescaped": "^1.1.0",
|
"eslint-plugin-no-unsanitized": "^2.0.0",
|
||||||
"eslint-plugin-promise": "^3.4.0",
|
"eslint-plugin-promise": "^3.4.0",
|
||||||
"htmllint-cli": "^0.0.5",
|
"htmllint-cli": "^0.0.5",
|
||||||
"jpm": "^1.2.2",
|
"jpm": "^1.2.2",
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
|
const MAJOR_VERSIONS = ["2.3.0"];
|
||||||
|
const LOOKUP_KEY = "$ref";
|
||||||
|
|
||||||
const assignManager = {
|
const assignManager = {
|
||||||
CLOSEABLE_WINDOWS: new Set([
|
|
||||||
"about:startpage",
|
|
||||||
"about:newtab",
|
|
||||||
"about:home",
|
|
||||||
"about:blank"
|
|
||||||
]),
|
|
||||||
MENU_ASSIGN_ID: "open-in-this-container",
|
MENU_ASSIGN_ID: "open-in-this-container",
|
||||||
MENU_REMOVE_ID: "remove-open-in-this-container",
|
MENU_REMOVE_ID: "remove-open-in-this-container",
|
||||||
storageArea: {
|
storageArea: {
|
||||||
|
@ -58,22 +55,22 @@ const assignManager = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
init() {
|
_neverAsk(m) {
|
||||||
browser.runtime.onMessage.addListener((neverAskMessage) => {
|
const pageUrl = m.pageUrl;
|
||||||
const pageUrl = neverAskMessage.pageUrl;
|
if (m.neverAsk === true) {
|
||||||
if (neverAskMessage.neverAsk === true) {
|
// If we have existing data and for some reason it hasn't been deleted etc lets update it
|
||||||
// If we have existing data and for some reason it hasn't been deleted etc lets update it
|
this.storageArea.get(pageUrl).then((siteSettings) => {
|
||||||
this.storageArea.get(pageUrl).then((siteSettings) => {
|
if (siteSettings) {
|
||||||
if (siteSettings) {
|
siteSettings.neverAsk = true;
|
||||||
siteSettings.neverAsk = true;
|
this.storageArea.set(pageUrl, siteSettings);
|
||||||
this.storageArea.set(pageUrl, siteSettings);
|
}
|
||||||
}
|
}).catch((e) => {
|
||||||
}).catch((e) => {
|
throw e;
|
||||||
throw e;
|
});
|
||||||
});
|
}
|
||||||
}
|
},
|
||||||
});
|
|
||||||
|
|
||||||
|
init() {
|
||||||
browser.contextMenus.onClicked.addListener((info, tab) => {
|
browser.contextMenus.onClicked.addListener((info, tab) => {
|
||||||
const userContextId = this.getUserContextIdFromCookieStore(tab);
|
const userContextId = this.getUserContextIdFromCookieStore(tab);
|
||||||
// Mapping ${URL(info.pageUrl).hostname} to ${userContextId}
|
// Mapping ${URL(info.pageUrl).hostname} to ${userContextId}
|
||||||
|
@ -97,8 +94,7 @@ const assignManager = {
|
||||||
message: `Successfully ${actionName} site to always open in this container`,
|
message: `Successfully ${actionName} site to always open in this container`,
|
||||||
iconUrl: browser.extension.getURL("/img/onboarding-1.png")
|
iconUrl: browser.extension.getURL("/img/onboarding-1.png")
|
||||||
});
|
});
|
||||||
browser.runtime.sendMessage({
|
backgroundLogic.sendTelemetryPayload({
|
||||||
method: "sendTelemetryPayload",
|
|
||||||
event: `${actionName}-container-assignment`,
|
event: `${actionName}-container-assignment`,
|
||||||
userContextId: userContextId,
|
userContextId: userContextId,
|
||||||
});
|
});
|
||||||
|
@ -132,14 +128,14 @@ const assignManager = {
|
||||||
We aim to open the new assigned container tab / warning prompt in it's own tab:
|
We aim to open the new assigned container tab / warning prompt in it's own tab:
|
||||||
- As the history won't span from one container to another it seems most sane to not try and reopen a tab on history.back()
|
- As the history won't span from one container to another it seems most sane to not try and reopen a tab on history.back()
|
||||||
- When users open a new tab themselves we want to make sure we don't end up with three tabs as per: https://github.com/mozilla/testpilot-containers/issues/421
|
- When users open a new tab themselves we want to make sure we don't end up with three tabs as per: https://github.com/mozilla/testpilot-containers/issues/421
|
||||||
If we are coming from an internal url that are used for the new tab page (CLOSEABLE_WINDOWS), we can safely close as user is unlikely losing history
|
If we are coming from an internal url that are used for the new tab page (NEW_TAB_PAGES), we can safely close as user is unlikely losing history
|
||||||
Detecting redirects on "new tab" opening actions is pretty hard as we don't get tab history:
|
Detecting redirects on "new tab" opening actions is pretty hard as we don't get tab history:
|
||||||
- Redirects happen from Short URLs and tracking links that act as a gateway
|
- Redirects happen from Short URLs and tracking links that act as a gateway
|
||||||
- Extensions don't provide a way to history crawl for tabs, we could inject content scripts to do this
|
- Extensions don't provide a way to history crawl for tabs, we could inject content scripts to do this
|
||||||
however they don't run on about:blank so this would likely be just as hacky.
|
however they don't run on about:blank so this would likely be just as hacky.
|
||||||
We capture the time the tab was created and close if it was within the timeout to try to capture pages which haven't had user interaction or history.
|
We capture the time the tab was created and close if it was within the timeout to try to capture pages which haven't had user interaction or history.
|
||||||
*/
|
*/
|
||||||
if (this.CLOSEABLE_WINDOWS.has(tab.url)
|
if (backgroundLogic.NEW_TAB_PAGES.has(tab.url)
|
||||||
|| (messageHandler.lastCreatedTab
|
|| (messageHandler.lastCreatedTab
|
||||||
&& messageHandler.lastCreatedTab.id === tab.id)) {
|
&& messageHandler.lastCreatedTab.id === tab.id)) {
|
||||||
browser.tabs.remove(tab.id);
|
browser.tabs.remove(tab.id);
|
||||||
|
@ -217,20 +213,18 @@ const assignManager = {
|
||||||
const loadPage = browser.extension.getURL("confirm-page.html");
|
const loadPage = browser.extension.getURL("confirm-page.html");
|
||||||
// If the user has explicitly checked "Never Ask Again" on the warning page we will send them straight there
|
// If the user has explicitly checked "Never Ask Again" on the warning page we will send them straight there
|
||||||
if (neverAsk) {
|
if (neverAsk) {
|
||||||
browser.tabs.create({url, cookieStoreId: `firefox-container-${userContextId}`, index});
|
browser.tabs.create({url, cookieStoreId: backgroundLogic.cookieStoreId(userContextId), index});
|
||||||
browser.runtime.sendMessage({
|
backgroundLogic.sendTelemetryPayload({
|
||||||
method: "sendTelemetryPayload",
|
|
||||||
event: "auto-reload-page-in-container",
|
event: "auto-reload-page-in-container",
|
||||||
userContextId: userContextId,
|
userContextId: userContextId,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
browser.runtime.sendMessage({
|
backgroundLogic.sendTelemetryPayload({
|
||||||
method: "sendTelemetryPayload",
|
|
||||||
event: "prompt-to-reload-page-in-container",
|
event: "prompt-to-reload-page-in-container",
|
||||||
userContextId: userContextId,
|
userContextId: userContextId,
|
||||||
});
|
});
|
||||||
const confirmUrl = `${loadPage}?url=${url}`;
|
const confirmUrl = `${loadPage}?url=${url}`;
|
||||||
browser.tabs.create({url: confirmUrl, cookieStoreId: `firefox-container-${userContextId}`, index}).then(() => {
|
browser.tabs.create({url: confirmUrl, cookieStoreId: backgroundLogic.cookieStoreId(userContextId), index}).then(() => {
|
||||||
// We don't want to sync this URL ever nor clutter the users history
|
// We don't want to sync this URL ever nor clutter the users history
|
||||||
browser.history.deleteUrl({url: confirmUrl});
|
browser.history.deleteUrl({url: confirmUrl});
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
|
@ -240,6 +234,119 @@ const assignManager = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const backgroundLogic = {
|
||||||
|
NEW_TAB_PAGES: new Set([
|
||||||
|
"about:startpage",
|
||||||
|
"about:newtab",
|
||||||
|
"about:home",
|
||||||
|
"about:blank"
|
||||||
|
]),
|
||||||
|
|
||||||
|
deleteContainer(userContextId) {
|
||||||
|
this.sendTelemetryPayload({
|
||||||
|
event: "delete-container",
|
||||||
|
userContextId
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeTabsPromise = this._containerTabs(userContextId).then((tabs) => {
|
||||||
|
const tabIds = tabs.map((tab) => tab.id);
|
||||||
|
return browser.tabs.remove(tabIds);
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
removeTabsPromise.then(() => {
|
||||||
|
const removed = browser.contextualIdentities.remove(this.cookieStoreId(userContextId));
|
||||||
|
removed.then(() => {
|
||||||
|
assignManager.deleteContainer(userContextId);
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
method: "forgetIdentityAndRefresh"
|
||||||
|
}).then(() => {
|
||||||
|
resolve({done: true, userContextId});
|
||||||
|
}).catch((e) => {throw e;});
|
||||||
|
}).catch((e) => {throw e;});
|
||||||
|
}).catch((e) => {throw e;});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
createOrUpdateContainer(options) {
|
||||||
|
let donePromise;
|
||||||
|
if (options.userContextId) {
|
||||||
|
donePromise = browser.contextualIdentities.update(
|
||||||
|
this.cookieStoreId(options.userContextId),
|
||||||
|
options.params
|
||||||
|
);
|
||||||
|
this.sendTelemetryPayload({
|
||||||
|
event: "edit-container",
|
||||||
|
userContextId: options.userContextId
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
donePromise = browser.contextualIdentities.create(options.params);
|
||||||
|
this.sendTelemetryPayload({
|
||||||
|
event: "add-container"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return donePromise.then(() => {
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
method: "refreshNeeded"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
openTab(options) {
|
||||||
|
let url = options.url || undefined;
|
||||||
|
const userContextId = ("userContextId" in options) ? options.userContextId : 0;
|
||||||
|
const active = ("nofocus" in options) ? options.nofocus : true;
|
||||||
|
const source = ("source" in options) ? options.source : null;
|
||||||
|
|
||||||
|
// Only send telemetry for tabs opened by UI - i.e., not via showTabs
|
||||||
|
if (source && userContextId) {
|
||||||
|
this.sendTelemetryPayload({
|
||||||
|
"event": "open-tab",
|
||||||
|
"eventSource": source,
|
||||||
|
"userContextId": userContextId,
|
||||||
|
"clickedContainerTabCount": LOOKUP_KEY
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Autofocus url bar will happen in 54: https://bugzilla.mozilla.org/show_bug.cgi?id=1295072
|
||||||
|
|
||||||
|
// We can't open new tab pages, so open a blank tab. Used in tab un-hide
|
||||||
|
if (this.NEW_TAB_PAGES.has(url)) {
|
||||||
|
url = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unhide all hidden tabs
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
method: "showTabs",
|
||||||
|
userContextId: options.userContextId
|
||||||
|
});
|
||||||
|
return browser.tabs.create({
|
||||||
|
url,
|
||||||
|
active,
|
||||||
|
pinned: options.pinned || false,
|
||||||
|
cookieStoreId: backgroundLogic.cookieStoreId(options.userContextId)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
sendTelemetryPayload(message = {}) {
|
||||||
|
if (!message.event) {
|
||||||
|
throw new Error("Missing event name for telemetry");
|
||||||
|
}
|
||||||
|
message.method = "sendTelemetryPayload";
|
||||||
|
browser.runtime.sendMessage(message);
|
||||||
|
},
|
||||||
|
|
||||||
|
cookieStoreId(userContextId) {
|
||||||
|
return `firefox-container-${userContextId}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
_containerTabs(userContextId) {
|
||||||
|
return browser.tabs.query({
|
||||||
|
cookieStoreId: this.cookieStoreId(userContextId)
|
||||||
|
}).catch((e) => {throw e;});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const messageHandler = {
|
const messageHandler = {
|
||||||
// After the timer completes we assume it's a tab the user meant to keep open
|
// After the timer completes we assume it's a tab the user meant to keep open
|
||||||
// We use this to catch redirected tabs that have just opened
|
// We use this to catch redirected tabs that have just opened
|
||||||
|
@ -247,6 +354,28 @@ const messageHandler = {
|
||||||
LAST_CREATED_TAB_TIMER: 2000,
|
LAST_CREATED_TAB_TIMER: 2000,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
// Handles messages from webextension/js/popup.js
|
||||||
|
browser.runtime.onMessage.addListener((m) => {
|
||||||
|
let response;
|
||||||
|
|
||||||
|
switch (m.method) {
|
||||||
|
case "deleteContainer":
|
||||||
|
response = backgroundLogic.deleteContainer(m.message.userContextId);
|
||||||
|
break;
|
||||||
|
case "createOrUpdateContainer":
|
||||||
|
response = backgroundLogic.createOrUpdateContainer(m.message);
|
||||||
|
break;
|
||||||
|
case "openTab":
|
||||||
|
// Same as open-tab for index.js
|
||||||
|
response = backgroundLogic.openTab(m.message);
|
||||||
|
break;
|
||||||
|
case "neverAsk":
|
||||||
|
assignManager._neverAsk(m);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
|
||||||
// Handles messages from index.js
|
// Handles messages from index.js
|
||||||
const port = browser.runtime.connect();
|
const port = browser.runtime.connect();
|
||||||
port.onMessage.addListener(m => {
|
port.onMessage.addListener(m => {
|
||||||
|
@ -254,8 +383,8 @@ const messageHandler = {
|
||||||
case "lightweight-theme-changed":
|
case "lightweight-theme-changed":
|
||||||
themeManager.update(m.message);
|
themeManager.update(m.message);
|
||||||
break;
|
break;
|
||||||
case "delete-container":
|
case "open-tab":
|
||||||
assignManager.deleteContainer(m.message.userContextId);
|
backgroundLogic.openTab(m.message);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled message type: ${m.message}`);
|
throw new Error(`Unhandled message type: ${m.message}`);
|
||||||
|
@ -408,8 +537,7 @@ const tabPageCounter = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (why === "user-closed-tab" && this.counters[tabId].tab) {
|
if (why === "user-closed-tab" && this.counters[tabId].tab) {
|
||||||
browser.runtime.sendMessage({
|
backgroundLogic.sendTelemetryPayload({
|
||||||
method: "sendTelemetryPayload",
|
|
||||||
event: "page-requests-completed-per-tab",
|
event: "page-requests-completed-per-tab",
|
||||||
userContextId: this.counters[tabId].tab.cookieStoreId,
|
userContextId: this.counters[tabId].tab.cookieStoreId,
|
||||||
pageRequestCount: this.counters[tabId].tab.pageRequests
|
pageRequestCount: this.counters[tabId].tab.pageRequests
|
||||||
|
@ -418,8 +546,7 @@ const tabPageCounter = {
|
||||||
// delete both the 'tab' and 'activity' counters
|
// delete both the 'tab' and 'activity' counters
|
||||||
delete this.counters[tabId];
|
delete this.counters[tabId];
|
||||||
} else if (why === "user-went-idle" && this.counters[tabId].activity) {
|
} else if (why === "user-went-idle" && this.counters[tabId].activity) {
|
||||||
browser.runtime.sendMessage({
|
backgroundLogic.sendTelemetryPayload({
|
||||||
method: "sendTelemetryPayload",
|
|
||||||
event: "page-requests-completed-per-activity",
|
event: "page-requests-completed-per-activity",
|
||||||
userContextId: this.counters[tabId].activity.cookieStoreId,
|
userContextId: this.counters[tabId].activity.cookieStoreId,
|
||||||
pageRequestCount: this.counters[tabId].activity.pageRequests
|
pageRequestCount: this.counters[tabId].activity.pageRequests
|
||||||
|
@ -471,3 +598,22 @@ function disableAddon(tabId) {
|
||||||
browser.browserAction.disable(tabId);
|
browser.browserAction.disable(tabId);
|
||||||
browser.browserAction.setTitle({ tabId, title: "Containers disabled in Private Browsing Mode" });
|
browser.browserAction.setTitle({ tabId, title: "Containers disabled in Private Browsing Mode" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getExtensionInfo() {
|
||||||
|
const manifestPath = browser.extension.getURL("manifest.json");
|
||||||
|
const response = await fetch(manifestPath);
|
||||||
|
const extensionInfo = await response.json();
|
||||||
|
return extensionInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function displayBrowserActionBadge() {
|
||||||
|
const extensionInfo = await getExtensionInfo();
|
||||||
|
const storage = await browser.storage.local.get({browserActionBadgesClicked: []});
|
||||||
|
|
||||||
|
if (MAJOR_VERSIONS.indexOf(extensionInfo.version) > -1 &&
|
||||||
|
storage.browserActionBadgesClicked.indexOf(extensionInfo.version) < 0) {
|
||||||
|
browser.browserAction.setBadgeBackgroundColor({color: "rgba(0,217,0,255)"});
|
||||||
|
browser.browserAction.setBadgeText({text: "NEW"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
displayBrowserActionBadge();
|
||||||
|
|
|
@ -130,11 +130,13 @@ table {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.primary:hover {
|
.button.primary:hover,
|
||||||
|
.button.primary:focus {
|
||||||
background-color: #0675d3;
|
background-color: #0675d3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.secondary:hover {
|
.button.secondary:hover,
|
||||||
|
.button.secondary:focus {
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,7 +200,8 @@ table {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-back-arrow:hover {
|
.panel-back-arrow:hover,
|
||||||
|
.panel-back-arrow:focus {
|
||||||
background: #dedede;
|
background: #dedede;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,7 +252,8 @@ table {
|
||||||
transition: background-color 75ms;
|
transition: background-color 75ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.onboarding-button:hover {
|
.onboarding-button:hover,
|
||||||
|
.onboarding-button:active {
|
||||||
background-color: #0675d3;
|
background-color: #0675d3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,15 +268,23 @@ manage things like container crud */
|
||||||
}
|
}
|
||||||
|
|
||||||
.pop-button:hover,
|
.pop-button:hover,
|
||||||
|
.pop-button:focus,
|
||||||
|
.panel-footer-secondary:focus,
|
||||||
.panel-footer-secondary:hover {
|
.panel-footer-secondary:hover {
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pop-button:active,
|
.pop-button:focus,
|
||||||
.panel-footer-secondary:active {
|
.panel-footer-secondary:focus {
|
||||||
background-color: rgba(0, 0, 0, 0.08);
|
background-color: rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pop-button a,
|
||||||
|
.panel-footer a,
|
||||||
|
.panel-footer-secondary a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.pop-button-image {
|
.pop-button-image {
|
||||||
block-size: 20px;
|
block-size: 20px;
|
||||||
flex: 0 0 20px;
|
flex: 0 0 20px;
|
||||||
|
@ -350,7 +362,8 @@ span ~ .panel-header-text {
|
||||||
transition: background-color 75ms;
|
transition: background-color 75ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clickable.userContext-wrapper:hover {
|
.container-panel-row:hover .clickable.userContext-wrapper,
|
||||||
|
.container-panel-row:focus .clickable.userContext-wrapper {
|
||||||
background: #f2f2f2;
|
background: #f2f2f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -360,7 +373,6 @@ span ~ .panel-header-text {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .userContext-icon is used natively, Bug 1333811 was raised to fix */
|
/* .userContext-icon is used natively, Bug 1333811 was raised to fix */
|
||||||
.userContext-icon,
|
|
||||||
.usercontext-icon {
|
.usercontext-icon {
|
||||||
background-image: var(--identity-icon);
|
background-image: var(--identity-icon);
|
||||||
background-position: center center;
|
background-position: center center;
|
||||||
|
@ -372,8 +384,8 @@ span ~ .panel-header-text {
|
||||||
flex: 0 0 48px;
|
flex: 0 0 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clickable:hover .userContext-icon,
|
.container-panel-row:hover .clickable .usercontext-icon,
|
||||||
.clickable:hover .usercontext-icon {
|
.container-panel-row:focus .clickable .usercontext-icon {
|
||||||
background-image: url('/img/container-newtab.svg');
|
background-image: url('/img/container-newtab.svg');
|
||||||
fill: 'gray';
|
fill: 'gray';
|
||||||
filter: url('/img/filters.svg#fill');
|
filter: url('/img/filters.svg#fill');
|
||||||
|
@ -482,7 +494,8 @@ span ~ .panel-header-text {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clickable:hover {
|
.clickable:hover,
|
||||||
|
.clickable:focus {
|
||||||
background-color: #ebebeb;
|
background-color: #ebebeb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
BIN
webextension/img/onboarding-4.png
Normal file
BIN
webextension/img/onboarding-4.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
|
@ -9,6 +9,7 @@ document.getElementById("redirect-form").addEventListener("submit", (e) => {
|
||||||
// Sending neverAsk message to background to store for next time we see this process
|
// Sending neverAsk message to background to store for next time we see this process
|
||||||
if (neverAsk) {
|
if (neverAsk) {
|
||||||
browser.runtime.sendMessage({
|
browser.runtime.sendMessage({
|
||||||
|
method: "neverAsk",
|
||||||
neverAsk: true,
|
neverAsk: true,
|
||||||
pageUrl: redirectUrl
|
pageUrl: redirectUrl
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
|
|
@ -12,6 +12,7 @@ const DEFAULT_ICON = "circle";
|
||||||
const P_ONBOARDING_1 = "onboarding1";
|
const P_ONBOARDING_1 = "onboarding1";
|
||||||
const P_ONBOARDING_2 = "onboarding2";
|
const P_ONBOARDING_2 = "onboarding2";
|
||||||
const P_ONBOARDING_3 = "onboarding3";
|
const P_ONBOARDING_3 = "onboarding3";
|
||||||
|
const P_ONBOARDING_4 = "onboarding4";
|
||||||
const P_CONTAINERS_LIST = "containersList";
|
const P_CONTAINERS_LIST = "containersList";
|
||||||
const P_CONTAINERS_EDIT = "containersEdit";
|
const P_CONTAINERS_EDIT = "containersEdit";
|
||||||
const P_CONTAINER_INFO = "containerInfo";
|
const P_CONTAINER_INFO = "containerInfo";
|
||||||
|
@ -54,6 +55,13 @@ function escaped(strings, ...values) {
|
||||||
return result.join("");
|
return result.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getExtensionInfo() {
|
||||||
|
const manifestPath = browser.extension.getURL("manifest.json");
|
||||||
|
const response = await fetch(manifestPath);
|
||||||
|
const extensionInfo = await response.json();
|
||||||
|
return extensionInfo;
|
||||||
|
}
|
||||||
|
|
||||||
// This object controls all the panels, identities and many other things.
|
// This object controls all the panels, identities and many other things.
|
||||||
const Logic = {
|
const Logic = {
|
||||||
_identities: [],
|
_identities: [],
|
||||||
|
@ -63,14 +71,19 @@ const Logic = {
|
||||||
_panels: {},
|
_panels: {},
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
// Remove browserAction "upgraded" badge when opening panel
|
||||||
|
this.clearBrowserActionBadge();
|
||||||
|
|
||||||
// Retrieve the list of identities.
|
// Retrieve the list of identities.
|
||||||
this.refreshIdentities()
|
this.refreshIdentities()
|
||||||
|
|
||||||
// Routing to the correct panel.
|
// Routing to the correct panel.
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// If localStorage is disabled, we don't show the onboarding.
|
// If localStorage is disabled, we don't show the onboarding.
|
||||||
if (!localStorage || localStorage.getItem("onboarded3")) {
|
if (!localStorage || localStorage.getItem("onboarded4")) {
|
||||||
this.showPanel(P_CONTAINERS_LIST);
|
this.showPanel(P_CONTAINERS_LIST);
|
||||||
|
} else if (localStorage.getItem("onboarded3")) {
|
||||||
|
this.showPanel(P_ONBOARDING_4);
|
||||||
} else if (localStorage.getItem("onboarded2")) {
|
} else if (localStorage.getItem("onboarded2")) {
|
||||||
this.showPanel(P_ONBOARDING_3);
|
this.showPanel(P_ONBOARDING_3);
|
||||||
} else if (localStorage.getItem("onboarded1")) {
|
} else if (localStorage.getItem("onboarded1")) {
|
||||||
|
@ -85,15 +98,47 @@ const Logic = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
refreshIdentities() {
|
async clearBrowserActionBadge() {
|
||||||
return browser.runtime.sendMessage({
|
const extensionInfo = await getExtensionInfo();
|
||||||
method: "queryIdentities"
|
const storage = await browser.storage.local.get({browserActionBadgesClicked: []});
|
||||||
})
|
browser.browserAction.setBadgeBackgroundColor({color: ""});
|
||||||
.then(identities => {
|
browser.browserAction.setBadgeText({text: ""});
|
||||||
this._identities = identities;
|
storage.browserActionBadgesClicked.push(extensionInfo.version);
|
||||||
|
browser.storage.local.set({browserActionBadgesClicked: storage.browserActionBadgesClicked});
|
||||||
|
},
|
||||||
|
|
||||||
|
addEnterHandler(element, handler) {
|
||||||
|
element.addEventListener("click", handler);
|
||||||
|
element.addEventListener("keydown", (e) => {
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
handler(e);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
userContextId(cookieStoreId = "") {
|
||||||
|
const userContextId = cookieStoreId.replace("firefox-container-", "");
|
||||||
|
return (userContextId !== cookieStoreId) ? Number(userContextId) : false;
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshIdentities() {
|
||||||
|
return Promise.all([
|
||||||
|
browser.contextualIdentities.query({}),
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
method: "queryIdentitiesState"
|
||||||
|
})
|
||||||
|
]).then(([identities, state]) => {
|
||||||
|
this._identities = identities.map((identity) => {
|
||||||
|
const stateObject = state[Logic.userContextId(identity.cookieStoreId)];
|
||||||
|
if (stateObject) {
|
||||||
|
identity.hasOpenTabs = stateObject.hasOpenTabs;
|
||||||
|
identity.hasHiddenTabs = stateObject.hasHiddenTabs;
|
||||||
|
}
|
||||||
|
return identity;
|
||||||
|
});
|
||||||
|
}).catch((e) => {throw e;});
|
||||||
|
},
|
||||||
|
|
||||||
showPanel(panel, currentIdentity = null) {
|
showPanel(panel, currentIdentity = null) {
|
||||||
// Invalid panel... ?!?
|
// Invalid panel... ?!?
|
||||||
if (!(panel in this._panels)) {
|
if (!(panel in this._panels)) {
|
||||||
|
@ -141,6 +186,25 @@ const Logic = {
|
||||||
return this._currentIdentity;
|
return this._currentIdentity;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
sendTelemetryPayload(message = {}) {
|
||||||
|
if (!message.event) {
|
||||||
|
throw new Error("Missing event name for telemetry");
|
||||||
|
}
|
||||||
|
message.method = "sendTelemetryPayload";
|
||||||
|
browser.runtime.sendMessage(message);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeIdentity(userContextId) {
|
||||||
|
if (!userContextId) {
|
||||||
|
return Promise.reject("removeIdentity must be called with userContextId argument.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return browser.runtime.sendMessage({
|
||||||
|
method: "deleteContainer",
|
||||||
|
message: {userContextId}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
generateIdentityName() {
|
generateIdentityName() {
|
||||||
const defaultName = "Container #";
|
const defaultName = "Container #";
|
||||||
const ids = [];
|
const ids = [];
|
||||||
|
@ -173,7 +237,7 @@ Logic.registerPanel(P_ONBOARDING_1, {
|
||||||
// This method is called when the object is registered.
|
// This method is called when the object is registered.
|
||||||
initialize() {
|
initialize() {
|
||||||
// Let's move to the next panel.
|
// Let's move to the next panel.
|
||||||
document.querySelector("#onboarding-start-button").addEventListener("click", () => {
|
Logic.addEnterHandler(document.querySelector("#onboarding-start-button"), () => {
|
||||||
localStorage.setItem("onboarded1", true);
|
localStorage.setItem("onboarded1", true);
|
||||||
Logic.showPanel(P_ONBOARDING_2);
|
Logic.showPanel(P_ONBOARDING_2);
|
||||||
});
|
});
|
||||||
|
@ -194,7 +258,7 @@ Logic.registerPanel(P_ONBOARDING_2, {
|
||||||
// This method is called when the object is registered.
|
// This method is called when the object is registered.
|
||||||
initialize() {
|
initialize() {
|
||||||
// Let's move to the containers list panel.
|
// Let's move to the containers list panel.
|
||||||
document.querySelector("#onboarding-next-button").addEventListener("click", () => {
|
Logic.addEnterHandler(document.querySelector("#onboarding-next-button"), () => {
|
||||||
localStorage.setItem("onboarded2", true);
|
localStorage.setItem("onboarded2", true);
|
||||||
Logic.showPanel(P_ONBOARDING_3);
|
Logic.showPanel(P_ONBOARDING_3);
|
||||||
});
|
});
|
||||||
|
@ -215,8 +279,29 @@ Logic.registerPanel(P_ONBOARDING_3, {
|
||||||
// This method is called when the object is registered.
|
// This method is called when the object is registered.
|
||||||
initialize() {
|
initialize() {
|
||||||
// Let's move to the containers list panel.
|
// Let's move to the containers list panel.
|
||||||
document.querySelector("#onboarding-done-button").addEventListener("click", () => {
|
Logic.addEnterHandler(document.querySelector("#onboarding-almost-done-button"), () => {
|
||||||
localStorage.setItem("onboarded3", true);
|
localStorage.setItem("onboarded3", true);
|
||||||
|
Logic.showPanel(P_ONBOARDING_4);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// This method is called when the panel is shown.
|
||||||
|
prepare() {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// P_ONBOARDING_4: Fourth page for Onboarding.
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Logic.registerPanel(P_ONBOARDING_4, {
|
||||||
|
panelSelector: ".onboarding-panel-4",
|
||||||
|
|
||||||
|
// This method is called when the object is registered.
|
||||||
|
initialize() {
|
||||||
|
// Let's move to the containers list panel.
|
||||||
|
document.querySelector("#onboarding-done-button").addEventListener("click", () => {
|
||||||
|
localStorage.setItem("onboarded4", true);
|
||||||
Logic.showPanel(P_CONTAINERS_LIST);
|
Logic.showPanel(P_CONTAINERS_LIST);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -235,19 +320,18 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||||
|
|
||||||
// This method is called when the object is registered.
|
// This method is called when the object is registered.
|
||||||
initialize() {
|
initialize() {
|
||||||
document.querySelector("#container-add-link").addEventListener("click", () => {
|
Logic.addEnterHandler(document.querySelector("#container-add-link"), () => {
|
||||||
Logic.showPanel(P_CONTAINER_EDIT, { name: Logic.generateIdentityName() });
|
Logic.showPanel(P_CONTAINER_EDIT, { name: Logic.generateIdentityName() });
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelector("#edit-containers-link").addEventListener("click", () => {
|
Logic.addEnterHandler(document.querySelector("#edit-containers-link"), () => {
|
||||||
browser.runtime.sendMessage({
|
Logic.sendTelemetryPayload({
|
||||||
method: "sendTelemetryPayload",
|
|
||||||
event: "edit-containers"
|
event: "edit-containers"
|
||||||
});
|
});
|
||||||
Logic.showPanel(P_CONTAINERS_EDIT);
|
Logic.showPanel(P_CONTAINERS_EDIT);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelector("#sort-containers-link").addEventListener("click", () => {
|
Logic.addEnterHandler(document.querySelector("#sort-containers-link"), () => {
|
||||||
browser.runtime.sendMessage({
|
browser.runtime.sendMessage({
|
||||||
method: "sortTabs"
|
method: "sortTabs"
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
@ -256,6 +340,30 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||||
window.close();
|
window.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
const element = document.activeElement;
|
||||||
|
function next() {
|
||||||
|
const nextElement = element.nextElementSibling;
|
||||||
|
if (nextElement) {
|
||||||
|
nextElement.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function previous() {
|
||||||
|
const previousElement = element.previousElementSibling;
|
||||||
|
if (previousElement) {
|
||||||
|
previousElement.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (e.keyCode) {
|
||||||
|
case 40:
|
||||||
|
next();
|
||||||
|
break;
|
||||||
|
case 38:
|
||||||
|
previous();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// This method is called when the panel is shown.
|
// This method is called when the panel is shown.
|
||||||
|
@ -269,12 +377,15 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||||
const manage = document.createElement("td");
|
const manage = document.createElement("td");
|
||||||
|
|
||||||
tr.classList.add("container-panel-row");
|
tr.classList.add("container-panel-row");
|
||||||
|
|
||||||
|
tr.setAttribute("tabindex", "0");
|
||||||
|
|
||||||
context.classList.add("userContext-wrapper", "open-newtab", "clickable");
|
context.classList.add("userContext-wrapper", "open-newtab", "clickable");
|
||||||
manage.classList.add("show-tabs", "pop-button");
|
manage.classList.add("show-tabs", "pop-button");
|
||||||
context.innerHTML = escaped`
|
context.innerHTML = escaped`
|
||||||
<div class="userContext-icon-wrapper open-newtab">
|
<div class="userContext-icon-wrapper open-newtab">
|
||||||
<div class="userContext-icon"
|
<div class="usercontext-icon"
|
||||||
data-identity-icon="${identity.image}"
|
data-identity-icon="${identity.icon}"
|
||||||
data-identity-color="${identity.color}">
|
data-identity-color="${identity.color}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -290,12 +401,16 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||||
tr.appendChild(manage);
|
tr.appendChild(manage);
|
||||||
}
|
}
|
||||||
|
|
||||||
tr.addEventListener("click", e => {
|
Logic.addEnterHandler(tr, e => {
|
||||||
if (e.target.matches(".open-newtab") || e.target.parentNode.matches(".open-newtab")) {
|
if (e.target.matches(".open-newtab")
|
||||||
|
|| e.target.parentNode.matches(".open-newtab")
|
||||||
|
|| e.type === "keydown") {
|
||||||
browser.runtime.sendMessage({
|
browser.runtime.sendMessage({
|
||||||
method: "openTab",
|
method: "openTab",
|
||||||
userContextId: identity.userContextId,
|
message: {
|
||||||
source: "pop-up"
|
userContextId: Logic.userContextId(identity.cookieStoreId),
|
||||||
|
source: "pop-up"
|
||||||
|
}
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
window.close();
|
window.close();
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
|
@ -311,6 +426,12 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
||||||
|
|
||||||
list.innerHTML = "";
|
list.innerHTML = "";
|
||||||
list.appendChild(fragment);
|
list.appendChild(fragment);
|
||||||
|
/* Not sure why extensions require a focus for the doorhanger,
|
||||||
|
however it allows us to have a tabindex before the first selected item
|
||||||
|
*/
|
||||||
|
document.addEventListener("focus", () => {
|
||||||
|
list.querySelector("tr").focus();
|
||||||
|
});
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
|
@ -324,15 +445,15 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
||||||
|
|
||||||
// This method is called when the object is registered.
|
// This method is called when the object is registered.
|
||||||
initialize() {
|
initialize() {
|
||||||
document.querySelector("#close-container-info-panel").addEventListener("click", () => {
|
Logic.addEnterHandler(document.querySelector("#close-container-info-panel"), () => {
|
||||||
Logic.showPreviousPanel();
|
Logic.showPreviousPanel();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelector("#container-info-hideorshow").addEventListener("click", () => {
|
Logic.addEnterHandler(document.querySelector("#container-info-hideorshow"), () => {
|
||||||
const identity = Logic.currentIdentity();
|
const identity = Logic.currentIdentity();
|
||||||
browser.runtime.sendMessage({
|
browser.runtime.sendMessage({
|
||||||
method: identity.hasHiddenTabs ? "showTabs" : "hideTabs",
|
method: identity.hasHiddenTabs ? "showTabs" : "hideTabs",
|
||||||
userContextId: identity.userContextId
|
userContextId: Logic.userContextId(identity.cookieStoreId)
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
window.close();
|
window.close();
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
|
@ -359,13 +480,13 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
||||||
|
|
||||||
moveTabsEl.parentNode.insertBefore(fragment, moveTabsEl.nextSibling);
|
moveTabsEl.parentNode.insertBefore(fragment, moveTabsEl.nextSibling);
|
||||||
} else {
|
} else {
|
||||||
moveTabsEl.addEventListener("click", () => {
|
Logic.addEnterHandler(moveTabsEl, () => {
|
||||||
return browser.runtime.sendMessage({
|
browser.runtime.sendMessage({
|
||||||
method: "moveTabsToWindow",
|
method: "moveTabsToWindow",
|
||||||
userContextId: Logic.currentIdentity().userContextId,
|
userContextId: Logic.userContextId(Logic.currentIdentity().cookieStoreId),
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
window.close();
|
window.close();
|
||||||
});
|
}).catch((e) => { throw e; });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
|
@ -381,7 +502,7 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
||||||
document.getElementById("container-info-name").textContent = identity.name;
|
document.getElementById("container-info-name").textContent = identity.name;
|
||||||
|
|
||||||
const icon = document.getElementById("container-info-icon");
|
const icon = document.getElementById("container-info-icon");
|
||||||
icon.setAttribute("data-identity-icon", identity.image);
|
icon.setAttribute("data-identity-icon", identity.icon);
|
||||||
icon.setAttribute("data-identity-color", identity.color);
|
icon.setAttribute("data-identity-color", identity.color);
|
||||||
|
|
||||||
// Show or not the has-tabs section.
|
// Show or not the has-tabs section.
|
||||||
|
@ -404,7 +525,7 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
||||||
// Let's retrieve the list of tabs.
|
// Let's retrieve the list of tabs.
|
||||||
return browser.runtime.sendMessage({
|
return browser.runtime.sendMessage({
|
||||||
method: "getTabs",
|
method: "getTabs",
|
||||||
userContextId: identity.userContextId,
|
userContextId: Logic.userContextId(identity.cookieStoreId),
|
||||||
}).then(this.buildInfoTable);
|
}).then(this.buildInfoTable);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -422,7 +543,7 @@ Logic.registerPanel(P_CONTAINER_INFO, {
|
||||||
// On click, we activate this tab. But only if this tab is active.
|
// On click, we activate this tab. But only if this tab is active.
|
||||||
if (tab.active) {
|
if (tab.active) {
|
||||||
tr.classList.add("clickable");
|
tr.classList.add("clickable");
|
||||||
tr.addEventListener("click", () => {
|
Logic.addEnterHandler(tr, () => {
|
||||||
browser.runtime.sendMessage({
|
browser.runtime.sendMessage({
|
||||||
method: "showTab",
|
method: "showTab",
|
||||||
tabId: tab.id,
|
tabId: tab.id,
|
||||||
|
@ -447,7 +568,7 @@ Logic.registerPanel(P_CONTAINERS_EDIT, {
|
||||||
|
|
||||||
// This method is called when the object is registered.
|
// This method is called when the object is registered.
|
||||||
initialize() {
|
initialize() {
|
||||||
document.querySelector("#exit-edit-mode-link").addEventListener("click", () => {
|
Logic.addEnterHandler(document.querySelector("#exit-edit-mode-link"), () => {
|
||||||
Logic.showPanel(P_CONTAINERS_LIST);
|
Logic.showPanel(P_CONTAINERS_LIST);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -462,8 +583,8 @@ Logic.registerPanel(P_CONTAINERS_EDIT, {
|
||||||
tr.innerHTML = escaped`
|
tr.innerHTML = escaped`
|
||||||
<td class="userContext-wrapper">
|
<td class="userContext-wrapper">
|
||||||
<div class="userContext-icon-wrapper">
|
<div class="userContext-icon-wrapper">
|
||||||
<div class="userContext-icon"
|
<div class="usercontext-icon"
|
||||||
data-identity-icon="${identity.image}"
|
data-identity-icon="${identity.icon}"
|
||||||
data-identity-color="${identity.color}">
|
data-identity-color="${identity.color}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -485,7 +606,7 @@ Logic.registerPanel(P_CONTAINERS_EDIT, {
|
||||||
tr.querySelector(".remove-container .pop-button-image").setAttribute("title", `Edit ${identity.name} container`);
|
tr.querySelector(".remove-container .pop-button-image").setAttribute("title", `Edit ${identity.name} container`);
|
||||||
|
|
||||||
|
|
||||||
tr.addEventListener("click", e => {
|
Logic.addEnterHandler(tr, e => {
|
||||||
if (e.target.matches(".edit-container-icon") || e.target.parentNode.matches(".edit-container-icon")) {
|
if (e.target.matches(".edit-container-icon") || e.target.parentNode.matches(".edit-container-icon")) {
|
||||||
Logic.showPanel(P_CONTAINER_EDIT, identity);
|
Logic.showPanel(P_CONTAINER_EDIT, identity);
|
||||||
} else if (e.target.matches(".delete-container-icon") || e.target.parentNode.matches(".delete-container-icon")) {
|
} else if (e.target.matches(".delete-container-icon") || e.target.parentNode.matches(".delete-container-icon")) {
|
||||||
|
@ -513,17 +634,17 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
||||||
initialize() {
|
initialize() {
|
||||||
this.initializeRadioButtons();
|
this.initializeRadioButtons();
|
||||||
|
|
||||||
document.querySelector("#edit-container-panel-back-arrow").addEventListener("click", () => {
|
Logic.addEnterHandler(document.querySelector("#edit-container-panel-back-arrow"), () => {
|
||||||
Logic.showPreviousPanel();
|
Logic.showPreviousPanel();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelector("#edit-container-cancel-link").addEventListener("click", () => {
|
Logic.addEnterHandler(document.querySelector("#edit-container-cancel-link"), () => {
|
||||||
Logic.showPreviousPanel();
|
Logic.showPreviousPanel();
|
||||||
});
|
});
|
||||||
|
|
||||||
this._editForm = document.getElementById("edit-container-panel-form");
|
this._editForm = document.getElementById("edit-container-panel-form");
|
||||||
const editLink = document.querySelector("#edit-container-ok-link");
|
const editLink = document.querySelector("#edit-container-ok-link");
|
||||||
editLink.addEventListener("click", this._submitForm.bind(this));
|
Logic.addEnterHandler(editLink, this._submitForm.bind(this));
|
||||||
editLink.addEventListener("submit", this._submitForm.bind(this));
|
editLink.addEventListener("submit", this._submitForm.bind(this));
|
||||||
this._editForm.addEventListener("submit", this._submitForm.bind(this));
|
this._editForm.addEventListener("submit", this._submitForm.bind(this));
|
||||||
},
|
},
|
||||||
|
@ -531,12 +652,16 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
||||||
_submitForm() {
|
_submitForm() {
|
||||||
const identity = Logic.currentIdentity();
|
const identity = Logic.currentIdentity();
|
||||||
const formValues = new FormData(this._editForm);
|
const formValues = new FormData(this._editForm);
|
||||||
browser.runtime.sendMessage({
|
return browser.runtime.sendMessage({
|
||||||
method: identity.userContextId ? "updateIdentity" : "createIdentity",
|
method: "createOrUpdateContainer",
|
||||||
userContextId: identity.userContextId || 0,
|
message: {
|
||||||
name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(),
|
userContextId: Logic.userContextId(identity.cookieStoreId) || false,
|
||||||
icon: formValues.get("container-icon") || DEFAULT_ICON,
|
params: {
|
||||||
color: formValues.get("container-color") || DEFAULT_COLOR,
|
name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(),
|
||||||
|
icon: formValues.get("container-icon") || DEFAULT_ICON,
|
||||||
|
color: formValues.get("container-color") || DEFAULT_COLOR,
|
||||||
|
}
|
||||||
|
}
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
return Logic.refreshIdentities();
|
return Logic.refreshIdentities();
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
@ -555,7 +680,7 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
||||||
const colorRadioFieldset = document.getElementById("edit-container-panel-choose-color");
|
const colorRadioFieldset = document.getElementById("edit-container-panel-choose-color");
|
||||||
colors.forEach((containerColor) => {
|
colors.forEach((containerColor) => {
|
||||||
const templateInstance = document.createElement("span");
|
const templateInstance = document.createElement("span");
|
||||||
// eslint-disable-next-line no-unescaped/enforce
|
// eslint-disable-next-line no-unsanitized/property
|
||||||
templateInstance.innerHTML = colorRadioTemplate(containerColor);
|
templateInstance.innerHTML = colorRadioTemplate(containerColor);
|
||||||
colorRadioFieldset.appendChild(templateInstance);
|
colorRadioFieldset.appendChild(templateInstance);
|
||||||
});
|
});
|
||||||
|
@ -568,7 +693,7 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
||||||
const iconRadioFieldset = document.getElementById("edit-container-panel-choose-icon");
|
const iconRadioFieldset = document.getElementById("edit-container-panel-choose-icon");
|
||||||
icons.forEach((containerIcon) => {
|
icons.forEach((containerIcon) => {
|
||||||
const templateInstance = document.createElement("span");
|
const templateInstance = document.createElement("span");
|
||||||
// eslint-disable-next-line no-unescaped/enforce
|
// eslint-disable-next-line no-unsanitized/property
|
||||||
templateInstance.innerHTML = iconRadioTemplate(containerIcon);
|
templateInstance.innerHTML = iconRadioTemplate(containerIcon);
|
||||||
iconRadioFieldset.appendChild(templateInstance);
|
iconRadioFieldset.appendChild(templateInstance);
|
||||||
});
|
});
|
||||||
|
@ -582,7 +707,7 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
||||||
colorInput.checked = colorInput.value === identity.color;
|
colorInput.checked = colorInput.value === identity.color;
|
||||||
});
|
});
|
||||||
[...document.querySelectorAll("[name='container-icon']")].forEach(iconInput => {
|
[...document.querySelectorAll("[name='container-icon']")].forEach(iconInput => {
|
||||||
iconInput.checked = iconInput.value === identity.image;
|
iconInput.checked = iconInput.value === identity.icon;
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
|
@ -598,15 +723,17 @@ Logic.registerPanel(P_CONTAINER_DELETE, {
|
||||||
|
|
||||||
// This method is called when the object is registered.
|
// This method is called when the object is registered.
|
||||||
initialize() {
|
initialize() {
|
||||||
document.querySelector("#delete-container-cancel-link").addEventListener("click", () => {
|
Logic.addEnterHandler(document.querySelector("#delete-container-cancel-link"), () => {
|
||||||
Logic.showPreviousPanel();
|
Logic.showPreviousPanel();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelector("#delete-container-ok-link").addEventListener("click", () => {
|
Logic.addEnterHandler(document.querySelector("#delete-container-ok-link"), () => {
|
||||||
browser.runtime.sendMessage({
|
/* This promise wont resolve if the last tab was removed from the window.
|
||||||
method: "removeIdentity",
|
as the message async callback stops listening, this isn't an issue for us however it might be in future
|
||||||
userContextId: Logic.currentIdentity().userContextId,
|
if you want to do anything post delete do it in the background script.
|
||||||
}).then(() => {
|
Browser console currently warns about not listening also.
|
||||||
|
*/
|
||||||
|
Logic.removeIdentity(Logic.userContextId(Logic.currentIdentity().cookieStoreId)).then(() => {
|
||||||
return Logic.refreshIdentities();
|
return Logic.refreshIdentities();
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
Logic.showPreviousPanel();
|
Logic.showPreviousPanel();
|
||||||
|
@ -624,7 +751,7 @@ Logic.registerPanel(P_CONTAINER_DELETE, {
|
||||||
document.getElementById("delete-container-name").textContent = identity.name;
|
document.getElementById("delete-container-name").textContent = identity.name;
|
||||||
|
|
||||||
const icon = document.getElementById("delete-container-icon");
|
const icon = document.getElementById("delete-container-icon");
|
||||||
icon.setAttribute("data-identity-icon", identity.image);
|
icon.setAttribute("data-identity-icon", identity.icon);
|
||||||
icon.setAttribute("data-identity-color", identity.color);
|
icon.setAttribute("data-identity-color", identity.color);
|
||||||
|
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "Containers Experiment",
|
"name": "Containers Experiment",
|
||||||
"version": "2.2.0",
|
"version": "2.3.0",
|
||||||
|
|
||||||
"description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.",
|
"description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.",
|
||||||
"icons": {
|
"icons": {
|
||||||
|
@ -23,6 +23,7 @@
|
||||||
"activeTab",
|
"activeTab",
|
||||||
"cookies",
|
"cookies",
|
||||||
"contextMenus",
|
"contextMenus",
|
||||||
|
"contextualIdentities",
|
||||||
"history",
|
"history",
|
||||||
"idle",
|
"idle",
|
||||||
"notifications",
|
"notifications",
|
||||||
|
@ -32,6 +33,15 @@
|
||||||
"webRequest"
|
"webRequest"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
"commands": {
|
||||||
|
"_execute_browser_action": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Ctrl+Y"
|
||||||
|
},
|
||||||
|
"description": "Open containers panel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
"browser_action": {
|
"browser_action": {
|
||||||
"browser_style": true,
|
"browser_style": true,
|
||||||
"default_icon": {
|
"default_icon": {
|
||||||
|
|
|
@ -27,31 +27,38 @@
|
||||||
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-3.png" />
|
<img class="onboarding-img" alt="How Containers Work" src="/img/onboarding-3.png" />
|
||||||
<h3 class="onboarding-title">A place for everything, and everything in its place.</h3>
|
<h3 class="onboarding-title">A place for everything, and everything in its place.</h3>
|
||||||
<p>Start with the containers we've created, or create your own.</p>
|
<p>Start with the containers we've created, or create your own.</p>
|
||||||
|
<a href="#" id="onboarding-almost-done-button" class="onboarding-button">Next</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel onboarding onboarding-panel-4 hide" id="onboarding-panel-4">
|
||||||
|
<img class="onboarding-img" alt="How to assign sites to containers" src="/img/onboarding-4.png" />
|
||||||
|
<h3 class="onboarding-title">Always open sites in the containers you want.</h3>
|
||||||
|
<p>Right-click inside a container tab to assign the site to always open in the container.</p>
|
||||||
<a href="#" id="onboarding-done-button" class="onboarding-button">Done</a>
|
<a href="#" id="onboarding-done-button" class="onboarding-button">Done</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel container-panel hide" id="container-panel">
|
<div class="panel container-panel hide" id="container-panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h3 class="panel-header-text">Containers</h3>
|
<h3 class="panel-header-text">Containers</h3>
|
||||||
<a class="pop-button" id="sort-containers-link"><img class="pop-button-image" alt="Sort Containers" title="Sort Containers" src="/img/container-sort.svg"></a>
|
<a href="#" class="pop-button" id="sort-containers-link"><img class="pop-button-image" alt="Sort Containers" title="Sort Containers" src="/img/container-sort.svg"></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="scrollable panel-content">
|
<div class="scrollable panel-content" tabindex="-1">
|
||||||
<table>
|
<table>
|
||||||
<tbody class="identities-list"></tbody>
|
<tbody class="identities-list"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-footer edit-identities">
|
<div class="panel-footer edit-identities">
|
||||||
<div class="edit-containers-text panel-footer-secondary">
|
<div class="edit-containers-text panel-footer-secondary">
|
||||||
<a id="edit-containers-link">Edit Containers</a>
|
<a href="#" tabindex="0" id="edit-containers-link">Edit Containers</a>
|
||||||
</div>
|
</div>
|
||||||
<a class="add-container-link pop-button" id="container-add-link">
|
<a href="#" tabindex="0" class="add-container-link pop-button" id="container-add-link">
|
||||||
<img class="pop-button-image-small icon" alt="Create new container icon" title="Create new container" src="/img/container-add.svg" />
|
<img class="pop-button-image-small icon" alt="Create new container icon" title="Create new container" src="/img/container-add.svg" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="hide panel container-info-panel" id="container-info-panel">
|
<div class="hide panel container-info-panel" id="container-info-panel" tabindex="-1">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="panel-back-arrow" id="close-container-info-panel">
|
<div class="panel-back-arrow" id="close-container-info-panel">
|
||||||
<img alt="Panel Back Arrow" src="/img/container-arrow.svg" class="back-arrow-img" />
|
<img alt="Panel Back Arrow" src="/img/container-arrow.svg" class="back-arrow-img" />
|
||||||
|
@ -85,7 +92,7 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-footer edit-containers-panel-footer">
|
<div class="panel-footer edit-containers-panel-footer">
|
||||||
<a id="exit-edit-mode-link" class="exit-edit-mode-link edit-containers-exit-text">Exit Edit Mode</a>
|
<a href="#" id="exit-edit-mode-link" class="exit-edit-mode-link edit-containers-exit-text">Exit Edit Mode</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -109,7 +116,7 @@
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
<div class="panel-footer">
|
<div class="panel-footer">
|
||||||
<a class="button secondary expanded footer-button cancel-button" id="edit-container-cancel-link">Cancel</a>
|
<a href="#" class="button secondary expanded footer-button cancel-button" id="edit-container-cancel-link">Cancel</a>
|
||||||
<a class="button primary expanded footer-button" id="edit-container-ok-link">OK</a>
|
<a class="button primary expanded footer-button" id="edit-container-ok-link">OK</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -126,8 +133,8 @@
|
||||||
<p>If you remove this container now, <span id="delete-container-tab-count"></span> container tabs will be closed. Are you sure you want to remove this Container?</p>
|
<p>If you remove this container now, <span id="delete-container-tab-count"></span> container tabs will be closed. Are you sure you want to remove this Container?</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-footer">
|
<div class="panel-footer">
|
||||||
<a class="button expanded secondary footer-button cancel-button" id="delete-container-cancel-link">Cancel</a>
|
<a href="#" class="button expanded secondary footer-button cancel-button" id="delete-container-cancel-link">Cancel</a>
|
||||||
<a class="button expanded primary footer-button" id="delete-container-ok-link">OK</a>
|
<a href="#" class="button expanded primary footer-button" id="delete-container-ok-link">OK</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue