Index: lib/content/elemHideEmulation.js |
=================================================================== |
--- a/lib/content/elemHideEmulation.js |
+++ b/lib/content/elemHideEmulation.js |
@@ -469,23 +469,121 @@ |
return patterns.some(pattern => pattern.maybeDependsOnAttributes); |
} |
function shouldObserveCharacterData(patterns) |
{ |
return patterns.some(pattern => pattern.dependsOnCharacterData); |
} |
+function updateSelectorsForMutation(mutation, selectorSet, selectors, |
+ selectorFilters, root) |
+{ |
+ let {target} = mutation; |
+ |
+ // Existing selectors aren't affected by non-childList mutations. |
+ if (mutation.type != "childList" || !root.contains(target)) |
+ return false; |
+ |
+ // Find the mutation index, i.e. the index in the parent element where the |
+ // mutation has occurred. |
+ let index = 0; |
+ let ref = mutation.previousSibling; |
+ if (ref) |
+ { |
+ if (!(ref instanceof Element)) |
+ ref = ref.previousElementSibling; |
+ |
+ if (ref) |
+ index = indexOf(ref.parentNode.children, ref) + 1; |
+ } |
+ |
+ // Count the number of elements added and removed. Both can be non-zero in |
+ // some cases, e.g. when Element.innerHTML is set on the parent element to |
+ // overwrite all the existing child nodes. |
+ let numAddedElements = 0; |
+ let numRemovedElements = 0; |
+ |
+ for (let node of mutation.addedNodes) |
+ { |
+ if (node instanceof Element) |
+ numAddedElements++; |
+ } |
+ |
+ for (let node of mutation.removedNodes) |
+ { |
+ if (node instanceof Element) |
+ numRemovedElements++; |
+ } |
+ |
+ let changed = false; |
+ |
+ // Go through all the selectors and find any that match the mutation target. |
+ let targetSelector = makeSelector(target) + " > "; |
+ for (let i = 0; i < selectors.length; i++) |
+ { |
+ let selector = selectors[i]; |
+ if (selector.startsWith(targetSelector)) |
+ { |
+ // If there's a match, extract the one-based child index. |
+ let subSelector = selector.substring(targetSelector.length); |
+ let match = /^[^:]*:nth-child\((\d+)\)/.exec(subSelector); |
+ let nthIndex = match ? +match[1] : 0; |
+ |
+ // If the child being targeted by the selector is before the mutation |
+ // index (i.e. the one-based child index is not affected), do nothing. |
+ if (nthIndex <= index) |
+ continue; |
+ |
+ nthIndex -= numRemovedElements; |
+ |
+ // After adjusting for the number of removed elements, if the one-based |
+ // child index appears to be lower than the mutation index, it means the |
+ // child got removed. In that case we must delete the selector. |
+ if (nthIndex <= index) |
+ { |
+ selectorSet.delete(selector); |
+ |
+ selectors.splice(i, 1); |
+ selectorFilters.splice(i, 1); |
+ i--; |
+ |
+ changed = true; |
+ |
+ continue; |
+ } |
+ |
+ nthIndex += numAddedElements; |
+ |
+ // If the one-based child index has changed, we must update the selector. |
+ if (nthIndex != +match[1]) |
+ { |
+ selectorSet.delete(selector); |
+ selector = targetSelector + subSelector.replace(match[1], nthIndex); |
+ selectorSet.add(selector); |
+ selectors[i] = selector; |
+ |
+ changed = true; |
+ } |
+ } |
+ } |
+ |
+ return changed; |
+} |
+ |
function ElemHideEmulation(addSelectorsFunc, hideElemsFunc) |
{ |
this.document = document; |
this.addSelectorsFunc = addSelectorsFunc; |
this.hideElemsFunc = hideElemsFunc; |
this.observer = new MutationObserver(this.observe.bind(this)); |
this.useInlineStyles = true; |
+ this.selectorSet = new Set(); |
+ this.selectors = []; |
+ this.selectorFilters = []; |
} |
ElemHideEmulation.prototype = { |
isSameOrigin(stylesheet) |
{ |
try |
{ |
return new URL(stylesheet.href).origin == this.document.location.origin; |
@@ -574,16 +672,19 @@ |
* then and all rules, including the ones not dependent on the DOM. |
* @param {function} [done] |
* Callback to call when done. |
*/ |
_addSelectors(stylesheets, mutations, done) |
{ |
let patterns = filterPatterns(this.patterns, {stylesheets, mutations}); |
+ if (patterns.length == 0) |
+ return; |
+ |
let selectors = []; |
let selectorFilters = []; |
let elements = []; |
let elementFilters = []; |
let cssStyles = []; |
@@ -628,19 +729,30 @@ |
{ |
let cycleStart = performance.now(); |
if (!pattern) |
{ |
if (!patterns.length) |
{ |
if (selectors.length > 0) |
- this.addSelectorsFunc(selectors, selectorFilters); |
+ { |
+ for (let selector of selectors) |
+ this.selectorSet.add(selector); |
+ |
+ this.selectors.push(...selectors); |
+ this.selectorFilters.push(...selectorFilters); |
+ |
+ this.addSelectorsFunc(this.selectors, |
+ this.selectorFilters); |
+ } |
+ |
if (elements.length > 0) |
this.hideElemsFunc(elements, elementFilters); |
+ |
if (typeof done == "function") |
done(); |
return; |
} |
pattern = patterns.shift(); |
generator = evaluate(pattern.selectors, 0, "", |
@@ -648,18 +760,21 @@ |
} |
for (let selector of generator) |
{ |
if (selector != null) |
{ |
if (!this.useInlineStyles || |
pattern.isSelectorHidingOnlyPattern()) |
{ |
- selectors.push(selector); |
- selectorFilters.push(pattern.text); |
+ if (!this.selectorSet.has(selector)) |
+ { |
+ selectors.push(selector); |
+ selectorFilters.push(pattern.text); |
+ } |
} |
else |
{ |
for (let element of this.document.querySelectorAll(selector)) |
{ |
elements.push(element); |
elementFilters.push(pattern.text); |
} |
@@ -779,16 +894,29 @@ |
{ |
let stylesheet = event.target.sheet; |
if (stylesheet) |
this.queueFiltering([stylesheet]); |
}, |
observe(mutations) |
{ |
+ let selectorsChanged = false; |
+ for (let mutation of mutations) |
+ { |
+ if (updateSelectorsForMutation(mutation, this.selectorSet, this.selectors, |
+ this.selectorFilters, this.document)) |
+ { |
+ selectorsChanged = true; |
+ } |
+ } |
+ |
+ if (selectorsChanged) |
+ this.addSelectorsFunc(this.selectors, this.selectorFilters); |
+ |
this.queueFiltering(null, mutations); |
}, |
apply(patterns) |
{ |
this.patterns = []; |
for (let pattern of patterns) |
{ |