Left: | ||
Right: |
OLD | NEW |
---|---|
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-2015 Eyeo GmbH | 3 * Copyright (C) 2006-2015 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 "use strict"; | 18 "use strict"; |
19 | 19 |
20 (function() | 20 (function() |
21 { | 21 { |
22 var subscriptionsMap = Object.create(null); | 22 var subscriptionsMap = Object.create(null); |
23 var recommendationsMap = Object.create(null); | 23 var recommendationsMap = Object.create(null); |
24 var filtersMap = Object.create(null); | 24 var filtersMap = Object.create(null); |
25 var collections = Object.create(null); | 25 var collections = Object.create(null); |
26 var maxLabelId = 0; | |
26 | 27 |
27 function Collection(details) | 28 function Collection(details) |
28 { | 29 { |
29 this.details = details; | 30 this.details = details; |
30 this.items = []; | 31 this.items = []; |
31 } | 32 } |
32 | 33 |
33 Collection.prototype._setEmpty = function(table, text) | 34 Collection.prototype._setEmpty = function(table, text) |
34 { | 35 { |
35 var placeholder = table.querySelector(".empty-placeholder"); | 36 var placeholder = table.querySelector(".empty-placeholder"); |
(...skipping 21 matching lines...) Expand all Loading... | |
57 return aValue.localeCompare(bValue); | 58 return aValue.localeCompare(bValue); |
58 }); | 59 }); |
59 | 60 |
60 for (var j = 0; j < this.details.length; j++) | 61 for (var j = 0; j < this.details.length; j++) |
61 { | 62 { |
62 var table = E(this.details[j].id); | 63 var table = E(this.details[j].id); |
63 var template = table.querySelector("template"); | 64 var template = table.querySelector("template"); |
64 for (var i = 0; i < arguments.length; i++) | 65 for (var i = 0; i < arguments.length; i++) |
65 { | 66 { |
66 var item = arguments[i]; | 67 var item = arguments[i]; |
67 var text = item.title || item.url || item.text; | |
68 var listItem = document.createElement("li"); | 68 var listItem = document.createElement("li"); |
69 listItem.appendChild(document.importNode(template.content, true)); | 69 listItem.appendChild(document.importNode(template.content, true)); |
70 listItem.setAttribute("data-access", item.url || item.text); | 70 listItem.setAttribute("data-access", item.url || item.text); |
71 listItem.querySelector(".display").textContent = text; | |
72 if (text) | |
73 listItem.setAttribute("data-search", text.toLowerCase()); | |
74 | 71 |
72 var labelId = "label-" + (++maxLabelId); | |
73 listItem.querySelector(".display").setAttribute("id", labelId); | |
75 var control = listItem.querySelector(".control"); | 74 var control = listItem.querySelector(".control"); |
76 if (control) | 75 if (control) |
77 { | 76 { |
77 // We use aria-labelledby to avoid triggering the control when | |
78 // interacting with the label | |
79 control.setAttribute("aria-labelledby", labelId); | |
78 control.addEventListener("click", this.details[j].onClick, false); | 80 control.addEventListener("click", this.details[j].onClick, false); |
79 control.checked = item.disabled == false; | |
80 } | 81 } |
81 | 82 |
82 this._setEmpty(table, null); | 83 this._setEmpty(table, null); |
83 if (table.hasChildNodes()) | 84 if (table.hasChildNodes()) |
84 table.insertBefore(listItem, table.childNodes[this.items.indexOf(item) ]); | 85 table.insertBefore(listItem, table.childNodes[this.items.indexOf(item) ]); |
85 else | 86 else |
86 table.appendChild(listItem); | 87 table.appendChild(listItem); |
88 this.updateItem(item); | |
87 } | 89 } |
88 } | 90 } |
89 return length; | 91 return length; |
90 }; | 92 }; |
91 | 93 |
92 Collection.prototype.removeItem = function(item) | 94 Collection.prototype.removeItem = function(item) |
93 { | 95 { |
94 var index = this.items.indexOf(item); | 96 var index = this.items.indexOf(item); |
95 if (index == -1) | 97 if (index == -1) |
96 return; | 98 return; |
97 | 99 |
98 this.items.splice(index, 1); | 100 this.items.splice(index, 1); |
99 for (var i = 0; i < this.details.length; i++) | 101 for (var i = 0; i < this.details.length; i++) |
100 { | 102 { |
101 var table = E(this.details[i].id); | 103 var table = E(this.details[i].id); |
102 var element = table.childNodes[index]; | 104 var element = table.childNodes[index]; |
105 | |
106 // Element gets removed so make sure to handle focus appropriately | |
107 var control = element.querySelector(".control"); | |
108 if (control && control == document.activeElement) | |
109 { | |
110 if (!focusNextElement(element.parentElement, control)) | |
111 { | |
112 // Fall back to next focusable element within same tab or dialog | |
113 var tab = element.parentElement; | |
saroyanm
2016/01/25 14:45:43
Detail: This is not only tab already, so maybe mak
Thomas Greiner
2016/01/25 18:04:21
Done.
| |
114 while (tab) | |
saroyanm
2016/01/25 14:45:43
What if the dialog where the collection is impleme
Thomas Greiner
2016/01/25 18:04:21
The main challenge with focus management is that y
saroyanm
2016/01/26 18:40:12
Fare enough.
| |
115 { | |
116 if (tab.classList.contains("tab-content") | |
117 || tab.classList.contains("dialog-content")) | |
saroyanm
2016/01/25 14:45:43
I think we need to make it obvious whether it's so
Thomas Greiner
2016/01/25 18:04:21
We could put this into a separate function that's
| |
118 break; | |
119 | |
120 tab = tab.parentElement; | |
121 } | |
122 focusNextElement(tab || document, control); | |
123 } | |
124 } | |
125 | |
103 element.parentElement.removeChild(element); | 126 element.parentElement.removeChild(element); |
104 if (this.items.length == 0) | 127 if (this.items.length == 0) |
105 this._setEmpty(table, this.details[i].emptyText); | 128 this._setEmpty(table, this.details[i].emptyText); |
106 } | 129 } |
107 }; | 130 }; |
108 | 131 |
132 Collection.prototype.updateItem = function(item) | |
133 { | |
134 var access = (item.url || item.text).replace(/'/g, "\\'"); | |
135 for (var i = 0; i < this.details.length; i++) | |
136 { | |
137 var table = E(this.details[i].id); | |
138 var element = table.querySelector("[data-access='" + access + "']"); | |
139 if (!element) | |
140 continue; | |
141 | |
142 var text = item.title || item.url || item.text; | |
143 element.querySelector(".display").textContent = text; | |
144 if (text) | |
145 element.setAttribute("data-search", text.toLowerCase()); | |
146 var control = element.querySelector(".control[role='checkbox']"); | |
147 if (control) | |
148 control.setAttribute("aria-checked", item.disabled == false); | |
149 } | |
150 }; | |
151 | |
109 Collection.prototype.clearAll = function() | 152 Collection.prototype.clearAll = function() |
110 { | 153 { |
111 this.items = []; | 154 this.items = []; |
112 for (var i = 0; i < this.details.length; i++) | 155 for (var i = 0; i < this.details.length; i++) |
113 { | 156 { |
114 var table = E(this.details[i].id); | 157 var table = E(this.details[i].id); |
115 var template = table.querySelector("template"); | 158 var template = table.querySelector("template"); |
116 table.innerHTML = ""; | 159 table.innerHTML = ""; |
117 table.appendChild(template); | 160 table.appendChild(template); |
118 this._setEmpty(table, this.details[i].emptyText); | 161 this._setEmpty(table, this.details[i].emptyText); |
119 } | 162 } |
120 }; | 163 }; |
121 | 164 |
165 function focusNextElement(container, currentElement) | |
166 { | |
167 var focusables = container.querySelectorAll("a, button, input, .control"); | |
168 focusables = Array.prototype.slice.call(focusables); | |
169 var index = focusables.indexOf(currentElement); | |
170 index += (index == focusables.length - 1) ? -1 : 1; | |
171 | |
172 var nextElement = focusables[index]; | |
173 if (!nextElement) | |
174 return false; | |
175 | |
176 nextElement.focus(); | |
177 return true; | |
178 } | |
179 | |
122 function onToggleSubscriptionClick(e) | 180 function onToggleSubscriptionClick(e) |
123 { | 181 { |
124 e.preventDefault(); | 182 e.preventDefault(); |
125 var subscriptionUrl = e.target.parentNode.getAttribute("data-access"); | 183 var checkbox = e.target; |
126 if (!e.target.checked) | 184 var subscriptionUrl = checkbox.parentElement.getAttribute("data-access"); |
185 if (checkbox.getAttribute("aria-checked") == "true") | |
127 { | 186 { |
128 ext.backgroundPage.sendMessage({ | 187 ext.backgroundPage.sendMessage({ |
129 type: "subscriptions.remove", | 188 type: "subscriptions.remove", |
130 url: subscriptionUrl | 189 url: subscriptionUrl |
131 }); | 190 }); |
132 } | 191 } |
133 else | 192 else |
134 addEnableSubscription(subscriptionUrl); | 193 addEnableSubscription(subscriptionUrl); |
135 } | 194 } |
136 | 195 |
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
213 var subscriptionUrl = subscription.url; | 272 var subscriptionUrl = subscription.url; |
214 var knownSubscription = subscriptionsMap[subscriptionUrl]; | 273 var knownSubscription = subscriptionsMap[subscriptionUrl]; |
215 if (knownSubscription) | 274 if (knownSubscription) |
216 knownSubscription.disabled = subscription.disabled; | 275 knownSubscription.disabled = subscription.disabled; |
217 else | 276 else |
218 { | 277 { |
219 getAcceptableAdsURL(function(acceptableAdsUrl) | 278 getAcceptableAdsURL(function(acceptableAdsUrl) |
220 { | 279 { |
221 function onObjectChanged() | 280 function onObjectChanged() |
222 { | 281 { |
223 var access = (subscriptionUrl || subscription.text).replace(/'/g, "\\' "); | 282 for (var i in collections) |
224 var elements = document.querySelectorAll("[data-access='" + access + " ']"); | 283 collections[i].updateItem(subscription); |
225 for (var i = 0; i < elements.length; i++) | 284 |
285 var recommendation = recommendationsMap[subscriptionUrl]; | |
286 if (recommendation && recommendation.type == "ads") | |
226 { | 287 { |
227 var element = elements[i]; | 288 if (subscription.disabled == false) |
228 var control = element.querySelector(".control"); | |
229 if (control.localName == "input") | |
230 control.checked = subscription.disabled == false; | |
231 if (subscriptionUrl in recommendationsMap) | |
232 { | 289 { |
233 var recommendation = recommendationsMap[subscriptionUrl]; | 290 collections.allLangs.removeItem(subscription); |
234 if (recommendation.type == "ads") | 291 collections.langs.addItems(subscription); |
235 { | 292 } |
236 if (subscription.disabled == false) | 293 else |
237 { | 294 { |
238 collections.allLangs.removeItem(subscription); | 295 collections.allLangs.addItems(subscription); |
239 collections.langs.addItems(subscription); | 296 collections.langs.removeItem(subscription); |
240 } | |
241 else | |
242 { | |
243 collections.allLangs.addItems(subscription); | |
244 collections.langs.removeItem(subscription); | |
245 } | |
246 } | |
247 } | 297 } |
248 } | 298 } |
249 } | 299 } |
250 | 300 |
251 if (!Object.observe) | 301 if (!Object.observe) |
252 { | 302 { |
253 // Currently only "disabled" property of subscription used for observa tion | 303 // Currently only "disabled" property of subscription used for observa tion |
254 // but with Advanced tab implementation we should also add more proper ties. | 304 // but with Advanced tab implementation we should also add more proper ties. |
255 ["disabled"].forEach(function(property) | 305 ["disabled"].forEach(function(property) |
256 { | 306 { |
(...skipping 560 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
817 filter: ["added", "loaded", "removed"] | 867 filter: ["added", "loaded", "removed"] |
818 }); | 868 }); |
819 ext.backgroundPage.sendMessage( | 869 ext.backgroundPage.sendMessage( |
820 { | 870 { |
821 type: "subscriptions.listen", | 871 type: "subscriptions.listen", |
822 filter: ["added", "disabled", "homepage", "removed", "title"] | 872 filter: ["added", "disabled", "homepage", "removed", "title"] |
823 }); | 873 }); |
824 | 874 |
825 window.addEventListener("DOMContentLoaded", onDOMLoaded, false); | 875 window.addEventListener("DOMContentLoaded", onDOMLoaded, false); |
826 })(); | 876 })(); |
OLD | NEW |