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:
http://SomeLocation.com/historyIFrame.html?currentLocation=helloworld
http://SomeLocation.com/historyIFrame.html?currentLocation=helloworld2
http://SomeLocation.com/historyIFrame.html?currentLocation=helloworld3
http://SomeLocation.com/historyIFrame.html?currentLocation=helloworld4
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.
You can see the full source code for this technique here; it will work if you save it to your local file system, but not over http. I tested it in Safari 1.3. I've also tried the remote version in Safari 2.0 and it still does not work over HTTP.
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.
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:
<iframe>
<form id="historyForm" method="GET">
<textarea id="currentLocation"
name="currentLocation">
</textarea>
</form>
</iframe>
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:
http://SomeLocation.com/historyIFrame.html?currentLocation=helloworld
http://SomeLocation.com/historyIFrame.html?currentLocation=helloworld2
http://SomeLocation.com/historyIFrame.html?currentLocation=helloworld3
http://SomeLocation.com/historyIFrame.html?currentLocation=helloworld4
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.
You can see the full source code for this technique here; it will work if you save it to your local file system, but not over http. I tested it in Safari 1.3. I've also tried the remote version in Safari 2.0 and it still does not work over HTTP.
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.
Comments
This and many other serious issues have gone unresolved with multiple releases of Safari.
http://www.quirksmode.org/blog/mt-search.cgi?IncludeBlogs=2&search=Safari&CategoryID=3
I suggest we need to boycott Safari until Apple decides to revive it.
regards
Al
http://webkit.org/blog/?p=96
I find it handy for whats happening with Safari
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.
You've tested on Konqueror?
function internalLink(hash){
if ( /Konqueror|Safari|KHTML/.test(navigator.userAgent ) ) {
df=document.createElement('form');
df.setAttribute("method","get");
df.setAttribute("action", "#"+hash);
document.body.appendChild(df);
df.submit();
document.body.removeChild(df);
} else
document.location.href = "#"+hash;
return false;
}
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.
Check out my implementation for ideas.
http://positionabsolute.net/blog/2007/07/javascript-history-service.php
Cheers,
Matt
http://www.icoblog.com/2008/06/dhtml-bookmarks-using-jsurl.html
Full source and demos included - enjoy.