Problem Description
There is a time-consuming async function f
that takes a URL string and returns the modified one. For example:
async function f(url) { // A dummy time-consuming function
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(url + "#processed");
}, 1000)
});
}
I would like all links opened from a certain page to be processed by f
before actually opening. Note that I have no control over the actual webpage, only the ability to inject scripts via Tampermonkey. Also, the contents of the webpage can be generated dynamically.
What I’ve tried
Handling links opened with window.open
is rather easy (simply hook the method), but I encountered some problems handling <a>
elements. My thought is to add an capture: true
listener on document
, pause the event and modify the href
attribute of <a>
when it detects interaction with <a>
elements, and then resume the event. This is what I came up with:
const tag1 = "url-handling"; // The element is being handled
const tag2 = "url-handled"; // The element has been handled
const eventName = "url-handle-done"; // The event name signaling completion of handling
function cloneAndStop(e) { // Clone an event and stop the original
const newEvt = new e.constructor(e.type, e);
e.preventDefault();
e.stopImmediatePropagation();
return newEvt;
}
async function eventHandler(e) { // Intercept mouse events
const ele = e.target.tagName === "A" ? e.target : e.target.closest("a");
if (ele && !ele.hasAttribute(tag2) && ele.href) {
if (!ele.hasAttribute(tag1)) { // The element is not being handled
ele.toggleAttribute(tag1, true); // Mark the element as being handled
const href = ele.href;
if (!href.startsWith("https://") && !href.startsWith("http://")) return; // Ignore non-HTTP(S) URLs
const newEvt = cloneAndStop(e);
console.log(`Intercepted: "${ele.href}"`);
const url = ele.href;
const processed = await f(url);
ele.href = processed; // Modify the URL
console.log(`Processed: "${ele.href}"`);
ele.toggleAttribute(tag2, true); // Mark the element as handled
ele.removeAttribute(tag1); // Remove the being-handled mark
ele.dispatchEvent(newEvt); // "Resume" the initial event
ele.dispatchEvent(new Event(eventName, { bubbles: false, cancelable: true })); // Notify other potential handlers
} else { // The element is being handled by another handler
const newEvt = cloneAndStop(e);
console.log(`Waiting: "${ele.href}"`);
ele.addEventListener(eventName, function () {
console.log(`Waited: "${ele.href}"`);
ele.dispatchEvent(newEvt);
}, { once: true });
}
}
}
["click", "mousedown", "auxclick", "contextmenu"].forEach((name) => {
document.addEventListener(name, eventHandler, { capture: true });
});
It works fine for clicking <a>
elements, even with modifier keys, where the processed URL opens after 1 second. But it does not work for auxclick
and contextmenu
, where I simply receive the following log, but no opening in new tab and showing up context menu:
Intercepted: "https://example.org/"
Waiting: "https://example.org/"
Processed: "https://example.org/#processed"
Waited: "https://example.org/#processed"
By calling window.open
for auxclick
events, I can somewhat simulate the browser’s default action of opening the link on a new page, but I assume there’s a way to simulate mouse/pointer events instead of calling window.open
.
Related Questions
- Use JavaScript to intercept all document link clicks
- Intercept all current and future links