Index: mobile-options.js |
=================================================================== |
new file mode 100644 |
--- /dev/null |
+++ b/mobile-options.js |
@@ -0,0 +1,432 @@ |
+/* |
+ * This file is part of Adblock Plus <https://adblockplus.org/>, |
+ * Copyright (C) 2006-2017 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/>. |
+ */ |
+ |
+/* globals getDocLink */ |
+ |
+"use strict"; |
+ |
saroyanm
2017/07/18 12:46:28
I'll suggest to encapsulate this file with own blo
Thomas Greiner
2017/07/18 17:42:32
Done.
|
+const {getMessage} = ext.i18n; |
+ |
+let whitelistFilter = null; |
+let promisedAcceptableAdsUrl = getAcceptableAdsUrl(); |
+ |
+/* Utility functions */ |
+ |
+function get(selector, origin) |
saroyanm
2017/07/18 12:46:28
Detail: Calling function "get" is too generic, as
Thomas Greiner
2017/07/18 17:42:31
Acknowledged.
Note that the inspiration for this
saroyanm
2017/07/31 11:08:54
Now I see your intention, but in that case I think
|
+{ |
+ return (origin || document).querySelector(selector); |
+} |
+ |
+function getAll(selector, origin) |
+{ |
+ return (origin || document).querySelectorAll(selector); |
+} |
+ |
+function create(parent, tagName, content, attributes, onclick) |
+{ |
+ let element = document.createElement(tagName); |
+ |
+ if (typeof content == "string") |
+ { |
+ element.textContent = content; |
+ } |
+ |
+ if (attributes) |
+ { |
+ for (let name in attributes) |
+ { |
+ element.setAttribute(name, attributes[name]); |
+ } |
+ } |
+ |
+ if (onclick) |
+ { |
+ element.addEventListener("click", (ev) => |
+ { |
+ onclick(ev); |
+ ev.stopPropagation(); |
+ }); |
saroyanm
2017/07/18 12:46:28
Note: I do remember before it was required to spec
Thomas Greiner
2017/07/18 17:42:32
We dropped that requirement a while ago. From what
|
+ } |
+ |
+ parent.appendChild(element); |
+ return element; |
+} |
+ |
+/* Extension interactions */ |
+ |
+function getInstalled() |
+{ |
+ return new Promise((resolve, reject) => |
+ { |
+ ext.backgroundPage.sendMessage( |
+ {type: "subscriptions.get", downloadable: true}, |
+ resolve |
+ ); |
+ }); |
+} |
+ |
+function getAcceptableAdsUrl() |
+{ |
+ return new Promise((resolve, reject) => |
+ { |
+ ext.backgroundPage.sendMessage( |
+ {type: "prefs.get", key: "subscriptions_exceptionsurl"}, |
+ resolve |
+ ); |
+ }); |
+} |
+ |
+function getRecommended() |
saroyanm
2017/07/18 12:46:28
This function only return recommended subscription
Thomas Greiner
2017/07/18 17:42:33
Good point. Done.
|
+{ |
+ return fetch("subscriptions.xml") |
+ .then((resp) => resp.text()) |
saroyanm
2017/07/18 12:46:27
Detail: we usually are not using short form of the
saroyanm
2017/07/18 12:46:28
Shouldn't this be returned (return response.text()
Thomas Greiner
2017/07/18 17:42:31
No, this is one of the features of arrow functions
Thomas Greiner
2017/07/18 17:42:31
I agree that it makes sense for "response", so I'l
|
+ .then((text) => |
+ { |
+ let doc = new DOMParser().parseFromString(text, "application/xml"); |
+ let elements = Array.from(doc.getElementsByTagName("subscription")); |
+ |
+ return elements |
+ .filter((element) => element.getAttribute("type") == "ads") |
+ .map((element) => |
+ { |
+ return { |
saroyanm
2017/07/18 12:46:27
Detail: the brace should go to the next row for co
Thomas Greiner
2017/07/18 17:42:31
Moving this brace to the next row would be interpr
|
+ title: element.getAttribute("title"), |
saroyanm
2017/07/18 12:46:28
Shouldn't this titles be translated ?
I think we
Thomas Greiner
2017/07/18 17:42:31
We don't translate filter list titles (e.g. "EasyL
|
+ url: element.getAttribute("url") |
+ }; |
+ }); |
+ }); |
+} |
+ |
+function installSubscription(url, title) |
+{ |
+ ext.backgroundPage.sendMessage({type: "subscriptions.add", url, title}); |
+} |
+ |
+function uninstallSubscription(url) |
+{ |
+ ext.backgroundPage.sendMessage({type: "subscriptions.remove", url}); |
+} |
+ |
+/* Actions */ |
+ |
+function setSubscription({disabled, title, url}, shouldAdd) |
+{ |
+ if (disabled) |
+ return; |
+ |
+ promisedAcceptableAdsUrl.then((acceptableAdsUrl) => |
+ { |
+ if (url == acceptableAdsUrl) |
+ { |
+ get("#acceptableAds").checked = true; |
+ return; |
+ } |
+ |
+ let listInstalled = get("#subscriptions-installed"); |
+ let installed = get(`[data-url="${url}"]`, listInstalled); |
saroyanm
2017/07/18 12:46:28
Detail: I think we are using single quotes "'", ra
Thomas Greiner
2017/07/18 17:42:32
This is a template literal, not a string literal.
|
+ |
+ if (installed) |
+ { |
+ let titleElement = get("span", installed); |
+ titleElement.textContent = title || url; |
+ } |
+ else if (shouldAdd) |
saroyanm
2017/07/18 12:46:28
Why are you using "shouldAdd" parameter ?
If the
Thomas Greiner
2017/07/18 17:42:31
Why should we show subscriptions that are not inst
saroyanm
2017/07/31 11:08:54
I think I meant "installed", nevermind, I don't re
|
+ { |
+ let element = create(listInstalled, "li", null, {"data-url": url}); |
+ create(element, "span", title || url); |
+ create(element, "button", null, {class: "remove"}, |
+ () => uninstallSubscription(url) |
+ ); |
+ |
+ let recommended = get(`#subscriptions-recommended [data-url="${url}"]`); |
+ if (recommended) |
+ { |
+ recommended.classList.add("installed"); |
+ } |
+ } |
+ }); |
+} |
+ |
+function removeSubscription(url) |
+{ |
+ promisedAcceptableAdsUrl.then((acceptableAdsUrl) => |
+ { |
+ if (url == acceptableAdsUrl) |
+ { |
+ get("#acceptable-ads").checked = false; |
saroyanm
2017/07/18 12:46:27
I think you forget to change all camelCase IDs.
Thomas Greiner
2017/07/18 17:42:32
Yep, seems so. Done.
|
+ return; |
+ } |
+ |
+ let installed = get(`#subscriptions-installed [data-url="${url}"]`); |
+ if (installed) |
+ { |
+ installed.parentNode.removeChild(installed); |
+ } |
+ |
+ let recommended = get(`#subscriptions-recommended [data-url="${url}"]`); |
+ if (recommended) |
+ { |
+ recommended.classList.remove("installed"); |
+ } |
+ }); |
+} |
+ |
+function setDialog(id, options) |
+{ |
+ if (!id) |
+ { |
+ delete document.body.dataset.dialog; |
+ return; |
+ } |
+ |
+ let fields = getAll(`#dialog-${id} input`); |
+ for (let field of fields) |
+ { |
+ field.value = (options && field.name in options) ? options[field.name] : ""; |
+ } |
+ setError(id, null); |
+ |
+ document.body.dataset.dialog = id; |
+} |
+ |
+function setError(dialogId, message) |
saroyanm
2017/07/18 12:46:28
Detail: Parameter name "message" is misleading, in
Thomas Greiner
2017/07/18 17:42:33
Looks like I forgot to adapt that. Done.
|
+{ |
+ let dialog = get(`#dialog-${dialogId}`); |
+ if (message) |
+ { |
+ dialog.dataset.error = message; |
+ } |
+ else |
+ { |
+ delete dialog.dataset.error; |
+ } |
+} |
+ |
+function populateLists() |
+{ |
+ Promise.all([getInstalled(), getRecommended()]) |
+ .then(([installed, recommended]) => |
+ { |
+ let listRecommended = get("#subscriptions-recommended"); |
+ for (let {title, url} of recommended) |
+ { |
+ create(listRecommended, "li", title, {"data-url": url}, |
+ (ev) => |
+ { |
+ if (ev.target.classList.contains("installed")) |
+ return; |
+ |
+ setDialog("subscribe", {title, url}); |
saroyanm
2017/07/18 12:46:27
Detail: reference to "subscribe" dialog is used qu
Thomas Greiner
2017/07/18 17:42:32
Done.
|
+ } |
+ ); |
+ } |
+ |
+ for (let subscription of installed) |
+ { |
+ if (subscription.disabled) |
+ continue; |
+ |
+ setSubscription(subscription, true); |
+ } |
+ }) |
+ .catch((err) => console.error(err)); |
saroyanm
2017/07/18 12:46:28
I think you have mentioned this, so I'll ignore th
Thomas Greiner
2017/07/18 17:42:32
Acknowledged.
|
+} |
+ |
+/* Listeners */ |
+ |
+function onChange(ev) |
+{ |
+ if (ev.target.id != "acceptable-ads") |
saroyanm
2017/07/18 12:46:27
Same as mentioned above, there is no element with
Thomas Greiner
2017/07/18 17:42:33
Done.
|
+ return; |
+ |
+ promisedAcceptableAdsUrl.then((acceptableAdsUrl) => |
+ { |
+ if (ev.target.checked) |
+ { |
+ installSubscription(acceptableAdsUrl, null); |
+ } |
+ else |
+ { |
+ uninstallSubscription(acceptableAdsUrl); |
+ } |
+ }); |
+} |
+document.addEventListener("change", onChange); |
+ |
+function onClick(ev) |
+{ |
+ switch (ev.target.dataset.action) |
+ { |
+ case "close-dialog": |
saroyanm
2017/07/18 12:46:27
Note: Why not to handle all other click events her
Thomas Greiner
2017/07/18 17:42:32
Done. I'm also using it now for the "enabled" togg
|
+ setDialog(null); |
+ break; |
+ case "open-dialog": |
+ setDialog(ev.target.dataset.dialog); |
+ break; |
+ } |
+} |
+document.addEventListener("click", onClick); |
+ |
+function onSubmit(ev) |
+{ |
+ let fields = ev.target.elements; |
+ let title = fields.title.value; |
+ let url = fields.url.value; |
+ |
+ if (!title) |
+ { |
+ setError("subscribe", "title"); |
+ } |
+ else if (!url) |
+ { |
+ setError("subscribe", "url"); |
+ } |
+ else |
+ { |
+ installSubscription(url, title); |
+ setDialog(null); |
+ } |
+ |
+ ev.preventDefault(); |
+} |
+document.addEventListener("submit", onSubmit); |
+ |
+function onToggleWhitelistFilter(ev) |
+{ |
+ let checkbox = ev.target; |
saroyanm
2017/07/18 12:46:28
Suggestion: We can avoid using element name as a v
Thomas Greiner
2017/07/18 17:42:32
In this case the implementation depends on it bein
|
+ ext.backgroundPage.sendMessage( |
+ { |
+ type: (checkbox.checked) ? "filters.remove" : "filters.add", |
+ text: whitelistFilter |
+ }, (errors) => |
+ { |
+ if (errors.length < 1) |
+ return; |
+ |
+ console.error(errors); |
+ checkbox.checked = !checkbox.checked; |
+ } |
+ ); |
+ ev.preventDefault(); |
+} |
+ |
+function onMessage(msg) |
+{ |
+ switch (msg.type) |
+ { |
+ case "app.respond": { |
+ switch (msg.action) |
+ { |
+ case "addSubscription": |
+ let [subscription] = msg.args; |
+ setDialog("subscribe", { |
+ title: subscription.title, |
+ url: subscription.url |
+ }); |
+ break; |
+ case "showPageOptions": |
+ let [{host, whitelisted}] = msg.args; |
+ whitelistFilter = `@@||${host}^$document`; |
saroyanm
2017/07/18 12:46:28
Shouldn't this be promise as well, similar to the
Thomas Greiner
2017/07/18 17:42:32
Even though it's not necessary because the UI usin
|
+ |
+ ext.i18n.setElementText( |
+ get("#enabled-label"), |
+ "mops_enabled_label", |
+ [host] |
+ ); |
+ |
+ let checkbox = get("#enabled"); |
+ checkbox.checked = !whitelisted; |
+ checkbox.addEventListener("click", onToggleWhitelistFilter); |
+ |
+ get("#enabled-container").hidden = false; |
+ break; |
+ } |
+ break; |
+ } |
+ case "filters.respond": { |
+ let [filter] = msg.args; |
+ if (!whitelistFilter || filter.text != whitelistFilter) |
saroyanm
2017/07/18 12:46:28
First check for "!whitelistFilter" seems to redund
Thomas Greiner
2017/07/18 17:42:31
While I don't expect `filter.text` to be `null`, i
|
+ break; |
+ |
+ get("#enabled").checked = (msg.action == "removed"); |
+ break; |
+ } |
+ case "subscriptions.respond": { |
+ let [subscription] = msg.args; |
+ switch (msg.action) |
+ { |
+ case "added": |
+ setSubscription(subscription, true); |
+ break; |
+ case "disabled": |
+ if (subscription.disabled) |
+ { |
+ removeSubscription(subscription.url); |
+ } |
+ else |
+ { |
+ setSubscription(subscription, true); |
+ } |
+ break; |
+ case "removed": |
+ removeSubscription(subscription.url); |
+ break; |
+ case "title": |
+ // We're also receiving these messages for subscriptions that are not |
+ // installed so we shouldn't add those by accident |
+ setSubscription(subscription, false); |
+ break; |
+ } |
+ break; |
+ } |
+ } |
+} |
+ext.onMessage.addListener(onMessage); |
+ |
+ext.backgroundPage.sendMessage({ |
+ type: "app.listen", |
+ filter: ["addSubscription", "showPageOptions"] |
+}); |
+ |
+ext.backgroundPage.sendMessage({ |
+ type: "filters.listen", |
+ filter: ["added", "removed"] |
+}); |
+ |
+ext.backgroundPage.sendMessage({ |
+ type: "subscriptions.listen", |
+ filter: ["added", "disabled", "removed", "title"] |
+}); |
+ |
+/* Initialization */ |
+ |
+populateLists(); |
+ |
+getDocLink("acceptable_ads", (link) => |
+{ |
+ get("#acceptableAds-more").href = link; |
+}); |
+ |
+get("#dialog-subscribe [name='title']").setAttribute( |
+ "placeholder", |
+ getMessage("mops_subscribe_title") |
+); |
+ |
+get("#dialog-subscribe [name='url']").setAttribute( |
+ "placeholder", |
+ getMessage("mops_subscribe_url") |
+); |