Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Unified Diff: options.js

Issue 6088024630755328: issue 1526 - Implement new options page design for Chrome/Opera/Safari (Closed)
Patch Set: Created Feb. 26, 2015, 11:50 a.m.
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: options.js
===================================================================
new file mode 100644
--- /dev/null
+++ b/options.js
@@ -0,0 +1,701 @@
+/*
+ * This file is part of Adblock Plus <https://adblockplus.org/>,
+ * Copyright (C) 2006-2015 Eyeo GmbH
+ *
+ * Adblock Plus is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * Adblock Plus is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+"use strict";
+
+(function()
+{
+ var acceptableAdsUrl = null;
+ var subscriptionsMap = Object.create(null);
+ var recommendationsMap = Object.create(null);
+ var filtersMap = Object.create(null);
+ var collections = Object.create(null);
+
+ function Collection(details)
saroyanm 2015/02/26 12:18:33 We can rethink about the name of current Class in
Thomas Greiner 2015/03/05 11:36:03 I guess "Collection" should be fine (see Java's Co
Thomas Greiner 2015/03/05 11:36:03 Why "details"? It's an array of items and not a co
saroyanm 2015/03/06 11:54:32 I used details as I thought that it's corresponds
Thomas Greiner 2015/03/12 11:41:56 Makes sense, let's stick with "details" then.
+ {
+ this.details = details;
+ }
+
+ Collection.prototype = Object.create(Array.prototype);
+ Collection.prototype.addItems = function()
Thomas Greiner 2015/03/05 11:36:03 I couldn't find a case where you add multiple item
saroyanm 2015/03/06 11:54:32 I actually were thinking that this could be useful
Thomas Greiner 2015/03/12 11:41:56 Ok, then let's leave it like it is for now (no nee
saroyanm 2015/03/12 14:03:22 Done.
+ {
+ var length = Array.prototype.push.apply(this, arguments);
+ if (length == 0)
+ return;
+
+ this.sort(function(a, b)
+ {
+ var aValue = (a.title || a.url || a.text).toLowerCase();
+ var bValue = (b.title || b.url || a.text).toLowerCase();
+ if (aValue < bValue)
+ return -1;
+ if (aValue > bValue)
+ return 1;
+ return 0;
+ });
+
+ for (var j = 0; j < this.details.length; j++)
+ {
+ var table = E(this.details[j].id);
+ var template = table.querySelector("template");
+ var listener = this.details[j].listener;
+ for (var i = 0; i < arguments.length; i++)
+ {
+ var object = arguments[i];
+ var text = object.title || object.text;
Thomas Greiner 2015/03/05 11:36:03 A subscription might not have a title. In those ca
+ var access = object.url || object.text;
Thomas Greiner 2015/03/05 11:36:03 You're only using this variable once so no need to
saroyanm 2015/03/06 11:54:32 Done, but now I'm using access, to shorten the lin
Thomas Greiner 2015/03/12 11:41:56 That'd be ok but "index" is only used in that one
saroyanm 2015/03/12 14:03:22 This is already fixed in current patch: http://cod
+ var index = this.indexOf(object);
+ var listItem = document.createElement("li");
+ listItem.appendChild(document.importNode(template.content, true));
+ listItem.dataset.access = access;
Thomas Greiner 2015/03/05 11:36:03 You need to normalize this string or otherwise `El
saroyanm 2015/03/06 11:54:32 while current query throw error: document.querySel
Thomas Greiner 2015/03/12 11:41:56 Should be fine. Alternatively, you could use `enc
+ listItem.querySelector(".display").textContent = text;
+ if (text)
+ listItem.dataset.search = text.toLowerCase();
+
+ var control = listItem.querySelector(".control");
+ if (control)
+ {
+ control.addEventListener("click", listener, false);
Thomas Greiner 2015/03/05 11:36:03 Just FYI: I'd still prefer a single global event l
+ control.checked = object.disabled == false;
+ }
+
+ var popular = listItem.querySelector(".popular");
+ if (popular)
+ popular.textContent = ext.i18n.getMessage("options_popular");
Thomas Greiner 2015/03/05 11:36:03 Setting that text right inside the template tag on
saroyanm 2015/03/06 11:54:32 Done.
+
+ if (table.hasChildNodes)
+ table.insertBefore(listItem, table.childNodes[index]);
+ else
+ table.appendChild(listItem);
+ }
+ }
+ return length;
+ };
+
+ Collection.prototype.removeItem = function(obj)
Thomas Greiner 2015/03/05 11:36:03 The function is called "removeItem" but the item i
saroyanm 2015/03/06 11:54:32 Done.
+ {
+ var index = this.indexOf(obj);
+ if (index == -1)
+ return;
+
+ this.splice(index, 1);
+ var access = obj.url || obj.text;
+ for (var i = 0; i < this.details.length; i++)
+ {
+ var table = E(this.details[i].id);
+ var element = table.querySelector("[data-access='"+access+"']");
+ element.parentElement.removeChild(element);
+ }
+ };
+
+ function toggleSubscription(e)
+ {
+ e.preventDefault();
+ var isChecked = e.target.checked;
Thomas Greiner 2015/03/05 11:36:03 Again, this variable is only used once.
saroyanm 2015/03/06 11:54:32 Done.
+ var subscriptionUrl = e.target.parentNode.dataset.access;
+ if (!isChecked)
+ removeSubscription(subscriptionUrl);
+ else
+ addEnableSubscription(subscriptionUrl);
+ }
+
+ function addLanguageSubscription(e)
+ {
+ e.preventDefault();
+ var url = this.parentNode.dataset.access;
+ addEnableSubscription(url);
+ }
+
+ function triggerRemoveFilter()
+ {
+ var filter = this.parentNode.dataset.access;
+ removeFilter(filter);
+ }
+
+ collections.popular = new Collection(
+ [
+ {
+ id: "recommend-list-table",
+ listener: toggleSubscription
Thomas Greiner 2015/03/05 11:36:03 I'd rename "listener" to "onClick". Otherwise it's
saroyanm 2015/03/06 11:54:32 Done.
+ }
+ ]);
+ collections.langs = new Collection(
+ [
+ {
+ id: "blocking-languages-table",
+ listener: toggleSubscription
+ },
+ {
+ id: "blocking-languages-modal-table"
+ }
+ ]);
+ collections.allLangs = new Collection(
+ [
+ {
+ id: "all-lang-table",
+ listener: addLanguageSubscription
+ }
+ ]);
+ collections.acceptableAds = new Collection(
+ [
+ {
+ id: "acceptableads-table",
+ listener: toggleSubscription
+ }
+ ]);
+ collections.custom = new Collection(
+ [
+ {
+ id: "custom-list-table",
+ listener: toggleSubscription
+ }
+ ]);
+ collections.whitelist = new Collection(
+ [
+ {
+ id: "whitelisting-table",
+ listener: triggerRemoveFilter
+ }
+ ]);
+
+ function updateSubscription(subscription)
+ {
+ var subscriptionUrl = subscription.url;
+ var knownSubscription = getKnownSubscription(subscriptionUrl);
+ if (knownSubscription)
+ knownSubscription.disabled = subscription.disabled;
+ else
+ {
+ getAcceptableAdsURL(function(acceptableAdsUrl)
+ {
+ function onObjectChanged()
+ {
+ var access = subscriptionUrl || subscription.text;
+ var elements = document.querySelectorAll("[data-access='" + access + "']");
+ for (var i = 0; i < elements.length; i++)
+ {
+ var element = elements[i];
+ var control = element.getElementsByClassName("control")[0];
Thomas Greiner 2015/03/05 11:36:03 Usually, you use `element.querySelector(".control"
saroyanm 2015/03/06 11:54:32 Done.
+ if (control.localName == "input")
+ control.checked = subscription.disabled == false;
+ if (isRecommendation(subscriptionUrl))
+ {
+ var recommendation = getRecommendation(subscriptionUrl);
+ if (recommendation.isAdsType && subscription.disabled == false)
+ {
+ collections.allLangs.removeItem(subscription);
+ collections.langs.addItems(subscription);
+ }
+ else
+ {
+ collections.allLangs.addItems(subscription);
+ collections.langs.removeItem(subscription);
+ }
+ }
+ }
+ }
+
+ if (!Object.observe)
+ {
+ ["disabled"].forEach(function(property)
+ {
+ subscription["$"+property] = subscription[property];
Thomas Greiner 2015/03/05 11:36:03 By now I'd expect you to know that operators, like
saroyanm 2015/03/06 11:54:32 Done.
+ Object.defineProperty(subscription, property,
+ {
+ get: function()
+ {
+ return this["$"+property];
+ },
+ set: function(value)
+ {
+ this["$"+property] = value;
+ onObjectChanged();
+ }
+ });
+ });
+ }
+ else
+ {
+ Object.observe(subscription, function(changes)
+ {
+ onObjectChanged();
+ });
+ }
+
+ var collection = null;
+ if (isRecommendation(subscriptionUrl))
+ {
+ var recommendation = getRecommendation(subscriptionUrl);
+ if (recommendation.isPopular)
+ collection = collections.popular;
+ else if (recommendation.isAdsType && subscription.disabled == false)
+ collection = collections.langs;
+ else
+ collection = collections.allLangs;
+ }
+ else if (subscriptionUrl == acceptableAdsUrl)
+ collection = collections.acceptableAds;
+ else
+ collection = collections.custom;
+
+ collection.addItems(subscription);
+ setKnownSubscription(subscriptionUrl, subscription);
+ });
+ }
+ }
+
+ function updateFilter(filter)
+ {
+ var knownFilter = getKnownFilter(filter.text);
+ var match = filter.text.match(/^@@\|\|([^\/:]+)\^\$document$/);
+ if (match && !knownFilter)
+ {
+ filter.title = match[1];
+ collections.whitelist.addItems(filter);
+ setKnownFilter(filter.text, filter);
+ }
+ else
+ {
+ // TODO: add `filters[i].text` to list of custom filters
+ }
+ }
+
+ function loadRecommendations()
+ {
+ var request = new XMLHttpRequest();
+ request.open("GET", "subscriptions.xml", false);
+ request.onload = function()
+ {
+ var list = document.getElementById("subscriptionSelector");
+ var docElem = request.responseXML.documentElement;
+ var elements = docElem.getElementsByTagName("subscription");
+ for (var i = 0; i < elements.length; i++)
+ {
+ var element = elements[i];
+ var subscription = Object.create(null);
+ subscription.title = element.getAttribute("title");
+ subscription.url = element.getAttribute("url");
+ subscription.disabled = null;
+ subscription.downloadStatus = null;
+ subscription.homepage = null;
+ subscription.lastSuccess = null;
+ var recommendation = Object.create(null);
+ recommendation.isAdsType = false;
+ recommendation.isPopular = false;
+ var prefix = element.getAttribute("prefixes");
+ if (prefix)
+ {
+ var prefix = element.getAttribute("prefixes").replace(/,/g, "_");
+ subscription.title = ext.i18n.getMessage("options_language_" + prefix);
+ recommendation.isAdsType = true;
+ }
+ else
+ subscription.title = element.getAttribute("specialization");
+
+ if (element.getAttribute("popular"))
+ recommendation.isPopular = true;
+
+ setRecommendation(subscription.url, recommendation);
+ updateSubscription(subscription);
+ }
+ };
+ request.send(null);
+ }
+
+ function onDOMLoaded()
+ {
+ updateShareLink();
+ populateLists();
+
+ var tabList = document.querySelectorAll("#main-navigation-tabs li");
+ for (var i = 0; i < tabList.length; i++)
+ {
+ tabList[i].addEventListener("click", function(e)
+ {
+ document.body.dataset.tab = e.currentTarget.dataset.show;
+ }, false);
+ }
+
+ function searchLanguage()
+ {
+ var searchStyle = E("search-style");
+ if (!this.value)
+ searchStyle.innerHTML = "";
+ else
+ searchStyle.innerHTML = "#all-lang-table li:not([data-search*=\"" + this.value.toLowerCase() + "\"]) { display: none; }";
+ }
+
+ // Update version number in navigation sidebar
+ ext.backgroundPage.sendMessage(
+ {
+ method: "app.get",
+ what: "addonVersion"
+ },
+ function(addonVersion)
+ {
+ E("abp-version").textContent = addonVersion;
+ });
+
+ var placeholderValue = ext.i18n.getMessage("options_modal_language_find");
+ E("find-language").setAttribute("placeholder", placeholderValue);
+ E("add-blocking-list").addEventListener("click", function()
+ {
+ openModal("customlist");
+ }, false);
+ E("add-website-language").addEventListener("click", function()
+ {
+ openModal("language");
+ }, false);
+ E("modal-close").addEventListener("click", function()
+ {
+ delete document.body.dataset.modal;
+ }, false);
+ E("edit-ownBlockingList-btn").addEventListener("click", editCustomFilters, false);
+ E("find-language").addEventListener("keyup", searchLanguage, false);
+ E("find-language").addEventListener("search", searchLanguage, false);
Thomas Greiner 2015/03/05 11:36:03 Turns out that the "onsearch" event listener is We
saroyanm 2015/03/06 11:54:32 I'm using this event to listen for "X" button clic
Thomas Greiner 2015/03/12 11:41:56 Since that "X" is also not in the standard I'd sug
saroyanm 2015/03/12 14:03:22 Done.
+ E("whitelisting-add-icon").addEventListener("click", addWhitelistedDomain, false);
+ E("whitelisting-add-btn").addEventListener("click", addWhitelistedDomain, false);
+ E("whitelisting-enter-icon").addEventListener("click", addWhitelistedDomain, false);
+ E("whitelisting-textbox").addEventListener("keypress", function(e)
+ {
+ // keyCode value of 13 corresponds to "Enter" key
Thomas Greiner 2015/03/05 11:36:03 Please also explain why you need to check for both
saroyanm 2015/03/06 11:54:32 Done.
+ if (e.key && e.key == "Enter")
+ addWhitelistedDomain();
+ else if (!e.key && e.keyCode == 13)
+ addWhitelistedDomain();
+ }, false);
+ E("whitelisting-cancel-btn").addEventListener("click", function()
+ {
+ E("whitelisting-textbox").value = "";
+ }, false);
+ E("import-blockingList-btn").addEventListener("click", function()
+ {
+ var url = E("blockingList-textbox").value;
+ addEnableSubscription(url);
+ delete document.body.dataset.modal;
+ }, false);
+ }
+
+ function openModal(name)
+ {
+ document.body.dataset.modal = name;
+ }
+
+ function populateLists()
+ {
+ subscriptionsMap = Object.create(null);
+ filtersMap = Object.create(null);
+ recommendationsMap = Object.create(null);
+
+ // Empty collections and lists
+ for (var property in collections)
+ {
+ var collection = collections[property];
+ collection.details.forEach(function(detail)
Thomas Greiner 2015/03/05 11:36:03 This property does not need to be exposed. It'd be
saroyanm 2015/03/06 11:54:32 Done.
+ {
+ var table = E(detail.id);
+ var template = table.querySelector("template");
+ table.innerHTML = "";
+ table.appendChild(template);
+ });
+ collection.length = 0;
+ }
+
+ ext.backgroundPage.sendMessage(
+ {
+ type: "subscriptions.get",
+ special: true
+ },
+ function(subscriptions)
+ {
+ // Load filters
+ for (var i = 0; i < subscriptions.length; i++)
+ {
+ ext.backgroundPage.sendMessage(
+ {
+ type: "filters.get",
+ subscriptionUrl: subscriptions[i].url
+ },
+ function(filters)
+ {
+ for (var i = 0; i < filters.length; i++)
+ updateFilter(filters[i]);
+ });
+ }
+ });
+ loadRecommendations();
+ getAcceptableAdsURL(function(acceptableAdsUrl)
+ {
+ var subscription = Object.create(null);
+ subscription.url = acceptableAdsUrl;
+ subscription.disabled = true;
+ subscription.title = ext.i18n.getMessage("options_acceptableAds_description");
+ updateSubscription(subscription);
+
+ // Load user subscriptions
+ ext.backgroundPage.sendMessage(
+ {
+ type: "subscriptions.get",
+ downloadable: true
+ },
+ function(subscriptions)
+ {
+ for (var i = 0; i < subscriptions.length; i++)
+ onSubscriptionMessage("added", subscriptions[i]);
+ });
+ });
+ }
+
+ function addWhitelistedDomain()
+ {
+ var domain = E("whitelisting-textbox");
+ if (domain.value)
+ {
+ ext.backgroundPage.sendMessage(
+ {
+ type: "filters.add",
+ text: "@@||" + domain.value.toLowerCase() + "^$document"
+ });
+ }
+
+ domain.value = "";
+ }
+
+ function editCustomFilters()
+ {
+
+ }
+
+ function getAcceptableAdsURL(callback)
+ {
+ if (acceptableAdsUrl)
Thomas Greiner 2015/03/05 11:36:03 This if-statement is not necessary and neither is
saroyanm 2015/03/06 11:54:32 Hmm :/ Totally missed your implementation, good po
+ callback(acceptableAdsUrl);
+ else
+ {
+ ext.backgroundPage.sendMessage(
+ {
+ type: "prefs.get",
+ key: "subscriptions_exceptionsurl"
+ },
+ function(value)
+ {
+ getAcceptableAdsURL = function(callback)
+ {
+ acceptableAdsUrl = value;
+ callback(value);
+ }
+ getAcceptableAdsURL(callback);
+ });
+ }
+ }
+
+ function getKnownSubscription(url)
Thomas Greiner 2015/03/05 11:36:03 The next few functions are all one-liners. There's
saroyanm 2015/03/06 11:54:32 Done.
+ {
+ return subscriptionsMap[url];
+ }
+
+ function setKnownSubscription(url, subscription)
+ {
+ subscriptionsMap[url] = subscription;
+ }
+
+ function getKnownFilter(text)
+ {
+ return filtersMap[text];
+ }
+
+ function setKnownFilter(text, filter)
+ {
+ filtersMap[text] = filter;
+ }
+
+ function getRecommendation(url)
+ {
+ return recommendationsMap[url];
+ }
+
+ function setRecommendation(url, recommendation)
+ {
+ recommendationsMap[url] = recommendation;
+ }
+
+ function isRecommendation(url)
+ {
+ return url in recommendationsMap;
+ }
+
+ function addEnableSubscription(url, title, homepage)
+ {
+ var messageType = null;
+ var knownSubscription = getKnownSubscription(url);
+ if (knownSubscription && knownSubscription.disabled == true)
+ messageType = "subscriptions.toggle"
+ else
+ messageType = "subscriptions.add"
+
+ var message = {
+ type: messageType,
+ url: url
+ };
+ if (title)
+ message.title = title;
+ if (homepage)
+ message.homepage = homepage;
+
+ ext.backgroundPage.sendMessage(message);
+ }
+
+ function removeSubscription(url)
+ {
+ ext.backgroundPage.sendMessage(
+ {
+ type: "subscriptions.remove",
+ url: url
+ });
+ }
+
+ function removeFilter(filter)
+ {
+ ext.backgroundPage.sendMessage(
+ {
+ type: "filters.remove",
+ text: filter
+ });
+ }
+
+ function onFilterMessage(action, filter)
+ {
+ switch (action)
+ {
+ case "added":
+ updateFilter(filter);
+ updateShareLink();
+ break;
+ case "loaded":
+ populateLists();
+ break;
+ case "removed":
+ var knownFilter = getKnownFilter(filter.text);
+ collections.whitelist.removeItem(knownFilter);
+ updateShareLink();
+ break;
+ }
+ }
+
+ function onSubscriptionMessage(action, subscription)
+ {
+ switch (action)
+ {
+ case "added":
+ updateSubscription(subscription);
+ updateShareLink();
+ break;
+ case "disabled":
+ updateSubscription(subscription);
+ updateShareLink();
+ break;
+ case "homepage":
+ // TODO: NYI
+ break;
+ case "removed":
+ getAcceptableAdsURL(function(acceptableAdsUrl)
+ {
+ if (subscription.url == acceptableAdsUrl)
+ {
+ subscription.disabled = true;
+ updateSubscription(subscription);
+ }
+ else
+ {
+ var knownSubscription = getKnownSubscription(subscription.url);
+ var subscriptionUrl = subscription.url;
Thomas Greiner 2015/03/05 11:36:03 This variable is not necessary. It's only used onc
saroyanm 2015/03/06 11:54:32 Done.
+ if (isRecommendation(subscriptionUrl))
+ knownSubscription.disabled = true;
+ else
+ collections.custom.removeItem(knownSubscription);
+ }
+ updateShareLink();
+ });
+ break;
+ case "title":
+ // TODO: NYI
+ break;
+ }
+ }
+
+ function showAddSubscriptionDialog(subscription)
+ {
+ E("blockingList-textbox").value = subscription.url;
+ openModal("customlist");
+ }
+
+ function updateShareLink()
+ {
+ ext.backgroundPage.sendMessage(
+ {
+ type: "filters.blocked",
+ url: "https://platform.twitter.com/widgets/",
+ requestType: "SCRIPT",
+ docDomain: "adblockplus.org",
+ thirdParty: true
+ },
+ function(blocked)
+ {
+ // TODO: modify "share" link accordingly
+ });
+ }
+
+ function E(id)
+ {
+ return document.getElementById(id);
+ }
+
+ ext.onMessage.addListener(function(message)
+ {
+ switch (message.type)
+ {
+ case "app.listen":
+ if (message.action == "addSubscription")
+ showAddSubscriptionDialog(message.args[0]);
+ break;
+ case "filters.listen":
+ onFilterMessage(message.action, message.args[0]);
+ break;
+ case "subscriptions.listen":
+ onSubscriptionMessage(message.action, message.args[0]);
+ break;
+ }
+ });
+
+ ext.backgroundPage.sendMessage(
+ {
+ type: "app.listen",
+ filter: ["addSubscription"]
+ });
+ ext.backgroundPage.sendMessage(
+ {
+ type: "filters.listen",
+ filter: ["added", "loaded", "removed"]
+ });
+ ext.backgroundPage.sendMessage(
+ {
+ type: "subscriptions.listen",
+ filter: ["added", "disabled", "homepage", "removed", "title"]
+ });
+
+ window.addEventListener("DOMContentLoaded", onDOMLoaded, false);
+})();

Powered by Google App Engine
This is Rietveld