diff --git a/src/css/popup.css b/src/css/popup.css
index 5980894cf..1ad1e80b3 100644
--- a/src/css/popup.css
+++ b/src/css/popup.css
@@ -1923,6 +1923,19 @@ manage things like container crud */
margin-inline: 4px;
}
+.edit-container-panel input[type="text"]#edit-container-panel-site-input {
+ inline-size: 80%;
+}
+
+#edit-container-site-link {
+ background: #ebebeb;
+ block-size: 36px;
+}
+
+#edit-container-site-link:hover {
+ background: #e3e3e3;
+}
+
input[type="text"]:focus {
/* Both a border and box-shadow of 1px are needed because using a 2px border
* would redraw the input 1px farther to the left.
diff --git a/src/js/popup.js b/src/js/popup.js
index ec5300604..7106f1086 100644
--- a/src/js/popup.js
+++ b/src/js/popup.js
@@ -1818,6 +1818,12 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
Utils.addEnterHandler(document.querySelector("#create-container-ok-link"), () => {
this._submitForm();
});
+
+ // Add new site to current container
+ const siteLink = document.querySelector("#edit-container-site-link");
+ Utils.addEnterHandler(siteLink, () => {
+ this._addSite();
+ });
},
async _submitForm() {
@@ -1860,6 +1866,113 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
return editedIdentity;
},
+ async _addSite() {
+ // Get URL and container ID from form
+ const formValues = new FormData(this._editForm);
+ const url = formValues.get("site-name");
+ const userContextId = formValues.get("container-id");
+ const currentTab = await Logic.currentTab();
+ const tabId = currentTab.id;
+ const baseURL = this.normalizeUrl(url);
+
+ if (baseURL !== null) {
+ // Assign URL to container
+ await Utils.setOrRemoveAssignment(tabId, baseURL, userContextId, false);
+
+ // Clear form
+ document.querySelector("#edit-container-panel-site-input").value = "";
+
+ // Show new assignments
+ const assignments = await Logic.getAssignmentObjectByContainer(userContextId);
+ this.showAssignedContainers(assignments);
+ }
+ },
+
+ normalizeUrl(url){
+ /*
+ * Important: the rules that automatically open a site in a container only
+ * look at the URL up to but excluding the / after the domainname.
+ *
+ * Furthermore, containers are only useful for http & https URLs because
+ * those are the only protocols that transmit cookies to maintain
+ * sessions.
+ */
+
+ // Preface with "https://" if no protocol present
+ const startsWithProtocol = /^\w+:\/\/|^mailto:/; // all protocols are followed by :// (except mailto)
+ if (!url.match(startsWithProtocol)) {
+ url = "https://" + url;
+ }
+
+ /*
+ * Dual-purpose match: (1) check that url start with http or https proto,
+ * and (2) exclude everything from the / after the domain (any path, any
+ * query string, and any anchor)
+ */
+ const basePart = /^(https?:\/\/)([^/?#]*)/;
+ const r = url.match(basePart);
+ if (!r) return null;
+ const urlProto = r[1]; // includes :// if any
+ const urlConnection = r[2]; // [user[:passwd]@]domain[:port]
+
+ // Extract domain from [user[:passwd]@]domain[:port]
+ const domainPart = /^(?:.*@)?([^:]+)/;
+ const d = urlConnection.match(domainPart);
+ if (!d) return null;
+ const urlDomain = d[1];
+
+ // Check that the domain is valid (RFC-1034)
+ const validDomain = /^(?:\w(?:[\w-]*\w)?)(?:\.\w(?:[\w-]*\w)?)+$/;
+ if (!urlDomain.match(validDomain)) return null;
+
+ return urlProto+urlDomain;
+ },
+
+ showAssignedContainers(assignments) {
+ const assignmentPanel = document.getElementById("edit-sites-assigned");
+ const assignmentKeys = Object.keys(assignments);
+ assignmentPanel.hidden = !(assignmentKeys.length > 0);
+ if (assignments) {
+ const tableElement = assignmentPanel.querySelector(".assigned-sites-list");
+ /* 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("div");
+ /* 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`
+
+
+ ${site.hostname}
+
+ `;
+ trElement.getElementsByClassName("favicon")[0].appendChild(Utils.createFavIconElement(assumedUrl));
+ const deleteButton = trElement.querySelector(".delete-assignment");
+ const that = this;
+ Utils.addEnterHandler(deleteButton, async () => {
+ const userContextId = Logic.currentUserContextId();
+ // Lets show the message to the current tab
+ // TODO remove then when firefox supports arrow fn async
+ const currentTab = await Logic.currentTab();
+ Utils.setOrRemoveAssignment(currentTab.id, assumedUrl, userContextId, true);
+ delete assignments[siteKey];
+ that.showAssignedContainers(assignments);
+ });
+ trElement.classList.add("container-info-tab-row", "clickable");
+ tableElement.appendChild(trElement);
+ });
+ }
+ },
+
initializeRadioButtons() {
const colorRadioTemplate = (containerColor) => {
return Utils.escaped`
@@ -1910,7 +2023,10 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
Utils.addEnterHandler(document.querySelector("#manage-assigned-sites-list"), () => {
Logic.showPanel(P_CONTAINER_ASSIGNMENTS, this.getEditInProgressIdentity(), false, false);
});
-
+ // Only show ability to add site if it's an existing container
+ document.querySelector("#edit-container-panel-add-site").hidden = !userContextId;
+ // Make site input empty
+ document.querySelector("#edit-container-panel-site-input").value = "";
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");
diff --git a/src/js/utils.js b/src/js/utils.js
index f1932acd5..c65a84c4d 100644
--- a/src/js/utils.js
+++ b/src/js/utils.js
@@ -59,29 +59,29 @@ const Utils = {
},
/**
- * Escapes any occurances of &, ", <, > or / with XML entities.
- *
- * @param {string} str
- * The string to escape.
- * @return {string} The escaped string.
- */
+ * Escapes any occurances of &, ", <, > or / with XML entities.
+ *
+ * @param {string} str
+ * The string to escape.
+ * @return {string} The escaped string.
+ */
escapeXML(str) {
const replacements = { "&": "&", "\"": """, "'": "'", "<": "<", ">": ">", "/": "/" };
return String(str).replace(/[&"'<>/]/g, m => replacements[m]);
},
/**
- * A tagged template function which escapes any XML metacharacters in
- * interpolated values.
- *
- * @param {Array} strings
- * An array of literal strings extracted from the templates.
- * @param {Array} values
- * An array of interpolated values extracted from the template.
- * @returns {string}
- * The result of the escaped values interpolated with the literal
- * strings.
- */
+ * A tagged template function which escapes any XML metacharacters in
+ * interpolated values.
+ *
+ * @param {Array} strings
+ * An array of literal strings extracted from the templates.
+ * @param {Array} values
+ * An array of interpolated values extracted from the template.
+ * @returns {string}
+ * The result of the escaped values interpolated with the literal
+ * strings.
+ */
escaped(strings, ...values) {
const result = [];
diff --git a/src/manifest.json b/src/manifest.json
index 5dd8cf26f..b05f6cc3f 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -131,7 +131,9 @@
"default_title": "__MSG_alwaysOpenSiteInContainer__",
"default_popup": "pageActionPopup.html",
"pinned": false,
- "show_matches": ["*://*/*"]
+ "show_matches": [
+ "*://*/*"
+ ]
},
"background": {
"page": "js/background/index.html"
diff --git a/src/popup.html b/src/popup.html
index fac21fe2a..78d322860 100644
--- a/src/popup.html
+++ b/src/popup.html
@@ -309,12 +309,17 @@
-