WIP assignment controls. Fixes #499
This commit is contained in:
parent
69d497bacd
commit
4f6e91336f
5 changed files with 309 additions and 67 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,6 +3,7 @@ node_modules
|
|||
README.html
|
||||
*.xpi
|
||||
*.swp
|
||||
*.swo
|
||||
.vimrc
|
||||
.env
|
||||
addon.env
|
||||
|
|
|
@ -84,38 +84,7 @@ const assignManager = {
|
|||
|
||||
init() {
|
||||
browser.contextMenus.onClicked.addListener((info, tab) => {
|
||||
const userContextId = this.getUserContextIdFromCookieStore(tab);
|
||||
// Mapping ${URL(info.pageUrl).hostname} to ${userContextId}
|
||||
if (userContextId) {
|
||||
let actionName;
|
||||
let storageAction;
|
||||
if (info.menuItemId === this.MENU_ASSIGN_ID) {
|
||||
actionName = "added";
|
||||
storageAction = this.storageArea.set(info.pageUrl, {
|
||||
userContextId,
|
||||
neverAsk: false,
|
||||
exempted: []
|
||||
});
|
||||
} else {
|
||||
actionName = "removed";
|
||||
storageAction = this.storageArea.remove(info.pageUrl);
|
||||
}
|
||||
storageAction.then(() => {
|
||||
browser.notifications.create({
|
||||
type: "basic",
|
||||
title: "Containers",
|
||||
message: `Successfully ${actionName} site to always open in this container`,
|
||||
iconUrl: browser.extension.getURL("/img/onboarding-1.png")
|
||||
});
|
||||
backgroundLogic.sendTelemetryPayload({
|
||||
event: `${actionName}-container-assignment`,
|
||||
userContextId: userContextId,
|
||||
});
|
||||
this.calculateContextMenu(tab);
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
this._onClickedHandler(info, tab);
|
||||
});
|
||||
|
||||
// Before a request is handled by the browser we decide if we should route through a different container
|
||||
|
@ -163,6 +132,26 @@ const assignManager = {
|
|||
},{urls: ["<all_urls>"], types: ["main_frame"]}, ["blocking"]);
|
||||
},
|
||||
|
||||
async _onClickedHandler(info, tab) {
|
||||
const userContextId = this.getUserContextIdFromCookieStore(tab);
|
||||
// Mapping ${URL(info.pageUrl).hostname} to ${userContextId}
|
||||
if (userContextId) {
|
||||
// let actionName;
|
||||
let remove;
|
||||
if (info.menuItemId === this.MENU_ASSIGN_ID) {
|
||||
//actionName = "added";
|
||||
// storageAction = this._setAssignment(info.pageUrl, userContextId, setOrRemove);
|
||||
remove = false;
|
||||
} else {
|
||||
// actionName = "removed";
|
||||
//storageAction = this.storageArea.remove(info.pageUrl);
|
||||
remove = true;
|
||||
}
|
||||
await this._setOrRemoveAssignment(info.pageUrl, userContextId, remove);
|
||||
this.calculateContextMenu(tab);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
deleteContainer(userContextId) {
|
||||
this.storageArea.deleteContainer(userContextId);
|
||||
|
@ -191,19 +180,50 @@ const assignManager = {
|
|||
return true;
|
||||
},
|
||||
|
||||
calculateContextMenu(tab) {
|
||||
async _setOrRemoveAssignment(pageUrl, userContextId, remove) {
|
||||
let storageAction;
|
||||
if (!remove) {
|
||||
await this.storageArea.set(pageUrl, {
|
||||
userContextId,
|
||||
neverAsk: false,
|
||||
exempted: []
|
||||
});
|
||||
actionName = "added";
|
||||
} else {
|
||||
await this.storageArea.remove(pageUrl);
|
||||
actionName = "removed";
|
||||
}
|
||||
browser.notifications.create({
|
||||
type: "basic",
|
||||
title: "Containers",
|
||||
message: `Successfully ${actionName} site to always open in this container`,
|
||||
iconUrl: browser.extension.getURL("/img/onboarding-1.png")
|
||||
});
|
||||
backgroundLogic.sendTelemetryPayload({
|
||||
event: `${actionName}-container-assignment`,
|
||||
userContextId: userContextId,
|
||||
});
|
||||
},
|
||||
|
||||
async _getAssignment(tab) {
|
||||
const cookieStore = this.getUserContextIdFromCookieStore(tab);
|
||||
// Ensure we have a cookieStore to assign to
|
||||
if (cookieStore
|
||||
&& this.isTabPermittedAssign(tab)) {
|
||||
return await this.storageArea.get(tab.url);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
async calculateContextMenu(tab) {
|
||||
// There is a focus issue in this menu where if you change window with a context menu click
|
||||
// you get the wrong menu display because of async
|
||||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1215376#c16
|
||||
// We also can't change for always private mode
|
||||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1352102
|
||||
const cookieStore = this.getUserContextIdFromCookieStore(tab);
|
||||
browser.contextMenus.remove(this.MENU_ASSIGN_ID);
|
||||
browser.contextMenus.remove(this.MENU_REMOVE_ID);
|
||||
// Ensure we have a cookieStore to assign to
|
||||
if (cookieStore
|
||||
&& this.isTabPermittedAssign(tab)) {
|
||||
this.storageArea.get(tab.url).then((siteSettings) => {
|
||||
const siteSettings = await this._getAssignment(tab);
|
||||
// ✓ This is to mitigate https://bugzilla.mozilla.org/show_bug.cgi?id=1351418
|
||||
let prefix = " "; // Alignment of non breaking space, unknown why this requires so many spaces to align with the tick
|
||||
let menuId = this.MENU_ASSIGN_ID;
|
||||
|
@ -217,10 +237,6 @@ const assignManager = {
|
|||
checked: true,
|
||||
contexts: ["all"],
|
||||
});
|
||||
}).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
reloadPageInContainer(url, currentUserContextId, userContextId, index, neverAsk = false) {
|
||||
|
@ -396,6 +412,17 @@ const messageHandler = {
|
|||
case "neverAsk":
|
||||
assignManager._neverAsk(m);
|
||||
break;
|
||||
case "getAssignment":
|
||||
response = browser.tabs.get(m.tabId).then((tab) => {
|
||||
return assignManager._getAssignment(tab);
|
||||
});
|
||||
break;
|
||||
case "setOrRemoveAssignment":
|
||||
response = browser.tabs.get(m.tabId).then((tab) => {
|
||||
const userContextId = assignManager.getUserContextIdFromCookieStore(tab);
|
||||
return assignManager._setOrRemoveAssignment(tab.url, userContextId, m.value);
|
||||
});
|
||||
break;
|
||||
case "exemptContainerAssignment":
|
||||
response = assignManager._exemptTab(m);
|
||||
break;
|
||||
|
|
|
@ -8,6 +8,11 @@ html {
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-size-heading: 16px;
|
||||
--primary-action-color: #248aeb;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
|
@ -35,6 +40,10 @@ table {
|
|||
overflow: auto;
|
||||
}
|
||||
|
||||
.offpage {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Color and icon helpers */
|
||||
[data-identity-color="blue"] {
|
||||
--identity-tab-color: #37adff;
|
||||
|
@ -140,6 +149,18 @@ table {
|
|||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Text links with actions */
|
||||
|
||||
.action-link:link {
|
||||
color: var(--primary-action-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.action-link:active,
|
||||
.action-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Panels keep everything togethert */
|
||||
.panel {
|
||||
display: flex;
|
||||
|
@ -223,7 +244,7 @@ table {
|
|||
|
||||
.onboarding-title {
|
||||
color: #43484e;
|
||||
font-size: 16px;
|
||||
font-size: var(--font-size-heading);
|
||||
margin-block-end: 0;
|
||||
margin-block-start: 0;
|
||||
margin-inline-end: 0;
|
||||
|
@ -312,7 +333,7 @@ manage things like container crud */
|
|||
.panel-header-text {
|
||||
color: #4a4a4a;
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
font-size: var(--font-size-heading);
|
||||
font-weight: normal;
|
||||
margin-block-end: 0;
|
||||
margin-block-start: 0;
|
||||
|
@ -324,6 +345,24 @@ manage things like container crud */
|
|||
padding-inline-start: 16px;
|
||||
}
|
||||
|
||||
#container-panel .panel-header {
|
||||
block-size: 26px;
|
||||
background-color: #efefef;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#container-panel .panel-header-text {
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
color: #727272;
|
||||
padding-block-end: 0;
|
||||
padding-block-start: 0;
|
||||
}
|
||||
|
||||
#container-panel .sort-containers-link {
|
||||
margin-inline-end: 16px;
|
||||
}
|
||||
|
||||
span ~ .panel-header-text {
|
||||
padding-block-end: 0;
|
||||
padding-block-start: 0;
|
||||
|
@ -331,6 +370,62 @@ span ~ .panel-header-text {
|
|||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
#current-tab {
|
||||
max-inline-size: 100%;
|
||||
min-block-size: 94px;
|
||||
padding-block-end: 16px;
|
||||
padding-block-start: 16px;
|
||||
padding-inline-end: 16px;
|
||||
padding-inline-start: 16px;
|
||||
}
|
||||
|
||||
#current-tab > h3 {
|
||||
color: #4a4a4a;
|
||||
font-size: var(--font-size-heading);
|
||||
font-weight: normal;
|
||||
margin-block-end: 0;
|
||||
margin-block-start: 0;
|
||||
}
|
||||
|
||||
#current-page {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#current-page > img {
|
||||
block-size: 16px;
|
||||
inline-size: 16px;
|
||||
}
|
||||
|
||||
#current-tab > label {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-inline-start: 17px;
|
||||
}
|
||||
|
||||
#current-tab > label > input {
|
||||
display: inline;
|
||||
}
|
||||
#current-tab > label > img {
|
||||
block-size: 12px;
|
||||
display: inline-block;
|
||||
inline-size: 12px;
|
||||
}
|
||||
|
||||
#current-container {
|
||||
display: contents;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
#current-container > .usercontext-icon {
|
||||
block-size: 16px;
|
||||
display: block;
|
||||
flex: 0 0 20px;
|
||||
inline-size: 20px;
|
||||
background-size: 16px;
|
||||
}
|
||||
|
||||
/* Rows used when iterating over panels */
|
||||
.container-panel-row {
|
||||
align-items: center;
|
||||
|
@ -501,7 +596,7 @@ span ~ .panel-header-text {
|
|||
|
||||
.edit-containers-exit-text {
|
||||
align-items: center;
|
||||
background: #248aeb;
|
||||
background: var(--primary-action-color);
|
||||
block-size: 100%;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
|
@ -528,7 +623,7 @@ span ~ .panel-header-text {
|
|||
|
||||
.delete-container-confirm-title {
|
||||
color: #000;
|
||||
font-size: 16px;
|
||||
font-size: var(--font-size-heading);
|
||||
}
|
||||
|
||||
/* Form info */
|
||||
|
|
|
@ -18,6 +18,7 @@ const P_CONTAINERS_EDIT = "containersEdit";
|
|||
const P_CONTAINER_INFO = "containerInfo";
|
||||
const P_CONTAINER_EDIT = "containerEdit";
|
||||
const P_CONTAINER_DELETE = "containerDelete";
|
||||
const DEFAULT_FAVICON = "moz-icon://goat?size=16";
|
||||
|
||||
/**
|
||||
* Escapes any occurances of &, ", <, > or / with XML entities.
|
||||
|
@ -107,6 +108,16 @@ const Logic = {
|
|||
browser.storage.local.set({browserActionBadgesClicked: storage.browserActionBadgesClicked});
|
||||
},
|
||||
|
||||
async identity(cookieStoreId) {
|
||||
const identity = await browser.contextualIdentities.get(cookieStoreId);
|
||||
return identity || {
|
||||
name: "Default",
|
||||
cookieStoreId,
|
||||
icon: "circle",
|
||||
color: "black"
|
||||
};
|
||||
},
|
||||
|
||||
addEnterHandler(element, handler) {
|
||||
element.addEventListener("click", handler);
|
||||
element.addEventListener("keydown", (e) => {
|
||||
|
@ -121,6 +132,14 @@ const Logic = {
|
|||
return (userContextId !== cookieStoreId) ? Number(userContextId) : false;
|
||||
},
|
||||
|
||||
async currentTab() {
|
||||
const activeTabs = await browser.tabs.query({active: true});
|
||||
if (activeTabs.length > 0) {
|
||||
return activeTabs[0]
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
refreshIdentities() {
|
||||
return Promise.all([
|
||||
browser.contextualIdentities.query({}),
|
||||
|
@ -139,7 +158,7 @@ const Logic = {
|
|||
}).catch((e) => {throw e;});
|
||||
},
|
||||
|
||||
showPanel(panel, currentIdentity = null) {
|
||||
async showPanel(panel, currentIdentity = null) {
|
||||
// Invalid panel... ?!?
|
||||
if (!(panel in this._panels)) {
|
||||
throw new Error("Something really bad happened. Unknown panel: " + panel);
|
||||
|
@ -151,15 +170,18 @@ const Logic = {
|
|||
this._currentIdentity = currentIdentity;
|
||||
|
||||
// Initialize the panel before showing it.
|
||||
this._panels[panel].prepare().then(() => {
|
||||
for (let panelElement of document.querySelectorAll(".panel")) { // eslint-disable-line prefer-const
|
||||
await this._panels[panel].prepare();
|
||||
Object.keys(this._panels).forEach((panelKey) => {
|
||||
const panelItem = this._panels[panelKey];
|
||||
const panelElement = document.querySelector(panelItem.panelSelector);
|
||||
if (!panelElement.classList.contains("hide")) {
|
||||
panelElement.classList.add("hide");
|
||||
if ("unregister" in panelItem) {
|
||||
panelItem.unregister();
|
||||
}
|
||||
}
|
||||
document.querySelector(this._panels[panel].panelSelector).classList.remove("hide");
|
||||
})
|
||||
.catch(() => {
|
||||
throw new Error("Failed to show panel " + panel);
|
||||
});
|
||||
document.querySelector(this._panels[panel].panelSelector).classList.remove("hide");
|
||||
},
|
||||
|
||||
showPreviousPanel() {
|
||||
|
@ -205,6 +227,21 @@ const Logic = {
|
|||
});
|
||||
},
|
||||
|
||||
getAssignment(tab) {
|
||||
return browser.runtime.sendMessage({
|
||||
method: "getAssignment",
|
||||
tabId: tab.id
|
||||
});
|
||||
},
|
||||
|
||||
setOrRemoveAssignment(tab, value) {
|
||||
return browser.runtime.sendMessage({
|
||||
method: "setOrRemoveAssignment",
|
||||
tabId: tab.id,
|
||||
value
|
||||
});
|
||||
},
|
||||
|
||||
generateIdentityName() {
|
||||
const defaultName = "Container #";
|
||||
const ids = [];
|
||||
|
@ -364,12 +401,86 @@ Logic.registerPanel(P_CONTAINERS_LIST, {
|
|||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// When the popup is open sometimes the tab will still be updating it's state
|
||||
this.tabUpdateHandler = (tabId, changeInfo) => {
|
||||
const propertiesToUpdate = ["title", "favIconUrl"];
|
||||
const hasChanged = Object.keys(changeInfo).find((changeInfoKey) => {
|
||||
if (propertiesToUpdate.includes(changeInfoKey)) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (hasChanged) {
|
||||
this.prepareCurrentTabHeader();
|
||||
}
|
||||
};
|
||||
browser.tabs.onUpdated.addListener(this.tabUpdateHandler);
|
||||
},
|
||||
|
||||
unregister() {
|
||||
browser.tabs.onUpdated.removeListener(this.tabUpdateHandler);
|
||||
},
|
||||
|
||||
setupAssignmentCheckbox(siteSettings) {
|
||||
const assignmentCheckboxElement = document.getElementById("container-page-assigned");
|
||||
// Cater for null and false
|
||||
assignmentCheckboxElement.checked = !!siteSettings;
|
||||
let disabled = false;
|
||||
if (siteSettings === false) {
|
||||
disabled = true;
|
||||
}
|
||||
assignmentCheckboxElement.disabled = disabled;
|
||||
},
|
||||
|
||||
async prepareCurrentTabHeader() {
|
||||
const currentTab = await Logic.currentTab();
|
||||
const currentTabElement = document.getElementById("current-tab");
|
||||
const assignmentCheckboxElement = document.getElementById("container-page-assigned");
|
||||
assignmentCheckboxElement.addEventListener("change", () => {
|
||||
Logic.setOrRemoveAssignment(currentTab, !assignmentCheckboxElement.checked);
|
||||
});
|
||||
currentTabElement.hidden = !currentTab;
|
||||
this.setupAssignmentCheckbox(false);
|
||||
if (currentTab) {
|
||||
const identity = await Logic.identity(currentTab.cookieStoreId);
|
||||
const siteSettings = await Logic.getAssignment(currentTab);
|
||||
this.setupAssignmentCheckbox(siteSettings);
|
||||
const currentPage = document.getElementById("current-page");
|
||||
const favIconUrl = currentTab.favIconUrl || "";
|
||||
currentPage.innerHTML = escaped`
|
||||
<img class="offpage" src="${favIconUrl}" /> ${currentTab.title}
|
||||
`;
|
||||
|
||||
const imageElement = currentPage.querySelector("img");
|
||||
const loadListener = (e) => {
|
||||
e.target.classList.remove("offpage");
|
||||
e.target.removeEventListener("load", loadListener);
|
||||
e.target.removeEventListener("error", errorListener);
|
||||
};
|
||||
const errorListener = (e) => {
|
||||
e.target.src = DEFAULT_FAVICON;
|
||||
};
|
||||
imageElement.addEventListener("error", errorListener);
|
||||
imageElement.addEventListener("load", loadListener);
|
||||
|
||||
const currentContainer = document.getElementById("current-container");
|
||||
currentContainer.innerHTML = escaped`
|
||||
<div
|
||||
class="usercontext-icon"
|
||||
data-identity-icon="${identity.icon}"
|
||||
data-identity-color="${identity.color}">
|
||||
</div>
|
||||
${identity.name}
|
||||
`;
|
||||
}
|
||||
},
|
||||
|
||||
// This method is called when the panel is shown.
|
||||
prepare() {
|
||||
async prepare() {
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
this.prepareCurrentTabHeader();
|
||||
|
||||
Logic.identities().forEach(identity => {
|
||||
const hasTabs = (identity.hasHiddenTabs || identity.hasOpenTabs);
|
||||
const tr = document.createElement("tr");
|
||||
|
|
|
@ -38,9 +38,17 @@
|
|||
</div>
|
||||
|
||||
<div class="panel container-panel hide" id="container-panel">
|
||||
<div id="current-tab">
|
||||
<h3>Current Tab</h3>
|
||||
<div id="current-page"></div>
|
||||
<label for="container-page-assigned">
|
||||
<input type="checkbox" id="container-page-assigned" />
|
||||
Always open in <span id="current-container"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="panel-header">
|
||||
<h3 class="panel-header-text">Containers</h3>
|
||||
<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>
|
||||
<a href="#" class="action-link" id="sort-containers-link">Sort Tabs</a>
|
||||
</div>
|
||||
<div class="scrollable panel-content" tabindex="-1">
|
||||
<table>
|
||||
|
|
Loading…
Add table
Reference in a new issue