| Index: include.preload.js | 
| =================================================================== | 
| --- a/include.preload.js | 
| +++ b/include.preload.js | 
| @@ -31,16 +31,18 @@ | 
| ["audio", "MEDIA"], | 
| ["video", "MEDIA"], | 
| ["frame", "SUBDOCUMENT"], | 
| ["iframe", "SUBDOCUMENT"], | 
| ["object", "OBJECT"], | 
| ["embed", "OBJECT"] | 
| ]); | 
|  | 
| +let collapsingSelectors = new Set(); | 
| + | 
| function getURLsFromObjectElement(element) | 
| { | 
| let url = element.getAttribute("data"); | 
| if (url) | 
| return [url]; | 
|  | 
| for (let child of element.children) | 
| { | 
| @@ -124,16 +126,53 @@ | 
| { | 
| if (/^(?!https?:)[\w-]+:/i.test(urls[i])) | 
| urls.splice(i--, 1); | 
| } | 
|  | 
| return urls; | 
| } | 
|  | 
| +function isCollapsibleMediaElement(element, mediatype) | 
| +{ | 
| +  if (mediatype != "MEDIA") | 
| +    return false; | 
| + | 
| +  if (!element.getAttribute("src")) | 
| +    return false; | 
| + | 
| +  for (let child of element.children) | 
| +  { | 
| +    // If the <video> or <audio> element contains any <source> or <track> | 
| +    // children, we cannot address it in CSS by the source URL; in that case we | 
| +    // don't "collapse" it using a CSS selector but rather hide it directly by | 
| +    // setting the style="..." attribute. | 
| +    if (child.localName == "source" || child.localName == "track") | 
| +      return false; | 
| +  } | 
| + | 
| +  return true; | 
| +} | 
| + | 
| +function collapseMediaElement(element, srcValue) | 
| +{ | 
| +  if (!srcValue) | 
| +    return; | 
| + | 
| +  let selector = element.localName + "[src=" + CSS.escape(srcValue) + "]"; | 
| + | 
| +  // Adding selectors is expensive so do it only if we really have a new | 
| +  // selector. | 
| +  if (!collapsingSelectors.has(selector)) | 
| +  { | 
| +    collapsingSelectors.add(selector); | 
| +    elemhide.addSelectors([selector], null, "collapsing", true); | 
| +  } | 
| +} | 
| + | 
| function hideElement(element) | 
| { | 
| function doHide() | 
| { | 
| let propertyName = "display"; | 
| let propertyValue = "none"; | 
| if (element.localName == "frame") | 
| { | 
| @@ -161,29 +200,37 @@ | 
| let mediatype = typeMap.get(element.localName); | 
| if (!mediatype) | 
| return; | 
|  | 
| let urls = getURLsFromElement(element); | 
| if (urls.length == 0) | 
| return; | 
|  | 
| +  let collapsibleMediaElement = isCollapsibleMediaElement(element, mediatype); | 
| + | 
| +  // Save the value of the src attribute because it can change between now and | 
| +  // when we get the response from the background page. | 
| +  let srcValue = collapsibleMediaElement ? element.getAttribute("src") : null; | 
| + | 
| browser.runtime.sendMessage( | 
| { | 
| type: "filters.collapse", | 
| urls, | 
| mediatype, | 
| baseURL: document.location.href | 
| }, | 
| - | 
| collapse => | 
| { | 
| if (collapse) | 
| { | 
| -        hideElement(element); | 
| +        if (collapsibleMediaElement) | 
| +          collapseMediaElement(element, srcValue); | 
| +        else | 
| +          hideElement(element); | 
| } | 
| } | 
| ); | 
| } | 
|  | 
| function checkSitekey() | 
| { | 
| let attr = document.documentElement.getAttribute("data-adblockkey"); | 
| @@ -381,21 +428,21 @@ | 
| // avoid creating the shadowRoot twice. | 
| let shadow = document.documentElement.shadowRoot || | 
| document.documentElement.createShadowRoot(); | 
| shadow.appendChild(document.createElement("content")); | 
|  | 
| return shadow; | 
| }, | 
|  | 
| -  addSelectorsInline(selectors, groupName) | 
| +  addSelectorsInline(selectors, groupName, appendOnly = false) | 
| { | 
| let style = this.styles.get(groupName); | 
|  | 
| -    if (style) | 
| +    if (style && !appendOnly) | 
| { | 
| while (style.sheet.cssRules.length > 0) | 
| style.sheet.deleteRule(0); | 
| } | 
|  | 
| if (selectors.length == 0) | 
| return; | 
|  | 
| @@ -453,41 +500,44 @@ | 
| let selector = preparedSelectors.slice( | 
| i, i + this.selectorGroupSize | 
| ).join(", "); | 
| style.sheet.insertRule(selector + "{display: none !important;}", | 
| style.sheet.cssRules.length); | 
| } | 
| }, | 
|  | 
| -  addSelectors(selectors, filters) | 
| +  addSelectors(selectors, filters, groupName = "emulated", appendOnly = false) | 
| { | 
| if (this.inline || this.inlineEmulated) | 
| { | 
| // Insert the style rules inline if we have been instructed by the | 
| // background page to do so. This is usually the case, except on platforms | 
| // that do support user stylesheets via the browser.tabs.insertCSS API | 
| // (Firefox 53 onwards for now and possibly Chrome in the near future). | 
| // Once all supported platforms have implemented this API, we can remove | 
| // the code below. See issue #5090. | 
| // Related Chrome and Firefox issues: | 
| // https://bugs.chromium.org/p/chromium/issues/detail?id=632009 | 
| // https://bugzilla.mozilla.org/show_bug.cgi?id=1310026 | 
| -      this.addSelectorsInline(selectors, "emulated"); | 
| +      this.addSelectorsInline(selectors, groupName, appendOnly); | 
| } | 
| else | 
| { | 
| browser.runtime.sendMessage({ | 
| type: "elemhide.injectSelectors", | 
| selectors, | 
| -        groupName: "emulated" | 
| +        groupName, | 
| +        appendOnly | 
| }); | 
| } | 
|  | 
| -    if (this.tracer) | 
| +    // Only trace selectors that are based directly on hiding filters | 
| +    // (i.e. leave out collapsing selectors). | 
| +    if (this.tracer && groupName != "collapsing") | 
| this.tracer.addSelectors(selectors, filters); | 
| }, | 
|  | 
| hideElements(elements, filters) | 
| { | 
| for (let element of elements) | 
| hideElement(element); | 
|  | 
|  |