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

Side by Side Diff: options.js

Issue 6088024630755328: issue 1526 - Implement new options page design for Chrome/Opera/Safari (Closed)
Patch Set: Created Feb. 26, 2015, 11:50 a.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 /*
2 * This file is part of Adblock Plus <https://adblockplus.org/>,
3 * Copyright (C) 2006-2015 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 "use strict";
19
20 (function()
21 {
22 var acceptableAdsUrl = null;
23 var subscriptionsMap = Object.create(null);
24 var recommendationsMap = Object.create(null);
25 var filtersMap = Object.create(null);
26 var collections = Object.create(null);
27
28 function Collection(details)
saroyanm 2015/02/26 12:18:33 We can rethink about the name of current Class in
Thomas Greiner 2015/03/05 11:36:03 I guess "Collection" should be fine (see Java's Co
Thomas Greiner 2015/03/05 11:36:03 Why "details"? It's an array of items and not a co
saroyanm 2015/03/06 11:54:32 I used details as I thought that it's corresponds
Thomas Greiner 2015/03/12 11:41:56 Makes sense, let's stick with "details" then.
29 {
30 this.details = details;
31 }
32
33 Collection.prototype = Object.create(Array.prototype);
34 Collection.prototype.addItems = function()
Thomas Greiner 2015/03/05 11:36:03 I couldn't find a case where you add multiple item
saroyanm 2015/03/06 11:54:32 I actually were thinking that this could be useful
Thomas Greiner 2015/03/12 11:41:56 Ok, then let's leave it like it is for now (no nee
saroyanm 2015/03/12 14:03:22 Done.
35 {
36 var length = Array.prototype.push.apply(this, arguments);
37 if (length == 0)
38 return;
39
40 this.sort(function(a, b)
41 {
42 var aValue = (a.title || a.url || a.text).toLowerCase();
43 var bValue = (b.title || b.url || a.text).toLowerCase();
44 if (aValue < bValue)
45 return -1;
46 if (aValue > bValue)
47 return 1;
48 return 0;
49 });
50
51 for (var j = 0; j < this.details.length; j++)
52 {
53 var table = E(this.details[j].id);
54 var template = table.querySelector("template");
55 var listener = this.details[j].listener;
56 for (var i = 0; i < arguments.length; i++)
57 {
58 var object = arguments[i];
59 var text = object.title || object.text;
Thomas Greiner 2015/03/05 11:36:03 A subscription might not have a title. In those ca
60 var access = object.url || object.text;
Thomas Greiner 2015/03/05 11:36:03 You're only using this variable once so no need to
saroyanm 2015/03/06 11:54:32 Done, but now I'm using access, to shorten the lin
Thomas Greiner 2015/03/12 11:41:56 That'd be ok but "index" is only used in that one
saroyanm 2015/03/12 14:03:22 This is already fixed in current patch: http://cod
61 var index = this.indexOf(object);
62 var listItem = document.createElement("li");
63 listItem.appendChild(document.importNode(template.content, true));
64 listItem.dataset.access = access;
Thomas Greiner 2015/03/05 11:36:03 You need to normalize this string or otherwise `El
saroyanm 2015/03/06 11:54:32 while current query throw error: document.querySel
Thomas Greiner 2015/03/12 11:41:56 Should be fine. Alternatively, you could use `enc
65 listItem.querySelector(".display").textContent = text;
66 if (text)
67 listItem.dataset.search = text.toLowerCase();
68
69 var control = listItem.querySelector(".control");
70 if (control)
71 {
72 control.addEventListener("click", listener, false);
Thomas Greiner 2015/03/05 11:36:03 Just FYI: I'd still prefer a single global event l
73 control.checked = object.disabled == false;
74 }
75
76 var popular = listItem.querySelector(".popular");
77 if (popular)
78 popular.textContent = ext.i18n.getMessage("options_popular");
Thomas Greiner 2015/03/05 11:36:03 Setting that text right inside the template tag on
saroyanm 2015/03/06 11:54:32 Done.
79
80 if (table.hasChildNodes)
81 table.insertBefore(listItem, table.childNodes[index]);
82 else
83 table.appendChild(listItem);
84 }
85 }
86 return length;
87 };
88
89 Collection.prototype.removeItem = function(obj)
Thomas Greiner 2015/03/05 11:36:03 The function is called "removeItem" but the item i
saroyanm 2015/03/06 11:54:32 Done.
90 {
91 var index = this.indexOf(obj);
92 if (index == -1)
93 return;
94
95 this.splice(index, 1);
96 var access = obj.url || obj.text;
97 for (var i = 0; i < this.details.length; i++)
98 {
99 var table = E(this.details[i].id);
100 var element = table.querySelector("[data-access='"+access+"']");
101 element.parentElement.removeChild(element);
102 }
103 };
104
105 function toggleSubscription(e)
106 {
107 e.preventDefault();
108 var isChecked = e.target.checked;
Thomas Greiner 2015/03/05 11:36:03 Again, this variable is only used once.
saroyanm 2015/03/06 11:54:32 Done.
109 var subscriptionUrl = e.target.parentNode.dataset.access;
110 if (!isChecked)
111 removeSubscription(subscriptionUrl);
112 else
113 addEnableSubscription(subscriptionUrl);
114 }
115
116 function addLanguageSubscription(e)
117 {
118 e.preventDefault();
119 var url = this.parentNode.dataset.access;
120 addEnableSubscription(url);
121 }
122
123 function triggerRemoveFilter()
124 {
125 var filter = this.parentNode.dataset.access;
126 removeFilter(filter);
127 }
128
129 collections.popular = new Collection(
130 [
131 {
132 id: "recommend-list-table",
133 listener: toggleSubscription
Thomas Greiner 2015/03/05 11:36:03 I'd rename "listener" to "onClick". Otherwise it's
saroyanm 2015/03/06 11:54:32 Done.
134 }
135 ]);
136 collections.langs = new Collection(
137 [
138 {
139 id: "blocking-languages-table",
140 listener: toggleSubscription
141 },
142 {
143 id: "blocking-languages-modal-table"
144 }
145 ]);
146 collections.allLangs = new Collection(
147 [
148 {
149 id: "all-lang-table",
150 listener: addLanguageSubscription
151 }
152 ]);
153 collections.acceptableAds = new Collection(
154 [
155 {
156 id: "acceptableads-table",
157 listener: toggleSubscription
158 }
159 ]);
160 collections.custom = new Collection(
161 [
162 {
163 id: "custom-list-table",
164 listener: toggleSubscription
165 }
166 ]);
167 collections.whitelist = new Collection(
168 [
169 {
170 id: "whitelisting-table",
171 listener: triggerRemoveFilter
172 }
173 ]);
174
175 function updateSubscription(subscription)
176 {
177 var subscriptionUrl = subscription.url;
178 var knownSubscription = getKnownSubscription(subscriptionUrl);
179 if (knownSubscription)
180 knownSubscription.disabled = subscription.disabled;
181 else
182 {
183 getAcceptableAdsURL(function(acceptableAdsUrl)
184 {
185 function onObjectChanged()
186 {
187 var access = subscriptionUrl || subscription.text;
188 var elements = document.querySelectorAll("[data-access='" + access + " ']");
189 for (var i = 0; i < elements.length; i++)
190 {
191 var element = elements[i];
192 var control = element.getElementsByClassName("control")[0];
Thomas Greiner 2015/03/05 11:36:03 Usually, you use `element.querySelector(".control"
saroyanm 2015/03/06 11:54:32 Done.
193 if (control.localName == "input")
194 control.checked = subscription.disabled == false;
195 if (isRecommendation(subscriptionUrl))
196 {
197 var recommendation = getRecommendation(subscriptionUrl);
198 if (recommendation.isAdsType && subscription.disabled == false)
199 {
200 collections.allLangs.removeItem(subscription);
201 collections.langs.addItems(subscription);
202 }
203 else
204 {
205 collections.allLangs.addItems(subscription);
206 collections.langs.removeItem(subscription);
207 }
208 }
209 }
210 }
211
212 if (!Object.observe)
213 {
214 ["disabled"].forEach(function(property)
215 {
216 subscription["$"+property] = subscription[property];
Thomas Greiner 2015/03/05 11:36:03 By now I'd expect you to know that operators, like
saroyanm 2015/03/06 11:54:32 Done.
217 Object.defineProperty(subscription, property,
218 {
219 get: function()
220 {
221 return this["$"+property];
222 },
223 set: function(value)
224 {
225 this["$"+property] = value;
226 onObjectChanged();
227 }
228 });
229 });
230 }
231 else
232 {
233 Object.observe(subscription, function(changes)
234 {
235 onObjectChanged();
236 });
237 }
238
239 var collection = null;
240 if (isRecommendation(subscriptionUrl))
241 {
242 var recommendation = getRecommendation(subscriptionUrl);
243 if (recommendation.isPopular)
244 collection = collections.popular;
245 else if (recommendation.isAdsType && subscription.disabled == false)
246 collection = collections.langs;
247 else
248 collection = collections.allLangs;
249 }
250 else if (subscriptionUrl == acceptableAdsUrl)
251 collection = collections.acceptableAds;
252 else
253 collection = collections.custom;
254
255 collection.addItems(subscription);
256 setKnownSubscription(subscriptionUrl, subscription);
257 });
258 }
259 }
260
261 function updateFilter(filter)
262 {
263 var knownFilter = getKnownFilter(filter.text);
264 var match = filter.text.match(/^@@\|\|([^\/:]+)\^\$document$/);
265 if (match && !knownFilter)
266 {
267 filter.title = match[1];
268 collections.whitelist.addItems(filter);
269 setKnownFilter(filter.text, filter);
270 }
271 else
272 {
273 // TODO: add `filters[i].text` to list of custom filters
274 }
275 }
276
277 function loadRecommendations()
278 {
279 var request = new XMLHttpRequest();
280 request.open("GET", "subscriptions.xml", false);
281 request.onload = function()
282 {
283 var list = document.getElementById("subscriptionSelector");
284 var docElem = request.responseXML.documentElement;
285 var elements = docElem.getElementsByTagName("subscription");
286 for (var i = 0; i < elements.length; i++)
287 {
288 var element = elements[i];
289 var subscription = Object.create(null);
290 subscription.title = element.getAttribute("title");
291 subscription.url = element.getAttribute("url");
292 subscription.disabled = null;
293 subscription.downloadStatus = null;
294 subscription.homepage = null;
295 subscription.lastSuccess = null;
296 var recommendation = Object.create(null);
297 recommendation.isAdsType = false;
298 recommendation.isPopular = false;
299 var prefix = element.getAttribute("prefixes");
300 if (prefix)
301 {
302 var prefix = element.getAttribute("prefixes").replace(/,/g, "_");
303 subscription.title = ext.i18n.getMessage("options_language_" + prefix) ;
304 recommendation.isAdsType = true;
305 }
306 else
307 subscription.title = element.getAttribute("specialization");
308
309 if (element.getAttribute("popular"))
310 recommendation.isPopular = true;
311
312 setRecommendation(subscription.url, recommendation);
313 updateSubscription(subscription);
314 }
315 };
316 request.send(null);
317 }
318
319 function onDOMLoaded()
320 {
321 updateShareLink();
322 populateLists();
323
324 var tabList = document.querySelectorAll("#main-navigation-tabs li");
325 for (var i = 0; i < tabList.length; i++)
326 {
327 tabList[i].addEventListener("click", function(e)
328 {
329 document.body.dataset.tab = e.currentTarget.dataset.show;
330 }, false);
331 }
332
333 function searchLanguage()
334 {
335 var searchStyle = E("search-style");
336 if (!this.value)
337 searchStyle.innerHTML = "";
338 else
339 searchStyle.innerHTML = "#all-lang-table li:not([data-search*=\"" + this .value.toLowerCase() + "\"]) { display: none; }";
340 }
341
342 // Update version number in navigation sidebar
343 ext.backgroundPage.sendMessage(
344 {
345 method: "app.get",
346 what: "addonVersion"
347 },
348 function(addonVersion)
349 {
350 E("abp-version").textContent = addonVersion;
351 });
352
353 var placeholderValue = ext.i18n.getMessage("options_modal_language_find");
354 E("find-language").setAttribute("placeholder", placeholderValue);
355 E("add-blocking-list").addEventListener("click", function()
356 {
357 openModal("customlist");
358 }, false);
359 E("add-website-language").addEventListener("click", function()
360 {
361 openModal("language");
362 }, false);
363 E("modal-close").addEventListener("click", function()
364 {
365 delete document.body.dataset.modal;
366 }, false);
367 E("edit-ownBlockingList-btn").addEventListener("click", editCustomFilters, f alse);
368 E("find-language").addEventListener("keyup", searchLanguage, false);
369 E("find-language").addEventListener("search", searchLanguage, false);
Thomas Greiner 2015/03/05 11:36:03 Turns out that the "onsearch" event listener is We
saroyanm 2015/03/06 11:54:32 I'm using this event to listen for "X" button clic
Thomas Greiner 2015/03/12 11:41:56 Since that "X" is also not in the standard I'd sug
saroyanm 2015/03/12 14:03:22 Done.
370 E("whitelisting-add-icon").addEventListener("click", addWhitelistedDomain, f alse);
371 E("whitelisting-add-btn").addEventListener("click", addWhitelistedDomain, fa lse);
372 E("whitelisting-enter-icon").addEventListener("click", addWhitelistedDomain, false);
373 E("whitelisting-textbox").addEventListener("keypress", function(e)
374 {
375 // keyCode value of 13 corresponds to "Enter" key
Thomas Greiner 2015/03/05 11:36:03 Please also explain why you need to check for both
saroyanm 2015/03/06 11:54:32 Done.
376 if (e.key && e.key == "Enter")
377 addWhitelistedDomain();
378 else if (!e.key && e.keyCode == 13)
379 addWhitelistedDomain();
380 }, false);
381 E("whitelisting-cancel-btn").addEventListener("click", function()
382 {
383 E("whitelisting-textbox").value = "";
384 }, false);
385 E("import-blockingList-btn").addEventListener("click", function()
386 {
387 var url = E("blockingList-textbox").value;
388 addEnableSubscription(url);
389 delete document.body.dataset.modal;
390 }, false);
391 }
392
393 function openModal(name)
394 {
395 document.body.dataset.modal = name;
396 }
397
398 function populateLists()
399 {
400 subscriptionsMap = Object.create(null);
401 filtersMap = Object.create(null);
402 recommendationsMap = Object.create(null);
403
404 // Empty collections and lists
405 for (var property in collections)
406 {
407 var collection = collections[property];
408 collection.details.forEach(function(detail)
Thomas Greiner 2015/03/05 11:36:03 This property does not need to be exposed. It'd be
saroyanm 2015/03/06 11:54:32 Done.
409 {
410 var table = E(detail.id);
411 var template = table.querySelector("template");
412 table.innerHTML = "";
413 table.appendChild(template);
414 });
415 collection.length = 0;
416 }
417
418 ext.backgroundPage.sendMessage(
419 {
420 type: "subscriptions.get",
421 special: true
422 },
423 function(subscriptions)
424 {
425 // Load filters
426 for (var i = 0; i < subscriptions.length; i++)
427 {
428 ext.backgroundPage.sendMessage(
429 {
430 type: "filters.get",
431 subscriptionUrl: subscriptions[i].url
432 },
433 function(filters)
434 {
435 for (var i = 0; i < filters.length; i++)
436 updateFilter(filters[i]);
437 });
438 }
439 });
440 loadRecommendations();
441 getAcceptableAdsURL(function(acceptableAdsUrl)
442 {
443 var subscription = Object.create(null);
444 subscription.url = acceptableAdsUrl;
445 subscription.disabled = true;
446 subscription.title = ext.i18n.getMessage("options_acceptableAds_descriptio n");
447 updateSubscription(subscription);
448
449 // Load user subscriptions
450 ext.backgroundPage.sendMessage(
451 {
452 type: "subscriptions.get",
453 downloadable: true
454 },
455 function(subscriptions)
456 {
457 for (var i = 0; i < subscriptions.length; i++)
458 onSubscriptionMessage("added", subscriptions[i]);
459 });
460 });
461 }
462
463 function addWhitelistedDomain()
464 {
465 var domain = E("whitelisting-textbox");
466 if (domain.value)
467 {
468 ext.backgroundPage.sendMessage(
469 {
470 type: "filters.add",
471 text: "@@||" + domain.value.toLowerCase() + "^$document"
472 });
473 }
474
475 domain.value = "";
476 }
477
478 function editCustomFilters()
479 {
480
481 }
482
483 function getAcceptableAdsURL(callback)
484 {
485 if (acceptableAdsUrl)
Thomas Greiner 2015/03/05 11:36:03 This if-statement is not necessary and neither is
saroyanm 2015/03/06 11:54:32 Hmm :/ Totally missed your implementation, good po
486 callback(acceptableAdsUrl);
487 else
488 {
489 ext.backgroundPage.sendMessage(
490 {
491 type: "prefs.get",
492 key: "subscriptions_exceptionsurl"
493 },
494 function(value)
495 {
496 getAcceptableAdsURL = function(callback)
497 {
498 acceptableAdsUrl = value;
499 callback(value);
500 }
501 getAcceptableAdsURL(callback);
502 });
503 }
504 }
505
506 function getKnownSubscription(url)
Thomas Greiner 2015/03/05 11:36:03 The next few functions are all one-liners. There's
saroyanm 2015/03/06 11:54:32 Done.
507 {
508 return subscriptionsMap[url];
509 }
510
511 function setKnownSubscription(url, subscription)
512 {
513 subscriptionsMap[url] = subscription;
514 }
515
516 function getKnownFilter(text)
517 {
518 return filtersMap[text];
519 }
520
521 function setKnownFilter(text, filter)
522 {
523 filtersMap[text] = filter;
524 }
525
526 function getRecommendation(url)
527 {
528 return recommendationsMap[url];
529 }
530
531 function setRecommendation(url, recommendation)
532 {
533 recommendationsMap[url] = recommendation;
534 }
535
536 function isRecommendation(url)
537 {
538 return url in recommendationsMap;
539 }
540
541 function addEnableSubscription(url, title, homepage)
542 {
543 var messageType = null;
544 var knownSubscription = getKnownSubscription(url);
545 if (knownSubscription && knownSubscription.disabled == true)
546 messageType = "subscriptions.toggle"
547 else
548 messageType = "subscriptions.add"
549
550 var message = {
551 type: messageType,
552 url: url
553 };
554 if (title)
555 message.title = title;
556 if (homepage)
557 message.homepage = homepage;
558
559 ext.backgroundPage.sendMessage(message);
560 }
561
562 function removeSubscription(url)
563 {
564 ext.backgroundPage.sendMessage(
565 {
566 type: "subscriptions.remove",
567 url: url
568 });
569 }
570
571 function removeFilter(filter)
572 {
573 ext.backgroundPage.sendMessage(
574 {
575 type: "filters.remove",
576 text: filter
577 });
578 }
579
580 function onFilterMessage(action, filter)
581 {
582 switch (action)
583 {
584 case "added":
585 updateFilter(filter);
586 updateShareLink();
587 break;
588 case "loaded":
589 populateLists();
590 break;
591 case "removed":
592 var knownFilter = getKnownFilter(filter.text);
593 collections.whitelist.removeItem(knownFilter);
594 updateShareLink();
595 break;
596 }
597 }
598
599 function onSubscriptionMessage(action, subscription)
600 {
601 switch (action)
602 {
603 case "added":
604 updateSubscription(subscription);
605 updateShareLink();
606 break;
607 case "disabled":
608 updateSubscription(subscription);
609 updateShareLink();
610 break;
611 case "homepage":
612 // TODO: NYI
613 break;
614 case "removed":
615 getAcceptableAdsURL(function(acceptableAdsUrl)
616 {
617 if (subscription.url == acceptableAdsUrl)
618 {
619 subscription.disabled = true;
620 updateSubscription(subscription);
621 }
622 else
623 {
624 var knownSubscription = getKnownSubscription(subscription.url);
625 var subscriptionUrl = subscription.url;
Thomas Greiner 2015/03/05 11:36:03 This variable is not necessary. It's only used onc
saroyanm 2015/03/06 11:54:32 Done.
626 if (isRecommendation(subscriptionUrl))
627 knownSubscription.disabled = true;
628 else
629 collections.custom.removeItem(knownSubscription);
630 }
631 updateShareLink();
632 });
633 break;
634 case "title":
635 // TODO: NYI
636 break;
637 }
638 }
639
640 function showAddSubscriptionDialog(subscription)
641 {
642 E("blockingList-textbox").value = subscription.url;
643 openModal("customlist");
644 }
645
646 function updateShareLink()
647 {
648 ext.backgroundPage.sendMessage(
649 {
650 type: "filters.blocked",
651 url: "https://platform.twitter.com/widgets/",
652 requestType: "SCRIPT",
653 docDomain: "adblockplus.org",
654 thirdParty: true
655 },
656 function(blocked)
657 {
658 // TODO: modify "share" link accordingly
659 });
660 }
661
662 function E(id)
663 {
664 return document.getElementById(id);
665 }
666
667 ext.onMessage.addListener(function(message)
668 {
669 switch (message.type)
670 {
671 case "app.listen":
672 if (message.action == "addSubscription")
673 showAddSubscriptionDialog(message.args[0]);
674 break;
675 case "filters.listen":
676 onFilterMessage(message.action, message.args[0]);
677 break;
678 case "subscriptions.listen":
679 onSubscriptionMessage(message.action, message.args[0]);
680 break;
681 }
682 });
683
684 ext.backgroundPage.sendMessage(
685 {
686 type: "app.listen",
687 filter: ["addSubscription"]
688 });
689 ext.backgroundPage.sendMessage(
690 {
691 type: "filters.listen",
692 filter: ["added", "loaded", "removed"]
693 });
694 ext.backgroundPage.sendMessage(
695 {
696 type: "subscriptions.listen",
697 filter: ["added", "disabled", "homepage", "removed", "title"]
698 });
699
700 window.addEventListener("DOMContentLoaded", onDOMLoaded, false);
701 })();
OLDNEW

Powered by Google App Engine
This is Rietveld