Incredibly Strange iOS Issue

Update 8/19/2010 @ 11:55 EDT

It looks like this may be a cross-domain issue after all.  I changed my testing methodology and was able to reproduce this issue without Gmail being involved. That’s the good news, the bad news is that even in a cross-domain scenario, access should simply be denied to the properties or an exception should be thrown.  In this case in Mobile Safari, the script dies at the point in execution where it attempts to access the property.

</update>

I’m posting this to my blog in hopes of finding some help; I’d love to file this issue as a bug report but I have no idea if it’s a bug in Gmail or a bug in Mobile Safari.  I’m inclined to think the latter; but the fact that I can consistently reproduce this issue in the manner described below leads me to think it could be something crazy that Gmail does.

Window.opener

The window.opener object is available on browser windows that have been spawned by other browser windows (either using Javascript or the classic target=”_blank”). It’s a reference to a window object. In general, it behaves entirely as expected on the iOS; except when you click a link from Gmail (and the issue occurs whether you’re using the Mobile Gmail version or the desktop version on an iOS device).

What Happens?

Clicking a link from Gmail on an iOS device causes the link to open in a new browser window. However, if you have a page that attempts to reference any properties of the window.opener object, you’ll receive the error:

TypeError: no default value

Here’s where it gets really strange, if I evaluate the window.opener object itself in any way, it reports that its a DOMWindow object. However, if I try to enumerate the properties of that object none are found.  If I try to access a property of a DOMWindow object by name, I receive the above error.

What I’ve Tried

I’ve tried and ruled out several possible methods of reproducing this issue without Gmail; in each case the window.opener object behaved normally and I was able to both enumerate the properties and read a property directly without an error:

  • I created a page with a link using target=”_blank”
  • I created a page using Javascript’s window.open to open a link
  • I hosted the pages on different domains to see if there was some cross-domain issue affecting it.

I’ve also tried a few workarounds; none of which prevented the error:

  • Check for the presence window.opener.location (or any specific property) before using it in an assignment.
  • Wrapping any references to properties of window.opener in a try / catch.

At this point, I’m pretty well stumped on what’s happening here; or if there’s any way for me to work around it.  I’ve set up a demo that I encourage you to email to yourself so you can see this bug in action.   Turn on the debug console in MobileSafari, click that link from the gmail web app, and you’ll see the errors.  Click that link from anywhere else, you’ll get every property of the window.opener object written to your console.

You can test this on the devices themselves or the emulators; the behavior is consistent.

One more thing…

It just wouldn’t be an Apple-related post without that line; Once you see the error, tap the icon to switch browser windows.  Safari will crash.

JavaScript Event & Event Method Bugs and Workarounds

Today I spent a good deal of my time dealing with Javascript event handling and delegation using the Prototype javascript library with relation to some forms in our current project. In addition to simply firing and catching events using actual events, this applications also make use of the click() and focus() methods to fire these events in certain circumstances without user interaction. The issues I’m discussing here are specific to radio buttons and checkboxes across the three major browsers–but primarily focused on Firefox and Safari. The provided example does not use Prototype–it is plain vanilla Javascript. However, the proposed workaround does rely on the Prototype library (sorry, it was just faster that way–and it’s also the implementation I used to solve the problem on my own).

What (I believe) the spec calls for:

When a radio button or checkbox is clicked with the mouse, the mouse click event will fire with it’s target object set to the radio button that was clicked. The radio button will also gain focus.

Both Internet Explorer and Firefox exhibit this behavior exactly (on true user clicks–we’ll get to the event simulation methods shortly). Safari, however, only fires the click event. The checkbox or radio button that was clicked does not receive focus as it should.

So let’s just always observe clicks and leave focus to the birds

I told you we’d get back to the event simulation methods in a minute. Both Firefox and Internet Explorer exhibit some strange behavior when using these methods vs. actual events. In Firefox, the click() method generates an event with the target set to the calling element–not the actual target of the click. This is inconsistent with both Firefox’s focus() method and Internet Exlorer and Safari’s handling of both the click() and focus() methods. This example (Firefox, Safari, and Opera compatible) demonstrates the issue.

So where does the problem start?

In the case when you need to force a particular radio button to be selected by default and you have additional Javascript logic that must run based on that selection. Let’s say you have 4 radio buttons as in the previous example and you wish the 3rd radio button to be selected by default (we’ll assume there’s some Javascript logic that must happen based on the user’s selection that must also occur with the default selection). Because the user will not be interacting with the default selection, we must rely on event methods to fire our events. So we have some challenges:

  • If we use click() and have our radio buttons listen for a click, Firefox will believe the target of the event is document. The radio button will be selected, but any additional logic based on knowing what was clicked (using event.target) will fail.
  • If we use focus() and have our radio buttons listen for focus, Safari will see the radio button receive focus correctly when the page loads as we will be using focus(). However, any actual user interaction will fail as the focus() event will not fire.

A workaround

I intend to open a bug in the Firefox bug tracker for this issue (if one is not already open; I didn’t find one with a quick glance through Bugzilla). Until then, I’ve written a little workaround that requires the Prototype library to function.

First, ensure that your radio buttons are set up to respond to focus events (since we know that Firefox will only react properly to those events when called programmatically.

$('my-form').getInputs("radio").invoke('observe', 'focus', eventHandler);

Now, observe for clicks to forward on:

$('my-form').getInputs("radio").invoke('observe', 'click', fakeClick);

Your fakeClick method should look like this:

fakeClick: function(e){
   var el = e.element();
   if(el.identify) { /* Filter out click() for FireFox */
      if(this.focused.identify() != el.identify()) {
         e.element.focus(); /* Throws the proper focus() for Safari */
      }
   }
}

The other aspect of this is that within your actual event handler, you need to be sure to set the this.focused to the element that currently has focus.

I’ve created an example Prototype-enabled JS class to show how the functionality works.

Conclusion

So there you have it, some basic information about a bug in Firefox and a bug in Safari that together make for some interesting times when handling events; and a workaround which I hope you will find useful in getting around these two bugs. Please note that the code is more of an example on how to implement it; though it can be copied verbatim if you wish.

Download ClickFix.js (2kb)

Update 5/14/2009: By pure coincidence, I’ve added to this post exactly 1 year to the day after I first wrote it. Please see a new post on a Workaround for form submit events not firing with the submit method.