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