I'm writing a Chrome extension. It's used for recording users' behavior on browsing web pages. It does that by adding event listeners to customers' web pages, using Chrome content script.
Code in content script looks like:
var recordingEvents = ['click', 'input', 'change'];
recordingEvents.forEach(function (e) {
window.addEventListener(e, handler, true);
});
Example of custom page:
<script>
function reload() {
var ifrw = document.getElementById("iframeResult").contentWindow;
ifrw.document.open();
ifrw.document.write("<div>abc</div>");
ifrw.document.close();
}
</script>
<body>
<input type="submit" onclick="reload();" value="Reload" />
<iframe id="iframeResult"></iframe>
</body>
It uses document.open, document.write to rewrite content of iframe.
Here is the question. My event listeners are attached to window object. And document.open removes all its event listeners. Like picture below shows.
Is there a way to avoid document.open removing event listeners? Or to observe document.open, so I can manually re-add listeners after it?
I've found this issue trying to solve exactly the same problem.
Here is a spec https://html.spec.whatwg.org/multipage/webappapis.html#dom-document-open that says that on document.open current document is destroyed and replaced with a fresh one. I had a hope that some event's like "load" are still preserved, no luck.
Here is my detection code:
const testEventName = 'TestEvent';
let tm;
function onTestEvent() {
clearTimeout(tm);
}
function listenToTestEvent() {
document.addEventListener(testEventName, onTestEvent);
}
listenToTestEvent();
function onLostEvents() {
console.log('events are lost');
listenToTestEvent();
// DO THING HERE
}
function checkIfEventsAreLost() {
document.dispatchEvent(new CustomEvent(testEventName));
tm = setTimeout(onLostEvents);
}
new MutationObserver(checkIfEventsAreLost).observe(document, { childList: true });
When document is recreated its childList is changed(new documentElementnode), this is the best trigger I've thought of to detect document replacement.
Note that even listeners fire before setTimeout(..., 0)
This is a detailed explanation of why #Viller's answer works. I'm making this a new answer as it didn't fit into a comment
The TestEvent event is a special event that monitors when the events that were previously setup in a document are removed.
In particular, this accounts for the case of document.open, which removes all listeners not only from the document but also from the window.
The general idea is to setup a listener for a custom event called TestEvent, which clears a timeout. Such timeout is setup only when the document mutates and is triggered by a mutation observer.
Since the timeout schedules the operation to happen at least during the next tick of the event loop, such timeout can be cleared before that, avoiding the execution of its callback all together. And, since the TestEvent event handler clears that timeout, the fact that the timeout is cleared implies that the listener is still attached. On the other hand, if the timeout is not cleared before the next tick, the would signify the events were removed and a new "setup" is needed.
According to MDN:
The Document.open() method [...] come(s) with some side effects. For example:
All event listeners currently registered on the document, nodes inside
the document, or the document's window are removed.
Below I provide a module (onGlobalListenerRemoval) where one can easily register some callback functions to get notified whenever listeners get cleared. This uses the same working principle as the code in Viller's answer.
Usage principle:
onGlobalListenerRemoval.addListener(() => {
alert("All event listeners got removed!")
});
Module code:
const onGlobalListenerRemoval = (() => {
const callbacks = new Set();
const eventName = "listenerStillAttached";
window.addEventListener(eventName, _handleListenerStillAttached);
new MutationObserver((entries) => {
const documentReplaced = entries.some(entry =>
Array.from(entry.addedNodes).includes(document.documentElement)
);
if (documentReplaced) {
const timeoutId = setTimeout(_handleListenerDetached);
window.dispatchEvent(new CustomEvent(eventName, {detail: timeoutId}));
}
}).observe(document, { childList: true });
function _handleListenerDetached() {
// reattach event listener
window.addEventListener(eventName, _handleListenerStillAttached);
// run registered callbacks
callbacks.forEach((callback) => callback());
}
function _handleListenerStillAttached(event) {
clearTimeout(event.detail);
}
return {
addListener: c => void callbacks.add(c),
hasListener: c => callbacks.has(c),
removeListener: c => callbacks.delete(c)
}
})();
Related
I'm working on the "Approve All" button. The process here is when I click "Approve All," each individual "Approve" button will be triggered as "click" all at once, and then it will send POST requests to the controller. However, when I clicked Approve All button, there was a race condition causing the controller returns Error 500: Internal server error. I have tried using JS setTimeout() with value 1500*iter, but when the iterator gets higher, for example at i = 100, then it would take 1500*100 => 150000ms (150s). I hope that explains the problem clearly. Is there a way to prevent such a case?
Here is my code, I'm using JQuery:
let inspection = $this.parents("li").find("ul button.approve"); // this will get all 'approve' button to be clicked at once
inspection.each((i,e)=>{
(function () {
setTimeout(function () {
$(e).data("note",r);
$(e).click();
}, 1500 * i); // this acts like a queue, but when i > 100, it takes even longer to send POST requests.
})(this,i,e,r);
});
// then, each iteration will send a POST request to the controller.
$("#data-inspection ul button.approve").on("click", function() {
// send POST requests
});
Any help would be much appreciated. Thank you.
That 500 error may also be the server crashing from being unable to process all the requests simultaneously.
What I'd recommend is using an event-driven approach instead of setTimeout. Your 1500ms is basically a guess - you don't know whether clicks will happen too quickly, or if you'll leave users waiting unnecessarily.
I'll demonstrate without jQuery how to do it, and leave the jQuery implementation up to you:
// use a .js- class to target buttons your buttons directly,
// simplifying your selectors, and making them DOM agnostic
const buttonEls = document.querySelectorAll('.js-my-button');
const buttonsContainer = document.querySelector('.js-buttons-container');
const startRequestsEvent = new CustomEvent('customrequestsuccess');
// convert the DOMCollection to an array when passing it in
const handleRequestSuccess = dispatchNextClickFactory([...buttonEls]);
buttonsContainer.addEventListener('click', handleButtonClick);
buttonsContainer.addEventListener(
'customrequestsuccess',
handleRequestSuccess
);
// start the requests by dispatching the event buttonsContainer
// is listening for
buttonsContainer.dispatchEvent(startRequestsEvent);
// This function is a closure:
// - it accepts an argument
// - it returns a new function (the actual event listener)
// - the returned function has access to the variables defined
// in its outer scope
// Note that we don't care what elements are passed in - all we
// know is that we have a list of elements
function dispatchNextClickFactory(elements) {
let pendingElements = [...elements];
function dispatchNextClick() {
// get the first element that hasn't been clicked
const element = pendingElements.find(Boolean);
if (element) {
const clickEvent = new MouseEvent('click', {bubbles: true});
// dispatch a click on the element
element.dispatchEvent(clickEvent);
// remove the element from the pending elements
pendingElements = pendingElements.filter((_, i) => i > 0);
}
}
return dispatchNextClick;
}
// use event delegation to mitigate adding n number of listeners to
// n number of buttons - attach to a common parent
function handleButtonClick(event => {
const {target} = event
if (target.classList.contains('js-my-button')) {
fetch(myUrl)
.then(() => {
// dispatch event to DOM indicating request is complete when the
// request succeeds
const completeEvent = new CustomEvent('customrequestsuccess');
target.dispatchEvent(completeEvent);
})
}
})
There are a number of improvements that can be made here, but the main ideas here are that:
one should avoid magic numbers - we don't know how slowly or quickly requests are going to be processed
requests are asynchronous - we can determine explicitly when they succeed or fail
DOM events are powerful
when a DOM event is handled, we do something with the event
when some event happens that we want other things to know about, we can dispatch custom events. We can attach as many handlers to as many elements as we want for each event we dispatch - it's just an event, and any element may do anything with that event. e.g. we could make every element in the DOM flash if we wanted to by attaching a listener to every element for a specific event
Note: this code is untested
I was confused when reading code of stream.Readable in Node.js.
here is source code:
https://github.com/nodejs/node/blob/master/lib/_stream_readable.js#L778-L799
Readable.prototype.on = function(ev, fn) {
const res = Stream.prototype.on.call(this, ev, fn);
if (ev === 'data') {
// Start flowing on next tick if stream isn't explicitly paused
if (this._readableState.flowing !== false)
this.resume();
} else if (ev === 'readable') {
const state = this._readableState;
if (!state.endEmitted && !state.readableListening) {
state.readableListening = state.needReadable = true;
state.emittedReadable = false;
if (!state.reading) {
process.nextTick(nReadingNextTick, this);
} else if (state.length) {
emitReadable(this);
}
}
}
return res;
};
Obviously, the if statements only handle data and Readable event, but according to the API document, the on method of stream.readable also accept other events such as close , end , error.
So my question is :
According to the source code, how did stream.Readable handle other events except data and readable?
What you are seeing here is an override for the .on() method so that the Readable class can watch what event listeners are being attached and can do something special when someone installs a listener for the data event or the readable event.
The first line of this function:
const res = Stream.prototype.on.call(this, ev, fn);
is where the Readable passes the callback and event name arguments to its parent so that the normal implementation will be run. A Stream implements the EventEmitter interface so calling the super method with Stream.prototype.on.call(this, ev, fn) will give .on() it's expected default behavior.
Then after calling the parent, it checks to see if the event that someone is listening to is the data event or readable event and then implements a little extra functionality when one of those event listeners is attached.
For the data event, it resumes the stream so that it will start flowing if it was paused and if it was set to flowing mode. This is probably because when a Readable is being initially created and configured, if it starts flowing the stream before the data event listener is attached, then data on the stream could be missed. So, it doesn't start flowing until someone is around to listen to data events.
Note, there are potentially lots of over events that occur on the stream and those are all handled by the call to the base class in the first line. What you are seeing here is just some special behavior that the Readable class wants to implement when two specific event listeners are first added. This code does not affect when those events are sent or how they are listened to. It just triggers a little behavior in the Readable state when a listener for one of these events is first attached.
I'm binding then unbinding the ready event listener to the document.
$(document).bind("ready", readyEventHandler);
function readyEventHandler() {
// run some code
$(document).unbind("ready");
}
The code produces no errors and will work. However, my javascript is cached and duplicates the code so I'll end up with having this code run more than once if I go back and then forward a page in the browser. When this happens, the ready event listener is not called at all. Am I properly unbinding this event listener? I know the caching issue becomes problematic(it's own separate issue) but I just want to bind the ready event listener, have it run code, then unbind it.
Not so sure it will help, but here are my 2 cents - instead of trying to unbind the readyEventHandler - make sure that if you run the function once it will not run twice:
var readyHandlerRun = false;
$(document).bind("ready", readyEventHandler);
function readyEventHandler() {
if (readyHandlerRun) {
return;
}
readyHandlerRun = true;
// Rest of your code...
}
Another options that popped just now:
$(document).bind("ready", readyEventHandler);
function readyEventHandler() {
readyEventHandler = function() { }
console.log('ready');
// Rest of your code...
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
UPDATE (by #jason328)
After talking with Dekel he provided me the appropriate answer.
$(document).bind("ready", function() {
readyEventHandler();
readyEventHandler = function() { }
});
Elegant and works like a charm!
if you just would like to use an eventhamdler only once, you could use one instead of bind
$(document).one("ready", function() {
// code to run on document ready just for once
});
I am working on a JavaScript library for some data collection. I need to capture all click events, and want to traverse the event path looking for specific tags/ids/classes on click.
If I run the following code without the setTimeout, the event path always comes back with a single object in the array for Window. Add the setTimeout and the event path comes as expected.
window.addEventListener('click', function (event) {
// setTimeout with a 4ms delay to push to the end of the JS execution queue.
setTimeout(function() {
console.log(event.path);
}, 4);
});
You should wait for window to load, use below code so you should get the same result without the time delay:
window.onload = function() {
document.addEventListener('click', function (event) {
console.log(event.path);
});
};
I am writing a script where I would like to handle mouse events only if they have not been handled by any other element before.
I can attach an event listener to the document object, but it will receive all events regardless of whether they have been already handled.
I have no control over the elements in the HTML page so I cannot manually stopPropagation() when the event is handled.
Any ideas?
From this article here.
It seems its not yet possible to do this.
Which event handlers are registered?
One problem of the current implementation of W3C’s event registration
model is that you can’t find out if any event handlers are already
registered to an element. In the traditional model you could do:
alert(element.onclick)
and you see the function that’s registered to
it, or undefined if nothing is registered. Only in its very recent DOM
Level 3 Events W3C adds an
eventListenerList
to store a list of event
handlers that are currently registered on an element. This
functionality is not yet supported by any browser, it’s too new.
However, the problem has been addressed.
Fortunately
removeEventListener()
doesn’t give any errors if the event
listener you want to remove has not been added to the element, so when
in doubt you can always use removeEventListener().
So, you can accomplish something like you want under a certain set of circumstances. Specifically, if
You can load your own custom JavaScript before the original
You know which elements you are listening for others to throw events on
Effectively, what you do is replace the original addEventListener method on the target element with a custom one that intercepts the call, does some special processing, and then lets it continue per normal. This 'special processing' is a new function that wraps the original callback, and marks the event arguments with some state to let you know someone else handeled the event already. Here is a proof of concept (with a jsFiddle)
Target HTML:
<div id='asdf'>asdf</div>
JavaScript:
var target = document.getElementById('asdf');
var original = target.addEventListener;
var updated = function(){
// Grab the original callback, so we can use it in our wrapper
var originalFunc = arguments[1];
// Create new function for the callback, that lets us set a state letting us know it has been handled by someone
// Finish the callback by executing the original callback
var newFunc = function(e){
console.log('haha, intercepted you');
e.intercepted = true;
originalFunc.call(this, e);
};
// Set the new function in place in the original arguments 'array'
arguments[1] = newFunc;
// Perform the standard addEventListener logic with our new wrapper function
original.apply(this, arguments);
};
// Set the addEventListener on our target to our modified version
target.addEventListener = updated;
// Standard event handling
target.addEventListener('click', function(e){
console.log('original click');
console.log('intercepted?', e.intercepted);
})