aboutsummaryrefslogtreecommitdiff
path: root/html/preferences_panel/pref.js
diff options
context:
space:
mode:
Diffstat (limited to 'html/preferences_panel/pref.js')
-rw-r--r--html/preferences_panel/pref.js316
1 files changed, 273 insertions, 43 deletions
diff --git a/html/preferences_panel/pref.js b/html/preferences_panel/pref.js
index 4642d32..9cecbb6 100644
--- a/html/preferences_panel/pref.js
+++ b/html/preferences_panel/pref.js
@@ -1,7 +1,8 @@
/**
* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
-* *
+*
* Copyright (C) 2017 Nathan Nichols
+* Copyright (C) 2018 Giorgio maone
*
* This file is part of GNU LibreJS.
*
@@ -19,59 +20,288 @@
* along with GNU LibreJS. If not, see <http://www.gnu.org/licenses/>.
*/
-var store;
+(() => {
+ "use strict";
-function storage_got(items){
- var inputs = document.getElementsByTagName("input");
+ const LIST_NAMES = ["white", "black"];
- if(items["pref_whitelist"] == "undefined"){
- items["pref_whitelist"] = "";
- }
+ var Model = {
+ lists: {},
+ prefs: null,
- if(items["pref_subject"] == "undefined" || items["pref_subject"] == ""){
- items["pref_subject"] = "Issues with Javascript on your website";
- }
+ malformedUrl(url) {
+ let error = null;
+ try {
+ let objUrl = new URL(url);
+ url = objUrl.href;
+ if (!objUrl.protocol.startsWith("http")) {
+ error = "Please enter http:// or https:// URLs only";
+ } else if (!/^[^*]+\*?$/.test(url)) {
+ error = "Only one single trailing path wildcard (/*) allowed";
+ }
+ } catch (e) {
+ error = "Invalid URL";
+ if (url && !url.includes("://")) error += ": missing protocol, either http:// or https://";
+ else if (url.endsWith("://")) error += ": missing domain name";
+ }
+ return error;
+ },
- if(items["pref_body"] == "undefined" || items["pref_body"] == ""){
- items["pref_body"] = "Please consider using a free license for the Javascript on your website. [Message generated by LibreJS. See https://www.gnu.org/software/librejs/ for more information]";
- }
+ async save(prefs = this.prefs) {
+ if (prefs !== this.prefs) {
+ this.prefs = Object.assign(this.prefs, prefs);
+ }
+ this.saving = true;
+ try {
+ return await browser.storage.local.set(prefs);
+ } finally {
+ this.saving = false;
+ }
+ },
- for(var i = 0; i < inputs.length; i++){
- if(inputs[i].id.indexOf("pref_") != -1){
- if(inputs[i].type == "checkbox" && items[inputs[i].id]){
- inputs[i].checked = true;
+ async addToList(list, ...items) {
+ let other = list === Model.lists.black ? Model.lists.white : Model.lists.black;
+ this.saving = true;
+ try {
+ await Promise.all([
+ other.remove(...items),
+ list.store(...items)
+ ]);
+ } finally {
+ this.saving = false;
}
- if(inputs[i].type == "text" && items[inputs[i].id] != undefined){
- inputs[i].value = items[inputs[i].id];
- }
}
- }
+ };
+ Model.loading = (async () => {
+ let prefsNames = [
+ "whitelist",
+ "blacklist",
+ "subject",
+ "body"
+ ];
+ Model.prefs = await browser.storage.local.get(prefsNames.map(name => `pref_${name}`));
+
+ for (let listName of LIST_NAMES) {
+ let prefName = `pref_${listName}list`;
+ await (Model.lists[listName] = new ListStore(prefName, Storage.CSV))
+ .load(Model.prefs[prefName]);
+ }
+ })();
+
+ var Controller = {
+ init() {
+ let widgetsRoot = this.root = document.getElementById("widgets");
+ for (let widget of widgetsRoot.querySelectorAll('[id^="pref_"]')) {
+ if (widget.id in Model.lists) {
+ populateListUI(widget);
+ } else if (widget.id in Model.prefs) {
+ widget.value = Model.prefs[widget.id];
+ }
+ }
+
+ this.populateListUI();
+ this.syncAll();
+
+ for (let ev in Listeners) {
+ widgetsRoot.addEventListener(ev, Listeners[ev]);
+ }
+ document.getElementById("site").onfocus = e => {
+ if (!e.target.value.trim()) {
+ e.target.value = "https://";
+ }
+ };
+
+ browser.storage.onChanged.addListener(changes => {
+ if (!Model.saving &&
+ ("pref_whitelist" in changes || "pref_blacklist" in changes)) {
+ setTimeout(() => {
+ this.populateListUI();
+ this.syncAll();
+ }, 10);
+ }
+ });
+ },
+
+ async addSite(list) {
+ let url = document.getElementById("site").value.trim();
+
+ if (url && !Model.malformedUrl(url)) {
+ await this.addToList(list, url);
+ }
+ },
+ async addToList(list, ...items) {
+ await Model.addToList(list, ...items);
+ this.populateListUI();
+ this.syncAll();
+ },
+ async swapSelection(list) {
+ let origin = list === Model.lists.black ? "white" : "black";
+ await this.addToList(list, ...Array.map(
+ document.querySelectorAll(`select#${origin} option:checked`),
+ option => option.value)
+ );
+ },
+
+ syncAll() {
+ this.syncListsUI();
+ this.syncSiteUI();
+ },
+
+ syncSiteUI() {
+ let widget = document.getElementById("site");
+ let list2button = listName => document.getElementById(`cmd-${listName}list-site`);
+
+ for (let bi of LIST_NAMES.map(list2button)) {
+ bi.disabled = true;
+ }
+
+ let url = widget.value.trim();
+ let malformedUrl = url && Model.malformedUrl(url);
+ widget.classList.toggle("error", !!malformedUrl);
+ document.getElementById("site-error").textContent = malformedUrl || "";
+ if (!url) return;
+ if (url !== widget.value) {
+ widget.value = url;
+ }
+
+ for (let listName of LIST_NAMES) {
+ let list = Model.lists[listName];
+ if (!list.contains(url)) {
+ list2button(listName).disabled = false;
+ }
+ }
+ },
+
+ syncListsUI() {
+ let total = 0;
+ for (let id of ["black", "white"]) {
+ let selected = document.querySelectorAll(`select#${id} option:checked`).length;
+ let other = id === "black" ? "white" : "black";
+ document.getElementById(`cmd-${other}list`).disabled = selected === 0;
+ total += selected;
+ }
+ document.getElementById("cmd-delete").disabled = total === 0;
+ },
+ async deleteSelection() {
+ for (let id of ["black", "white"]) {
+ let selection = document.querySelectorAll(`select#${id} option:checked`);
+ await Model.lists[id].remove(...Array.map(selection, option => option.value));
+ }
+ this.populateListUI();
+ this.syncAll();
+ },
-}
-browser.storage.local.get(storage_got);
-
-document.getElementById("save_changes").addEventListener("click", function(){
- var inputs = document.getElementsByTagName("input");
- // TODO: validate/sanitize the user inputs
- var data = {};
- for(var i = 0; i < inputs.length; i++){
- if(inputs[i].id.indexOf("pref_") != -1){
- var input_val = "";
- if(inputs[i].type == "checkbox"){
- input_val = inputs[i].checked;
- } else{
- if(inputs[i.value] != "undefined"){
- input_val = inputs[i].value;
- } else{
- input_val = "";
+ populateListUI(widget) {
+ if (!widget) {
+ for(let id of ["white", "black"]) {
+ this.populateListUI(document.getElementById(id));
}
+ return;
+ }
+ widget.innerHTML = "";
+ let items = [...Model.lists[widget.id].items].sort();
+ let options = new DocumentFragment();
+ for (let item of items) {
+ let option = document.createElement("option");
+ option.value = option.textContent = option.title = item;
+ options.appendChild(option);
+ }
+ widget.appendChild(options);
+ }
+ };
+
+ var KeyEvents = {
+ Delete(e) {
+ if (e.target.matches("#lists select")) {
+ Controller.deleteSelection();
+ }
+ },
+ Enter(e) {
+ if (e.target.id === "site") {
+ e.target.parentElement.querySelector("button[default]").click();
+ }
+ },
+ KeyA(e) {
+ if (e.target.matches("select") && e.ctrlKey) {
+ for (let o of e.target.options) {
+ o.selected = true;
+ }
+ Controller.syncListsUI();
}
- var input_id = inputs[i].id;
- data[input_id] = input_val;
}
}
- console.log(data);
- browser.storage.local.set(data);
-});
+ var Listeners = {
+ async change(e) {
+ let {target} = e;
+ let {id} = target;
+
+ if (id in Model.lists) {
+ Controller.syncListsUI();
+ let selection = target.querySelectorAll("option:checked");
+ if (selection.length === 1) {
+ document.getElementById("site").value = selection[0].value;
+ }
+ return;
+ }
+ },
+
+ click(e) {
+ let {target} = e;
+
+ if (!/^cmd-(white|black|delete)(list-site)?/.test(target.id)) return;
+ e.preventDefault();
+ let cmd = RegExp.$1;
+ if (cmd === "delete") {
+ Controller.deleteSelection();
+ return;
+ }
+ let list = Model.lists[cmd];
+ if (list) {
+ Controller[RegExp.$2 ? "addSite" : "swapSelection"](list);
+ return;
+ }
+ },
+
+ keypress(e) {
+ let {code} = e;
+ if (code && typeof KeyEvents[code] === "function") {
+ if (KeyEvents[code](e) === false) {
+ e.preventDefault();
+ }
+ return;
+ }
+ },
+
+ async input(e) {
+ let {target} = e;
+ let {id} = target;
+ if (!id) return;
+
+ if (id === "site") {
+ Controller.syncSiteUI();
+ let url = target.value;
+ if (url) {
+ let o = document.querySelector(`#lists select option[value="${url}"]`);
+ if (o) {
+ o.scrollIntoView();
+ o.selected = true;
+ }
+ }
+ return;
+ }
+
+ if (id.startsWith("pref_")) {
+ await Model.save({[id]: target.value});
+ return;
+ }
+ }
+ };
+
+ window.addEventListener("DOMContentLoaded", async e => {
+ await Model.loading;
+ Controller.init();
+ });
+
+})();