Here is my entry for the QuirksMode addEvent() recoding contest.
Its not the smallest or simplest, but this was a project that I started before the competition was announced and its main purpose is to solve a much larger problem.
My solution provides an implementation of addEventListener and removeEventListener that is consistent across today’s most popular browsers, supporting event bubbling and event capture (both the strict and non strict variants).
It..
- Implements full W3C emulation layer for browsers that don’t have a native addEventListener.
- Emulates both event capture and event bubbling and provides a way to emulate the strict W3C and ‘consensual’ modes by dealing with some quirks in current browsers.
- Plays well with other code and the mixing of pre-existing inline and assigned event listeners.
- Allows event cancelling.
- Does not leak in itself but is aware of the IE leak patterns and implements a focused scrubbing garbage collector to deal with the common and not so common leakage issues. The GC can be set to to agressive or lazy.
Read the full blog entry if you want to get the whole story.
I have tested this under IE5 (thanks Emanuele Aina) , IE5.5, IE6, Safari1.3.1, Firefox1.0.5, Firefox1.5beta, Opera8.5, Netscape 6.2.3, Netscape 7.2, Netscape 8, Mozilla 1.7.12.
I have not tested it under v4 browsers, but I see no reason why it would not work in emulation mode with some minor tweaks.
It won’t work under IE5.0 unless I can somehow implement call or apply and splice in JavaScript for IE5 🙂
You can view the actual implementation in mw_lib_page_element_addhandler.js and there is some supporting code in mw_lib_page_element.js
And if you just want the js files, here they are.
mw_lib_page_element_addhandler.js
mw_lib_page_element.js
mw_lib_page.js
mw_lib.js
How To Use
Download the JavaScript library files
Then include them using the following script includes
<script language="JavaScript" src="mw_lib.js"></script>
<script language="JavaScript" src="mw_lib_page.js"></script>
<script language="JavaScript" src="mw_lib_page_element.js"></script>
<script language="JavaScript" src="mw_lib_page_element_addhandler.js"></script>
Four functions are provided
addEventListener
MetaWrap.Page.Element.addEventListener
(p_element,p_event_type,p_function,p_capture);
This is a complete replacement for W3C element.addEventListener. The parameters are the same as the W3C version of the function with the same name
removeEventListener
MetaWrap.Page.Element.removeEventListener
(p_element,p_event_type,p_function,p_capture);
This is a complete replacement for W3C element.removeEventListener.The parameters are the same as the W3C version of the function with the same name
Here is a testcase that tests both addEventListener and removeEventListener.
deleteListeners
MetaWrap.Page.deleteListeners();
This function implements the classic IE memory leak fix and should only be executed at window.onunload
garbageCollectListeners
MetaWrap.Page.garbageCollectListeners
();
This function will garbage collect any dangling element references. There is a global variable MetaWrap.Page.m_listeners_auto_garbage that if set to true will force a garbage collect after every event. Its default value is false.
Here is a testcase for garbageCollectListeners and here is another.
If requested by enough people, I will release a ‘compact’ stand alone version of the code. I have an explanation for why my code is so flowery at the end of this post with the heading “My Strange Coding Style”. 🙂
How It Works And Why
Hot on the heels of my last JavaScript project, I decided to get back into my JavaScript behavior library and found myself needing an implementation of addEventListener and removeEventListener that was consistent across all browsers. I researched the current state of the art and found an article on how you should not do it.
Taking this on board I decided to write own implementation, my aim being to fully support in IE everything that the W3C specification defines, so I ended up developing a solution that supports event bubbling and both the strict and non strict variants of event capture.
Now its debatable whether or not event capture is of any use, but in the past I have found it very useful in writing clean minimal code and I personally have a requirement for it.
The rules for how addEventListener (and the listeners themselves) should behave are very clearly defined, but perhaps not perfectly clear as it seems that only one of the major browsers (Opera) has followed the spec to the letter, and the rest seem to have either emulated each other, forming a de facto standard (Mozilla / Firefox / Netscape and Safari) and last but not least the browser with the most market share simply does not implement it at all (IE). Here is previous post about this issue that also points to a testcase.
The 6 Golden Rules Of W3C Events
I distilled the essential requirements from the W3C spec into the following 6 rules.
Rule 1
After initiating capture, all events of the specified type will be dispatched to the registered EventListener before being dispatched to any EventTargets beneath them in the tree. Events which are bubbling upward through the tree will not trigger an EventListener designated to use capture.
Rule 2
If an EventListener is added to an EventTarget while it is processing an event, it will *not* be triggered by the current actions but may be triggered during a later stage of event flow, such as the bubbling phase.
Rule 3
If multiple identical EventListeners are registered on the same EventTarget with the same parameters the duplicate instances are discarded. They do not cause the EventListener to be called twice and since they are discarded they do not need to be removed with the removeEventListener method.
Rule 4
If a listener was registered twice, one with capture and one without, each must be removed separately. Removal of a capturing listener does not affect a non-capturing version of the same listener, and vice versa.
Rule 5
Even if an event is captured or bubbles up, the target/srcElement always remains the element the event took place on.
Rule 6
A capturing EventListener will not be triggered by events dispatched directly to the EventTarget upon which it is registered.
Capture vs Bubble
Rule 1 describes the order in which the listeners are triggered and Rule 5 describes what should be passed to the listener. I try to think of the top level of a document, the <HTML> element for example, as the surface of a pond with all the other elements being below the surface at depths determined by parent/child relationships. Imagine you throw a rock into the pool at a particular element, the water captures the rock and down it falls till it hits that element. The element then bubbles and the bubbles rise to the surface.
That is essentially what is happening during the capture and bubbling phases.
The added step is that as the rock passes each element, in the capture phase, if the element has events listening on capture, those events are triggered, and in the bubbling phase, only those listeners not registered to capture are triggered. The most shallow capture event fires first, and the most shallow bubbling event fires last.
Down And Up
I wrote a series of test cases and then I implemented MetaWrap.Page.Element.addEventListener. When I completed the first version, I went looking for some critical test cases, I came across this competition.
“Hence I’d like to take the opportunity to launch an
addEvent()
recoding contest. Write your own version ofaddEvent()
andremoveEvent()
, submit it by adding a comment to this page, and win JavaScript fame.”
When I first saw it, there were no comments and thus no entries submitted, so I thought to myself.. hmmm.. “First Post?” 🙂 It was 11pm, and approaching the dark side of breakfast, but I decided to go for it.
I downloaded the test page and substituted my code. I was disappointed, It kind of worked in IE, but didn’t have the same behavior in the sub menu elements as it did under W3C browsers, do I decided that there must be something wrong with my implementation, which was odd given the number of test-cases that I threw at it.
I decided to give first post a miss because I had to get some sleep and another project deadline was looming and I needed my wits about me.
A week later I audited my code and found something that was in theory, an issue, but it did not look like it would affect the competition code. The issue was so do with what happened when you registered event listeners during an event (see Rule 2). Eg. on element X, onclick is fired and then then modifies the onclick for element X and Y.
My interpretation of Rule 2 is that, if during an event of a given event type, you modify the listeners for that event type on the current element, you won’t see the new listeners executed till the next time that event is triggered. If you modify listeners of that event type on other elements that the event will be visiting as part of the bubbling or capture flow for this event, these listeners will be executed.
My code was violating this, you could in theory add a new handler on the event which would get fired and then add a new but different handler that removed the first and so on, so it was possible to throw the browser into an infinite loop of event handlers that leap-frogged each other into oblivion, which in retrospect makes Rule 2 a Good Thing.
Here is is a set of tests case for Rule 2. one, two, three, four.
Rule 3 describes what should happen when a listener is registered and how to deal with duplicates my implemenation obeys this, so no problem there.
So that was a dead end. So still no joy. So I decided to see if a solution to the competition actually existed, maybe I was missing something fundamental. I recoded the competition page with the added events in-lined in the correct order and it behaved exactly the same as my code.
So something else was obviously up.
I wrote a test case so that I could log exactly which event was being fired and in what order and compared results for IE6 and Firefox when from above the elements dragging the mouse down over “Item 1” then up off it again.
node | event | type | target | this/currentTarget |
LI | showSubNav | mouseover | A1 | li1 |
LI | showBorder | mouseover | A1 | li1 |
LI | hideSubNav | mouseout | A1 | li1 |
LI | hideBorder | mouseout | A1 | li1 |
IE6
node | event | type | target | this/currentTarget |
LI | showSubNav | mouseover | li1 | li1 |
LI | showBorder | mouseover | li1 | li1 |
LI | hideSubNav | mouseout | li1 | li1 |
LI | hideBorder | mouseout | li1 | li1 |
Firefox
Looks like the issue was the mouseover/mouseout was targeted on the UL in Firefox, but on the A element in IE6.
Now the events are only listening on the LI elements but in IE the bubbled event is targeted by the A. So the mouseover/mouseouts are bubbling down to the parent LI and are being executed, but they are only reacting to movement on the A element. The trick was to make the LI wider.
To clarify this, from the point of view of the events, in IE, you are rolling the mouse over the A, but in Firefox, you are rolling the mouse over the LI which has plenty of area to roll around and activate the sub menu items.
So the competition has a sting, its not a simple case of sorting out addEvent, the competition example itself suffers from a CSS compatibility error which is going to mean its always a little flaky under IE even if you restyle the LI to be wider.
After making this width adjustment, my solution worked perfectly!
Later on after re-reading the competition page It dawned on me that I had been warned about this but had mis-interpreted the warning.
- Your entry should exhibit the same behaviour as this example page, which uses Scott Andrew’s old functions. The example page doesn’t work in Explorer, but your entry should. The mouseout functions on the example page don’t work perfectly, but for the contest that doesn’t matter.
Browser Quirks
Mixed Mode Events
The following code results in different behavior in IE6 and Firefox 1.5
MetaWrap.Page.Element.addEventListener(l_a1,"click",f1,true);
MetaWrap.Page.Element.addEventListener(l_a1,"click",f2,true);
l_a1.onclick = f3;
l_a1.onclick = f4;
In IE or Opera if you click on the element, just the f4 listener is executed.
In Firefox and Safari if you click on the element, just the f1,f2 and f4 listener are executed, in that order.
I have created a test case that tests mixed mode event listener assignment in various orders.
It looks like Firefox treats the assignment
element.onevent = listener;
as the following sequence
removeEventListener(element,event,element.onevent,false);
addEventListener(element,event,listener,false);
It would be ‘possible’ to emulate this behavior, but it would be computationally expensive, not 100% reliable and probably not very practical. A possible method would be to, on each event, walk the list of all elements event types and check for consistency eg. event with listener stacks with a different master event handler than MetaWrap.Page.Element.listenerShimFunction.
[UPDATE….
With very little change I have emulated two issues which were naging at me.
The first is an issue when listeners were added with the following code.
l_a1.onclick = f1;
MetaWrap.Page.Element.addEventListener(l_a1,"click",f2,true);
Under IE emulation mode and Opera, f1 will never fire because it is overwritten by f2. In Safari, they fire in the orfer f1,f2. In Mozilla based browsers f1 is preserved so both fire in the order f1,f2. This is because the Mozilla based browsers do something extra in addEventListener, they preserve the existing inline/assigned listeners as a captured event. Here is a testcase that shows the default behavior using the standard addEventListener reacting with different mixed mode events.
In the latest version of my library I emulate the default behavior of modern Mozilla based browsers for all browsers.
I am only trialing this emulation, but it seems to make a lot of sense. It could however be effort for very little gain – my only justification for this is.
- It is low cost and standardises behaviors (and yes I choose Mozilla as the default behavior because of point 2)
- Its going to make more robust code. When using the library in existing code I don’t want to inadvertently clobber the existing inline listeners so I have decided to (hopefuly) ‘do no harm’.
In Safari, they fire in the order f1,f2 – from what I can tell Safari events are rather broken.
The following code in Safari (tested against version 1.3.1)
var l_a5 = document.getElementById("a5");
l_a5.onclick = f1;
l_a5.onclick = null;
l_a5.addEventListener("click",f2,true);
Clicking on the element referenced by l_a5 will result in both events triggering in the order f2,f1
On every other browser that supports element.addEventListener, only f2 triggers.
Here is a testcase – try the last test and you will see what I mean.
The second is for the following sequence
MetaWrap.Page.Element.addEventListener(l_a3,"click",f1,true); l_a3.onclick = f3; l_a3.onclick = f4; MetaWrap.Page.Element.addEventListener(l_a3,"click",f2,true);
Which now works consistently across all browsers, even in emulation mode.
I’m not sure if I will keep these patches (they are clearly labeled in the source code). They make the code ugly, and they are little more then a token effort to fix a whole family of problems that occur if you start mixing the way you add event listeners. See this testcase.
They do however fix some obvious collisions that occur when adding this library to pre-existing code. The majority of the remaining problems would be an issue in the existing code anyway.
so..
I am of two minds on this one.
…]
Deleted Elements
Rule 4 covers listener removal. When an element is removed, what happens to its registered events? At the moment these are going to stick around in memory. Until the DOM mutation notification event becomes widely available and all other browsers are long dead (5 years?) we can’t really deal with this efficiently, so the best we can do is have a simple garbage collect phase. To this effect the following method is made available.
MetaWrap.Page.garbageCollectListeners();
This will perform a garbage collect pass and remove all listeners for elements that are no longer part of the document.
There are two testcases for this method.. one tests elements removed by overwriting using innerHTML and the other tests elements that have been removed surgically with removeNode. The resulting element states are different in each case which garbage collector has to deal with cleanly.
Browser Compliance To W3 Spec
Opera seems to be the only browser that follows Rule 6, which complicates things. I can emulate the right way, or I can emulate the way that everyone else has implemented it. I have decided to do both, to which end the following global variable exists.
MetaWrap.Page.m_listeners_strict_w3c =
false;
Its default behavior (false) is to force all browsers to behave like Netscape/Mozilla/Firefox and Safari. If we are running Opera, we execute the same emulation mode that we use for IE.
If you set its value to true however, it will emulate for all other browsers the way that Opera handles its event propagation.
The logic behind this decision is rational yet complicated but it comes down to providing a choice for the developer who can choose performance vs compliance but still maintain consistency across all browsers.
Here is previous post about this issue about the Rest Of The world vs Opera.
Here is a testcase that demonstrates the library with MetaWrap.Page.m_listeners_strict_w3c set to false.
Here is a testcase that demonstrates the library with MetaWrap.Page.m_listeners_strict_w3c set to true.
My Strange Coding Style
I work at a web development company called Massive Interactive in Sydney Australia where part of my role is to be a technology storm-trooper and develop code and libraries that can be passed on to others for use or further development.
I too can code strange JavaScript built with nothing but single character variables, ternary statements, object hash tricks and K&R styling but I choose not to because much of what I develop needs to be maintained by other people. I code to be understandable and maintainable. I am painfully aware of the cost of name spacing, the cost of the size of variable names and even the load time cost of comments , luckily the run time cost for comments and formatting is close to zero. But I choose to code in this way because I would rather have the performance hit than have code that is hard to maintain.
I’m a firm believer in keeping the primary JavaScript code base neat and tidy, when it comes to rationalising the code for production – that is where the automatic composition and compression utilities come in.
If enough people want a ‘compressed’ version of the functions, I will happily provide some, but I’m sure there is going to be some bugfixes as soon as people start playing with this.
> It won’t work under IE5.0 unless I can somehow implement
> call or apply in JavaScript for IE5 🙂
Maybe it can be emulated using the square backets syntax to access properties.
This is the code if we have call():
func.call(obj, arg1, arg2);
That should become:
obj[‘asdf-long-name-to-avoid-collision’] = func;
obj[‘asdf-long-name-to-avoid-collision’](arg1, arg2);
Thanks for that. I was thinking that that would be my only hope, assuming the IE5 event object allowed me to fiddle with it.
Here is a testcase for a fake "call()"
http://test.metawrap.com/javascript/tests/fundamental/test_24_call.html