LEFT | RIGHT |
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-2016 Eyeo GmbH | 3 * Copyright (C) 2006-2016 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 // Click-to-hide stuff | 18 "use strict"; |
19 var clickHide_activated = false; | 19 |
20 var clickHide_filters = null; | 20 // The page ID for the popup filter selection dialog (top frame only). |
21 var currentElement = null; | 21 let blockelementPopupId = null; |
22 var highlightedElementsSelector = null; | 22 |
23 var highlightedElementsInterval = null; | 23 // Element picking state (top frame only). |
24 var clickHideFiltersDialog = null; | 24 let currentlyPickingElement = false; |
25 var lastRightClickEvent = null; | 25 let lastMouseOverEvent = null; |
26 var lastRightClickEventValid = false; | 26 |
27 var lastMouseOverEvent = null; | 27 // During element picking this is the currently highlighted element. When |
| 28 // element has been picked this is the element that is due to be blocked. |
| 29 let currentElement = null; |
| 30 |
| 31 // Highlighting state, used by the top frame during element picking and all |
| 32 // frames when the chosen element is highlighted red. |
| 33 let highlightedElementsSelector = null; |
| 34 let highlightedElementsInterval = null; |
| 35 |
| 36 // Last right click state stored for element blocking via the context menu. |
| 37 let lastRightClickEvent = null; |
| 38 let lastRightClickEventIsMostRecent = false; |
| 39 |
| 40 |
| 41 /* Utilities */ |
| 42 |
| 43 function getFiltersForElement(element, callback) |
| 44 { |
| 45 ext.backgroundPage.sendMessage( |
| 46 { |
| 47 type: "compose-filters", |
| 48 tagName: element.localName, |
| 49 id: element.id, |
| 50 src: element.getAttribute("src"), |
| 51 style: element.getAttribute("style"), |
| 52 classes: Array.prototype.slice.call(element.classList), |
| 53 urls: getURLsFromElement(element), |
| 54 mediatype: typeMap[element.localName], |
| 55 baseURL: document.location.href |
| 56 }, |
| 57 response => |
| 58 { |
| 59 callback(response.filters, response.selectors); |
| 60 }); |
| 61 } |
| 62 |
| 63 function getBlockableElementOrAncestor(element, callback) |
| 64 { |
| 65 // We assume that the user doesn't want to block the whole page. |
| 66 // So we never consider the <html> or <body> element. |
| 67 while (element && element != document.documentElement && |
| 68 element != document.body) |
| 69 { |
| 70 // We can't handle non-HTML (like SVG) elements, as well as |
| 71 // <area> elements (see below). So fall back to the parent element. |
| 72 if (!(element instanceof HTMLElement) || element.localName == "area") |
| 73 element = element.parentElement; |
| 74 |
| 75 // If image maps are used mouse events occur for the <area> element. |
| 76 // But we have to block the image associated with the <map> element. |
| 77 else if (element.localName == "map") |
| 78 { |
| 79 let images = document.querySelectorAll("img[usemap]"); |
| 80 let image = null; |
| 81 |
| 82 for (let i = 0; i < images.length; i++) |
| 83 { |
| 84 let usemap = images[i].getAttribute("usemap"); |
| 85 let index = usemap.indexOf("#"); |
| 86 |
| 87 if (index != -1 && usemap.substr(index + 1) == element.name) |
| 88 { |
| 89 image = images[i]; |
| 90 break; |
| 91 } |
| 92 } |
| 93 |
| 94 element = image; |
| 95 } |
| 96 |
| 97 // Finally, if none of the above is true, check whether we can generate |
| 98 // any filters for this element. Otherwise fall back to its parent element. |
| 99 else |
| 100 { |
| 101 getFiltersForElement(element, filters => |
| 102 { |
| 103 if (filters.length > 0) |
| 104 callback(element); |
| 105 else |
| 106 getBlockableElementOrAncestor(element.parentElement, callback); |
| 107 }); |
| 108 |
| 109 return; |
| 110 } |
| 111 } |
| 112 |
| 113 // We reached the document root without finding a blockable element. |
| 114 callback(null); |
| 115 } |
| 116 |
| 117 |
| 118 /* Element highlighting */ |
| 119 |
| 120 // Adds an overlay to an element, which is probably a Flash object. |
| 121 function addElementOverlay(element) |
| 122 { |
| 123 let position = "absolute"; |
| 124 let offsetX = window.scrollX; |
| 125 let offsetY = window.scrollY; |
| 126 |
| 127 for (let e = element; e; e = e.parentElement) |
| 128 { |
| 129 let style = getComputedStyle(e); |
| 130 |
| 131 // If the element isn't rendered (since its or one of its ancestor's |
| 132 // "display" property is "none"), the overlay wouldn't match the element. |
| 133 if (style.display == "none") |
| 134 return null; |
| 135 |
| 136 // If the element or one of its ancestors uses fixed postioning, the overlay |
| 137 // must too. Otherwise its position might not match the element's. |
| 138 if (style.position == "fixed") |
| 139 { |
| 140 position = "fixed"; |
| 141 offsetX = offsetY = 0; |
| 142 } |
| 143 } |
| 144 |
| 145 let overlay = document.createElement("div"); |
| 146 overlay.prisoner = element; |
| 147 overlay.className = "__adblockplus__overlay"; |
| 148 overlay.setAttribute("style", "opacity:0.4; display:inline-box; " + |
| 149 "overflow:hidden; box-sizing:border-box;"); |
| 150 let rect = element.getBoundingClientRect(); |
| 151 overlay.style.width = rect.width + "px"; |
| 152 overlay.style.height = rect.height + "px"; |
| 153 overlay.style.left = (rect.left + offsetX) + "px"; |
| 154 overlay.style.top = (rect.top + offsetY) + "px"; |
| 155 overlay.style.position = position; |
| 156 overlay.style.zIndex = 0x7FFFFFFE; |
| 157 |
| 158 document.documentElement.appendChild(overlay); |
| 159 return overlay; |
| 160 } |
28 | 161 |
29 function highlightElement(element, shadowColor, backgroundColor) | 162 function highlightElement(element, shadowColor, backgroundColor) |
30 { | 163 { |
31 unhighlightElement(element); | 164 unhighlightElement(element); |
32 | 165 |
33 var highlightWithOverlay = function() | 166 let highlightWithOverlay = function() |
34 { | 167 { |
35 var overlay = addElementOverlay(element); | 168 let overlay = addElementOverlay(element); |
36 | 169 |
37 // If the element isn't displayed no overlay will be added. | 170 // If the element isn't displayed no overlay will be added. |
38 // Moreover, we don't need to highlight anything then. | 171 // Moreover, we don't need to highlight anything then. |
39 if (!overlay) | 172 if (!overlay) |
40 return; | 173 return; |
41 | 174 |
42 highlightElement(overlay, shadowColor, backgroundColor); | 175 highlightElement(overlay, shadowColor, backgroundColor); |
43 overlay.style.pointerEvents = "none"; | 176 overlay.style.pointerEvents = "none"; |
44 | 177 |
45 element._unhighlight = function() | 178 element._unhighlight = () => |
46 { | 179 { |
47 overlay.parentNode.removeChild(overlay); | 180 overlay.parentNode.removeChild(overlay); |
48 }; | 181 }; |
49 }; | 182 }; |
50 | 183 |
51 var highlightWithStyleAttribute = function() | 184 let highlightWithStyleAttribute = function() |
52 { | 185 { |
53 var originalBoxShadow = element.style.getPropertyValue("box-shadow"); | 186 let originalBoxShadow = element.style.getPropertyValue("box-shadow"); |
54 var originalBoxShadowPriority = element.style.getPropertyPriority("box-shado
w"); | 187 let originalBoxShadowPriority = |
55 var originalBackgroundColor = element.style.getPropertyValue("background-col
or"); | 188 element.style.getPropertyPriority("box-shadow"); |
56 var originalBackgroundColorPriority = element.style.getPropertyPriority("bac
kground-color"); | 189 let originalBackgroundColor = |
57 | 190 element.style.getPropertyValue("background-color"); |
58 element.style.setProperty("box-shadow", "inset 0px 0px 5px " + shadowColor,
"important"); | 191 let originalBackgroundColorPriority = |
| 192 element.style.getPropertyPriority("background-color"); |
| 193 |
| 194 element.style.setProperty("box-shadow", "inset 0px 0px 5px " + shadowColor, |
| 195 "important"); |
59 element.style.setProperty("background-color", backgroundColor, "important"); | 196 element.style.setProperty("background-color", backgroundColor, "important"); |
60 | 197 |
61 element._unhighlight = function() | 198 element._unhighlight = () => |
62 { | 199 { |
63 this.style.removeProperty("box-shadow"); | 200 element.style.removeProperty("box-shadow"); |
64 this.style.setProperty( | 201 element.style.setProperty( |
65 "box-shadow", | 202 "box-shadow", |
66 originalBoxShadow, | 203 originalBoxShadow, |
67 originalBoxShadowPriority | 204 originalBoxShadowPriority |
68 ); | 205 ); |
69 | 206 |
70 this.style.removeProperty("background-color"); | 207 element.style.removeProperty("background-color"); |
71 this.style.setProperty( | 208 element.style.setProperty( |
72 "background-color", | 209 "background-color", |
73 originalBackgroundColor, | 210 originalBackgroundColor, |
74 originalBackgroundColorPriority | 211 originalBackgroundColorPriority |
75 ); | 212 ); |
76 }; | 213 }; |
77 }; | 214 }; |
78 | 215 |
79 if ("prisoner" in element) | 216 if ("prisoner" in element) |
80 highlightWithStyleAttribute(); | 217 highlightWithStyleAttribute(); |
81 else | 218 else |
82 highlightWithOverlay(); | 219 highlightWithOverlay(); |
83 } | 220 } |
84 | 221 |
85 | |
86 function unhighlightElement(element) | 222 function unhighlightElement(element) |
87 { | 223 { |
88 if ("_unhighlight" in element) | 224 if (element && "_unhighlight" in element) |
89 { | 225 { |
90 element._unhighlight(); | 226 element._unhighlight(); |
91 delete element._unhighlight; | 227 delete element._unhighlight; |
92 } | 228 } |
93 } | 229 } |
94 | 230 |
95 // Highlight elements according to selector string. This would include | 231 // Highlight elements matching the selector string red. |
96 // all elements that would be affected by proposed filters. | 232 // (All elements that would be blocked by the proposed filters.) |
97 function highlightElements(selectorString) { | 233 function highlightElements(selectorString) |
| 234 { |
98 unhighlightElements(); | 235 unhighlightElements(); |
99 | 236 |
100 var elements = Array.prototype.slice.call(document.querySelectorAll(selectorSt
ring)); | 237 let elements = Array.prototype.slice.call( |
| 238 document.querySelectorAll(selectorString) |
| 239 ); |
101 highlightedElementsSelector = selectorString; | 240 highlightedElementsSelector = selectorString; |
102 | 241 |
103 // Highlight elements progressively. Otherwise the page freezes | 242 // Highlight elements progressively. Otherwise the page freezes |
104 // when a lot of elements get highlighted at the same time. | 243 // when a lot of elements get highlighted at the same time. |
105 highlightedElementsInterval = setInterval(function() | 244 highlightedElementsInterval = setInterval(() => |
106 { | 245 { |
107 if (elements.length > 0) | 246 if (elements.length > 0) |
108 { | 247 { |
109 var element = elements.shift(); | 248 let element = elements.shift(); |
110 if (element != currentElement) | 249 if (element != currentElement) |
111 highlightElement(element, "#fd6738", "#f6e1e5"); | 250 highlightElement(element, "#fd6738", "#f6e1e5"); |
112 } | 251 } |
113 else | 252 else |
114 { | 253 { |
115 clearInterval(highlightedElementsInterval); | 254 clearInterval(highlightedElementsInterval); |
116 highlightedElementsInterval = null; | 255 highlightedElementsInterval = null; |
117 } | 256 } |
118 }, 0); | 257 }, 0); |
119 } | 258 } |
120 | 259 |
121 // Unhighlight all elements, including those that would be affected by | 260 // Unhighlight the elements that were highlighted by selector string previously. |
122 // the proposed filters | 261 function unhighlightElements() |
123 function unhighlightElements() { | 262 { |
124 if (highlightedElementsInterval) | 263 if (highlightedElementsInterval) |
125 { | 264 { |
126 clearInterval(highlightedElementsInterval) | 265 clearInterval(highlightedElementsInterval); |
127 highlightedElementsInterval = null; | 266 highlightedElementsInterval = null; |
128 } | 267 } |
129 | 268 |
130 if (highlightedElementsSelector) | 269 if (highlightedElementsSelector) |
131 { | 270 { |
132 Array.prototype.forEach.call( | 271 Array.prototype.forEach.call( |
133 document.querySelectorAll(highlightedElementsSelector), | 272 document.querySelectorAll(highlightedElementsSelector), |
134 unhighlightElement | 273 unhighlightElement |
135 ); | 274 ); |
136 | 275 |
137 highlightedElementsSelector = null; | 276 highlightedElementsSelector = null; |
138 } | 277 } |
139 } | 278 } |
140 | 279 |
141 // Adds an overlay to an element, which is probably a Flash object | 280 |
142 function addElementOverlay(elt) { | 281 /* Input event handlers */ |
143 var position = "absolute"; | 282 |
144 var offsetX = window.scrollX; | 283 function stopEventPropagation(event) |
145 var offsetY = window.scrollY; | 284 { |
146 | 285 event.stopPropagation(); |
147 for (var e = elt; e; e = e.parentElement) | 286 } |
148 { | 287 |
149 var style = getComputedStyle(e); | 288 // Hovering over an element so highlight it. |
150 | 289 function mouseOver(event) |
151 // If the element isn't rendered (since its or one of its ancestor's | 290 { |
152 // "display" property is "none"), the overlay wouldn't match the element. | 291 lastMouseOverEvent = event; |
153 if (style.display == "none") | 292 |
154 return null; | 293 getBlockableElementOrAncestor(event.target, element => |
155 | 294 { |
156 // If the element or one of its ancestors uses fixed postioning, the overlay | 295 if (event == lastMouseOverEvent) |
157 // has to use fixed postioning too. Otherwise it might not match the element
. | 296 { |
158 if (style.position == "fixed") | 297 lastMouseOverEvent = null; |
159 { | 298 |
160 position = "fixed"; | 299 if (currentlyPickingElement) |
161 offsetX = offsetY = 0; | 300 { |
162 } | 301 if (currentElement) |
163 } | 302 unhighlightElement(currentElement); |
164 | 303 |
165 var overlay = document.createElement('div'); | 304 if (element) |
166 overlay.prisoner = elt; | 305 highlightElement(element, "#d6d84b", "#f8fa47"); |
167 overlay.className = "__adblockplus__overlay"; | 306 |
168 overlay.setAttribute('style', 'opacity:0.4; display:inline-box; overflow:hidde
n; box-sizing:border-box;'); | 307 currentElement = element; |
169 var rect = elt.getBoundingClientRect(); | 308 } |
170 overlay.style.width = rect.width + "px"; | 309 } |
171 overlay.style.height = rect.height + "px"; | 310 }); |
172 overlay.style.left = (rect.left + offsetX) + "px"; | 311 |
173 overlay.style.top = (rect.top + offsetY) + "px"; | 312 event.stopPropagation(); |
174 overlay.style.position = position; | 313 } |
175 overlay.style.zIndex = 0x7FFFFFFE; | 314 |
176 | 315 // No longer hovering over this element so unhighlight it. |
177 // elt.parentNode.appendChild(overlay, elt); | 316 function mouseOut(event) |
178 document.documentElement.appendChild(overlay); | 317 { |
179 return overlay; | 318 if (!currentlyPickingElement || currentElement != event.target) |
180 } | |
181 | |
182 // Show dialog asking user whether she wants to add the proposed filters derived | |
183 // from selected page element | |
184 function clickHide_showDialog(filters) | |
185 { | |
186 clickHide_filters = filters; | |
187 | |
188 clickHideFiltersDialog = document.createElement("iframe"); | |
189 clickHideFiltersDialog.src = ext.getURL("block.html"); | |
190 clickHideFiltersDialog.setAttribute("style", "position: fixed !important; visi
bility: hidden; display: block !important; border: 0px !important;"); | |
191 clickHideFiltersDialog.style.WebkitBoxShadow = "5px 5px 20px rgba(0,0,0,0.5)"; | |
192 clickHideFiltersDialog.style.zIndex = 0x7FFFFFFF; | |
193 | |
194 // Position in upper-left all the time | |
195 clickHideFiltersDialog.style.left = "50px"; | |
196 clickHideFiltersDialog.style.top = "50px"; | |
197 | |
198 // Make dialog partly transparent when mouse isn't over it so user has a bette
r | |
199 // view of what's going to be blocked | |
200 clickHideFiltersDialog.onmouseout = function() | |
201 { | |
202 if (clickHideFiltersDialog) | |
203 clickHideFiltersDialog.style.setProperty("opacity", "0.7"); | |
204 }; | |
205 clickHideFiltersDialog.onmouseover = function() | |
206 { | |
207 if (clickHideFiltersDialog) | |
208 clickHideFiltersDialog.style.setProperty("opacity", "1.0"); | |
209 }; | |
210 | |
211 document.documentElement.appendChild(clickHideFiltersDialog); | |
212 } | |
213 | |
214 // Turn on the choose element to create filter thing | |
215 function clickHide_activate() { | |
216 if(document == null) | |
217 return; | 319 return; |
218 | 320 |
219 // If we are already selecting, abort now | 321 unhighlightElement(currentElement); |
220 if (clickHide_activated || clickHideFiltersDialog) | 322 event.stopPropagation(); |
221 clickHide_deactivate(); | 323 } |
| 324 |
| 325 // Key events - Return selects currently hovered-over element, escape aborts. |
| 326 function keyDown(event) |
| 327 { |
| 328 if (!event.ctrlKey && !event.altKey && !event.shiftKey) |
| 329 { |
| 330 if (event.keyCode == 13) // Return |
| 331 elementPicked(event); |
| 332 else if (event.keyCode == 27) // Escape |
| 333 deactivateBlockElement(); |
| 334 } |
| 335 } |
| 336 |
| 337 |
| 338 /* Element selection */ |
| 339 |
| 340 // Start highlighting elements yellow as the mouse moves over them, when one is |
| 341 // chosen launch the popup dialog for the user to confirm the generated filters. |
| 342 function startPickingElement() |
| 343 { |
| 344 currentlyPickingElement = true; |
222 | 345 |
223 // Add overlays for blockable elements that don't emit mouse events, | 346 // Add overlays for blockable elements that don't emit mouse events, |
224 // so that they can still be selected. | 347 // so that they can still be selected. |
225 [].forEach.call( | 348 Array.prototype.forEach.call( |
226 document.querySelectorAll('object,embed,iframe,frame'), | 349 document.querySelectorAll("object,embed,iframe,frame"), |
227 function(element) | 350 element => |
228 { | 351 { |
229 getFiltersForElement(element, function(filters) | 352 getFiltersForElement(element, filters => |
230 { | 353 { |
231 if (filters.length > 0) | 354 if (filters.length > 0) |
232 addElementOverlay(element); | 355 addElementOverlay(element); |
233 }); | 356 }); |
234 } | 357 } |
235 ); | 358 ); |
236 | 359 |
237 clickHide_activated = true; | 360 document.addEventListener("mousedown", stopEventPropagation, true); |
238 document.addEventListener("mousedown", clickHide_stopPropagation, true); | 361 document.addEventListener("mouseup", stopEventPropagation, true); |
239 document.addEventListener("mouseup", clickHide_stopPropagation, true); | 362 document.addEventListener("mouseenter", stopEventPropagation, true); |
240 document.addEventListener("mouseenter", clickHide_stopPropagation, true); | 363 document.addEventListener("mouseleave", stopEventPropagation, true); |
241 document.addEventListener("mouseleave", clickHide_stopPropagation, true); | 364 document.addEventListener("mouseover", mouseOver, true); |
242 document.addEventListener("mouseover", clickHide_mouseOver, true); | 365 document.addEventListener("mouseout", mouseOut, true); |
243 document.addEventListener("mouseout", clickHide_mouseOut, true); | 366 document.addEventListener("click", elementPicked, true); |
244 document.addEventListener("click", clickHide_mouseClick, true); | 367 document.addEventListener("contextmenu", elementPicked, true); |
245 document.addEventListener("keydown", clickHide_keyDown, true); | 368 document.addEventListener("keydown", keyDown, true); |
246 | 369 |
247 ext.onExtensionUnloaded.addListener(clickHide_deactivate); | 370 ext.onExtensionUnloaded.addListener(deactivateBlockElement); |
248 } | 371 } |
249 | 372 |
250 // Called when user has clicked on something and we are waiting for confirmation | 373 // The user has picked an element - currentElement. Highlight it red, generate |
251 // on whether the user actually wants these filters | 374 // filters for it and open a popup dialog so that the user can confirm. |
252 function clickHide_rulesPending() { | 375 function elementPicked(event) |
253 clickHide_activated = false; | 376 { |
254 | 377 if (!currentElement) |
255 if (clickHideFiltersDialog) | 378 return; |
256 { | 379 |
257 document.documentElement.removeChild(clickHideFiltersDialog); | 380 let element = currentElement.prisoner || currentElement; |
258 clickHideFiltersDialog = null; | 381 getFiltersForElement(element, (filters, selectors) => |
259 } | 382 { |
260 | 383 if (currentlyPickingElement) |
261 document.removeEventListener("mousedown", clickHide_stopPropagation, true); | 384 stopPickingElement(); |
262 document.removeEventListener("mouseup", clickHide_stopPropagation, true); | 385 |
263 document.removeEventListener("mouseenter", clickHide_stopPropagation, true); | 386 ext.backgroundPage.sendMessage( |
264 document.removeEventListener("mouseleave", clickHide_stopPropagation, true); | 387 { |
265 document.removeEventListener("mouseover", clickHide_mouseOver, true); | 388 type: "blockelement-open-popup" |
266 document.removeEventListener("mouseout", clickHide_mouseOut, true); | 389 }, |
267 document.removeEventListener("click", clickHide_mouseClick, true); | 390 response => |
268 document.removeEventListener("keydown", clickHide_keyDown, true); | 391 { |
269 } | 392 blockelementPopupId = response; |
270 | 393 ext.backgroundPage.sendMessage( |
271 function clickHide_deactivate() | 394 { |
272 { | 395 type: "forward", |
273 clickHide_rulesPending(); | 396 targetPageId: blockelementPopupId, |
274 | 397 payload: |
275 clickHide_filters = null; | 398 { |
| 399 type: "blockelement-popup-init", |
| 400 filters: filters |
| 401 } |
| 402 }); |
| 403 }); |
| 404 |
| 405 if (selectors.length > 0) |
| 406 highlightElements(selectors.join(",")); |
| 407 |
| 408 highlightElement(currentElement, "#fd1708", "#f6a1b5"); |
| 409 }); |
| 410 |
| 411 event.preventDefault(); |
| 412 event.stopPropagation(); |
| 413 } |
| 414 |
| 415 function stopPickingElement() |
| 416 { |
| 417 currentlyPickingElement = false; |
| 418 |
| 419 document.removeEventListener("mousedown", stopEventPropagation, true); |
| 420 document.removeEventListener("mouseup", stopEventPropagation, true); |
| 421 document.removeEventListener("mouseenter", stopEventPropagation, true); |
| 422 document.removeEventListener("mouseleave", stopEventPropagation, true); |
| 423 document.removeEventListener("mouseover", mouseOver, true); |
| 424 document.removeEventListener("mouseout", mouseOut, true); |
| 425 document.removeEventListener("click", elementPicked, true); |
| 426 document.removeEventListener("contextmenu", elementPicked, true); |
| 427 document.removeEventListener("keydown", keyDown, true); |
| 428 } |
| 429 |
| 430 |
| 431 /* Core logic */ |
| 432 |
| 433 // We're done with the block element feature for now, tidy everything up. |
| 434 function deactivateBlockElement() |
| 435 { |
| 436 if (currentlyPickingElement) |
| 437 stopPickingElement(); |
| 438 |
| 439 if (blockelementPopupId != null) |
| 440 { |
| 441 ext.backgroundPage.sendMessage( |
| 442 { |
| 443 type: "forward", |
| 444 targetPageId: blockelementPopupId, |
| 445 payload: |
| 446 { |
| 447 type: "blockelement-close-popup" |
| 448 } |
| 449 }); |
| 450 |
| 451 blockelementPopupId = null; |
| 452 } |
| 453 |
276 lastRightClickEvent = null; | 454 lastRightClickEvent = null; |
277 | 455 |
278 if (currentElement) | 456 if (currentElement) |
279 { | 457 { |
280 currentElement.removeEventListener("contextmenu", clickHide_elementClickHan
dler, true); | |
281 unhighlightElement(currentElement); | 458 unhighlightElement(currentElement); |
282 currentElement = null; | 459 currentElement = null; |
283 } | 460 } |
284 unhighlightElements(); | 461 unhighlightElements(); |
285 | 462 |
286 var overlays = document.getElementsByClassName("__adblockplus__overlay"); | 463 let overlays = document.getElementsByClassName("__adblockplus__overlay"); |
287 while (overlays.length > 0) | 464 while (overlays.length > 0) |
288 overlays[0].parentNode.removeChild(overlays[0]); | 465 overlays[0].parentNode.removeChild(overlays[0]); |
289 | 466 |
290 ext.onExtensionUnloaded.removeListener(clickHide_deactivate); | 467 ext.onExtensionUnloaded.removeListener(deactivateBlockElement); |
291 } | 468 } |
292 | 469 |
293 function clickHide_stopPropagation(e) | 470 // In Chrome 37-40, the document_end content script (this one) runs properly, |
294 { | 471 // while the document_start content scripts (that defines ext) might not. Check |
295 e.stopPropagation(); | 472 // whether variable ext exists before continuing to avoid |
296 } | 473 // "Uncaught ReferenceError: ext is not defined". See https://crbug.com/416907 |
297 | 474 if ("ext" in window && document instanceof HTMLDocument) |
298 function clickHide_elementClickHandler(e) { | 475 { |
299 e.preventDefault(); | 476 // Use a contextmenu handler to save the last element the user right-clicked |
300 e.stopPropagation(); | 477 // on. To make things easier, we actually save the DOM event. We have to do |
301 clickHide_mouseClick(e); | 478 // this because the contextMenu API only provides a URL, not the actual DOM |
302 } | 479 // element. |
303 | 480 // We also need to make sure that the previous right click event, |
304 function getBlockableElementOrAncestor(element, callback) | 481 // if there is one, is removed. We don't know which frame it is in so we must |
305 { | 482 // send a message to the other frames to clear their old right click events. |
306 // We assume that the user doesn't want to block the whole page. | 483 document.addEventListener("contextmenu", event => |
307 // So we never consider the <html> or <body> element. | 484 { |
308 while (element && element != document.documentElement | 485 lastRightClickEvent = event; |
309 && element != document.body) | 486 lastRightClickEventIsMostRecent = true; |
310 { | 487 |
311 // We can't handle non-HTML (like SVG) elements, as well as | |
312 // <area> elements (see below). So fall back to the parent element. | |
313 if (!(element instanceof HTMLElement) || element.localName == "area") | |
314 element = element.parentElement; | |
315 | |
316 // If image maps are used mouse events occur for the <area> element. | |
317 // But we have to block the image associated with the <map> element. | |
318 else if (element.localName == "map") | |
319 { | |
320 var images = document.querySelectorAll("img[usemap]"); | |
321 var image = null; | |
322 | |
323 for (var i = 0; i < images.length; i++) | |
324 { | |
325 var usemap = images[i].getAttribute("usemap"); | |
326 var index = usemap.indexOf("#"); | |
327 | |
328 if (index != -1 && usemap.substr(index + 1) == element.name) | |
329 { | |
330 image = images[i]; | |
331 break; | |
332 } | |
333 } | |
334 | |
335 element = image; | |
336 } | |
337 | |
338 // Finally, if none of the above is true, check whether we can generate | |
339 // any filters for this element. Otherwise fall back to its parent element. | |
340 else | |
341 { | |
342 getFiltersForElement(element, function(filters) | |
343 { | |
344 if (filters.length > 0) | |
345 callback(element); | |
346 else | |
347 getBlockableElementOrAncestor(element.parentElement, callback); | |
348 }); | |
349 | |
350 return; | |
351 } | |
352 } | |
353 | |
354 // We reached the document root without finding a blockable element. | |
355 callback(null); | |
356 } | |
357 | |
358 // Hovering over an element so highlight it | |
359 function clickHide_mouseOver(e) | |
360 { | |
361 lastMouseOverEvent = e; | |
362 | |
363 getBlockableElementOrAncestor(e.target, function(element) | |
364 { | |
365 if (e == lastMouseOverEvent) | |
366 { | |
367 lastMouseOverEvent = null; | |
368 | |
369 if (clickHide_activated) | |
370 { | |
371 if (currentElement) | |
372 unhighlightElement(currentElement); | |
373 | |
374 if (element) | |
375 { | |
376 highlightElement(element, "#d6d84b", "#f8fa47"); | |
377 element.addEventListener("contextmenu", clickHide_elementClickHandler,
true); | |
378 } | |
379 | |
380 currentElement = element; | |
381 } | |
382 } | |
383 }); | |
384 | |
385 e.stopPropagation(); | |
386 } | |
387 | |
388 // No longer hovering over this element so unhighlight it | |
389 function clickHide_mouseOut(e) | |
390 { | |
391 if (!clickHide_activated || currentElement != e.target) | |
392 return; | |
393 | |
394 unhighlightElement(currentElement); | |
395 currentElement.removeEventListener("contextmenu", clickHide_elementClickHandle
r, true); | |
396 e.stopPropagation(); | |
397 } | |
398 | |
399 // Selects the currently hovered-over filter or cancels selection | |
400 function clickHide_keyDown(e) | |
401 { | |
402 if (!e.ctrlKey && !e.altKey && !e.shiftKey && e.keyCode == 13 /*DOM_VK_RETURN*
/) | |
403 clickHide_mouseClick(e); | |
404 else if (!e.ctrlKey && !e.altKey && !e.shiftKey && e.keyCode == 27 /*DOM_VK_ES
CAPE*/) | |
405 { | |
406 ext.backgroundPage.sendMessage( | 488 ext.backgroundPage.sendMessage( |
407 { | 489 { |
408 type: "forward", | 490 type: "forward", |
409 payload: | 491 payload: |
410 { | 492 { |
411 type: "clickhide-deactivate" | 493 type: "blockelement-clear-previous-right-click-event" |
412 } | |
413 }); | |
414 e.preventDefault(); | |
415 e.stopPropagation(); | |
416 } | |
417 } | |
418 | |
419 function getFiltersForElement(element, callback) | |
420 { | |
421 ext.backgroundPage.sendMessage( | |
422 { | |
423 type: "compose-filters", | |
424 tagName: element.localName, | |
425 id: element.id, | |
426 src: element.getAttribute("src"), | |
427 style: element.getAttribute("style"), | |
428 classes: [].slice.call(element.classList), | |
429 urls: getURLsFromElement(element), | |
430 mediatype: typeMap[element.localName], | |
431 baseURL: document.location.href | |
432 }, | |
433 function(response) | |
434 { | |
435 callback(response.filters, response.selectors); | |
436 } | |
437 ); | |
438 } | |
439 | |
440 // When the user clicks, the currentElement is the one we want. | |
441 // We should have ABP rules ready for when the | |
442 // popup asks for them. | |
443 function clickHide_mouseClick(e) | |
444 { | |
445 if (!currentElement || !clickHide_activated) | |
446 return; | |
447 | |
448 var elt = currentElement; | |
449 if (currentElement.classList.contains("__adblockplus__overlay")) | |
450 elt = currentElement.prisoner; | |
451 | |
452 getFiltersForElement(elt, function(filters, selectors) | |
453 { | |
454 ext.backgroundPage.sendMessage( | |
455 { | |
456 type: "forward", | |
457 payload: | |
458 { | |
459 type: "clickhide-show-dialog", | |
460 clickHideFilters: filters | |
461 } | |
462 }); | |
463 | |
464 if (selectors.length > 0) | |
465 highlightElements(selectors.join(",")); | |
466 | |
467 highlightElement(currentElement, "#fd1708", "#f6a1b5"); | |
468 }); | |
469 | |
470 // Make sure the browser doesn't handle this click | |
471 e.preventDefault(); | |
472 e.stopPropagation(); | |
473 } | |
474 | |
475 // This function Copyright (c) 2008 Jeni Tennison, from jquery.uri.js | |
476 // and licensed under the MIT license. See jquery-*.min.js for details. | |
477 function removeDotSegments(u) { | |
478 var r = '', m = []; | |
479 if (/\./.test(u)) { | |
480 while (u !== undefined && u !== '') { | |
481 if (u === '.' || u === '..') { | |
482 u = ''; | |
483 } else if (/^\.\.\//.test(u)) { // starts with ../ | |
484 u = u.substring(3); | |
485 } else if (/^\.\//.test(u)) { // starts with ./ | |
486 u = u.substring(2); | |
487 } else if (/^\/\.(\/|$)/.test(u)) { // starts with /./ or consists of /. | |
488 u = '/' + u.substring(3); | |
489 } else if (/^\/\.\.(\/|$)/.test(u)) { // starts with /../ or consists of /
.. | |
490 u = '/' + u.substring(4); | |
491 r = r.replace(/\/?[^\/]+$/, ''); | |
492 } else { | |
493 m = u.match(/^(\/?[^\/]*)(\/.*)?$/); | |
494 u = m[2]; | |
495 r = r + m[1]; | |
496 } | |
497 } | |
498 return r; | |
499 } else { | |
500 return u; | |
501 } | |
502 } | |
503 | |
504 // In Chrome 37-40, the document_end content script (this one) runs properly, wh
ile the | |
505 // document_start content scripts (that defines ext) might not. Check whether va
riable ext | |
506 // exists before continuing to avoid "Uncaught ReferenceError: ext is not define
d". | |
507 // See https://crbug.com/416907 | |
508 if ("ext" in window && document instanceof HTMLDocument) | |
509 { | |
510 // Use a contextmenu handler to save the last element the user right-clicked o
n. | |
511 // To make things easier, we actually save the DOM event. | |
512 // We have to do this because the contextMenu API only provides a URL, not the
actual | |
513 // DOM element. | |
514 document.addEventListener('contextmenu', function(e) | |
515 { | |
516 lastRightClickEvent = e; | |
517 // We also need to ensure any old lastRightClickEvent variables in other | |
518 // frames are cleared. | |
519 lastRightClickEventValid = true; | |
520 ext.backgroundPage.sendMessage( | |
521 { | |
522 type: "forward", | |
523 payload: | |
524 { | |
525 type: "clickhide-clear-last-right-click-event" | |
526 } | 494 } |
527 }); | 495 }); |
528 }, true); | 496 }, true); |
529 | 497 |
530 document.addEventListener("click", function(event) | 498 ext.onMessage.addListener((msg, sender, sendResponse) => |
531 { | |
532 // Ignore right-clicks | |
533 if (event.button == 2) | |
534 return; | |
535 | |
536 // Search the link associated with the click | |
537 var link = event.target; | |
538 while (!(link instanceof HTMLAnchorElement)) | |
539 { | |
540 link = link.parentNode; | |
541 | |
542 if (!link) | |
543 return; | |
544 } | |
545 | |
546 if (link.protocol == "http:" || link.protocol == "https:") | |
547 { | |
548 if (link.host != "subscribe.adblockplus.org" || link.pathname != "/") | |
549 return; | |
550 } | |
551 else if (!/^abp:\/*subscribe\/*\?/i.test(link.href)) | |
552 return; | |
553 | |
554 // This is our link - make sure the browser doesn't handle it | |
555 event.preventDefault(); | |
556 event.stopPropagation(); | |
557 | |
558 // Decode URL parameters | |
559 var params = link.search.substr(1).split("&"); | |
560 var title = null; | |
561 var url = null; | |
562 for (var i = 0; i < params.length; i++) | |
563 { | |
564 var parts = params[i].split("=", 2); | |
565 if (parts.length != 2 || !/\S/.test(parts[1])) | |
566 continue; | |
567 switch (parts[0]) | |
568 { | |
569 case "title": | |
570 title = decodeURIComponent(parts[1]); | |
571 break; | |
572 case "location": | |
573 url = decodeURIComponent(parts[1]); | |
574 break; | |
575 } | |
576 } | |
577 if (!url) | |
578 return; | |
579 | |
580 // Default title to the URL | |
581 if (!title) | |
582 title = url; | |
583 | |
584 // Trim spaces in title and URL | |
585 title = title.trim(); | |
586 url = url.trim(); | |
587 if (!/^(https?|ftp):/.test(url)) | |
588 return; | |
589 | |
590 ext.backgroundPage.sendMessage({ | |
591 type: "add-subscription", | |
592 title: title, | |
593 url: url | |
594 }); | |
595 }, true); | |
596 | |
597 ext.onMessage.addListener(function(msg, sender, sendResponse) | |
598 { | 499 { |
599 switch (msg.type) | 500 switch (msg.type) |
600 { | 501 { |
601 case "get-clickhide-state": | 502 case "blockelement-get-state": |
602 sendResponse({active: clickHide_activated}); | 503 if (window == window.top) |
| 504 sendResponse({ |
| 505 active: currentlyPickingElement || blockelementPopupId != null |
| 506 }); |
603 break; | 507 break; |
604 case "clickhide-activate": | 508 case "blockelement-start-picking-element": |
605 clickHide_activate(); | 509 if (window == window.top) |
| 510 startPickingElement(); |
606 break; | 511 break; |
607 case "clickhide-deactivate": | 512 case "blockelement-context-menu-clicked": |
608 clickHide_deactivate(); | 513 let event = lastRightClickEvent; |
609 break; | 514 deactivateBlockElement(); |
610 case "clickhide-new-filter": | 515 if (event) |
611 if(lastRightClickEvent) | |
612 { | 516 { |
613 var event = lastRightClickEvent; | 517 getBlockableElementOrAncestor(event.target, element => |
614 getBlockableElementOrAncestor(event.target, function(element) | |
615 { | 518 { |
616 clickHide_activate(); | 519 if (element) |
617 currentElement = element; | 520 { |
618 clickHide_mouseClick(event); | 521 currentElement = element; |
| 522 elementPicked(event); |
| 523 } |
619 }); | 524 }); |
620 } | 525 } |
621 break; | 526 break; |
622 case "clickhide-init": | 527 case "blockelement-finished": |
623 if (clickHideFiltersDialog) | |
624 { | |
625 sendResponse({filters: clickHide_filters}); | |
626 | |
627 clickHideFiltersDialog.style.width = msg.width + "px"; | |
628 clickHideFiltersDialog.style.height = msg.height + "px"; | |
629 clickHideFiltersDialog.style.visibility = "visible"; | |
630 } | |
631 break; | |
632 case "clickhide-move": | |
633 if (clickHideFiltersDialog) | |
634 { | |
635 var rect = clickHideFiltersDialog.getBoundingClientRect(); | |
636 var x = Math.max(0, Math.min(rect.left + msg.x, window.innerWidth - re
ct.width)); | |
637 var y = Math.max(0, Math.min(rect.top + msg.y, window.innerHeight - re
ct.height)); | |
638 | |
639 clickHideFiltersDialog.style.left = x + "px"; | |
640 clickHideFiltersDialog.style.top = y + "px"; | |
641 } | |
642 break; | |
643 case "clickhide-close": | |
644 if (currentElement && msg.remove) | 528 if (currentElement && msg.remove) |
645 { | 529 { |
646 // Hide the selected element itself if an added blocking | 530 // Hide the selected element itself if an added blocking |
647 // filter is causing it to collapse. Note that this | 531 // filter is causing it to collapse. Note that this |
648 // behavior is incomplete, but the best we can do here, | 532 // behavior is incomplete, but the best we can do here, |
649 // e.g. if an added blocking filter matches other elements, | 533 // e.g. if an added blocking filter matches other elements, |
650 // the effect won't be visible until the page is is reloaded. | 534 // the effect won't be visible until the page is is reloaded. |
651 checkCollapse(currentElement.prisoner || currentElement); | 535 checkCollapse(currentElement.prisoner || currentElement); |
652 | 536 |
653 // Apply added element hiding filters. | 537 // Apply added element hiding filters. |
654 updateStylesheet(); | 538 updateStylesheet(); |
655 } | 539 } |
656 clickHide_deactivate(); | 540 deactivateBlockElement(); |
657 break; | 541 break; |
658 case "clickhide-show-dialog": | 542 case "blockelement-clear-previous-right-click-event": |
659 clickHide_rulesPending(); | 543 if (!lastRightClickEventIsMostRecent) |
660 if (window.self == window.top) | 544 lastRightClickEvent = null; |
661 clickHide_showDialog(msg.clickHideFilters); | 545 lastRightClickEventIsMostRecent = false; |
662 break; | 546 break; |
663 case "clickhide-clear-last-right-click-event": | 547 case "blockelement-popup-closed": |
664 if (lastRightClickEventValid) | 548 // The onRemoved hook for the popup can create a race condition, so we |
665 lastRightClickEventValid = false; | 549 // to be careful here. (This is not perfect, but best we can do.) |
666 else | 550 if (window == window.top && blockelementPopupId == msg.popupId) |
667 lastRightClickEvent = null; | 551 { |
| 552 ext.backgroundPage.sendMessage( |
| 553 { |
| 554 type: "forward", |
| 555 payload: |
| 556 { |
| 557 type: "blockelement-finished" |
| 558 } |
| 559 }); |
| 560 } |
668 break; | 561 break; |
669 } | 562 } |
670 }); | 563 }); |
671 | 564 |
672 if (window == window.top) | 565 if (window == window.top) |
673 ext.backgroundPage.sendMessage({type: "report-html-page"}); | 566 ext.backgroundPage.sendMessage({type: "report-html-page"}); |
674 } | 567 } |
LEFT | RIGHT |