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

Side by Side Diff: js/desktop-options.js

Issue 29705690: Issue 6310 - Start using JavaScript modularization tool (Closed)
Patch Set: Created Feb. 23, 2018, 9:24 a.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 | « README.md ('k') | package.json » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 /* 1 /*
2 * This file is part of Adblock Plus <https://adblockplus.org/>, 2 * This file is part of Adblock Plus <https://adblockplus.org/>,
3 * Copyright (C) 2006-present eyeo GmbH 3 * Copyright (C) 2006-present eyeo GmbH
4 * 4 *
5 * Adblock Plus is free software: you can redistribute it and/or modify 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 6 * it under the terms of the GNU General Public License version 3 as
7 * published by the Free Software Foundation. 7 * published by the Free Software Foundation.
8 * 8 *
9 * Adblock Plus is distributed in the hope that it will be useful, 9 * Adblock Plus is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details. 12 * GNU General Public License for more details.
13 * 13 *
14 * You should have received a copy of the GNU General Public License 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/>. 15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
16 */ 16 */
17 17
18 /* globals checkShareResource, getDocLink, i18nFormatDateTime, openSharePopup, 18 /* globals checkShareResource, getDocLink, i18nFormatDateTime, openSharePopup,
19 setLinks, E */ 19 setLinks, E */
20 20
21 "use strict"; 21 "use strict";
22 22
23 { 23 let subscriptionsMap = Object.create(null);
24 let subscriptionsMap = Object.create(null); 24 let filtersMap = Object.create(null);
25 let filtersMap = Object.create(null); 25 let collections = Object.create(null);
26 let collections = Object.create(null); 26 let acceptableAdsUrl = null;
27 let acceptableAdsUrl = null; 27 let acceptableAdsPrivacyUrl = null;
28 let acceptableAdsPrivacyUrl = null; 28 let isCustomFiltersLoaded = false;
29 let isCustomFiltersLoaded = false; 29 let {getMessage} = browser.i18n;
30 let {getMessage} = browser.i18n; 30 let {setElementText} = ext.i18n;
31 let {setElementText} = ext.i18n; 31 let customFilters = [];
32 let customFilters = []; 32 let filterErrors = new Map([
33 let filterErrors = new Map([ 33 ["synchronize_invalid_url",
34 ["synchronize_invalid_url", 34 "options_filterList_lastDownload_invalidURL"],
35 "options_filterList_lastDownload_invalidURL"], 35 ["synchronize_connection_error",
36 ["synchronize_connection_error", 36 "options_filterList_lastDownload_connectionError"],
37 "options_filterList_lastDownload_connectionError"], 37 ["synchronize_invalid_data",
38 ["synchronize_invalid_data", 38 "options_filterList_lastDownload_invalidData"],
39 "options_filterList_lastDownload_invalidData"], 39 ["synchronize_checksum_mismatch",
40 ["synchronize_checksum_mismatch", 40 "options_filterList_lastDownload_checksumMismatch"]
41 "options_filterList_lastDownload_checksumMismatch"] 41 ]);
42 ]); 42 const timestampUI = Symbol();
43 const timestampUI = Symbol(); 43 const whitelistedDomainRegexp = /^@@\|\|([^/:]+)\^\$document$/;
44 const whitelistedDomainRegexp = /^@@\|\|([^/:]+)\^\$document$/; 44 // Period of time in milliseconds
45 // Period of time in milliseconds 45 const minuteInMs = 60000;
46 const minuteInMs = 60000; 46 const hourInMs = 3600000;
47 const hourInMs = 3600000; 47 const fullDayInMs = 86400000;
48 const fullDayInMs = 86400000; 48
49 49 function Collection(details)
50 function Collection(details) 50 {
51 { 51 this.details = details;
52 this.details = details; 52 this.items = [];
53 this.items = []; 53 }
54 } 54
55 55 Collection.prototype._setEmpty = function(table, detail, removeEmpty)
56 Collection.prototype._setEmpty = function(table, detail, removeEmpty) 56 {
57 { 57 if (removeEmpty)
58 if (removeEmpty) 58 {
59 { 59 let placeholders = table.querySelectorAll(".empty-placeholder");
60 let placeholders = table.querySelectorAll(".empty-placeholder"); 60 for (let placeholder of placeholders)
61 for (let placeholder of placeholders) 61 table.removeChild(placeholder);
62 table.removeChild(placeholder); 62
63 63 execAction(detail.removeEmptyAction, table);
64 execAction(detail.removeEmptyAction, table); 64 }
65 } 65 else
66 {
67 let {emptyTexts = []} = detail;
68 for (let text of emptyTexts)
69 {
70 let placeholder = document.createElement("li");
71 placeholder.className = "empty-placeholder";
72 placeholder.textContent = getMessage(text);
73 table.appendChild(placeholder);
74 }
75
76 execAction(detail.setEmptyAction, table);
77 }
78 };
79
80 Collection.prototype._createElementQuery = function(item)
81 {
82 let access = (item.url || item.text).replace(/'/g, "\\'");
83 return function(container)
84 {
85 return container.querySelector("[data-access='" + access + "']");
86 };
87 };
88
89 Collection.prototype._getItemTitle = function(item, i)
90 {
91 if (this.details[i].getTitleFunction)
92 return this.details[i].getTitleFunction(item);
93 return item.title || item.url || item.text;
94 };
95
96 Collection.prototype._sortItems = function()
97 {
98 this.items.sort((a, b) =>
99 {
100 // Make sure that Acceptable Ads is always last, since it cannot be
101 // disabled, but only be removed. That way it's grouped together with
102 // the "Own filter list" which cannot be disabled either at the bottom
103 // of the filter lists in the Advanced tab.
104 if (isAcceptableAds(a.url))
105 return 1;
106 if (isAcceptableAds(b.url))
107 return -1;
108
109 // Make sure that newly added entries always appear on top in descending
110 // chronological order
111 let aTimestamp = a[timestampUI] || 0;
112 let bTimestamp = b[timestampUI] || 0;
113 if (aTimestamp || bTimestamp)
114 return bTimestamp - aTimestamp;
115
116 let aTitle = this._getItemTitle(a, 0).toLowerCase();
117 let bTitle = this._getItemTitle(b, 0).toLowerCase();
118 return aTitle.localeCompare(bTitle);
119 });
120 };
121
122 Collection.prototype.addItem = function(item)
123 {
124 if (this.items.indexOf(item) >= 0)
125 return;
126
127 this.items.push(item);
128 this._sortItems();
129 for (let j = 0; j < this.details.length; j++)
130 {
131 let detail = this.details[j];
132 let table = E(detail.id);
133 let template = table.querySelector("template");
134 let listItem = document.createElement("li");
135 listItem.appendChild(document.importNode(template.content, true));
136 listItem.setAttribute("aria-label", this._getItemTitle(item, j));
137 listItem.setAttribute("data-access", item.url || item.text);
138 listItem.setAttribute("role", "section");
139
140 let tooltip = listItem.querySelector("[data-tooltip]");
141 if (tooltip)
142 {
143 let tooltipId = tooltip.getAttribute("data-tooltip");
144 tooltipId = tooltipId.replace("%value%", item.recommended);
145 if (getMessage(tooltipId))
146 {
147 tooltip.setAttribute("data-tooltip", tooltipId);
148 }
149 }
150
151 for (let control of listItem.querySelectorAll(".control"))
152 {
153 if (control.hasAttribute("title"))
154 {
155 let titleValue = getMessage(control.getAttribute("title"));
156 control.setAttribute("title", titleValue);
157 }
158 }
159
160 this._setEmpty(table, detail, true);
161 if (table.children.length > 0)
162 table.insertBefore(listItem, table.children[this.items.indexOf(item)]);
66 else 163 else
67 { 164 table.appendChild(listItem);
68 let {emptyTexts = []} = detail; 165
69 for (let text of emptyTexts) 166 this.updateItem(item);
70 { 167 }
71 let placeholder = document.createElement("li"); 168 return length;
72 placeholder.className = "empty-placeholder"; 169 };
73 placeholder.textContent = getMessage(text); 170
74 table.appendChild(placeholder); 171 Collection.prototype.removeItem = function(item)
75 } 172 {
76 173 let index = this.items.indexOf(item);
77 execAction(detail.setEmptyAction, table); 174 if (index == -1)
78 } 175 return;
79 }; 176
80 177 this.items.splice(index, 1);
81 Collection.prototype._createElementQuery = function(item) 178 let getListElement = this._createElementQuery(item);
82 { 179 for (let detail of this.details)
83 let access = (item.url || item.text).replace(/'/g, "\\'"); 180 {
84 return function(container) 181 let table = E(detail.id);
85 { 182 let element = getListElement(table);
86 return container.querySelector("[data-access='" + access + "']"); 183
87 }; 184 // Element gets removed so make sure to handle focus appropriately
88 }; 185 let control = element.querySelector(".control");
89 186 if (control && control == document.activeElement)
90 Collection.prototype._getItemTitle = function(item, i) 187 {
91 { 188 if (!focusNextElement(element.parentElement, control))
92 if (this.details[i].getTitleFunction) 189 {
93 return this.details[i].getTitleFunction(item); 190 // Fall back to next focusable element within same tab or dialog
94 return item.title || item.url || item.text; 191 let focusableElement = element.parentElement;
95 }; 192 while (focusableElement)
96
97 Collection.prototype._sortItems = function()
98 {
99 this.items.sort((a, b) =>
100 {
101 // Make sure that Acceptable Ads is always last, since it cannot be
102 // disabled, but only be removed. That way it's grouped together with
103 // the "Own filter list" which cannot be disabled either at the bottom
104 // of the filter lists in the Advanced tab.
105 if (isAcceptableAds(a.url))
106 return 1;
107 if (isAcceptableAds(b.url))
108 return -1;
109
110 // Make sure that newly added entries always appear on top in descending
111 // chronological order
112 let aTimestamp = a[timestampUI] || 0;
113 let bTimestamp = b[timestampUI] || 0;
114 if (aTimestamp || bTimestamp)
115 return bTimestamp - aTimestamp;
116
117 let aTitle = this._getItemTitle(a, 0).toLowerCase();
118 let bTitle = this._getItemTitle(b, 0).toLowerCase();
119 return aTitle.localeCompare(bTitle);
120 });
121 };
122
123 Collection.prototype.addItem = function(item)
124 {
125 if (this.items.indexOf(item) >= 0)
126 return;
127
128 this.items.push(item);
129 this._sortItems();
130 for (let j = 0; j < this.details.length; j++)
131 {
132 let detail = this.details[j];
133 let table = E(detail.id);
134 let template = table.querySelector("template");
135 let listItem = document.createElement("li");
136 listItem.appendChild(document.importNode(template.content, true));
137 listItem.setAttribute("aria-label", this._getItemTitle(item, j));
138 listItem.setAttribute("data-access", item.url || item.text);
139 listItem.setAttribute("role", "section");
140
141 let tooltip = listItem.querySelector("[data-tooltip]");
142 if (tooltip)
143 {
144 let tooltipId = tooltip.getAttribute("data-tooltip");
145 tooltipId = tooltipId.replace("%value%", item.recommended);
146 if (getMessage(tooltipId))
147 { 193 {
148 tooltip.setAttribute("data-tooltip", tooltipId); 194 if (focusableElement.classList.contains("tab-content") ||
195 focusableElement.classList.contains("dialog-content"))
196 break;
197
198 focusableElement = focusableElement.parentElement;
149 } 199 }
150 } 200 focusNextElement(focusableElement || document, control);
151 201 }
152 for (let control of listItem.querySelectorAll(".control")) 202 }
153 { 203
154 if (control.hasAttribute("title")) 204 element.parentElement.removeChild(element);
205 if (this.items.length == 0)
206 this._setEmpty(table, detail);
207 }
208 };
209
210 Collection.prototype.updateItem = function(item)
211 {
212 let oldIndex = this.items.indexOf(item);
213 this._sortItems();
214 let access = (item.url || item.text).replace(/'/g, "\\'");
215 for (let i = 0; i < this.details.length; i++)
216 {
217 let table = E(this.details[i].id);
218 let element = table.querySelector("[data-access='" + access + "']");
219 if (!element)
220 continue;
221
222 let title = this._getItemTitle(item, i);
223 let displays = element.querySelectorAll("[data-display]");
224 for (let j = 0; j < displays.length; j++)
225 {
226 if (item[displays[j].dataset.display])
227 displays[j].textContent = item[displays[j].dataset.display];
228 else
229 displays[j].textContent = title;
230 }
231
232 element.setAttribute("aria-label", title);
233 if (this.details[i].searchable)
234 element.setAttribute("data-search", title.toLowerCase());
235 let controls = element.querySelectorAll(".control[role='checkbox']");
236 for (let control of controls)
237 {
238 control.setAttribute("aria-checked", item.disabled == false);
239 if (isAcceptableAds(item.url) && this == collections.filterLists)
240 control.disabled = true;
241 }
242
243 let lastUpdateElement = element.querySelector(".last-update");
244 if (lastUpdateElement)
245 {
246 let message = element.querySelector(".message");
247 if (item.isDownloading)
248 {
249 let text = getMessage("options_filterList_lastDownload_inProgress");
250 message.textContent = text;
251 element.classList.add("show-message");
252 }
253 else if (item.downloadStatus != "synchronize_ok")
254 {
255 let error = filterErrors.get(item.downloadStatus);
256 if (error)
257 message.textContent = getMessage(error);
258 else
259 message.textContent = item.downloadStatus;
260 element.classList.add("show-message");
261 }
262 else if (item.lastDownload > 0)
263 {
264 let lastUpdate = item.lastDownload * 1000;
265 let sinceUpdate = Date.now() - lastUpdate;
266 if (sinceUpdate > fullDayInMs)
155 { 267 {
156 let titleValue = getMessage(control.getAttribute("title")); 268 let lastUpdateDate = new Date(item.lastDownload * 1000);
157 control.setAttribute("title", titleValue); 269 let monthName = lastUpdateDate.toLocaleString(undefined,
270 {month: "short"});
271 let day = lastUpdateDate.getDate();
272 day = day < 10 ? "0" + day : day;
273 lastUpdateElement.textContent = day + " " + monthName + " " +
274 lastUpdateDate.getFullYear();
158 } 275 }
159 } 276 else if (sinceUpdate > hourInMs)
160
161 this._setEmpty(table, detail, true);
162 if (table.children.length > 0)
163 table.insertBefore(listItem, table.children[this.items.indexOf(item)]);
164 else
165 table.appendChild(listItem);
166
167 this.updateItem(item);
168 }
169 return length;
170 };
171
172 Collection.prototype.removeItem = function(item)
173 {
174 let index = this.items.indexOf(item);
175 if (index == -1)
176 return;
177
178 this.items.splice(index, 1);
179 let getListElement = this._createElementQuery(item);
180 for (let detail of this.details)
181 {
182 let table = E(detail.id);
183 let element = getListElement(table);
184
185 // Element gets removed so make sure to handle focus appropriately
186 let control = element.querySelector(".control");
187 if (control && control == document.activeElement)
188 {
189 if (!focusNextElement(element.parentElement, control))
190 { 277 {
191 // Fall back to next focusable element within same tab or dialog 278 lastUpdateElement.textContent =
192 let focusableElement = element.parentElement; 279 getMessage("options_filterList_hours");
193 while (focusableElement)
194 {
195 if (focusableElement.classList.contains("tab-content") ||
196 focusableElement.classList.contains("dialog-content"))
197 break;
198
199 focusableElement = focusableElement.parentElement;
200 }
201 focusNextElement(focusableElement || document, control);
202 } 280 }
203 } 281 else if (sinceUpdate > minuteInMs)
204
205 element.parentElement.removeChild(element);
206 if (this.items.length == 0)
207 this._setEmpty(table, detail);
208 }
209 };
210
211 Collection.prototype.updateItem = function(item)
212 {
213 let oldIndex = this.items.indexOf(item);
214 this._sortItems();
215 let access = (item.url || item.text).replace(/'/g, "\\'");
216 for (let i = 0; i < this.details.length; i++)
217 {
218 let table = E(this.details[i].id);
219 let element = table.querySelector("[data-access='" + access + "']");
220 if (!element)
221 continue;
222
223 let title = this._getItemTitle(item, i);
224 let displays = element.querySelectorAll("[data-display]");
225 for (let j = 0; j < displays.length; j++)
226 {
227 if (item[displays[j].dataset.display])
228 displays[j].textContent = item[displays[j].dataset.display];
229 else
230 displays[j].textContent = title;
231 }
232
233 element.setAttribute("aria-label", title);
234 if (this.details[i].searchable)
235 element.setAttribute("data-search", title.toLowerCase());
236 let controls = element.querySelectorAll(".control[role='checkbox']");
237 for (let control of controls)
238 {
239 control.setAttribute("aria-checked", item.disabled == false);
240 if (isAcceptableAds(item.url) && this == collections.filterLists)
241 control.disabled = true;
242 }
243
244 let lastUpdateElement = element.querySelector(".last-update");
245 if (lastUpdateElement)
246 {
247 let message = element.querySelector(".message");
248 if (item.isDownloading)
249 { 282 {
250 let text = getMessage("options_filterList_lastDownload_inProgress"); 283 lastUpdateElement.textContent =
251 message.textContent = text; 284 getMessage("options_filterList_minutes");
252 element.classList.add("show-message");
253 } 285 }
254 else if (item.downloadStatus != "synchronize_ok")
255 {
256 let error = filterErrors.get(item.downloadStatus);
257 if (error)
258 message.textContent = getMessage(error);
259 else
260 message.textContent = item.downloadStatus;
261 element.classList.add("show-message");
262 }
263 else if (item.lastDownload > 0)
264 {
265 let lastUpdate = item.lastDownload * 1000;
266 let sinceUpdate = Date.now() - lastUpdate;
267 if (sinceUpdate > fullDayInMs)
268 {
269 let lastUpdateDate = new Date(item.lastDownload * 1000);
270 let monthName = lastUpdateDate.toLocaleString(undefined,
271 {month: "short"});
272 let day = lastUpdateDate.getDate();
273 day = day < 10 ? "0" + day : day;
274 lastUpdateElement.textContent = day + " " + monthName + " " +
275 lastUpdateDate.getFullYear();
276 }
277 else if (sinceUpdate > hourInMs)
278 {
279 lastUpdateElement.textContent =
280 getMessage("options_filterList_hours");
281 }
282 else if (sinceUpdate > minuteInMs)
283 {
284 lastUpdateElement.textContent =
285 getMessage("options_filterList_minutes");
286 }
287 else
288 {
289 lastUpdateElement.textContent =
290 getMessage("options_filterList_now");
291 }
292 element.classList.remove("show-message");
293 }
294 }
295
296 let websiteElement = element.querySelector(".context-menu .website");
297 if (websiteElement)
298 {
299 if (item.homepage)
300 websiteElement.setAttribute("href", item.homepage);
301 else
302 websiteElement.setAttribute("aria-hidden", true);
303 }
304
305 let sourceElement = element.querySelector(".context-menu .source");
306 if (sourceElement)
307 sourceElement.setAttribute("href", item.url);
308
309 let newIndex = this.items.indexOf(item);
310 if (oldIndex != newIndex)
311 table.insertBefore(element, table.childNodes[newIndex]);
312 }
313 };
314
315 Collection.prototype.clearAll = function()
316 {
317 this.items = [];
318 for (let detail of this.details)
319 {
320 let table = E(detail.id);
321 let element = table.firstChild;
322 while (element)
323 {
324 if (element.tagName == "LI" && !element.classList.contains("static"))
325 table.removeChild(element);
326 element = element.nextElementSibling;
327 }
328
329 this._setEmpty(table, detail);
330 }
331 };
332
333 function focusNextElement(container, currentElement)
334 {
335 let focusables = container.querySelectorAll("a, button, input, .control");
336 focusables = Array.prototype.slice.call(focusables);
337 let index = focusables.indexOf(currentElement);
338 index += (index == focusables.length - 1) ? -1 : 1;
339
340 let nextElement = focusables[index];
341 if (!nextElement)
342 return false;
343
344 nextElement.focus();
345 return true;
346 }
347
348 collections.protection = new Collection([
349 {
350 id: "recommend-protection-list-table"
351 }
352 ]);
353 collections.langs = new Collection([
354 {
355 id: "blocking-languages-table",
356 emptyTexts: ["options_language_empty"],
357 getTitleFunction: getLanguageTitle
358 }
359 ]);
360 collections.allLangs = new Collection([
361 {
362 id: "all-lang-table-add",
363 emptyTexts: ["options_dialog_language_other_empty"],
364 getTitleFunction: getLanguageTitle
365 }
366 ]);
367 collections.more = new Collection([
368 {
369 id: "more-list-table",
370 setEmptyAction: "hide-more-filters-section",
371 removeEmptyAction: "show-more-filters-section"
372 }
373 ]);
374 collections.whitelist = new Collection([
375 {
376 id: "whitelisting-table",
377 emptyTexts: ["options_whitelist_empty_1", "options_whitelist_empty_2"]
378 }
379 ]);
380 collections.filterLists = new Collection([
381 {
382 id: "all-filter-lists-table",
383 emptyTexts: ["options_filterList_empty"]
384 }
385 ]);
386
387 function addSubscription(subscription)
388 {
389 let {disabled} = subscription;
390 let collection = null;
391 if (subscription.recommended)
392 {
393 if (subscription.recommended == "ads")
394 {
395 if (disabled == false)
396 collection = collections.langs;
397
398 collections.allLangs.addItem(subscription);
399 }
400 else
401 {
402 collection = collections.protection;
403 }
404 }
405 else if (!isAcceptableAds(subscription.url) && disabled == false)
406 {
407 collection = collections.more;
408 }
409
410 if (collection)
411 collection.addItem(subscription);
412
413 subscriptionsMap[subscription.url] = subscription;
414 updateTooltips();
415 }
416
417 function updateSubscription(subscription)
418 {
419 for (let name in collections)
420 collections[name].updateItem(subscription);
421
422 if (subscription.recommended == "ads")
423 {
424 if (subscription.disabled)
425 collections.langs.removeItem(subscription);
426 else
427 collections.langs.addItem(subscription);
428 }
429 else if (!subscription.recommended && !isAcceptableAds(subscription.url))
430 {
431 if (subscription.disabled == false)
432 {
433 collections.more.addItem(subscription);
434 updateTooltips();
435 }
436 else
437 {
438 collections.more.removeItem(subscription);
439 }
440 }
441 }
442
443 function updateFilter(filter)
444 {
445 let match = filter.text.match(whitelistedDomainRegexp);
446 if (match && !filtersMap[filter.text])
447 {
448 filter.title = match[1];
449 collections.whitelist.addItem(filter);
450 if (isCustomFiltersLoaded)
451 {
452 let text = getMessage("options_whitelist_notification", [filter.title]);
453 showNotification(text);
454 }
455 }
456 else
457 {
458 customFilters.push(filter.text);
459 if (isCustomFiltersLoaded)
460 updateCustomFiltersUi();
461 }
462
463 filtersMap[filter.text] = filter;
464 }
465
466 function loadCustomFilters(filters)
467 {
468 for (let filter of filters)
469 updateFilter(filter);
470
471 setCustomFiltersView("read");
472 isCustomFiltersLoaded = true;
473 }
474
475 function removeCustomFilter(text)
476 {
477 let index = customFilters.indexOf(text);
478 if (index >= 0)
479 customFilters.splice(index, 1);
480
481 updateCustomFiltersUi();
482 }
483
484 function updateCustomFiltersUi()
485 {
486 let customFiltersListElement = E("custom-filters-raw");
487 customFiltersListElement.value = customFilters.join("\n");
488 }
489
490 function getLanguageTitle(item)
491 {
492 let title = item.specialization;
493 if (item.originalTitle && item.originalTitle.indexOf("+EasyList") > -1)
494 title += " + " + getMessage("options_english");
495 return title;
496 }
497
498 function loadRecommendations()
499 {
500 fetch("subscriptions.xml")
501 .then((response) =>
502 {
503 return response.text();
504 })
505 .then((text) =>
506 {
507 let doc = new DOMParser().parseFromString(text, "application/xml");
508 let elements = doc.documentElement.getElementsByTagName("subscription");
509 for (let element of elements)
510 {
511 let type = element.getAttribute("type");
512 let subscription = {
513 disabled: true,
514 downloadStatus: null,
515 homepage: null,
516 specialization: element.getAttribute("specialization"),
517 originalTitle: element.getAttribute("title"),
518 recommended: type,
519 url: element.getAttribute("url")
520 };
521
522 if (subscription.recommended != "ads")
523 {
524 type = type.replace(/\W/g, "_");
525 subscription.title = getMessage("common_feature_" +
526 type + "_title");
527 }
528
529 addSubscription(subscription);
530 }
531 });
532 }
533
534 function findParentData(element, dataName, returnElement)
535 {
536 element = element.closest(`[data-${dataName}]`);
537 if (!element)
538 return null;
539 if (returnElement)
540 return element;
541 return element.getAttribute(`data-${dataName}`);
542 }
543
544 function sendMessageHandleErrors(message, onSuccess)
545 {
546 browser.runtime.sendMessage(message, (errors) =>
547 {
548 if (errors.length > 0)
549 alert(errors.join("\n"));
550 else if (onSuccess)
551 onSuccess();
552 });
553 }
554
555 function switchTab(id)
556 {
557 location.hash = id;
558 }
559
560 function execAction(action, element)
561 {
562 if (element.getAttribute("aria-disabled") == "true")
563 return;
564
565 switch (action)
566 {
567 case "add-domain-exception":
568 addWhitelistedDomain();
569 break;
570 case "add-language-subscription":
571 addEnableSubscription(findParentData(element, "access", false));
572 break;
573 case "add-predefined-subscription": {
574 let dialog = E("dialog-content-predefined");
575 let title = dialog.querySelector("h3").textContent;
576 let url = dialog.querySelector(".url").textContent;
577 addEnableSubscription(url, title);
578 closeDialog();
579 break;
580 }
581 case "cancel-custom-filters":
582 setCustomFiltersView("read");
583 break;
584 case "change-language-subscription":
585 for (let key in subscriptionsMap)
586 {
587 let subscription = subscriptionsMap[key];
588 let subscriptionType = subscription.recommended;
589 if (subscriptionType == "ads" && subscription.disabled == false)
590 {
591 browser.runtime.sendMessage({
592 type: "subscriptions.remove",
593 url: subscription.url
594 });
595 browser.runtime.sendMessage({
596 type: "subscriptions.add",
597 url: findParentData(element, "access", false)
598 });
599 break;
600 }
601 }
602 break;
603 case "close-dialog":
604 closeDialog();
605 break;
606 case "edit-custom-filters":
607 setCustomFiltersView("write");
608 break;
609 case "hide-more-filters-section":
610 E("more-filters").setAttribute("aria-hidden", true);
611 break;
612 case "hide-notification":
613 hideNotification();
614 break;
615 case "import-subscription": {
616 let url = E("blockingList-textbox").value;
617 addEnableSubscription(url);
618 closeDialog();
619 break;
620 }
621 case "open-context-menu": {
622 let listItem = findParentData(element, "access", true);
623 if (listItem && !listItem.classList.contains("show-context-menu"))
624 listItem.classList.add("show-context-menu");
625 break;
626 }
627 case "open-dialog": {
628 let dialog = findParentData(element, "dialog", false);
629 openDialog(dialog);
630 break;
631 }
632 case "remove-filter":
633 browser.runtime.sendMessage({
634 type: "filters.remove",
635 text: findParentData(element, "access", false)
636 });
637 break;
638 case "remove-subscription":
639 browser.runtime.sendMessage({
640 type: "subscriptions.remove",
641 url: findParentData(element, "access", false)
642 });
643 break;
644 case "save-custom-filters":
645 sendMessageHandleErrors({
646 type: "filters.importRaw",
647 text: E("custom-filters-raw").value,
648 removeExisting: true
649 },
650 () =>
651 {
652 setCustomFiltersView("read");
653 });
654 break;
655 case "show-more-filters-section":
656 E("more-filters").setAttribute("aria-hidden", false);
657 break;
658 case "switch-acceptable-ads":
659 let value = element.value || element.dataset.value;
660 // User check the checkbox
661 let shouldCheck = element.getAttribute("aria-checked") != "true";
662 let installAcceptableAds = false;
663 let installAcceptableAdsPrivacy = false;
664 // Acceptable Ads checkbox clicked
665 if (value == "ads")
666 {
667 installAcceptableAds = shouldCheck;
668 }
669 // Privacy Friendly Acceptable Ads checkbox clicked
670 else 286 else
671 { 287 {
672 installAcceptableAdsPrivacy = shouldCheck; 288 lastUpdateElement.textContent =
673 installAcceptableAds = !shouldCheck; 289 getMessage("options_filterList_now");
674 } 290 }
675 291 element.classList.remove("show-message");
676 browser.runtime.sendMessage({ 292 }
677 type: installAcceptableAds ? "subscriptions.add" : 293 }
678 "subscriptions.remove", 294
679 url: acceptableAdsUrl 295 let websiteElement = element.querySelector(".context-menu .website");
680 }); 296 if (websiteElement)
681 browser.runtime.sendMessage({ 297 {
682 type: installAcceptableAdsPrivacy ? "subscriptions.add" : 298 if (item.homepage)
683 "subscriptions.remove", 299 websiteElement.setAttribute("href", item.homepage);
684 url: acceptableAdsPrivacyUrl 300 else
685 }); 301 websiteElement.setAttribute("aria-hidden", true);
686 break; 302 }
687 case "switch-tab": 303
688 switchTab(element.getAttribute("href").substr(1)); 304 let sourceElement = element.querySelector(".context-menu .source");
689 break; 305 if (sourceElement)
690 case "toggle-disable-subscription": 306 sourceElement.setAttribute("href", item.url);
691 browser.runtime.sendMessage({ 307
692 type: "subscriptions.toggle", 308 let newIndex = this.items.indexOf(item);
693 keepInstalled: true, 309 if (oldIndex != newIndex)
694 url: findParentData(element, "access", false) 310 table.insertBefore(element, table.childNodes[newIndex]);
695 }); 311 }
696 break; 312 };
697 case "toggle-pref": 313
698 browser.runtime.sendMessage({ 314 Collection.prototype.clearAll = function()
699 type: "prefs.toggle", 315 {
700 key: findParentData(element, "pref", false) 316 this.items = [];
701 }); 317 for (let detail of this.details)
702 break; 318 {
703 case "toggle-remove-subscription": 319 let table = E(detail.id);
704 let subscriptionUrl = findParentData(element, "access", false); 320 let element = table.firstChild;
705 if (element.getAttribute("aria-checked") == "true") 321 while (element)
322 {
323 if (element.tagName == "LI" && !element.classList.contains("static"))
324 table.removeChild(element);
325 element = element.nextElementSibling;
326 }
327
328 this._setEmpty(table, detail);
329 }
330 };
331
332 function focusNextElement(container, currentElement)
333 {
334 let focusables = container.querySelectorAll("a, button, input, .control");
335 focusables = Array.prototype.slice.call(focusables);
336 let index = focusables.indexOf(currentElement);
337 index += (index == focusables.length - 1) ? -1 : 1;
338
339 let nextElement = focusables[index];
340 if (!nextElement)
341 return false;
342
343 nextElement.focus();
344 return true;
345 }
346
347 collections.protection = new Collection([
348 {
349 id: "recommend-protection-list-table"
350 }
351 ]);
352 collections.langs = new Collection([
353 {
354 id: "blocking-languages-table",
355 emptyTexts: ["options_language_empty"],
356 getTitleFunction: getLanguageTitle
357 }
358 ]);
359 collections.allLangs = new Collection([
360 {
361 id: "all-lang-table-add",
362 emptyTexts: ["options_dialog_language_other_empty"],
363 getTitleFunction: getLanguageTitle
364 }
365 ]);
366 collections.more = new Collection([
367 {
368 id: "more-list-table",
369 setEmptyAction: "hide-more-filters-section",
370 removeEmptyAction: "show-more-filters-section"
371 }
372 ]);
373 collections.whitelist = new Collection([
374 {
375 id: "whitelisting-table",
376 emptyTexts: ["options_whitelist_empty_1", "options_whitelist_empty_2"]
377 }
378 ]);
379 collections.filterLists = new Collection([
380 {
381 id: "all-filter-lists-table",
382 emptyTexts: ["options_filterList_empty"]
383 }
384 ]);
385
386 function addSubscription(subscription)
387 {
388 let {disabled} = subscription;
389 let collection = null;
390 if (subscription.recommended)
391 {
392 if (subscription.recommended == "ads")
393 {
394 if (disabled == false)
395 collection = collections.langs;
396
397 collections.allLangs.addItem(subscription);
398 }
399 else
400 {
401 collection = collections.protection;
402 }
403 }
404 else if (!isAcceptableAds(subscription.url) && disabled == false)
405 {
406 collection = collections.more;
407 }
408
409 if (collection)
410 collection.addItem(subscription);
411
412 subscriptionsMap[subscription.url] = subscription;
413 updateTooltips();
414 }
415
416 function updateSubscription(subscription)
417 {
418 for (let name in collections)
419 collections[name].updateItem(subscription);
420
421 if (subscription.recommended == "ads")
422 {
423 if (subscription.disabled)
424 collections.langs.removeItem(subscription);
425 else
426 collections.langs.addItem(subscription);
427 }
428 else if (!subscription.recommended && !isAcceptableAds(subscription.url))
429 {
430 if (subscription.disabled == false)
431 {
432 collections.more.addItem(subscription);
433 updateTooltips();
434 }
435 else
436 {
437 collections.more.removeItem(subscription);
438 }
439 }
440 }
441
442 function updateFilter(filter)
443 {
444 let match = filter.text.match(whitelistedDomainRegexp);
445 if (match && !filtersMap[filter.text])
446 {
447 filter.title = match[1];
448 collections.whitelist.addItem(filter);
449 if (isCustomFiltersLoaded)
450 {
451 let text = getMessage("options_whitelist_notification", [filter.title]);
452 showNotification(text);
453 }
454 }
455 else
456 {
457 customFilters.push(filter.text);
458 if (isCustomFiltersLoaded)
459 updateCustomFiltersUi();
460 }
461
462 filtersMap[filter.text] = filter;
463 }
464
465 function loadCustomFilters(filters)
466 {
467 for (let filter of filters)
468 updateFilter(filter);
469
470 setCustomFiltersView("read");
471 isCustomFiltersLoaded = true;
472 }
473
474 function removeCustomFilter(text)
475 {
476 let index = customFilters.indexOf(text);
477 if (index >= 0)
478 customFilters.splice(index, 1);
479
480 updateCustomFiltersUi();
481 }
482
483 function updateCustomFiltersUi()
484 {
485 let customFiltersListElement = E("custom-filters-raw");
486 customFiltersListElement.value = customFilters.join("\n");
487 }
488
489 function getLanguageTitle(item)
490 {
491 let title = item.specialization;
492 if (item.originalTitle && item.originalTitle.indexOf("+EasyList") > -1)
493 title += " + " + getMessage("options_english");
494 return title;
495 }
496
497 function loadRecommendations()
498 {
499 fetch("subscriptions.xml")
500 .then((response) =>
501 {
502 return response.text();
503 })
504 .then((text) =>
505 {
506 let doc = new DOMParser().parseFromString(text, "application/xml");
507 let elements = doc.documentElement.getElementsByTagName("subscription");
508 for (let element of elements)
509 {
510 let type = element.getAttribute("type");
511 let subscription = {
512 disabled: true,
513 downloadStatus: null,
514 homepage: null,
515 specialization: element.getAttribute("specialization"),
516 originalTitle: element.getAttribute("title"),
517 recommended: type,
518 url: element.getAttribute("url")
519 };
520
521 if (subscription.recommended != "ads")
522 {
523 type = type.replace(/\W/g, "_");
524 subscription.title = getMessage("common_feature_" +
525 type + "_title");
526 }
527
528 addSubscription(subscription);
529 }
530 });
531 }
532
533 function findParentData(element, dataName, returnElement)
534 {
535 element = element.closest(`[data-${dataName}]`);
536 if (!element)
537 return null;
538 if (returnElement)
539 return element;
540 return element.getAttribute(`data-${dataName}`);
541 }
542
543 function sendMessageHandleErrors(message, onSuccess)
544 {
545 browser.runtime.sendMessage(message, (errors) =>
546 {
547 if (errors.length > 0)
548 alert(errors.join("\n"));
549 else if (onSuccess)
550 onSuccess();
551 });
552 }
553
554 function switchTab(id)
555 {
556 location.hash = id;
557 }
558
559 function execAction(action, element)
560 {
561 if (element.getAttribute("aria-disabled") == "true")
562 return;
563
564 switch (action)
565 {
566 case "add-domain-exception":
567 addWhitelistedDomain();
568 break;
569 case "add-language-subscription":
570 addEnableSubscription(findParentData(element, "access", false));
571 break;
572 case "add-predefined-subscription": {
573 let dialog = E("dialog-content-predefined");
574 let title = dialog.querySelector("h3").textContent;
575 let url = dialog.querySelector(".url").textContent;
576 addEnableSubscription(url, title);
577 closeDialog();
578 break;
579 }
580 case "cancel-custom-filters":
581 setCustomFiltersView("read");
582 break;
583 case "change-language-subscription":
584 for (let key in subscriptionsMap)
585 {
586 let subscription = subscriptionsMap[key];
587 let subscriptionType = subscription.recommended;
588 if (subscriptionType == "ads" && subscription.disabled == false)
706 { 589 {
707 browser.runtime.sendMessage({ 590 browser.runtime.sendMessage({
708 type: "subscriptions.remove", 591 type: "subscriptions.remove",
709 url: subscriptionUrl 592 url: subscription.url
710 }); 593 });
594 browser.runtime.sendMessage({
595 type: "subscriptions.add",
596 url: findParentData(element, "access", false)
597 });
598 break;
711 } 599 }
712 else 600 }
713 addEnableSubscription(subscriptionUrl); 601 break;
602 case "close-dialog":
603 closeDialog();
604 break;
605 case "edit-custom-filters":
606 setCustomFiltersView("write");
607 break;
608 case "hide-more-filters-section":
609 E("more-filters").setAttribute("aria-hidden", true);
610 break;
611 case "hide-notification":
612 hideNotification();
613 break;
614 case "import-subscription": {
615 let url = E("blockingList-textbox").value;
616 addEnableSubscription(url);
617 closeDialog();
618 break;
619 }
620 case "open-context-menu": {
621 let listItem = findParentData(element, "access", true);
622 if (listItem && !listItem.classList.contains("show-context-menu"))
623 listItem.classList.add("show-context-menu");
624 break;
625 }
626 case "open-dialog": {
627 let dialog = findParentData(element, "dialog", false);
628 openDialog(dialog);
629 break;
630 }
631 case "remove-filter":
632 browser.runtime.sendMessage({
633 type: "filters.remove",
634 text: findParentData(element, "access", false)
635 });
636 break;
637 case "remove-subscription":
638 browser.runtime.sendMessage({
639 type: "subscriptions.remove",
640 url: findParentData(element, "access", false)
641 });
642 break;
643 case "save-custom-filters":
644 sendMessageHandleErrors({
645 type: "filters.importRaw",
646 text: E("custom-filters-raw").value,
647 removeExisting: true
648 },
649 () =>
650 {
651 setCustomFiltersView("read");
652 });
653 break;
654 case "show-more-filters-section":
655 E("more-filters").setAttribute("aria-hidden", false);
656 break;
657 case "switch-acceptable-ads":
658 let value = element.value || element.dataset.value;
659 // User check the checkbox
660 let shouldCheck = element.getAttribute("aria-checked") != "true";
661 let installAcceptableAds = false;
662 let installAcceptableAdsPrivacy = false;
663 // Acceptable Ads checkbox clicked
664 if (value == "ads")
665 {
666 installAcceptableAds = shouldCheck;
667 }
668 // Privacy Friendly Acceptable Ads checkbox clicked
669 else
670 {
671 installAcceptableAdsPrivacy = shouldCheck;
672 installAcceptableAds = !shouldCheck;
673 }
674
675 browser.runtime.sendMessage({
676 type: installAcceptableAds ? "subscriptions.add" :
677 "subscriptions.remove",
678 url: acceptableAdsUrl
679 });
680 browser.runtime.sendMessage({
681 type: installAcceptableAdsPrivacy ? "subscriptions.add" :
682 "subscriptions.remove",
683 url: acceptableAdsPrivacyUrl
684 });
685 break;
686 case "switch-tab":
687 switchTab(element.getAttribute("href").substr(1));
688 break;
689 case "toggle-disable-subscription":
690 browser.runtime.sendMessage({
691 type: "subscriptions.toggle",
692 keepInstalled: true,
693 url: findParentData(element, "access", false)
694 });
695 break;
696 case "toggle-pref":
697 browser.runtime.sendMessage({
698 type: "prefs.toggle",
699 key: findParentData(element, "pref", false)
700 });
701 break;
702 case "toggle-remove-subscription":
703 let subscriptionUrl = findParentData(element, "access", false);
704 if (element.getAttribute("aria-checked") == "true")
705 {
706 browser.runtime.sendMessage({
707 type: "subscriptions.remove",
708 url: subscriptionUrl
709 });
710 }
711 else
712 addEnableSubscription(subscriptionUrl);
713 break;
714 case "update-all-subscriptions":
715 browser.runtime.sendMessage({
716 type: "subscriptions.update"
717 });
718 break;
719 case "update-subscription":
720 browser.runtime.sendMessage({
721 type: "subscriptions.update",
722 url: findParentData(element, "access", false)
723 });
724 break;
725 case "validate-import-subscription":
726 let form = findParentData(element, "validation", true);
727 if (!form)
728 return;
729
730 if (form.checkValidity())
731 {
732 addEnableSubscription(E("import-list-url").value,
733 E("import-list-title").value);
734 form.reset();
735 closeDialog();
736 }
737 else
738 {
739 form.querySelector(":invalid").focus();
740 }
741 break;
742 }
743 }
744
745 function setCustomFiltersView(mode)
746 {
747 let customFiltersElement = E("custom-filters-raw");
748 updateCustomFiltersUi();
749 if (mode == "read")
750 {
751 customFiltersElement.disabled = true;
752 if (!customFiltersElement.value)
753 {
754 setCustomFiltersView("empty");
755 return;
756 }
757 }
758 else if (mode == "write")
759 {
760 customFiltersElement.disabled = false;
761 }
762
763 E("custom-filters").dataset.mode = mode;
764 }
765
766 function onClick(e)
767 {
768 let context = document.querySelector(".show-context-menu");
769 if (context)
770 context.classList.remove("show-context-menu");
771
772 let actions = findParentData(e.target, "action", false);
773 if (!actions)
774 return;
775
776 actions = actions.split(",");
777 for (let action of actions)
778 {
779 execAction(action, e.target);
780 }
781 }
782
783 function getKey(e)
784 {
785 // e.keyCode has been deprecated so we attempt to use e.key
786 if ("key" in e)
787 return e.key;
788 return getKey.keys[e.keyCode];
789 }
790 getKey.keys = {
791 9: "Tab",
792 13: "Enter",
793 27: "Escape",
794 37: "ArrowLeft",
795 38: "ArrowUp",
796 39: "ArrowRight",
797 40: "ArrowDown"
798 };
799
800 function onKeyUp(e)
801 {
802 let key = getKey(e);
803 let element = document.activeElement;
804 if (!key || !element)
805 return;
806
807 let container = findParentData(element, "action", true);
808 if (!container || !container.hasAttribute("data-keys"))
809 return;
810
811 let keys = container.getAttribute("data-keys").split(" ");
812 if (keys.indexOf(key) < 0)
813 return;
814
815 if (element.getAttribute("role") == "tab")
816 {
817 let parent = element.parentElement;
818 if (key == "ArrowLeft" || key == "ArrowUp")
819 parent = parent.previousElementSibling || container.lastElementChild;
820 else if (key == "ArrowRight" || key == "ArrowDown")
821 parent = parent.nextElementSibling || container.firstElementChild;
822 element = parent.firstElementChild;
823 }
824
825 let actions = container.getAttribute("data-action").split(",");
826 for (let action of actions)
827 {
828 execAction(action, element);
829 }
830 }
831
832 function selectTabItem(tabId, container, focus)
833 {
834 // Show tab content
835 document.body.setAttribute("data-tab", tabId);
836
837 // Select tab
838 let tabList = container.querySelector("[role='tablist']");
839 if (!tabList)
840 return null;
841
842 let previousTab = tabList.querySelector("[aria-selected]");
843 previousTab.removeAttribute("aria-selected");
844 previousTab.setAttribute("tabindex", -1);
845
846 let tab = tabList.querySelector("a[href='#" + tabId + "']");
847 tab.setAttribute("aria-selected", true);
848 tab.setAttribute("tabindex", 0);
849
850 let tabContentId = tab.getAttribute("aria-controls");
851 let tabContent = document.getElementById(tabContentId);
852
853 if (tab && focus)
854 tab.focus();
855
856 return tabContent;
857 }
858
859 function onHashChange()
860 {
861 let hash = location.hash.substr(1);
862 if (!hash)
863 return;
864
865 // Select tab and parent tabs
866 let tabIds = hash.split("-");
867 let tabContent = document.body;
868 for (let i = 0; i < tabIds.length; i++)
869 {
870 let tabId = tabIds.slice(0, i + 1).join("-");
871 tabContent = selectTabItem(tabId, tabContent, true);
872 if (!tabContent)
873 break;
874 }
875 }
876
877 function onDOMLoaded()
878 {
879 populateLists();
880
881 // Initialize navigation sidebar
882 browser.runtime.sendMessage({
883 type: "app.get",
884 what: "addonVersion"
885 },
886 (addonVersion) =>
887 {
888 E("abp-version").textContent = getMessage("options_dialog_about_version",
889 [addonVersion]);
890 });
891
892 updateTooltips();
893
894 // Initialize interactive UI elements
895 document.body.addEventListener("click", onClick, false);
896 document.body.addEventListener("keyup", onKeyUp, false);
897 let exampleValue = getMessage("options_whitelist_placeholder_example",
898 ["www.example.com"]);
899 E("whitelisting-textbox").setAttribute("placeholder", exampleValue);
900 E("whitelisting-textbox").addEventListener("keyup", (e) =>
901 {
902 E("whitelisting-add-button").disabled = !e.target.value;
903 }, false);
904
905 // General tab
906 getDocLink("contribute", (link) =>
907 {
908 E("contribute").href = link;
909 });
910 getDocLink("acceptable_ads_criteria", (link) =>
911 {
912 setLinks("enable-acceptable-ads-description", link);
913 });
914 setElementText(E("tracking-warning-1"), "options_tracking_warning_1",
915 [getMessage("common_feature_privacy_title"),
916 getMessage("options_acceptableAds_ads_label")]);
917 setElementText(E("tracking-warning-3"), "options_tracking_warning_3",
918 [getMessage("options_acceptableAds_privacy_label")]);
919
920 getDocLink("privacy_friendly_ads", (link) =>
921 {
922 E("enable-acceptable-ads-privacy-description").href = link;
923 });
924 getDocLink("adblock_plus_{browser}_dnt", url =>
925 {
926 setLinks("dnt", url);
927 });
928
929 // Whitelisted tab
930 getDocLink("whitelist", (link) =>
931 {
932 E("whitelist-learn-more").href = link;
933 });
934
935 // Advanced tab
936 let customize = document.querySelectorAll("#customize li[data-pref]");
937 customize = Array.prototype.map.call(customize, (checkbox) =>
938 {
939 return checkbox.getAttribute("data-pref");
940 });
941 for (let key of customize)
942 {
943 getPref(key, (value) =>
944 {
945 onPrefMessage(key, value, true);
946 });
947 }
948 browser.runtime.sendMessage({
949 type: "app.get",
950 what: "features"
951 },
952 (features) =>
953 {
954 hidePref("show_devtools_panel", !features.devToolsPanel);
955 });
956
957 getDocLink("filterdoc", (link) =>
958 {
959 E("link-filters").setAttribute("href", link);
960 });
961
962 getDocLink("subscriptions", (link) =>
963 {
964 E("filter-lists-learn-more").setAttribute("href", link);
965 });
966
967 E("custom-filters-raw").setAttribute("placeholder",
968 getMessage("options_customFilters_edit_placeholder", ["/ads/track/*"]));
969
970 // Help tab
971 getDocLink("adblock_plus_report_bug", (link) =>
972 {
973 setLinks("report-bug", link);
974 });
975 getDocLink("{browser}_support", url =>
976 {
977 setLinks("visit-forum", url);
978 });
979 getDocLink("social_twitter", (link) =>
980 {
981 E("twitter").setAttribute("href", link);
982 });
983 getDocLink("social_facebook", (link) =>
984 {
985 E("facebook").setAttribute("href", link);
986 });
987 getDocLink("social_gplus", (link) =>
988 {
989 E("google-plus").setAttribute("href", link);
990 });
991 getDocLink("social_weibo", (link) =>
992 {
993 E("weibo").setAttribute("href", link);
994 });
995
996 E("dialog").addEventListener("keydown", function(e)
997 {
998 switch (getKey(e))
999 {
1000 case "Escape":
1001 closeDialog();
714 break; 1002 break;
715 case "update-all-subscriptions": 1003 case "Tab":
716 browser.runtime.sendMessage({ 1004 if (e.shiftKey)
717 type: "subscriptions.update" 1005 {
718 }); 1006 if (e.target.classList.contains("focus-first"))
1007 {
1008 e.preventDefault();
1009 this.querySelector(".focus-last").focus();
1010 }
1011 }
1012 else if (e.target.classList.contains("focus-last"))
1013 {
1014 e.preventDefault();
1015 this.querySelector(".focus-first").focus();
1016 }
719 break; 1017 break;
720 case "update-subscription": 1018 }
721 browser.runtime.sendMessage({ 1019 }, false);
722 type: "subscriptions.update", 1020
723 url: findParentData(element, "access", false) 1021 onHashChange();
724 }); 1022 }
725 break; 1023
726 case "validate-import-subscription": 1024 let focusedBeforeDialog = null;
727 let form = findParentData(element, "validation", true); 1025 function openDialog(name)
728 if (!form) 1026 {
729 return; 1027 let dialog = E("dialog");
730 1028 dialog.setAttribute("aria-hidden", false);
731 if (form.checkValidity()) 1029 dialog.setAttribute("aria-labelledby", "dialog-title-" + name);
1030 document.body.setAttribute("data-dialog", name);
1031
1032 let defaultFocus = document.querySelector(
1033 "#dialog-content-" + name + " .default-focus"
1034 );
1035 if (!defaultFocus)
1036 defaultFocus = dialog.querySelector(".focus-first");
1037 focusedBeforeDialog = document.activeElement;
1038 defaultFocus.focus();
1039 }
1040
1041 function closeDialog()
1042 {
1043 let dialog = E("dialog");
1044 dialog.setAttribute("aria-hidden", true);
1045 dialog.removeAttribute("aria-labelledby");
1046 document.body.removeAttribute("data-dialog");
1047 focusedBeforeDialog.focus();
1048 }
1049
1050 function showNotification(text)
1051 {
1052 E("notification").setAttribute("aria-hidden", false);
1053 E("notification-text").textContent = text;
1054 setTimeout(hideNotification, 3000);
1055 }
1056
1057 function hideNotification()
1058 {
1059 E("notification").setAttribute("aria-hidden", true);
1060 E("notification-text").textContent = "";
1061 }
1062
1063 function setAcceptableAds()
1064 {
1065 let acceptableAdsForm = E("acceptable-ads");
1066 let acceptableAds = E("acceptable-ads-allow");
1067 let acceptableAdsPrivacy = E("acceptable-ads-privacy-allow");
1068 acceptableAdsForm.classList.remove("show-dnt-notification");
1069 acceptableAds.setAttribute("aria-checked", false);
1070 acceptableAdsPrivacy.setAttribute("aria-checked", false);
1071 acceptableAdsPrivacy.setAttribute("tabindex", 0);
1072 if (acceptableAdsUrl in subscriptionsMap)
1073 {
1074 acceptableAds.setAttribute("aria-checked", true);
1075 acceptableAdsPrivacy.setAttribute("aria-disabled", false);
1076 }
1077 else if (acceptableAdsPrivacyUrl in subscriptionsMap)
1078 {
1079 acceptableAds.setAttribute("aria-checked", true);
1080 acceptableAdsPrivacy.setAttribute("aria-checked", true);
1081 acceptableAdsPrivacy.setAttribute("aria-disabled", false);
1082
1083 // Edge uses window instead of navigator.
1084 // Prefer navigator first since it's the standard.
1085 if ((navigator.doNotTrack || window.doNotTrack) != 1)
1086 acceptableAdsForm.classList.add("show-dnt-notification");
1087 }
1088 else
1089 {
1090 // Using aria-disabled in order to keep the focus
1091 acceptableAdsPrivacy.setAttribute("aria-disabled", true);
1092 acceptableAdsPrivacy.setAttribute("tabindex", -1);
1093 }
1094 }
1095
1096 function isAcceptableAds(url)
1097 {
1098 return url == acceptableAdsUrl || url == acceptableAdsPrivacyUrl;
1099 }
1100
1101 function hasPrivacyConflict()
1102 {
1103 let acceptableAdsList = subscriptionsMap[acceptableAdsUrl];
1104 let privacyList = null;
1105 for (let url in subscriptionsMap)
1106 {
1107 let subscription = subscriptionsMap[url];
1108 if (subscription.recommended == "privacy")
1109 {
1110 privacyList = subscription;
1111 break;
1112 }
1113 }
1114 return acceptableAdsList && acceptableAdsList.disabled == false &&
1115 privacyList && privacyList.disabled == false;
1116 }
1117
1118 function setPrivacyConflict()
1119 {
1120 let acceptableAdsForm = E("acceptable-ads");
1121 if (hasPrivacyConflict())
1122 {
1123 getPref("ui_warn_tracking", (showTrackingWarning) =>
1124 {
1125 acceptableAdsForm.classList.toggle("show-warning", showTrackingWarning);
1126 });
1127 }
1128 else
1129 {
1130 acceptableAdsForm.classList.remove("show-warning");
1131 }
1132 }
1133
1134 function populateLists()
1135 {
1136 subscriptionsMap = Object.create(null);
1137 filtersMap = Object.create(null);
1138
1139 // Empty collections and lists
1140 for (let property in collections)
1141 collections[property].clearAll();
1142
1143 setCustomFiltersView("empty");
1144 browser.runtime.sendMessage({
1145 type: "subscriptions.get",
1146 special: true
1147 },
1148 (subscriptions) =>
1149 {
1150 // Load filters
1151 for (let subscription of subscriptions)
1152 {
1153 browser.runtime.sendMessage({
1154 type: "filters.get",
1155 subscriptionUrl: subscription.url
1156 },
1157 (filters) =>
1158 {
1159 loadCustomFilters(filters);
1160 });
1161 }
1162 });
1163 loadRecommendations();
1164 browser.runtime.sendMessage({
1165 type: "prefs.get",
1166 key: "subscriptions_exceptionsurl"
1167 },
1168 (url) =>
1169 {
1170 acceptableAdsUrl = url;
1171
1172 browser.runtime.sendMessage({
1173 type: "prefs.get",
1174 key: "subscriptions_exceptionsurl_privacy"
1175 },
1176 (urlPrivacy) =>
1177 {
1178 acceptableAdsPrivacyUrl = urlPrivacy;
1179
1180 // Load user subscriptions
1181 browser.runtime.sendMessage({
1182 type: "subscriptions.get",
1183 downloadable: true
1184 },
1185 (subscriptions) =>
1186 {
1187 for (let subscription of subscriptions)
1188 onSubscriptionMessage("added", subscription);
1189
1190 setAcceptableAds();
1191 });
1192 });
1193 });
1194 }
1195
1196 function addWhitelistedDomain()
1197 {
1198 let domain = E("whitelisting-textbox");
1199 for (let whitelistItem of collections.whitelist.items)
1200 {
1201 if (whitelistItem.title == domain.value)
1202 {
1203 whitelistItem[timestampUI] = Date.now();
1204 collections.whitelist.updateItem(whitelistItem);
1205 domain.value = "";
1206 break;
1207 }
1208 }
1209 const value = domain.value.trim();
1210 if (value)
1211 {
1212 const host = /^https?:\/\//i.test(value) ? new URL(value).host : value;
1213 sendMessageHandleErrors({
1214 type: "filters.add",
1215 text: "@@||" + host.toLowerCase() + "^$document"
1216 });
1217 }
1218
1219 domain.value = "";
1220 E("whitelisting-add-button").disabled = true;
1221 }
1222
1223 function addEnableSubscription(url, title, homepage)
1224 {
1225 let messageType = null;
1226 let knownSubscription = subscriptionsMap[url];
1227 if (knownSubscription && knownSubscription.disabled == true)
1228 messageType = "subscriptions.toggle";
1229 else
1230 messageType = "subscriptions.add";
1231
1232 let message = {
1233 type: messageType,
1234 url
1235 };
1236 if (title)
1237 message.title = title;
1238 if (homepage)
1239 message.homepage = homepage;
1240
1241 browser.runtime.sendMessage(message);
1242 }
1243
1244 function onFilterMessage(action, filter)
1245 {
1246 switch (action)
1247 {
1248 case "added":
1249 filter[timestampUI] = Date.now();
1250 updateFilter(filter);
1251 break;
1252 case "loaded":
1253 populateLists();
1254 break;
1255 case "removed":
1256 let knownFilter = filtersMap[filter.text];
1257 if (whitelistedDomainRegexp.test(knownFilter.text))
1258 collections.whitelist.removeItem(knownFilter);
1259 else
1260 removeCustomFilter(filter.text);
1261
1262 delete filtersMap[filter.text];
1263 break;
1264 }
1265 }
1266
1267 function onSubscriptionMessage(action, subscription)
1268 {
1269 if (subscription.url in subscriptionsMap)
1270 {
1271 let knownSubscription = subscriptionsMap[subscription.url];
1272 for (let property in subscription)
1273 {
1274 if (property == "title" && knownSubscription.recommended)
1275 knownSubscription.originalTitle = subscription.title;
1276 else
1277 knownSubscription[property] = subscription[property];
1278 }
1279 subscription = knownSubscription;
1280 }
1281 switch (action)
1282 {
1283 case "disabled":
1284 updateSubscription(subscription);
1285 setPrivacyConflict();
1286 break;
1287 case "downloading":
1288 case "downloadStatus":
1289 case "homepage":
1290 case "lastDownload":
1291 case "title":
1292 updateSubscription(subscription);
1293 break;
1294 case "added":
1295 let {url} = subscription;
1296 // Handle custom subscription
1297 if (/^~user/.test(url))
1298 {
1299 loadCustomFilters(subscription.filters);
1300 return;
1301 }
1302 else if (url in subscriptionsMap)
1303 updateSubscription(subscription);
1304 else
1305 addSubscription(subscription);
1306
1307 if (isAcceptableAds(url))
1308 setAcceptableAds();
1309
1310 collections.filterLists.addItem(subscription);
1311 setPrivacyConflict();
1312 break;
1313 case "removed":
1314 if (subscription.recommended)
1315 {
1316 subscription.disabled = true;
1317 onSubscriptionMessage("disabled", subscription);
1318 }
1319 else
1320 {
1321 delete subscriptionsMap[subscription.url];
1322 if (isAcceptableAds(subscription.url))
732 { 1323 {
733 addEnableSubscription(E("import-list-url").value, 1324 setAcceptableAds();
734 E("import-list-title").value);
735 form.reset();
736 closeDialog();
737 } 1325 }
738 else 1326 else
739 { 1327 {
740 form.querySelector(":invalid").focus(); 1328 collections.more.removeItem(subscription);
741 } 1329 }
742 break; 1330 }
743 } 1331
744 } 1332 collections.filterLists.removeItem(subscription);
745 1333 setPrivacyConflict();
746 function setCustomFiltersView(mode) 1334 break;
747 { 1335 }
748 let customFiltersElement = E("custom-filters-raw"); 1336 }
749 updateCustomFiltersUi(); 1337
750 if (mode == "read") 1338 function hidePref(key, value)
751 { 1339 {
752 customFiltersElement.disabled = true; 1340 let element = document.querySelector("[data-pref='" + key + "']");
753 if (!customFiltersElement.value) 1341 if (element)
754 { 1342 element.setAttribute("aria-hidden", value);
755 setCustomFiltersView("empty"); 1343 }
756 return; 1344
757 } 1345 function getPref(key, callback)
758 } 1346 {
759 else if (mode == "write") 1347 let checkPref = getPref.checks[key] || getPref.checkNone;
760 { 1348 checkPref((isActive) =>
761 customFiltersElement.disabled = false; 1349 {
762 } 1350 if (!isActive)
763 1351 {
764 E("custom-filters").dataset.mode = mode; 1352 hidePref(key, !isActive);
765 }
766
767 function onClick(e)
768 {
769 let context = document.querySelector(".show-context-menu");
770 if (context)
771 context.classList.remove("show-context-menu");
772
773 let actions = findParentData(e.target, "action", false);
774 if (!actions)
775 return; 1353 return;
776 1354 }
777 actions = actions.split(","); 1355
778 for (let action of actions)
779 {
780 execAction(action, e.target);
781 }
782 }
783
784 function getKey(e)
785 {
786 // e.keyCode has been deprecated so we attempt to use e.key
787 if ("key" in e)
788 return e.key;
789 return getKey.keys[e.keyCode];
790 }
791 getKey.keys = {
792 9: "Tab",
793 13: "Enter",
794 27: "Escape",
795 37: "ArrowLeft",
796 38: "ArrowUp",
797 39: "ArrowRight",
798 40: "ArrowDown"
799 };
800
801 function onKeyUp(e)
802 {
803 let key = getKey(e);
804 let element = document.activeElement;
805 if (!key || !element)
806 return;
807
808 let container = findParentData(element, "action", true);
809 if (!container || !container.hasAttribute("data-keys"))
810 return;
811
812 let keys = container.getAttribute("data-keys").split(" ");
813 if (keys.indexOf(key) < 0)
814 return;
815
816 if (element.getAttribute("role") == "tab")
817 {
818 let parent = element.parentElement;
819 if (key == "ArrowLeft" || key == "ArrowUp")
820 parent = parent.previousElementSibling || container.lastElementChild;
821 else if (key == "ArrowRight" || key == "ArrowDown")
822 parent = parent.nextElementSibling || container.firstElementChild;
823 element = parent.firstElementChild;
824 }
825
826 let actions = container.getAttribute("data-action").split(",");
827 for (let action of actions)
828 {
829 execAction(action, element);
830 }
831 }
832
833 function selectTabItem(tabId, container, focus)
834 {
835 // Show tab content
836 document.body.setAttribute("data-tab", tabId);
837
838 // Select tab
839 let tabList = container.querySelector("[role='tablist']");
840 if (!tabList)
841 return null;
842
843 let previousTab = tabList.querySelector("[aria-selected]");
844 previousTab.removeAttribute("aria-selected");
845 previousTab.setAttribute("tabindex", -1);
846
847 let tab = tabList.querySelector("a[href='#" + tabId + "']");
848 tab.setAttribute("aria-selected", true);
849 tab.setAttribute("tabindex", 0);
850
851 let tabContentId = tab.getAttribute("aria-controls");
852 let tabContent = document.getElementById(tabContentId);
853
854 if (tab && focus)
855 tab.focus();
856
857 return tabContent;
858 }
859
860 function onHashChange()
861 {
862 let hash = location.hash.substr(1);
863 if (!hash)
864 return;
865
866 // Select tab and parent tabs
867 let tabIds = hash.split("-");
868 let tabContent = document.body;
869 for (let i = 0; i < tabIds.length; i++)
870 {
871 let tabId = tabIds.slice(0, i + 1).join("-");
872 tabContent = selectTabItem(tabId, tabContent, true);
873 if (!tabContent)
874 break;
875 }
876 }
877
878 function onDOMLoaded()
879 {
880 populateLists();
881
882 // Initialize navigation sidebar
883 browser.runtime.sendMessage({
884 type: "app.get",
885 what: "addonVersion"
886 },
887 (addonVersion) =>
888 {
889 E("abp-version").textContent = getMessage("options_dialog_about_version",
890 [addonVersion]);
891 });
892
893 updateTooltips();
894
895 // Initialize interactive UI elements
896 document.body.addEventListener("click", onClick, false);
897 document.body.addEventListener("keyup", onKeyUp, false);
898 let exampleValue = getMessage("options_whitelist_placeholder_example",
899 ["www.example.com"]);
900 E("whitelisting-textbox").setAttribute("placeholder", exampleValue);
901 E("whitelisting-textbox").addEventListener("keyup", (e) =>
902 {
903 E("whitelisting-add-button").disabled = !e.target.value;
904 }, false);
905
906 // General tab
907 getDocLink("contribute", (link) =>
908 {
909 E("contribute").href = link;
910 });
911 getDocLink("acceptable_ads_criteria", (link) =>
912 {
913 setLinks("enable-acceptable-ads-description", link);
914 });
915 setElementText(E("tracking-warning-1"), "options_tracking_warning_1",
916 [getMessage("common_feature_privacy_title"),
917 getMessage("options_acceptableAds_ads_label")]);
918 setElementText(E("tracking-warning-3"), "options_tracking_warning_3",
919 [getMessage("options_acceptableAds_privacy_label")]);
920
921 getDocLink("privacy_friendly_ads", (link) =>
922 {
923 E("enable-acceptable-ads-privacy-description").href = link;
924 });
925 getDocLink("adblock_plus_{browser}_dnt", url =>
926 {
927 setLinks("dnt", url);
928 });
929
930 // Whitelisted tab
931 getDocLink("whitelist", (link) =>
932 {
933 E("whitelist-learn-more").href = link;
934 });
935
936 // Advanced tab
937 let customize = document.querySelectorAll("#customize li[data-pref]");
938 customize = Array.prototype.map.call(customize, (checkbox) =>
939 {
940 return checkbox.getAttribute("data-pref");
941 });
942 for (let key of customize)
943 {
944 getPref(key, (value) =>
945 {
946 onPrefMessage(key, value, true);
947 });
948 }
949 browser.runtime.sendMessage({
950 type: "app.get",
951 what: "features"
952 },
953 (features) =>
954 {
955 hidePref("show_devtools_panel", !features.devToolsPanel);
956 });
957
958 getDocLink("filterdoc", (link) =>
959 {
960 E("link-filters").setAttribute("href", link);
961 });
962
963 getDocLink("subscriptions", (link) =>
964 {
965 E("filter-lists-learn-more").setAttribute("href", link);
966 });
967
968 E("custom-filters-raw").setAttribute("placeholder",
969 getMessage("options_customFilters_edit_placeholder", ["/ads/track/*"]));
970
971 // Help tab
972 getDocLink("adblock_plus_report_bug", (link) =>
973 {
974 setLinks("report-bug", link);
975 });
976 getDocLink("{browser}_support", url =>
977 {
978 setLinks("visit-forum", url);
979 });
980 getDocLink("social_twitter", (link) =>
981 {
982 E("twitter").setAttribute("href", link);
983 });
984 getDocLink("social_facebook", (link) =>
985 {
986 E("facebook").setAttribute("href", link);
987 });
988 getDocLink("social_gplus", (link) =>
989 {
990 E("google-plus").setAttribute("href", link);
991 });
992 getDocLink("social_weibo", (link) =>
993 {
994 E("weibo").setAttribute("href", link);
995 });
996
997 E("dialog").addEventListener("keydown", function(e)
998 {
999 switch (getKey(e))
1000 {
1001 case "Escape":
1002 closeDialog();
1003 break;
1004 case "Tab":
1005 if (e.shiftKey)
1006 {
1007 if (e.target.classList.contains("focus-first"))
1008 {
1009 e.preventDefault();
1010 this.querySelector(".focus-last").focus();
1011 }
1012 }
1013 else if (e.target.classList.contains("focus-last"))
1014 {
1015 e.preventDefault();
1016 this.querySelector(".focus-first").focus();
1017 }
1018 break;
1019 }
1020 }, false);
1021
1022 onHashChange();
1023 }
1024
1025 let focusedBeforeDialog = null;
1026 function openDialog(name)
1027 {
1028 let dialog = E("dialog");
1029 dialog.setAttribute("aria-hidden", false);
1030 dialog.setAttribute("aria-labelledby", "dialog-title-" + name);
1031 document.body.setAttribute("data-dialog", name);
1032
1033 let defaultFocus = document.querySelector(
1034 "#dialog-content-" + name + " .default-focus"
1035 );
1036 if (!defaultFocus)
1037 defaultFocus = dialog.querySelector(".focus-first");
1038 focusedBeforeDialog = document.activeElement;
1039 defaultFocus.focus();
1040 }
1041
1042 function closeDialog()
1043 {
1044 let dialog = E("dialog");
1045 dialog.setAttribute("aria-hidden", true);
1046 dialog.removeAttribute("aria-labelledby");
1047 document.body.removeAttribute("data-dialog");
1048 focusedBeforeDialog.focus();
1049 }
1050
1051 function showNotification(text)
1052 {
1053 E("notification").setAttribute("aria-hidden", false);
1054 E("notification-text").textContent = text;
1055 setTimeout(hideNotification, 3000);
1056 }
1057
1058 function hideNotification()
1059 {
1060 E("notification").setAttribute("aria-hidden", true);
1061 E("notification-text").textContent = "";
1062 }
1063
1064 function setAcceptableAds()
1065 {
1066 let acceptableAdsForm = E("acceptable-ads");
1067 let acceptableAds = E("acceptable-ads-allow");
1068 let acceptableAdsPrivacy = E("acceptable-ads-privacy-allow");
1069 acceptableAdsForm.classList.remove("show-dnt-notification");
1070 acceptableAds.setAttribute("aria-checked", false);
1071 acceptableAdsPrivacy.setAttribute("aria-checked", false);
1072 acceptableAdsPrivacy.setAttribute("tabindex", 0);
1073 if (acceptableAdsUrl in subscriptionsMap)
1074 {
1075 acceptableAds.setAttribute("aria-checked", true);
1076 acceptableAdsPrivacy.setAttribute("aria-disabled", false);
1077 }
1078 else if (acceptableAdsPrivacyUrl in subscriptionsMap)
1079 {
1080 acceptableAds.setAttribute("aria-checked", true);
1081 acceptableAdsPrivacy.setAttribute("aria-checked", true);
1082 acceptableAdsPrivacy.setAttribute("aria-disabled", false);
1083
1084 // Edge uses window instead of navigator.
1085 // Prefer navigator first since it's the standard.
1086 if ((navigator.doNotTrack || window.doNotTrack) != 1)
1087 acceptableAdsForm.classList.add("show-dnt-notification");
1088 }
1089 else
1090 {
1091 // Using aria-disabled in order to keep the focus
1092 acceptableAdsPrivacy.setAttribute("aria-disabled", true);
1093 acceptableAdsPrivacy.setAttribute("tabindex", -1);
1094 }
1095 }
1096
1097 function isAcceptableAds(url)
1098 {
1099 return url == acceptableAdsUrl || url == acceptableAdsPrivacyUrl;
1100 }
1101
1102 function hasPrivacyConflict()
1103 {
1104 let acceptableAdsList = subscriptionsMap[acceptableAdsUrl];
1105 let privacyList = null;
1106 for (let url in subscriptionsMap)
1107 {
1108 let subscription = subscriptionsMap[url];
1109 if (subscription.recommended == "privacy")
1110 {
1111 privacyList = subscription;
1112 break;
1113 }
1114 }
1115 return acceptableAdsList && acceptableAdsList.disabled == false &&
1116 privacyList && privacyList.disabled == false;
1117 }
1118
1119 function setPrivacyConflict()
1120 {
1121 let acceptableAdsForm = E("acceptable-ads");
1122 if (hasPrivacyConflict())
1123 {
1124 getPref("ui_warn_tracking", (showTrackingWarning) =>
1125 {
1126 acceptableAdsForm.classList.toggle("show-warning", showTrackingWarning);
1127 });
1128 }
1129 else
1130 {
1131 acceptableAdsForm.classList.remove("show-warning");
1132 }
1133 }
1134
1135 function populateLists()
1136 {
1137 subscriptionsMap = Object.create(null);
1138 filtersMap = Object.create(null);
1139
1140 // Empty collections and lists
1141 for (let property in collections)
1142 collections[property].clearAll();
1143
1144 setCustomFiltersView("empty");
1145 browser.runtime.sendMessage({
1146 type: "subscriptions.get",
1147 special: true
1148 },
1149 (subscriptions) =>
1150 {
1151 // Load filters
1152 for (let subscription of subscriptions)
1153 {
1154 browser.runtime.sendMessage({
1155 type: "filters.get",
1156 subscriptionUrl: subscription.url
1157 },
1158 (filters) =>
1159 {
1160 loadCustomFilters(filters);
1161 });
1162 }
1163 });
1164 loadRecommendations();
1165 browser.runtime.sendMessage({ 1356 browser.runtime.sendMessage({
1166 type: "prefs.get", 1357 type: "prefs.get",
1167 key: "subscriptions_exceptionsurl" 1358 key
1168 }, 1359 }, callback);
1169 (url) => 1360 });
1170 { 1361 }
1171 acceptableAdsUrl = url; 1362
1172 1363 getPref.checkNone = function(callback)
1173 browser.runtime.sendMessage({ 1364 {
1174 type: "prefs.get", 1365 callback(true);
1175 key: "subscriptions_exceptionsurl_privacy" 1366 };
1176 }, 1367
1177 (urlPrivacy) => 1368 getPref.checks =
1178 { 1369 {
1179 acceptableAdsPrivacyUrl = urlPrivacy; 1370 notifications_ignoredcategories(callback)
1180 1371 {
1181 // Load user subscriptions 1372 getPref("notifications_showui", callback);
1182 browser.runtime.sendMessage({ 1373 }
1183 type: "subscriptions.get", 1374 };
1184 downloadable: true 1375
1185 }, 1376 function onPrefMessage(key, value, initial)
1186 (subscriptions) => 1377 {
1187 { 1378 switch (key)
1188 for (let subscription of subscriptions) 1379 {
1189 onSubscriptionMessage("added", subscription); 1380 case "notifications_ignoredcategories":
1190 1381 value = value.indexOf("*") == -1;
1191 setAcceptableAds(); 1382 break;
1192 }); 1383
1193 }); 1384 case "notifications_showui":
1194 }); 1385 hidePref("notifications_ignoredcategories", !value);
1195 } 1386 break;
1196 1387 case "ui_warn_tracking":
1197 function addWhitelistedDomain() 1388 setPrivacyConflict();
1198 { 1389 break;
1199 let domain = E("whitelisting-textbox"); 1390 }
1200 for (let whitelistItem of collections.whitelist.items) 1391
1201 { 1392 let checkbox = document.querySelector(
1202 if (whitelistItem.title == domain.value) 1393 "[data-pref='" + key + "'] button[role='checkbox']"
1203 { 1394 );
1204 whitelistItem[timestampUI] = Date.now(); 1395 if (checkbox)
1205 collections.whitelist.updateItem(whitelistItem); 1396 checkbox.setAttribute("aria-checked", value);
1206 domain.value = ""; 1397 }
1207 break; 1398
1208 } 1399 function updateTooltips()
1209 } 1400 {
1210 const value = domain.value.trim(); 1401 let anchors = document.querySelectorAll(":not(.tooltip) > [data-tooltip]");
1211 if (value) 1402 for (let anchor of anchors)
1212 { 1403 {
1213 const host = /^https?:\/\//i.test(value) ? new URL(value).host : value; 1404 let id = anchor.getAttribute("data-tooltip");
1214 sendMessageHandleErrors({ 1405
1215 type: "filters.add", 1406 let wrapper = document.createElement("div");
1216 text: "@@||" + host.toLowerCase() + "^$document" 1407 wrapper.className = "icon tooltip";
1217 }); 1408 anchor.parentNode.replaceChild(wrapper, anchor);
1218 } 1409 wrapper.appendChild(anchor);
1219 1410
1220 domain.value = ""; 1411 let tooltip = document.createElement("div");
1221 E("whitelisting-add-button").disabled = true; 1412 tooltip.setAttribute("role", "tooltip");
1222 } 1413
1223 1414 let paragraph = document.createElement("p");
1224 function addEnableSubscription(url, title, homepage) 1415 paragraph.textContent = getMessage(id);
1225 { 1416 tooltip.appendChild(paragraph);
1226 let messageType = null; 1417
1227 let knownSubscription = subscriptionsMap[url]; 1418 wrapper.appendChild(tooltip);
1228 if (knownSubscription && knownSubscription.disabled == true) 1419 }
1229 messageType = "subscriptions.toggle"; 1420 }
1230 else 1421
1231 messageType = "subscriptions.add"; 1422 ext.onMessage.addListener((message) =>
1232 1423 {
1233 let message = { 1424 switch (message.type)
1234 type: messageType, 1425 {
1235 url 1426 case "app.respond":
1236 }; 1427 switch (message.action)
1237 if (title) 1428 {
1238 message.title = title; 1429 case "addSubscription":
1239 if (homepage) 1430 let subscription = message.args[0];
1240 message.homepage = homepage; 1431 let dialog = E("dialog-content-predefined");
1241 1432 dialog.querySelector("h3").textContent = subscription.title || "";
1242 browser.runtime.sendMessage(message); 1433 dialog.querySelector(".url").textContent = subscription.url;
1243 } 1434 openDialog("predefined");
1244 1435 break;
1245 function onFilterMessage(action, filter) 1436 case "focusSection":
1246 { 1437 document.body.setAttribute("data-tab", message.args[0]);
1247 switch (action) 1438 break;
1248 { 1439 }
1249 case "added": 1440 break;
1250 filter[timestampUI] = Date.now(); 1441 case "filters.respond":
1251 updateFilter(filter); 1442 onFilterMessage(message.action, message.args[0]);
1252 break; 1443 break;
1253 case "loaded": 1444 case "prefs.respond":
1254 populateLists(); 1445 onPrefMessage(message.action, message.args[0], false);
1255 break; 1446 break;
1256 case "removed": 1447 case "subscriptions.respond":
1257 let knownFilter = filtersMap[filter.text]; 1448 onSubscriptionMessage(message.action, message.args[0]);
1258 if (whitelistedDomainRegexp.test(knownFilter.text)) 1449 break;
1259 collections.whitelist.removeItem(knownFilter); 1450 }
1260 else 1451 });
1261 removeCustomFilter(filter.text); 1452
1262 1453 browser.runtime.sendMessage({
1263 delete filtersMap[filter.text]; 1454 type: "app.listen",
1264 break; 1455 filter: ["addSubscription", "focusSection"]
1265 } 1456 });
1266 } 1457 browser.runtime.sendMessage({
1267 1458 type: "filters.listen",
1268 function onSubscriptionMessage(action, subscription) 1459 filter: ["added", "loaded", "removed"]
1269 { 1460 });
1270 if (subscription.url in subscriptionsMap) 1461 browser.runtime.sendMessage({
1271 { 1462 type: "prefs.listen",
1272 let knownSubscription = subscriptionsMap[subscription.url]; 1463 filter: ["notifications_ignoredcategories", "notifications_showui",
1273 for (let property in subscription) 1464 "show_devtools_panel", "shouldShowBlockElementMenu",
1274 { 1465 "ui_warn_tracking"]
1275 if (property == "title" && knownSubscription.recommended) 1466 });
1276 knownSubscription.originalTitle = subscription.title; 1467 browser.runtime.sendMessage({
1277 else 1468 type: "subscriptions.listen",
1278 knownSubscription[property] = subscription[property]; 1469 filter: ["added", "disabled", "homepage", "lastDownload", "removed",
1279 } 1470 "title", "downloadStatus", "downloading"]
1280 subscription = knownSubscription; 1471 });
1281 } 1472
1282 switch (action) 1473 window.addEventListener("DOMContentLoaded", onDOMLoaded, false);
1283 { 1474 window.addEventListener("hashchange", onHashChange, false);
1284 case "disabled":
1285 updateSubscription(subscription);
1286 setPrivacyConflict();
1287 break;
1288 case "downloading":
1289 case "downloadStatus":
1290 case "homepage":
1291 case "lastDownload":
1292 case "title":
1293 updateSubscription(subscription);
1294 break;
1295 case "added":
1296 let {url} = subscription;
1297 // Handle custom subscription
1298 if (/^~user/.test(url))
1299 {
1300 loadCustomFilters(subscription.filters);
1301 return;
1302 }
1303 else if (url in subscriptionsMap)
1304 updateSubscription(subscription);
1305 else
1306 addSubscription(subscription);
1307
1308 if (isAcceptableAds(url))
1309 setAcceptableAds();
1310
1311 collections.filterLists.addItem(subscription);
1312 setPrivacyConflict();
1313 break;
1314 case "removed":
1315 if (subscription.recommended)
1316 {
1317 subscription.disabled = true;
1318 onSubscriptionMessage("disabled", subscription);
1319 }
1320 else
1321 {
1322 delete subscriptionsMap[subscription.url];
1323 if (isAcceptableAds(subscription.url))
1324 {
1325 setAcceptableAds();
1326 }
1327 else
1328 {
1329 collections.more.removeItem(subscription);
1330 }
1331 }
1332
1333 collections.filterLists.removeItem(subscription);
1334 setPrivacyConflict();
1335 break;
1336 }
1337 }
1338
1339 function hidePref(key, value)
1340 {
1341 let element = document.querySelector("[data-pref='" + key + "']");
1342 if (element)
1343 element.setAttribute("aria-hidden", value);
1344 }
1345
1346 function getPref(key, callback)
1347 {
1348 let checkPref = getPref.checks[key] || getPref.checkNone;
1349 checkPref((isActive) =>
1350 {
1351 if (!isActive)
1352 {
1353 hidePref(key, !isActive);
1354 return;
1355 }
1356
1357 browser.runtime.sendMessage({
1358 type: "prefs.get",
1359 key
1360 }, callback);
1361 });
1362 }
1363
1364 getPref.checkNone = function(callback)
1365 {
1366 callback(true);
1367 };
1368
1369 getPref.checks =
1370 {
1371 notifications_ignoredcategories(callback)
1372 {
1373 getPref("notifications_showui", callback);
1374 }
1375 };
1376
1377 function onPrefMessage(key, value, initial)
1378 {
1379 switch (key)
1380 {
1381 case "notifications_ignoredcategories":
1382 value = value.indexOf("*") == -1;
1383 break;
1384
1385 case "notifications_showui":
1386 hidePref("notifications_ignoredcategories", !value);
1387 break;
1388 case "ui_warn_tracking":
1389 setPrivacyConflict();
1390 break;
1391 }
1392
1393 let checkbox = document.querySelector(
1394 "[data-pref='" + key + "'] button[role='checkbox']"
1395 );
1396 if (checkbox)
1397 checkbox.setAttribute("aria-checked", value);
1398 }
1399
1400 function updateTooltips()
1401 {
1402 let anchors = document.querySelectorAll(":not(.tooltip) > [data-tooltip]");
1403 for (let anchor of anchors)
1404 {
1405 let id = anchor.getAttribute("data-tooltip");
1406
1407 let wrapper = document.createElement("div");
1408 wrapper.className = "icon tooltip";
1409 anchor.parentNode.replaceChild(wrapper, anchor);
1410 wrapper.appendChild(anchor);
1411
1412 let tooltip = document.createElement("div");
1413 tooltip.setAttribute("role", "tooltip");
1414
1415 let paragraph = document.createElement("p");
1416 paragraph.textContent = getMessage(id);
1417 tooltip.appendChild(paragraph);
1418
1419 wrapper.appendChild(tooltip);
1420 }
1421 }
1422
1423 ext.onMessage.addListener((message) =>
1424 {
1425 switch (message.type)
1426 {
1427 case "app.respond":
1428 switch (message.action)
1429 {
1430 case "addSubscription":
1431 let subscription = message.args[0];
1432 let dialog = E("dialog-content-predefined");
1433 dialog.querySelector("h3").textContent = subscription.title || "";
1434 dialog.querySelector(".url").textContent = subscription.url;
1435 openDialog("predefined");
1436 break;
1437 case "focusSection":
1438 document.body.setAttribute("data-tab", message.args[0]);
1439 break;
1440 }
1441 break;
1442 case "filters.respond":
1443 onFilterMessage(message.action, message.args[0]);
1444 break;
1445 case "prefs.respond":
1446 onPrefMessage(message.action, message.args[0], false);
1447 break;
1448 case "subscriptions.respond":
1449 onSubscriptionMessage(message.action, message.args[0]);
1450 break;
1451 }
1452 });
1453
1454 browser.runtime.sendMessage({
1455 type: "app.listen",
1456 filter: ["addSubscription", "focusSection"]
1457 });
1458 browser.runtime.sendMessage({
1459 type: "filters.listen",
1460 filter: ["added", "loaded", "removed"]
1461 });
1462 browser.runtime.sendMessage({
1463 type: "prefs.listen",
1464 filter: ["notifications_ignoredcategories", "notifications_showui",
1465 "show_devtools_panel", "shouldShowBlockElementMenu",
1466 "ui_warn_tracking"]
1467 });
1468 browser.runtime.sendMessage({
1469 type: "subscriptions.listen",
1470 filter: ["added", "disabled", "homepage", "lastDownload", "removed",
1471 "title", "downloadStatus", "downloading"]
1472 });
1473
1474 window.addEventListener("DOMContentLoaded", onDOMLoaded, false);
1475 window.addEventListener("hashchange", onHashChange, false);
1476 }
OLDNEW
« no previous file with comments | « README.md ('k') | package.json » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld