Reputation: 59
While making a tampermonkey script that should change attribute on multiple document elements, I encountered a site that uses event listener to prevent any changes to that attribute. To counter such behavior I can replace elements with their clones, like below:
const map = new Map();
function read() {
for (let image of [...document.getElementsByTagName('img')]) {
const clone = image.cloneNode(/* with children */true);
// TODO: modify the clone
map.set(image, clone);
}
}
read();
function write() {
for (let [image, clone] of map) {
image.parentNode.replaceChild(clone, image);
}
}
write();
map.clear();
But with this method there is a problem: browser (Chrome 71) in the end of operation will recalculate style and sometimes thrash layout if the quantity of elements is large (10 elements - no thrashing, 100 elements - trashing). I tried to modify loop in write function with requesting animation frame:
window.requestAnimationFrame(document.replaceChild.bind(image.parentNode, clone, image));
But it still thrashes layout. Tried to insert a pause in the loop:
async function write() {
for (let [image, clone] of map) {
await new Promise(window.requestAnimationFrame);
image.parentNode.replaceChild(clone, image);
}
}
No change. Tried to split elements array in small chunks and operate each chunk, but browser still recalculates style lazily in the end and thrashes layout. So instead of replacing each element with its clone I can remove each event listener with something like:
for (let image of [...document.getElementsByTagName('img')]) {
const listeners = getEventListeners(image);
for (let event_type in listeners) {
for (let event of listeners[event_type]) {
image.removeEventListener(event.type, event.listener, event.useCapture);
}
}
}
While this fixes the problem, there is still the question: what if a site used not event listener, but MutationObserver to prevent modification of the document elements? Is there a way to remove MutationObserver without replacing document element with its clone?
Though I understand that modifying the attributes of lots of elements will still force browser to reflow, my question still stands.
Upvotes: 2
Views: 864
Reputation: 18378
There is no way to disconnect anonymous MutationObserver, but it does not mean there is no another solution.
I believe that you do not need to change all the links on the page.
You need to intercept just one link which the user would try to follow.
Please see the snippet below.
We want to change href
attribute and it's being catched by the observer:
// anonymous observer:
(new MutationObserver(function(mutationsList, observer) {
for (var mutation of mutationsList) {
if (
mutation.type == 'attributes' &&
mutation.target.href !== 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_120x44dp.png'
) {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
mutation.target.href = 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_120x44dp.png'
}
}
}))
.observe(
document.querySelector('html'), {
attributes: true,
subtree: true
}
);
document.querySelectorAll('a').forEach(function(a) {
a.href = 'https://yastatic.net/www/_/x/Q/xk8YidkhGjIGOrFm_dL5781YA.svg';
});
<a href="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_120x44dp.png">Google</a>
To avoid this, set event listeners to the links and on click
change location.href
(or open a new tab or window) instead of changing a.href
:
// anonymous observer:
(new MutationObserver(function(mutationsList, observer) {
for (var mutation of mutationsList) {
if (
mutation.type == 'attributes' &&
mutation.target.href !== 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_120x44dp.png'
) {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
mutation.target.href = 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_120x44dp.png'
}
}
}))
.observe(
document.querySelector('html'), {
attributes: true,
subtree: true
}
);
document.querySelectorAll('a').forEach(function(a) {
a.addEventListener('click', function(e) {
e.preventDefault();
location.href = 'https://yastatic.net/www/_/x/Q/xk8YidkhGjIGOrFm_dL5781YA.svg';
});
});
<a href="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_120x44dp.png">Google</a>
Upvotes: 1