multi-account-containers/src/js/popup.js
2020-02-21 11:07:22 -06:00

1190 lines
40 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
const CONTAINER_HIDE_SRC = "/img/password-hide.svg";
const CONTAINER_UNHIDE_SRC = "/img/password-hide.svg";
const DEFAULT_COLOR = "blue";
const DEFAULT_ICON = "circle";
const NEW_CONTAINER_ID = "new";
const ONBOARDING_STORAGE_KEY = "onboarding-stage";
// List of panels
const P_ONBOARDING_1 = "onboarding1";
const P_ONBOARDING_2 = "onboarding2";
const P_ONBOARDING_3 = "onboarding3";
const P_ONBOARDING_4 = "onboarding4";
const P_ONBOARDING_5 = "onboarding5";
const P_ONBOARDING_6 = "onboarding6";
const P_ONBOARDING_7 = "onboarding7";
const P_CONTAINERS_LIST = "containersList";
const P_CONTAINER_PICKER = "containerPicker";
const OPEN_NEW_CONTAINER_PICKER = "new-tab";
const MANAGE_CONTAINERS_PICKER = "manage";
const REOPEN_IN_CONTAINER = "reopen-in";
const ALWAYS_OPEN_IN_PICKER = "always-open-in";
const P_CONTAINER_INFO = "containerInfo";
const P_CONTAINER_EDIT = "containerEdit";
const P_CONTAINER_DELETE = "containerDelete";
const P_CONTAINERS_ACHIEVEMENT = "containersAchievement";
const P_CONTAINER_ASSIGNMENTS = "containerAssignments";
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.
const Logic = {
_identities: [],
_currentIdentity: null,
_currentPanel: null,
_previousPanelPath: [],
_panels: {},
_onboardingVariation: null,
async init() {
// Remove browserAction "upgraded" badge when opening panel
this.clearBrowserActionBadge();
// Retrieve the list of identities.
const identitiesPromise = this.refreshIdentities();
try {
await identitiesPromise;
} catch (e) {
throw new Error("Failed to retrieve the identities or variation. We cannot continue. ", e.message);
}
// Routing to the correct panel.
// If localStorage is disabled, we don't show the onboarding.
const onboardingData = await browser.storage.local.get([ONBOARDING_STORAGE_KEY]);
let onboarded = onboardingData[ONBOARDING_STORAGE_KEY];
if (!onboarded) {
onboarded = 0;
this.setOnboardingStage(onboarded);
}
switch (onboarded) {
case 7:
this.showAchievementOrContainersListPanel();
break;
case 6:
this.showPanel(P_ONBOARDING_7);
break;
case 5:
this.showPanel(P_ONBOARDING_6);
break;
case 4:
this.showPanel(P_ONBOARDING_5);
break;
case 3:
this.showPanel(P_ONBOARDING_4);
break;
case 2:
this.showPanel(P_ONBOARDING_3);
break;
case 1:
this.showPanel(P_ONBOARDING_2);
break;
case 0:
default:
this.showPanel(P_ONBOARDING_1);
break;
}
},
async showAchievementOrContainersListPanel() {
// Do we need to show an achievement panel?
let showAchievements = false;
const achievementsStorage = await browser.storage.local.get({ achievements: [] });
for (const achievement of achievementsStorage.achievements) {
if (!achievement.done) {
showAchievements = true;
}
}
if (showAchievements) {
this.showPanel(P_CONTAINERS_ACHIEVEMENT);
} else {
this.showPanel(P_CONTAINERS_LIST);
}
},
// In case the user wants to click multiple actions,
// they have to click the "Done" button to stop the panel
// from showing
async setAchievementDone(achievementName) {
const achievementsStorage = await browser.storage.local.get({ achievements: [] });
const achievements = achievementsStorage.achievements;
achievements.forEach((achievement, index, achievementsArray) => {
if (achievement.name === achievementName) {
achievement.done = true;
achievementsArray[index] = achievement;
}
});
browser.storage.local.set({ achievements });
},
setOnboardingStage(stage) {
return browser.storage.local.set({
[ONBOARDING_STORAGE_KEY]: stage
});
},
async clearBrowserActionBadge() {
const extensionInfo = await getExtensionInfo();
const storage = await browser.storage.local.get({ browserActionBadgesClicked: [] });
browser.browserAction.setBadgeBackgroundColor({ color: null });
browser.browserAction.setBadgeText({ text: "" });
storage.browserActionBadgesClicked.push(extensionInfo.version);
// use set and spread to create a unique array
const browserActionBadgesClicked = [...new Set(storage.browserActionBadgesClicked)];
browser.storage.local.set({
browserActionBadgesClicked
});
},
async identity(cookieStoreId) {
const defaultContainer = {
name: "Default",
cookieStoreId,
icon: "default-tab",
color: "default-tab",
numberOfHiddenTabs: 0,
numberOfOpenTabs: 0
};
// Handle old style rejection with null and also Promise.reject new style
try {
return await browser.contextualIdentities.get(cookieStoreId) || defaultContainer;
} catch (e) {
return defaultContainer;
}
},
async numTabs() {
const activeTabs = await browser.tabs.query({ windowId: browser.windows.WINDOW_ID_CURRENT });
return activeTabs.length;
},
_disableMenuItem(message, elementToDisable = document.querySelector("#move-to-new-window")) {
elementToDisable.setAttribute("title", message);
elementToDisable.classList.remove("hover-highlight");
elementToDisable.classList.add("disabled-menu-item");
},
_enableMenuItems(elementToDisable = document.querySelector("#move-to-new-window")) {
elementToDisable.removeAttribute("title");
elementToDisable.classList.add("hover-highlight");
elementToDisable.classList.remove("disabled-menu-item");
},
async refreshIdentities() {
const [identities, state] = await Promise.all([
browser.contextualIdentities.query({}),
browser.runtime.sendMessage({
method: "queryIdentitiesState",
message: {
windowId: browser.windows.WINDOW_ID_CURRENT
}
})
]);
this._identities = identities.map((identity) => {
const stateObject = state[identity.cookieStoreId];
if (stateObject) {
identity.hasOpenTabs = stateObject.hasOpenTabs;
identity.hasHiddenTabs = stateObject.hasHiddenTabs;
identity.numberOfHiddenTabs = stateObject.numberOfHiddenTabs;
identity.numberOfOpenTabs = stateObject.numberOfOpenTabs;
}
return identity;
});
},
getPanelSelector(panel) {
if (this._onboardingVariation === "securityOnboarding" &&
// eslint-disable-next-line no-prototype-builtins
panel.hasOwnProperty("securityPanelSelector")) {
return panel.securityPanelSelector;
} else {
return panel.panelSelector;
}
},
async showPanel(panel, currentIdentity = null, pickerType = null, backwards = false) {
// Invalid panel... ?!?
if (!(panel in this._panels)) {
throw new Error("Something really bad happened. Unknown panel: " + panel);
}
if (!backwards || !this._currentPanel) {
this._previousPanelPath.push(this._currentPanel);
console.log(this._previousPanelPath);
}
this._currentPanel = panel;
this.pickerType = pickerType;
this._currentIdentity = currentIdentity;
// Initialize the panel before showing it.
await this._panels[panel].prepare();
Object.keys(this._panels).forEach((panelKey) => {
const panelItem = this._panels[panelKey];
const panelElement = document.querySelector(this.getPanelSelector(panelItem));
if (!panelElement.classList.contains("hide")) {
panelElement.classList.add("hide");
if ("unregister" in panelItem) {
panelItem.unregister();
}
}
});
const panelEl = document.querySelector(this.getPanelSelector(this._panels[panel]));
panelEl.classList.remove("hide");
const focusEl = panelEl.querySelector(".firstTabindex");
if(focusEl) {
focusEl.focus();
}
},
showPreviousPanel() {
if (!this._previousPanelPath) {
throw new Error("Current panel not set!");
}
this.showPanel(this._previousPanelPath.pop(), this._currentIdentity, null, true);
},
registerPanel(panelName, panelObject) {
this._panels[panelName] = panelObject;
panelObject.initialize();
},
identities() {
return this._identities;
},
currentIdentity() {
if (!this._currentIdentity) {
throw new Error("CurrentIdentity must be set before calling Logic.currentIdentity.");
}
return this._currentIdentity;
},
currentUserContextId() {
const identity = Logic.currentIdentity();
return Utils.userContextId(identity.cookieStoreId);
},
currentCookieStoreId() {
const identity = Logic.currentIdentity();
return identity.cookieStoreId;
},
removeIdentity(userContextId) {
if (!userContextId) {
return Promise.reject("removeIdentity must be called with userContextId argument.");
}
return browser.runtime.sendMessage({
method: "deleteContainer",
message: { userContextId }
});
},
getAssignment(tab) {
return browser.runtime.sendMessage({
method: "getAssignment",
tabId: tab.id
});
},
getAssignmentObjectByContainer(userContextId) {
if (!userContextId) {
return {};
}
return browser.runtime.sendMessage({
method: "getAssignmentObjectByContainer",
message: { userContextId }
});
},
generateIdentityName() {
const defaultName = "Container #";
const ids = [];
// This loop populates the 'ids' array with all the already-used ids.
this._identities.forEach(identity => {
if (identity.name.startsWith(defaultName)) {
const id = parseInt(identity.name.substr(defaultName.length), 10);
if (id) {
ids.push(id);
}
}
});
// Here we find the first valid id.
for (let id = 1; ; ++id) {
if (ids.indexOf(id) === -1) {
return defaultName + (id < 10 ? "0" : "") + id;
}
}
},
getCurrentPanelElement() {
const panelItem = this._panels[this._currentPanel];
return document.querySelector(this.getPanelSelector(panelItem));
},
};
// P_ONBOARDING_1: First page for Onboarding.
// ----------------------------------------------------------------------------
Logic.registerPanel(P_ONBOARDING_1, {
panelSelector: ".onboarding-panel-1",
securityPanelSelector: ".security-onboarding-panel-1",
// This method is called when the object is registered.
initialize() {
// Let's move to the next panel.
[...document.querySelectorAll(".onboarding-start-button")].forEach(startElement => {
Utils.addEnterHandler(startElement, async () => {
await Logic.setOnboardingStage(1);
Logic.showPanel(P_ONBOARDING_2);
});
});
},
// This method is called when the panel is shown.
prepare() {
return Promise.resolve(null);
},
});
// P_ONBOARDING_2: Second page for Onboarding.
// ----------------------------------------------------------------------------
Logic.registerPanel(P_ONBOARDING_2, {
panelSelector: ".onboarding-panel-2",
securityPanelSelector: ".security-onboarding-panel-2",
// This method is called when the object is registered.
initialize() {
// Let's move to the containers list panel.
[...document.querySelectorAll(".onboarding-next-button")].forEach(nextElement => {
Utils.addEnterHandler(nextElement, async () => {
await Logic.setOnboardingStage(2);
Logic.showPanel(P_ONBOARDING_3);
});
});
},
// This method is called when the panel is shown.
prepare() {
return Promise.resolve(null);
},
});
// P_ONBOARDING_3: Third page for Onboarding.
// ----------------------------------------------------------------------------
Logic.registerPanel(P_ONBOARDING_3, {
panelSelector: ".onboarding-panel-3",
securityPanelSelector: ".security-onboarding-panel-3",
// This method is called when the object is registered.
initialize() {
// Let's move to the containers list panel.
[...document.querySelectorAll(".onboarding-almost-done-button")].forEach(almostElement => {
Utils.addEnterHandler(almostElement, async () => {
await Logic.setOnboardingStage(3);
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.
Utils.addEnterHandler(document.querySelector("#onboarding-done-button"), async () => {
await Logic.setOnboardingStage(4);
Logic.showPanel(P_ONBOARDING_5);
});
},
// This method is called when the panel is shown.
prepare() {
return Promise.resolve(null);
},
});
// P_ONBOARDING_5: Fifth page for Onboarding: new tab long-press behavior
// ----------------------------------------------------------------------------
Logic.registerPanel(P_ONBOARDING_5, {
panelSelector: ".onboarding-panel-5",
// This method is called when the object is registered.
initialize() {
// Let's move to the containers list panel.
Utils.addEnterHandler(document.querySelector("#onboarding-longpress-button"), async () => {
await Logic.setOnboardingStage(5);
Logic.showPanel(P_ONBOARDING_6);
});
},
// This method is called when the panel is shown.
prepare() {
return Promise.resolve(null);
},
});
// P_ONBOARDING_6: Sixth page for Onboarding: new tab long-press behavior
// ----------------------------------------------------------------------------
Logic.registerPanel(P_ONBOARDING_6, {
panelSelector: ".onboarding-panel-6",
// This method is called when the object is registered.
initialize() {
// Let's move to the containers list panel.
Utils.addEnterHandler(document.querySelector("#start-sync-button"), async () => {
await Logic.setOnboardingStage(6);
await browser.storage.local.set({syncEnabled: true});
await browser.runtime.sendMessage({
method: "resetSync"
});
Logic.showPanel(P_ONBOARDING_7);
});
Utils.addEnterHandler(document.querySelector("#no-sync"), async () => {
await Logic.setOnboardingStage(7);
await browser.storage.local.set({syncEnabled: false});
await browser.runtime.sendMessage({
method: "resetSync"
});
Logic.showPanel(P_CONTAINERS_LIST);
});
},
// This method is called when the panel is shown.
prepare() {
return Promise.resolve(null);
},
});
// P_ONBOARDING_6: Sixth page for Onboarding: new tab long-press behavior
// ----------------------------------------------------------------------------
Logic.registerPanel(P_ONBOARDING_7, {
panelSelector: ".onboarding-panel-7",
// This method is called when the object is registered.
initialize() {
// Let's move to the containers list panel.
Utils.addEnterHandler(document.querySelector("#sign-in"), async () => {
browser.tabs.create({
url: "https://accounts.firefox.com/?service=sync&action=email&context=fx_desktop_v3&entrypoint=multi-account-containers&utm_source=addon&utm_medium=panel&utm_campaign=container-sync",
});
await Logic.setOnboardingStage(7);
Logic.showPanel(P_CONTAINERS_LIST);
});
Utils.addEnterHandler(document.querySelector("#no-sign-in"), async () => {
await Logic.setOnboardingStage(7);
Logic.showPanel(P_CONTAINERS_LIST);
});
},
// This method is called when the panel is shown.
prepare() {
return Promise.resolve(null);
},
});
// P_CONTAINERS_LIST: The list of containers. The main page.
// ----------------------------------------------------------------------------
Logic.registerPanel(P_CONTAINERS_LIST, {
panelSelector: "#container-panel",
// This method is called when the object is registered.
async initialize() {
Utils.addEnterHandler(document.querySelector("#manage-containers-link"), (e) => {
if (!e.target.classList.contains("disable-edit-containers")) {
Logic.showPanel(P_CONTAINER_PICKER, null, MANAGE_CONTAINERS_PICKER);
}
});
Utils.addEnterHandler(document.querySelector("#open-new-tab-in"), () => {
Logic.showPanel(P_CONTAINER_PICKER, null, OPEN_NEW_CONTAINER_PICKER);
});
Utils.addEnterHandler(document.querySelector("#reopen-site-in"), () => {
Logic.showPanel(P_CONTAINER_PICKER, null, REOPEN_IN_CONTAINER);
});
Utils.addEnterHandler(document.querySelector("#always-open-in"), () => {
Logic.showPanel(P_CONTAINER_PICKER, null, ALWAYS_OPEN_IN_PICKER);
});
Utils.addEnterHandler(document.querySelector("#info-icon"), () => {
browser.runtime.openOptionsPage();
});
Utils.addEnterHandler(document.querySelector("#sort-containers-link"), async () => {
try {
await browser.runtime.sendMessage({
method: "sortTabs"
});
window.close();
} catch (e) {
window.close();
}
});
document.addEventListener("keydown", (e) => {
const selectables = [...document.querySelectorAll("[tabindex='0'], [tabindex='-1']")];
const element = document.activeElement;
const index = selectables.indexOf(element) || 0;
function next() {
const nextElement = selectables[index + 1];
if (nextElement) {
nextElement.focus();
}
}
function previous() {
const previousElement = selectables[index - 1];
if (previousElement) {
previousElement.focus();
}
}
switch (e.keyCode) {
case 40:
next();
break;
case 38:
previous();
break;
case 13: {
const panel = Logic.getCurrentPanelElement();
const button = panel.getElementsByTagName("A")[0];
if(button) {
button.click();
}
break;
}
case 39:
{
const showTabs = element.parentNode.querySelector(".show-tabs");
if(showTabs) {
showTabs.click();
}
break;
}
case 37:
{
const hideTabs = document.querySelector(".panel-back-arrow");
if(hideTabs) {
hideTabs.click();
}
break;
}
default:
if ((e.keyCode >= 49 && e.keyCode <= 57) &&
Logic._currentPanel === "containersList") {
const element = selectables[e.keyCode - 48];
if (element) {
element.click();
}
}
break;
}
});
},
unregister() {
},
// This method is called when the panel is shown.
async prepare() {
const fragment = document.createDocumentFragment();
Logic.identities().forEach(identity => {
const tr = document.createElement("tr");
tr.classList.add("menu-item", "hover-highlight");
tr.setAttribute("tabindex", "0");
const td = document.createElement("td");
const openTabs = identity.numberOfOpenTabs || "" ;
td.innerHTML = Utils.escaped`
<div class="menu-icon">
<div class="usercontext-icon"
data-identity-icon="${identity.icon}"
data-identity-color="${identity.color}">
</div>
</div>
<span class="menu-text">${identity.name}</span>
<span class="menu-right-float">
<span class="container-count">${openTabs}</span>
<span class="menu-arrow">
<img alt="Container Info" src="/img/arrow-icon-right.svg" />
</span>
</span>`;
fragment.appendChild(tr);
tr.appendChild(td);
Utils.addEnterHandler(tr, () => {
Logic.showPanel(P_CONTAINER_INFO, identity);
});
});
const list = document.querySelector("#identities-list");
list.innerHTML = "";
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
*/
const focusHandler = () => {
const identityList = list.querySelector("tr .clickable");
if (identityList) {
// otherwise this throws an error when there are no containers present.
identityList.focus();
document.removeEventListener("focus", focusHandler);
}
};
document.addEventListener("focus", focusHandler);
/* If the user mousedown's first then remove the focus handler */
document.addEventListener("mousedown", () => {
document.removeEventListener("focus", focusHandler);
});
return Promise.resolve();
},
});
// P_CONTAINER_INFO: More info about a container.
// ----------------------------------------------------------------------------
Logic.registerPanel(P_CONTAINER_INFO, {
panelSelector: "#container-info-panel",
// This method is called when the object is registered.
async initialize() {
const closeContEl = document.querySelector("#close-container-info-panel");
Utils.addEnterHandler(closeContEl, () => {
Logic.showPanel(P_CONTAINERS_LIST);
});
// Check if the user has incompatible add-ons installed
// Note: this is not implemented in messageHandler.js
let incompatible = false;
try {
incompatible = await browser.runtime.sendMessage({
method: "checkIncompatibleAddons"
});
} catch (e) {
throw new Error("Could not check for incompatible add-ons.");
}
const moveTabsEl = document.querySelector("#move-to-new-window");
const numTabs = await Logic.numTabs();
if (incompatible) {
Logic._disableMenuItem("Moving container tabs is incompatible with Pulse, PageShot, and SnoozeTabs.");
return;
} else if (numTabs === 1) {
Logic._disableMenuItem("Cannot move a tab from a single-tab window.");
return;
}
Utils.addEnterHandler(moveTabsEl, async () => {
await browser.runtime.sendMessage({
method: "moveTabsToWindow",
windowId: browser.windows.WINDOW_ID_CURRENT,
cookieStoreId: Logic.currentIdentity().cookieStoreId,
});
window.close();
});
const manageContainer = document.querySelector("#manage-container-link");
Utils.addEnterHandler(manageContainer, async () => {
Logic.showPanel(P_CONTAINER_EDIT, Logic.currentIdentity());
});
},
// This method is called when the panel is shown.
async prepare() {
const identity = Logic.currentIdentity();
// Populating the panel: name and icon
document.getElementById("container-info-title").textContent = identity.name;
const alwaysOpen = document.querySelector("#always-open-in-info-panel");
Utils.addEnterHandler(alwaysOpen, async () => {
Utils.alwaysOpenInContainer(identity);
window.close();
});
// Show or not the has-tabs section.
for (let trHasTabs of document.getElementsByClassName("container-info-has-tabs")) { // eslint-disable-line prefer-const
trHasTabs.style.display = !identity.hasHiddenTabs && !identity.hasOpenTabs ? "none" : "";
}
if (identity.numberOfOpenTabs === 0) {
Logic._disableMenuItem("No tabs available for this container");
} else {
Logic._enableMenuItems();
}
this.intializeShowHide(identity);
// Let's retrieve the list of tabs.
const tabs = await browser.runtime.sendMessage({
method: "getTabs",
windowId: browser.windows.WINDOW_ID_CURRENT,
cookieStoreId: Logic.currentIdentity().cookieStoreId
});
return this.buildOpenTabTable(tabs);
},
intializeShowHide(identity) {
const hideContEl = document.querySelector("#hideorshow-container");
if (identity.numberOfOpenTabs === 0 && !identity.hasHiddenTabs) {
return Logic._disableMenuItem("No tabs available for this container", hideContEl);
} else {
Logic._enableMenuItems(hideContEl);
}
Utils.addEnterHandler(hideContEl, async () => {
try {
browser.runtime.sendMessage({
method: identity.hasHiddenTabs ? "showTabs" : "hideTabs",
windowId: browser.windows.WINDOW_ID_CURRENT,
cookieStoreId: Logic.currentCookieStoreId()
});
window.close();
} catch (e) {
window.close();
}
});
const hideShowIcon = document.getElementById("container-info-hideorshow-icon");
hideShowIcon.src = identity.hasHiddenTabs ? CONTAINER_UNHIDE_SRC : CONTAINER_HIDE_SRC;
const hideShowLabel = document.getElementById("container-info-hideorshow-label");
hideShowLabel.textContent = identity.hasHiddenTabs ? "Show this container" : "Hide this container";
return;
},
buildOpenTabTable(tabs) {
// Let's remove all the previous tabs.
const table = document.getElementById("container-info-table");
while (table.firstChild) {
table.firstChild.remove();
}
// For each one, let's create a new line.
const fragment = document.createDocumentFragment();
for (let tab of tabs) { // eslint-disable-line prefer-const
const tr = document.createElement("tr");
fragment.appendChild(tr);
tr.classList.add("menu-item", "hover-highlight");
tr.setAttribute("tabindex", "0");
tr.innerHTML = Utils.escaped`
<td>
<div class="favicon"></div>
<span title="${tab.url}" class="menu-text truncate-text">${tab.title}</span>
<img id="${tab.id}" class="trash-button" src="/img/container-close-tab.svg" />
</td>`;
tr.querySelector(".favicon").appendChild(Utils.createFavIconElement(tab.favIconUrl));
tr.setAttribute("tabindex", "0");
table.appendChild(fragment);
// On click, we activate this tab. But only if this tab is active.
if (!tab.hiddenState) {
Utils.addEnterHandler(tr, async () => {
await browser.tabs.update(tab.id, { active: true });
window.close();
});
const closeTab = document.querySelector(".trash-button");
if (closeTab) {
Utils.addEnterHandler(closeTab, async (e) => {
await browser.tabs.remove(Number(e.target.id));
window.close();
});
}
}
}
},
});
// P_CONTAINER_PICKER: Makes the list editable.
// ----------------------------------------------------------------------------
Logic.registerPanel(P_CONTAINER_PICKER, {
panelSelector: "#container-picker-panel",
// This method is called when the object is registered.
initialize() {
const closeContEl = document.querySelector("#close-container-picker-panel");
Utils.addEnterHandler(closeContEl, () => {
Logic.showPanel(P_CONTAINERS_LIST);
});
},
// This method is called when the panel is shown.
prepare() {
const fragment = document.createDocumentFragment();
let pickedFunction;
switch (Logic.pickerType) {
case OPEN_NEW_CONTAINER_PICKER:
document.getElementById("picker-title").textContent = "Open a New Tab in";
pickedFunction = function (identity) {
try {
browser.tabs.create({
cookieStoreId: identity.cookieStoreId
});
window.close();
} catch (e) {
window.close();
}
};
break;
case MANAGE_CONTAINERS_PICKER:
document.getElementById("picker-title").textContent = "Manage Containers";
pickedFunction = function (identity) {
Logic.showPanel(P_CONTAINER_EDIT, identity);
};
break;
case REOPEN_IN_CONTAINER:
document.getElementById("picker-title").textContent = "Reopen This Site in";
pickedFunction = async function (identity) {
const currentTab = await Utils.currentTab();
const newUserContextId = Utils.userContextId(identity.cookieStoreId);
Utils.reloadInContainer(
currentTab.url,
false,
newUserContextId,
currentTab.index + 1,
currentTab.active
);
window.close();
};
break;
case ALWAYS_OPEN_IN_PICKER:
default:
document.getElementById("picker-title").textContent = "Always Open This Site in";
pickedFunction = async function (identity) {
Utils.alwaysOpenInContainer(identity);
window.close();
};
break;
}
Logic.identities().forEach(identity => {
const tr = document.createElement("tr");
tr.classList.add("menu-item");
const td = document.createElement("td");
td.innerHTML = Utils.escaped`
<div class="menu-icon hover-highlight">
<div class="usercontext-icon"
data-identity-icon="${identity.icon}"
data-identity-color="${identity.color}">
</div>
</div>
<span class="menu-text">${identity.name}</span>`;
fragment.appendChild(tr);
tr.appendChild(td);
Utils.addEnterHandler(tr, () => {
pickedFunction(identity);
});
});
const list = document.querySelector("#picker-identities-list");
list.innerHTML = "";
list.appendChild(fragment);
return Promise.resolve(null);
}
});
// P_CONTAINER_ASSIGNMENTS: Shows Site Assignments and allows editing.
// ----------------------------------------------------------------------------
Logic.registerPanel(P_CONTAINER_ASSIGNMENTS, {
panelSelector: "#edit-container-assignments",
// This method is called when the object is registered.
initialize() {
const closeContEl = document.querySelector("#close-container-assignment-panel");
closeContEl.setAttribute("tabindex", "0");
closeContEl.classList.add("firstTabindex");
Utils.addEnterHandler(closeContEl, () => {
Logic.showPreviousPanel();
});
},
// This method is called when the panel is shown.
async prepare() {
const identity = Logic.currentIdentity();
// Populating the panel: name and icon
document.getElementById("edit-assignments-title").textContent = identity.name;
const userContextId = Logic.currentUserContextId();
const assignments = await Logic.getAssignmentObjectByContainer(userContextId);
this.showAssignedContainers(assignments);
return Promise.resolve(null);
},
showAssignedContainers(assignments) {
const assignmentPanel = document.getElementById("edit-sites-assigned");
const assignmentKeys = Object.keys(assignments);
assignmentPanel.hidden = !(assignmentKeys.length > 0);
if (assignments) {
const tableElement = document.querySelector("#edit-sites-assigned");
/* Remove previous assignment list,
after removing one we rerender the list */
while (tableElement.firstChild) {
tableElement.firstChild.remove();
}
assignmentKeys.forEach((siteKey) => {
const site = assignments[siteKey];
const trElement = document.createElement("tr");
/* As we don't have the full or correct path the best we can assume is the path is HTTPS and then replace with a broken icon later if it doesn't load.
This is pending a better solution for favicons from web extensions */
const assumedUrl = `https://${site.hostname}/favicon.ico`;
trElement.innerHTML = Utils.escaped`
<td>
<div class="favicon"></div>
<span title="${site.hostname}" class="menu-text">${site.hostname}</span>
<img class="trash-button delete-assignment" src="/img/container-delete.svg" />
</td>`;
trElement.getElementsByClassName("favicon")[0].appendChild(Utils.createFavIconElement(assumedUrl));
const deleteButton = trElement.querySelector(".trash-button");
Utils.addEnterHandler(deleteButton, async () => {
const userContextId = Logic.currentUserContextId();
// Lets show the message to the current tab
const currentTab = await Utils.currentTab();
Utils.setOrRemoveAssignment(currentTab.id, assumedUrl, userContextId, true);
delete assignments[siteKey];
this.showAssignedContainers(assignments);
});
trElement.classList.add("menu-item", "hover-highlight");
tableElement.appendChild(trElement);
});
}
},
});
// P_CONTAINER_EDIT: Editor for a container.
// ----------------------------------------------------------------------------
Logic.registerPanel(P_CONTAINER_EDIT, {
panelSelector: "#edit-container-panel",
// This method is called when the object is registered.
initialize() {
this.initializeRadioButtons();
Utils.addEnterHandler(document.querySelector("#close-container-edit-panel"), () => {
const formValues = new FormData(this._editForm);
if (formValues.get("container-id") !== NEW_CONTAINER_ID) {
this._submitForm();
} else {
Logic.showPreviousPanel();
}
});
this._editForm = document.getElementById("edit-container-panel-form");
this._editForm.addEventListener("submit", () => {
this._submitForm();
});
},
async _submitForm() {
const formValues = new FormData(this._editForm);
try {
await browser.runtime.sendMessage({
method: "createOrUpdateContainer",
message: {
userContextId: formValues.get("container-id") || NEW_CONTAINER_ID,
params: {
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,
}
}
});
await Logic.refreshIdentities();
Logic.showPreviousPanel();
} catch (e) {
Logic.showPanel(P_CONTAINERS_LIST);
}
},
initializeRadioButtons() {
const colorRadioTemplate = (containerColor) => {
return Utils.escaped`<input type="radio" value="${containerColor}" name="container-color" id="edit-container-panel-choose-color-${containerColor}" />
<label for="edit-container-panel-choose-color-${containerColor}" class="usercontext-icon choose-color-icon" data-identity-icon="circle" data-identity-color="${containerColor}">`;
};
const colors = ["blue", "turquoise", "green", "yellow", "orange", "red", "pink", "purple"];
const colorRadioFieldset = document.getElementById("edit-container-panel-choose-color");
colors.forEach((containerColor) => {
const templateInstance = document.createElement("div");
templateInstance.classList.add("radio-container");
// eslint-disable-next-line no-unsanitized/property
templateInstance.innerHTML = colorRadioTemplate(containerColor);
colorRadioFieldset.appendChild(templateInstance);
});
const iconRadioTemplate = (containerIcon) => {
return Utils.escaped`<input type="radio" value="${containerIcon}" name="container-icon" id="edit-container-panel-choose-icon-${containerIcon}" />
<label for="edit-container-panel-choose-icon-${containerIcon}" class="usercontext-icon choose-color-icon" data-identity-color="grey" data-identity-icon="${containerIcon}">`;
};
const icons = ["fingerprint", "briefcase", "dollar", "cart", "vacation", "gift", "food", "fruit", "pet", "tree", "chill", "circle"];
const iconRadioFieldset = document.getElementById("edit-container-panel-choose-icon");
icons.forEach((containerIcon) => {
const templateInstance = document.createElement("div");
templateInstance.classList.add("radio-container");
// eslint-disable-next-line no-unsanitized/property
templateInstance.innerHTML = iconRadioTemplate(containerIcon);
iconRadioFieldset.appendChild(templateInstance);
});
},
// This method is called when the panel is shown.
async prepare() {
const identity = Logic.currentIdentity();
// Populating the panel: name and icon
document.getElementById("container-edit-title").textContent = identity.name;
const userContextId = Logic.currentUserContextId();
// const assignments = await Logic.getAssignmentObjectByContainer(userContextId);
// this.showAssignedContainers(assignments);
// document.querySelector("#edit-container-panel .panel-footer").hidden = !!userContextId;
Utils.addEnterHandler(document.querySelector("#manage-assigned-sites-list"), () => {
Logic.showPanel(P_CONTAINER_ASSIGNMENTS, identity);
});
document.querySelector("#edit-container-panel-name-input").value = identity.name || "";
document.querySelector("#edit-container-panel-usercontext-input").value = userContextId || NEW_CONTAINER_ID;
const containerName = document.querySelector("#edit-container-panel-name-input");
window.requestAnimationFrame(() => {
containerName.select();
containerName.focus();
});
[...document.querySelectorAll("[name='container-color']")].forEach(colorInput => {
colorInput.checked = colorInput.value === identity.color;
});
[...document.querySelectorAll("[name='container-icon']")].forEach(iconInput => {
iconInput.checked = iconInput.value === identity.icon;
});
const deleteButton = document.getElementById("delete-container-button");
Utils.addEnterHandler(deleteButton, () => {
Logic.showPanel(P_CONTAINER_DELETE, identity);
});
return Promise.resolve(null);
},
});
// P_CONTAINER_DELETE: Delete a container.
// ----------------------------------------------------------------------------
Logic.registerPanel(P_CONTAINER_DELETE, {
panelSelector: "#delete-container-panel",
// This method is called when the object is registered.
initialize() {
Utils.addEnterHandler(document.querySelector("#delete-container-cancel-link"), () => {
Logic.showPreviousPanel();
});
Utils.addEnterHandler(document.querySelector("#close-container-delete-panel"), () => {
Logic.showPreviousPanel();
});
Utils.addEnterHandler(document.querySelector("#delete-container-ok-link"), async () => {
/* This promise wont resolve if the last tab was removed from the window.
as the message async callback stops listening, this isn't an issue for us however it might be in future
if you want to do anything post delete do it in the background script.
Browser console currently warns about not listening also.
*/
try {
await Logic.removeIdentity(Utils.userContextId(Logic.currentIdentity().cookieStoreId));
await Logic.refreshIdentities();
Logic.showPanel(P_CONTAINERS_LIST);
} catch (e) {
Logic.showPanel(P_CONTAINERS_LIST);
}
});
},
// This method is called when the panel is shown.
prepare() {
const identity = Logic.currentIdentity();
// Populating the panel: name, icon, and warning message
document.getElementById("container-delete-title").textContent = identity.name;
const totalNumberOfTabs = identity.numberOfHiddenTabs + identity.numberOfOpenTabs;
let warningMessage = "";
if (totalNumberOfTabs > 0) {
const grammaticalNumTabs = totalNumberOfTabs > 1 ? "tabs" : "tab";
warningMessage = `If you remove this container now, ${totalNumberOfTabs} container ${grammaticalNumTabs} will be closed.`;
}
document.getElementById("delete-container-tab-warning").textContent = warningMessage;
return Promise.resolve(null);
},
});
// P_CONTAINERS_ACHIEVEMENT: Page for achievement.
// ----------------------------------------------------------------------------
Logic.registerPanel(P_CONTAINERS_ACHIEVEMENT, {
panelSelector: ".achievement-panel",
// This method is called when the object is registered.
initialize() {
// Set done and move to the containers list panel.
Utils.addEnterHandler(document.querySelector("#achievement-done-button"), async () => {
await Logic.setAchievementDone("manyContainersOpened");
Logic.showPanel(P_CONTAINERS_LIST);
});
},
// This method is called when the panel is shown.
prepare() {
return Promise.resolve(null);
},
});
Logic.init();
window.addEventListener("resize", function () {
//for overflow menu
const difference = window.innerWidth - document.body.offsetWidth;
if (difference > 2) {
//if popup is in the overflow menu, window will be larger than 300px
const root = document.documentElement;
root.style.setProperty("--overflow-size", difference + "px");
root.style.setProperty("--icon-fit", "12");
}
});