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

Side by Side Diff: options.js

Issue 29508671: Testing new options page with the extension (Closed)
Patch Set: Created Oct. 5, 2017, 12:13 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « options.html ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 /*
2 * This file is part of Adblock Plus <https://adblockplus.org/>,
3 * Copyright (C) 2006-present eyeo GmbH
4 *
5 * Adblock Plus is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License version 3 as
7 * published by the Free Software Foundation.
8 *
9 * Adblock Plus is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 /* global $, i18n, i18nTimeDateStrings */
19
20 "use strict";
21
22 /**
23 * Creates a wrapping function used to conveniently send a type of message.
24 *
25 * @param {Object} baseMessage The part of the message that's always sent
26 * @param {...string} paramKeys Any message keys that have dynamic values. The
27 * returned function will take the corresponding
28 * values as arguments.
29 * @return {function} The generated messaging function, optionally
30 * taking any values as specified by the paramKeys
31 * and finally an optional callback. (Although the
32 * value arguments are optional their index must be
33 * maintained. E.g. if you omit the first value you
34 * must omit the second too.)
35 */
36 function wrapper(baseMessage, ...paramKeys)
37 {
38 return function(...paramValues /* , callback */)
39 {
40 let message = Object.assign(Object.create(null), baseMessage);
41 let callback;
42
43 if (paramValues.length > 0)
44 {
45 let lastArg = paramValues[paramValues.length - 1];
46 if (typeof lastArg == "function")
47 callback = lastArg;
48
49 for (let i = 0; i < paramValues.length - (callback ? 1 : 0); i++)
50 message[paramKeys[i]] = paramValues[i];
51 }
52
53 ext.backgroundPage.sendMessage(message, callback);
54 };
55 }
56
57 const getDocLink = wrapper({type: "app.get", what: "doclink"}, "link");
58 const getInfo = wrapper({type: "app.get"}, "what");
59 const getPref = wrapper({type: "prefs.get"}, "key");
60 const togglePref = wrapper({type: "prefs.toggle"}, "key");
61 const getSubscriptions = wrapper({type: "subscriptions.get"},
62 "downloadable", "special");
63 const removeSubscription = wrapper({type: "subscriptions.remove"}, "url");
64 const addSubscription = wrapper({type: "subscriptions.add"},
65 "url", "title", "homepage");
66 const toggleSubscription = wrapper({type: "subscriptions.toggle"},
67 "url", "keepInstalled");
68 const updateSubscription = wrapper({type: "subscriptions.update"}, "url");
69 const importRawFilters = wrapper({type: "filters.importRaw"},
70 "text", "removeExisting");
71 const addFilter = wrapper({type: "filters.add"}, "text");
72 const removeFilter = wrapper({type: "filters.remove"}, "text");
73 const quoteCSS = wrapper({type: "composer.quoteCSS"}, "CSS");
74
75 const whitelistedDomainRegexp = /^@@\|\|([^/:]+)\^\$document$/;
76 const statusMessages = new Map([
77 ["synchronize_invalid_url",
78 "filters_subscription_lastDownload_invalidURL"],
79 ["synchronize_connection_error",
80 "filters_subscription_lastDownload_connectionError"],
81 ["synchronize_invalid_data",
82 "filters_subscription_lastDownload_invalidData"],
83 ["synchronize_checksum_mismatch",
84 "filters_subscription_lastDownload_checksumMismatch"]
85 ]);
86
87 let delayedSubscriptionSelection = null;
88 let acceptableAdsUrl;
89
90 // Loads options from localStorage and sets UI elements accordingly
91 function loadOptions()
92 {
93 // Set page title to i18n version of "Adblock Plus Options"
94 document.title = i18n.getMessage("options");
95
96 // Set links
97 getPref("subscriptions_exceptionsurl", url =>
98 {
99 acceptableAdsUrl = url;
100 $("#acceptableAdsLink").attr("href", acceptableAdsUrl);
101 });
102 getDocLink("acceptable_ads", url =>
103 {
104 $("#acceptableAdsDocs").attr("href", url);
105 });
106 getDocLink("filterdoc", url =>
107 {
108 setLinks("filter-must-follow-syntax", url);
109 });
110 getInfo("application", application =>
111 {
112 getInfo("platform", platform =>
113 {
114 if (platform == "chromium" && application != "opera")
115 application = "chrome";
116
117 getDocLink(application + "_support", url =>
118 {
119 setLinks("found-a-bug", url);
120 });
121
122 if (platform == "gecko")
123 $("#firefox-warning").removeAttr("hidden");
124 });
125 });
126
127 // Add event listeners
128 $("#updateFilterLists").click(updateFilterLists);
129 $("#startSubscriptionSelection").click(startSubscriptionSelection);
130 $("#subscriptionSelector").change(updateSubscriptionSelection);
131 $("#addSubscription").click(addSubscriptionClicked);
132 $("#acceptableAds").click(toggleAcceptableAds);
133 $("#whitelistForm").submit(addWhitelistDomain);
134 $("#removeWhitelist").click(removeSelectedExcludedDomain);
135 $("#customFilterForm").submit(addTypedFilter);
136 $("#removeCustomFilter").click(removeSelectedFilters);
137 $("#rawFiltersButton").click(toggleFiltersInRawFormat);
138 $("#importRawFilters").click(importRawFiltersText);
139
140 // Display jQuery UI elements
141 $("#tabs").tabs();
142 $("button:not(.subscriptionRemoveButton)").button();
143 $(".refreshButton").button("option", "icons", {primary: "ui-icon-refresh"});
144 $(".addButton").button("option", "icons", {primary: "ui-icon-plus"});
145 $(".removeButton").button("option", "icons", {primary: "ui-icon-minus"});
146
147 // Popuplate option checkboxes
148 initCheckbox("shouldShowBlockElementMenu");
149 initCheckbox("show_devtools_panel");
150 initCheckbox("shouldShowNotifications", "notifications_ignoredcategories");
151
152 getInfo("features", features =>
153 {
154 if (!features.devToolsPanel)
155 document.getElementById("showDevtoolsPanelContainer").hidden = true;
156 });
157 getPref("notifications_showui", showNotificationsUI =>
158 {
159 if (!showNotificationsUI)
160 document.getElementById("shouldShowNotificationsContainer").hidden = true;
161 });
162
163 // Register listeners in the background message responder
164 ext.backgroundPage.sendMessage({
165 type: "app.listen",
166 filter: ["addSubscription", "focusSection"]
167 });
168 ext.backgroundPage.sendMessage({
169 type: "filters.listen",
170 filter: ["added", "loaded", "removed"]
171 });
172 ext.backgroundPage.sendMessage({
173 type: "prefs.listen",
174 filter: ["notifications_ignoredcategories", "notifications_showui",
175 "show_devtools_panel", "shouldShowBlockElementMenu"]
176 });
177 ext.backgroundPage.sendMessage({
178 type: "subscriptions.listen",
179 filter: ["added", "disabled", "homepage", "lastDownload", "removed",
180 "title", "downloadStatus", "downloading"]
181 });
182
183 // Load recommended subscriptions
184 loadRecommendations();
185
186 // Show user's filters
187 reloadFilters();
188 }
189 $(loadOptions);
190
191 function convertSpecialSubscription(subscription)
192 {
193 for (let filter of subscription.filters)
194 {
195 if (whitelistedDomainRegexp.test(filter.text))
196 appendToListBox("excludedDomainsBox", RegExp.$1);
197 else
198 appendToListBox("userFiltersBox", filter.text);
199 }
200 }
201
202 // Reloads the displayed subscriptions and filters
203 function reloadFilters()
204 {
205 // Load user filter URLs
206 let container = document.getElementById("filterLists");
207 while (container.lastChild)
208 container.removeChild(container.lastChild);
209
210 getSubscriptions(true, false, subscriptions =>
211 {
212 for (let subscription of subscriptions)
213 {
214 if (subscription.url == acceptableAdsUrl)
215 $("#acceptableAds").prop("checked", !subscription.disabled);
216 else
217 addSubscriptionEntry(subscription);
218 }
219 });
220
221 // User-entered filters
222 getSubscriptions(false, true, subscriptions =>
223 {
224 document.getElementById("userFiltersBox").innerHTML = "";
225 document.getElementById("excludedDomainsBox").innerHTML = "";
226
227 for (let subscription of subscriptions)
228 convertSpecialSubscription(subscription);
229 });
230 }
231
232 function initCheckbox(id, key)
233 {
234 key = key || id;
235 let checkbox = document.getElementById(id);
236
237 getPref(key, value =>
238 {
239 onPrefMessage(key, value);
240 });
241
242 checkbox.addEventListener("click", () =>
243 {
244 togglePref(key);
245 }, false);
246 }
247
248 function loadRecommendations()
249 {
250 fetch("subscriptions.xml")
251 .then(response =>
252 {
253 return response.text();
254 })
255 .then(text =>
256 {
257 let selectedIndex = 0;
258 let selectedPrefix = null;
259 let matchCount = 0;
260
261 let list = document.getElementById("subscriptionSelector");
262 let doc = new DOMParser().parseFromString(text, "application/xml");
263 let elements = doc.documentElement.getElementsByTagName("subscription");
264
265 for (let i = 0; i < elements.length; i++)
266 {
267 let element = elements[i];
268 let option = new Option();
269 option.text = element.getAttribute("title") + " (" +
270 element.getAttribute("specialization") + ")";
271 option._data = {
272 title: element.getAttribute("title"),
273 url: element.getAttribute("url"),
274 homepage: element.getAttribute("homepage")
275 };
276
277 let prefix = element.getAttribute("prefixes");
278 if (prefix)
279 {
280 prefix = prefix.replace(/\W/g, "_");
281 option.style.fontWeight = "bold";
282 option.style.backgroundColor = "#E0FFE0";
283 option.style.color = "#000000";
284 if (!selectedPrefix || selectedPrefix.length < prefix.length)
285 {
286 selectedIndex = i;
287 selectedPrefix = prefix;
288 matchCount = 1;
289 }
290 else if (selectedPrefix && selectedPrefix.length == prefix.length)
291 {
292 matchCount++;
293
294 // If multiple items have a matching prefix of the same length:
295 // Select one of the items randomly, probability should be the same
296 // for all items. So we replace the previous match here with
297 // probability 1/N (N being the number of matches).
298 if (Math.random() * matchCount < 1)
299 {
300 selectedIndex = i;
301 selectedPrefix = prefix;
302 }
303 }
304 }
305 list.appendChild(option);
306 }
307
308 let option = new Option();
309 let label = i18n.getMessage("filters_addSubscriptionOther_label");
310 option.text = label + "\u2026";
311 option._data = null;
312 list.appendChild(option);
313
314 list.selectedIndex = selectedIndex;
315
316 if (delayedSubscriptionSelection)
317 startSubscriptionSelection(...delayedSubscriptionSelection);
318 });
319 }
320
321 function startSubscriptionSelection(title, url)
322 {
323 let list = document.getElementById("subscriptionSelector");
324 if (list.length == 0)
325 {
326 delayedSubscriptionSelection = [title, url];
327 return;
328 }
329
330 $("#tabs").tabs("option", "active", 0);
331 $("#addSubscriptionContainer").show();
332 $("#addSubscriptionButton").hide();
333 $("#subscriptionSelector").focus();
334 if (typeof url != "undefined")
335 {
336 list.selectedIndex = list.length - 1;
337 document.getElementById("customSubscriptionTitle").value = title;
338 document.getElementById("customSubscriptionLocation").value = url;
339 }
340 updateSubscriptionSelection();
341 document.getElementById("addSubscriptionContainer").scrollIntoView(true);
342 }
343
344 function updateSubscriptionSelection()
345 {
346 let list = document.getElementById("subscriptionSelector");
347 let data = list.options[list.selectedIndex]._data;
348 if (data)
349 $("#customSubscriptionContainer").hide();
350 else
351 {
352 $("#customSubscriptionContainer").show();
353 $("#customSubscriptionTitle").focus();
354 }
355 }
356
357 function addSubscriptionClicked()
358 {
359 let list = document.getElementById("subscriptionSelector");
360 let data = list.options[list.selectedIndex]._data;
361 if (data)
362 addSubscription(data.url, data.title, data.homepage);
363 else
364 {
365 let url = document.getElementById("customSubscriptionLocation")
366 .value.trim();
367 if (!/^https?:/i.test(url))
368 {
369 alert(i18n.getMessage("global_subscription_invalid_location"));
370 $("#customSubscriptionLocation").focus();
371 return;
372 }
373
374 let title = document.getElementById("customSubscriptionTitle").value.trim();
375 if (!title)
376 title = url;
377
378 addSubscription(url, title, null);
379 }
380
381 $("#addSubscriptionContainer").hide();
382 $("#customSubscriptionContainer").hide();
383 $("#addSubscriptionButton").show();
384 }
385
386 function toggleAcceptableAds()
387 {
388 toggleSubscription(acceptableAdsUrl, true);
389 }
390
391 function findSubscriptionElement(subscription)
392 {
393 for (let child of document.getElementById("filterLists").childNodes)
394 {
395 if (child._subscription.url == subscription.url)
396 return child;
397 }
398 return null;
399 }
400
401 function updateSubscriptionInfo(element, subscription)
402 {
403 if (subscription)
404 element._subscription = subscription;
405 else
406 subscription = element._subscription;
407
408 let title = element.getElementsByClassName("subscriptionTitle")[0];
409 title.textContent = subscription.title;
410 title.setAttribute("title", subscription.url);
411 if (subscription.homepage)
412 title.href = subscription.homepage;
413 else
414 title.href = subscription.url;
415
416 let enabled = element.getElementsByClassName("subscriptionEnabled")[0];
417 enabled.checked = !subscription.disabled;
418
419 let lastUpdate = element.getElementsByClassName("subscriptionUpdate")[0];
420 lastUpdate.classList.remove("error");
421
422 let {downloadStatus} = subscription;
423 if (subscription.isDownloading)
424 {
425 lastUpdate.textContent = i18n.getMessage(
426 "filters_subscription_lastDownload_inProgress"
427 );
428 }
429 else if (downloadStatus && downloadStatus != "synchronize_ok")
430 {
431 if (statusMessages.has(downloadStatus))
432 {
433 lastUpdate.textContent = i18n.getMessage(
434 statusMessages.get(downloadStatus)
435 );
436 }
437 else
438 lastUpdate.textContent = downloadStatus;
439 lastUpdate.classList.add("error");
440 }
441 else if (subscription.lastDownload > 0)
442 {
443 let timeDate = i18nTimeDateStrings(subscription.lastDownload * 1000);
444 let messageID = (timeDate[1] ? "last_updated_at" : "last_updated_at_today");
445 lastUpdate.textContent = i18n.getMessage(messageID, timeDate);
446 }
447 }
448
449 function onSubscriptionMessage(action, subscription)
450 {
451 let element = findSubscriptionElement(subscription);
452
453 switch (action)
454 {
455 case "disabled":
456 case "downloading":
457 case "downloadStatus":
458 case "homepage":
459 case "lastDownload":
460 case "title":
461 if (element)
462 updateSubscriptionInfo(element, subscription);
463 break;
464 case "added":
465 if (subscription.url.indexOf("~user") == 0)
466 convertSpecialSubscription(subscription);
467 else if (subscription.url == acceptableAdsUrl)
468 $("#acceptableAds").prop("checked", true);
469 else if (!element)
470 addSubscriptionEntry(subscription);
471 break;
472 case "removed":
473 if (subscription.url == acceptableAdsUrl)
474 $("#acceptableAds").prop("checked", false);
475 else if (element)
476 element.parentNode.removeChild(element);
477 break;
478 }
479 }
480
481 function onPrefMessage(key, value)
482 {
483 switch (key)
484 {
485 case "notifications_showui":
486 document.getElementById(
487 "shouldShowNotificationsContainer"
488 ).hidden = !value;
489 return;
490 case "notifications_ignoredcategories":
491 key = "shouldShowNotifications";
492 value = value.indexOf("*") == -1;
493 break;
494 }
495 let checkbox = document.getElementById(key);
496 if (checkbox)
497 checkbox.checked = value;
498 }
499
500 function onFilterMessage(action, filter)
501 {
502 switch (action)
503 {
504 case "loaded":
505 reloadFilters();
506 break;
507 case "added":
508 if (whitelistedDomainRegexp.test(filter.text))
509 appendToListBox("excludedDomainsBox", RegExp.$1);
510 else
511 appendToListBox("userFiltersBox", filter.text);
512 break;
513 case "removed":
514 if (whitelistedDomainRegexp.test(filter.text))
515 removeFromListBox("excludedDomainsBox", RegExp.$1);
516 else
517 removeFromListBox("userFiltersBox", filter.text);
518 break;
519 }
520 }
521
522 // Add a filter string to the list box.
523 function appendToListBox(boxId, text)
524 {
525 // Note: document.createElement("option") is unreliable in Opera
526 let elt = new Option();
527 elt.text = text;
528 elt.value = text;
529 document.getElementById(boxId).appendChild(elt);
530 }
531
532 // Remove a filter string from a list box.
533 function removeFromListBox(boxId, text)
534 {
535 let list = document.getElementById(boxId);
536 // Edge does not support CSS.escape yet:
537 // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/101410 /
538 quoteCSS(text, escapedCSS =>
539 {
540 let selector = "option[value=" + escapedCSS + "]";
541 for (let option of list.querySelectorAll(selector))
542 list.removeChild(option);
543 });
544 }
545
546 function addWhitelistDomain(event)
547 {
548 event.preventDefault();
549
550 let domain = document.getElementById(
551 "newWhitelistDomain"
552 ).value.replace(/\s/g, "");
553 document.getElementById("newWhitelistDomain").value = "";
554 if (!domain)
555 return;
556
557 let filterText = "@@||" + domain + "^$document";
558 addFilter(filterText);
559 }
560
561 // Adds filter text that user typed to the selection box
562 function addTypedFilter(event)
563 {
564 event.preventDefault();
565
566 let element = document.getElementById("newFilter");
567 addFilter(element.value, errors =>
568 {
569 if (errors.length > 0)
570 alert(errors.join("\n"));
571 else
572 element.value = "";
573 });
574 }
575
576 // Removes currently selected whitelisted domains
577 function removeSelectedExcludedDomain(event)
578 {
579 event.preventDefault();
580 let remove = [];
581 for (let option of document.getElementById("excludedDomainsBox").options)
582 {
583 if (option.selected)
584 remove.push(option.value);
585 }
586 if (!remove.length)
587 return;
588
589 for (let domain of remove)
590 removeFilter("@@||" + domain + "^$document");
591 }
592
593 // Removes all currently selected filters
594 function removeSelectedFilters(event)
595 {
596 event.preventDefault();
597 let options = document.querySelectorAll("#userFiltersBox > option:checked");
598 for (let option of options)
599 removeFilter(option.value);
600 }
601
602 // Shows raw filters box and fills it with the current user filters
603 function toggleFiltersInRawFormat(event)
604 {
605 event.preventDefault();
606
607 let rawFilters = document.getElementById("rawFilters");
608 let filters = [];
609
610 if (rawFilters.style.display != "table-row")
611 {
612 rawFilters.style.display = "table-row";
613 for (let option of document.getElementById("userFiltersBox").options)
614 filters.push(option.value);
615 }
616 else
617 {
618 rawFilters.style.display = "none";
619 }
620
621 document.getElementById("rawFiltersText").value = filters.join("\n");
622 }
623
624 // Imports filters in the raw text box
625 function importRawFiltersText()
626 {
627 let text = document.getElementById("rawFiltersText").value;
628
629 importRawFilters(text, true, errors =>
630 {
631 if (errors.length > 0)
632 alert(errors.join("\n"));
633 else
634 $("#rawFilters").hide();
635 });
636 }
637
638 // Called when user explicitly requests filter list updates
639 function updateFilterLists()
640 {
641 // Without the URL parameter this will update all subscriptions
642 updateSubscription();
643 }
644
645 // Adds a subscription entry to the UI.
646 function addSubscriptionEntry(subscription)
647 {
648 let template = document.getElementById("subscriptionTemplate");
649 let element = template.cloneNode(true);
650 element.removeAttribute("id");
651 element._subscription = subscription;
652
653 let removeButton = element.getElementsByClassName(
654 "subscriptionRemoveButton"
655 )[0];
656 removeButton.setAttribute("title", removeButton.textContent);
657 removeButton.textContent = "\xD7";
658 removeButton.addEventListener("click", () =>
659 {
660 if (!confirm(i18n.getMessage("global_remove_subscription_warning")))
661 return;
662
663 removeSubscription(subscription.url);
664 }, false);
665
666 getPref("additional_subscriptions", additionalSubscriptions =>
667 {
668 if (additionalSubscriptions.includes(subscription.url))
669 removeButton.style.visibility = "hidden";
670 });
671
672 let enabled = element.getElementsByClassName("subscriptionEnabled")[0];
673 enabled.addEventListener("click", () =>
674 {
675 subscription.disabled = !subscription.disabled;
676 toggleSubscription(subscription.url, true);
677 }, false);
678
679 updateSubscriptionInfo(element);
680
681 document.getElementById("filterLists").appendChild(element);
682 }
683
684 function setLinks(id, ...args)
685 {
686 let element = document.getElementById(id);
687 if (!element)
688 return;
689
690 let links = element.getElementsByTagName("a");
691 for (let i = 0; i < links.length; i++)
692 {
693 if (typeof args[i] == "string")
694 {
695 links[i].href = args[i];
696 links[i].setAttribute("target", "_blank");
697 }
698 else if (typeof args[i] == "function")
699 {
700 links[i].href = "javascript:void(0);";
701 links[i].addEventListener("click", args[i], false);
702 }
703 }
704 }
705
706 ext.onMessage.addListener(message =>
707 {
708 switch (message.type)
709 {
710 case "app.respond":
711 switch (message.action)
712 {
713 case "addSubscription":
714 let subscription = message.args[0];
715 startSubscriptionSelection(subscription.title, subscription.url);
716 break;
717 case "focusSection":
718 for (let tab of document.getElementsByClassName("ui-tabs-panel"))
719 {
720 let found = tab.querySelector(
721 "[data-section='" + message.args[0] + "']"
722 );
723 if (!found)
724 continue;
725
726 let previous = document.getElementsByClassName("focused");
727 if (previous.length > 0)
728 previous[0].classList.remove("focused");
729
730 let index = $("[href='#" + tab.id + "']").parent().index();
731 $("#tabs").tabs("option", "active", index);
732 found.classList.add("focused");
733 }
734 break;
735 }
736 break;
737 case "filters.respond":
738 onFilterMessage(message.action, message.args[0]);
739 break;
740 case "prefs.respond":
741 onPrefMessage(message.action, message.args[0]);
742 break;
743 case "subscriptions.respond":
744 onSubscriptionMessage(message.action, message.args[0]);
745 break;
746 }
747 });
OLDNEW
« no previous file with comments | « options.html ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld