| 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 |