Monday, September 05, 2005

Safari: No DHTML History Possible

I have bad news; it really is not possible to do either bookmarking or intercepting the back and forward buttons in the Safari web browser for AJAX applications.

But first, why would someone want to have bookmarking and back and forward support in AJAX/DHTML apps? First, users expect that web pages will behave a certain way, basicly that they can navigate through their actions using the back and forward controls. Second, bookmarking is important for users to be able to save their state in the middle of a web application, sharing it with friends through email and bookmarking it into their browser. AJAX applications must be able to support these functions to truly replace traditional non-AJAX web apps.

My main goal the last few weeks has been to create a simple API for AJAX applications, named DhtmlHistory, that supports these features. This API would make it possible to register an event handler to find out when the back and forward buttons have been pressed; the ability to register a new history event; and the ability for the framework to automatically update the browser's URL location bar when a new history event occurs, so that users can copy and paste the page's location, bookmark it, and send it around to friends to jump right into a specific state of an AJAX application.

I have the API working for Internet Explorer and FireFox (I need to do more QA work in those browsers though to make it rock solid), and spent much of last week trying to get this functionality going in Safari.

In Safari, this broke down into two segments: getting bookmarking working, and finding a reliable way to control the back and foward buttons and knowing when they are pressed.

On the bookmarking front, it is probably impossible due to two major problems in Safari. The only way to update a URL in an AJAX application is with an anchor, such as #foobar. This is because all other URL updates cause the page to completely reload due to browser security policies. In Safari, when you update the anchor, sometimes the page loading icon begins to spin continiously, never stopping; I tried many things to work around this behavior, but it did not work.

The other problem is that Safari is inconsistent in whether it puts URL changes due to anchors into the browser's history cache. It kinda does and kinda doesn't; if it just did one or the other things would be great. It turns out that if I change the page's location five times, using anchors, such as:

#helloworld1#helloworld2#helloworld3#helloworld4

that the browser places none of the page change locations into the history except the last one; if I press the back button, I am immediately brought back to the initial page load; pressing the forward button takes me back to the #helloworld4.

After trying a million combinations on this one, trying every wierd combination I could think of, I've come to the conclusion that Safari can not support the basic techniques needed for AJAX bookmarking. Strike one Safari.

Okay, so bookmarking isn't possible for AJAX apps. How about back and forward support; that would be good enough? After again trying every obscure and strange hack I could think of, I almost gave up thinking I could somehow hook myself into Safari's history until I finally found a way. This technique ultimately doesn't work out because of a strange, killer bug in Safari, but first, the technique.

The technique essentially involves using an iframe, with a form hidden inside of it that submits its values using a GET request. Here is the pseudocode for this technique:

When a developer wants to register a history event and new location, we simply grab the hidden iframe, set the 'currentLocation' field to the new location the developer wants, and then submit the hidden form. This causes the hidden iframe's location to change to match the results of the GET request, such as:

http://SomeLocation.com/historyIFrame.html?currentLocation=helloworld

Then, when the user jumps around with the back and forward buttons, the hidden iframe responds, jumping back and forth between different locations, with different results of the hidden form, such as:

which we can intercept from within the iframe, extract the currentLocation value from the URL, and pass back up to the containing page to notify a DHTML history listener.

Excited, I fleshed out the entire thing, adding in code to handle the small details necessary to make this technique effective, such as differentiating programmatic changes to the iframe's location from ones that happen from the back and forward buttons.

Thinking I was the man, I uploaded my code to my web server and accessed the page through Safari, and ran into a killer Safari bug that completely puts the kibosh on this technique. It turns out that Safari stores history for forms when loaded from "file://" URLs, but not from "http://" ones. These must be completely different code paths in Safari, where one works correctly and the other does not.

I've tried a variety of other techniques since this one, but all with diminishing returns and increasing complexity. The verdict: Safari really does not support what is necessary to intercept the back and forward buttons. Strike two Safari.

So here's the end result; Safari sucks as a platform for AJAX applications, which I will be writing a rant about soon. At this point I'm ending my explorations of trying to get advanced AJAX features working in Safari.

Safari is officially in the DHTML doghouse, joining our good buddy Internet Explorer. Unlike Internet Explorer, though, we don't have to bend to Safari's whims because it has close to nil market share; getting things to work in Safari is more about being a good Internet citizen versus a project necessity. Getting things working in Internet Explorer is the difference between success and failure.

To Safari I say: you owe me sixty bucks for the time I spent in Internet cafes trying to get this stuff to work, since I don't own a Mac. I will recommend to my clients that they not target Safari for advanced AJAX applications until Safari becomes a DHTML leader rather than a follower.

If Safari wants to get out of the doghouse, they must either completely emulate Firefox's behavior around hidden iframes and anchors, or they must support an API for DHTML history-like functions. I will be posting a Really Simple spec for DHTML history functions soon that it would be great for browser makers to support.

Bookmarkability is possible but only to an extent. If you try to update the location field from javascript you have the "infinite loading bug". When it's a normal link you don't have this problem.

So the solution is to use a normal link: do what you have to do with javascript when the user clicks the button (the link), then return "true" so that Safari follows the link (which is only an anchor so no page is reloaded).

Could this work with a dragable area? Maybe if you make it a link and change the href with javascript. Maybe not either.

I am correct in assuming that this testing does not cover the upcoming version of Safari for Leopard (beta builds of Webkit)? I ask because I am not familiar with the remote testing process your using Brad.

If Michel's tip doesn't strike gold, I'd try the contrived procedure of crafting a link and synthesizing a mouse click event on it to change the hash, rather than doing it by assignment.

But sanity lies the way your mind points; writing up good specs for how these things ought to work is a good path. I'm cheering you on from the back, hopeful the future holds fewer ridiculous hacks and needed toolkits to accomplish things on the web.

I needed a way to reliably set a hash value in the URL that worked on all the major browsers for AJAX bookmarking purposes, and I came across that same annoying forever-spinning loading icon bug in Safari. I wrote this function as a workaround and it did the trick for me.

What this does on the buggy browsers is create a dummy form whose sole purpose is to submit via the get method and then disappear. To use this function, simply call internalLink(hash) instead of setting document.location.href="#hash".I'm not sure if it's necessary for konqueror as I don't have it here to test, but supposedly it behaves similarly to safari with regard to internal links. I hope the code comes through OK in this comment and helps someone.

There is really strange. I've used a very similar technique and not had a problem with Safari. Instead of nesting the submission form, have it on the top level document and set the target to your frame.