Don't Tab Me Bro… (or jQuery's preventDefault vs. browser tabs)
The project I’m currently working just had a big update, and part of that update was a reworking of the integration with Mixpanel to track usage of the site. Mixpanel is great, except that they don’t provide a synchronous Javascript API for tracking events. This generally isn’t a problem as most of the events we want to track do not result in a full page reload. But. But. To track links to external pages correctly, we need to use Mixpanel’s callback API to log the event before the browser gets sent on its merry way:
Those of you in the audience who haven’t dozed off may have already
picked up on the problem: event.preventDefault();
+
document.location = a_href;
= inability to open said link in a new
tab/window – unless you do a context-menu dance.
-
We need to
preventDefault()
because if we don’t, the browser heads off to the link, possibly aborting our attempt to track the click. -
We need to pass the callback to
mpq()
because of #1, so that the browser eventually goes to the link. -
The browser is sent to the original
href
programmatically by the callback – after Mixpanel does it’s thing. This action is only indirectly in response to the original click, the native event that triggered this whole process has been discarded. -
If the user originally ⌘-clicked (ctrl-click for the non-Mac people) to open the link in the new tab, they lose. The link opens in the current window/tab. The user gets annoyed.
Ironically I’ve seen similar behaviour on other sites, and just thought the developers were incompetent, or perhaps slightly malicious. Now I’ve become what I hate…
A demo
Try to open the following link in a new tab:
What happens is, as described earlier, the browser’s default response
to the link is suppressed via preventDefault()
. Then, after our
hypothetical asynchronous logging method has completed, the callback
is, er, called back, setting the browser’s location:
Well, that just won’t do, so we need to come up with a workaround. How
about just retriggering the event? The first time I tried this, I just
ended up with Javascript errors, so I deleted that code. But for this
writing I tried dispatchEvent()
which actually retriggers the jQuery
handlers:
But since we called preventDefault()
the first time round, the
retriggered event also skips the browser’s default action. But,
we’re on the right track, and when you try something, and it doesn’t
work, then the next step is always do the same thing with a copy of
the original:
Cool! It works. Wrapping it up in a proper little function is left as an exercise to the reader. Another method to try would be triggering the callback via plain old jQuery:
It doesn’t work, and from reading the documentation:
Although
.trigger()
simulates an event activation, complete with a synthesized event object, it does not perfectly replicate a naturally-occurring event.To trigger handlers bound via jQuery without also triggering the native event, use
.triggerHandler()
instead.
(Emphasis mine) I’m not really sure if should work or not. The first paragraph would indicate maybe, the second implies yes. I’m probably doing something wrong.
I imagine this is documented somewhere in one of those Javascript: all the awesome stuff, none of the crap–type books, but I don’t have copies of them, so I can’t check. If you can point me to a good discussion about this, please do. As for my project, I’ll be reworking the tracking code to get my open-in-new-tab functionality back as soon as I get time, and test in Internet Explorer, and…