(back to the demo page…)

How it works in Safari

I've had a handful of people ask how I got this to work in Safari. Here's what I've told them:

The two problems that have prevented fragment identifier history from being implemented in Safari are:

  1. Safari will not normally add a changed fragment identifier to the history unless the change is initiated by a user clicking on a link.
    For example, a user clicking on <a href="#foo">click here</a> will reliably add an entry to Safari's history. But, <script type="text/javascript">location.href="#foo";</script> will only replace the current history entry instead of adding a new one.
    Example: a user is at "index.html", then clicks on a link that takes them to "index.html#start", then is taken to "index.html#foo" by a javascript, then clicks on a link that takes them to "index.html#end". If the user then clicks on the back button repeatedly, they will see the following sequence items in their address bar: "index.html#end", then "index.html#foo", then "index.html". "index.html#start" is missing, because it was overwritten by the scripted change to "index.html#foo".
  2. Safari does not report the correct value of location.hash when the user has clicked the "Back" button. Instead, it reports the location.hash from *before* the "Back" button was clicked.
    Example: I'm at "index.html#one". If I click a link to "index.html#two", then click on the "Back" button, the value of location.hash will still be "#two" (even though the address bar says "index.html#one").

The <IFRAME> workaround (which is used for Internet Explorer) cannot be used to work around either of these problem because Safari does not reliably create history entries for page changes that occur in frames.

So, I had to come up with a workaround for each of these problems.

  1. I found that I could overcome the first problem by submitting a form to the fragment identifier I wanted to load (example: <form name="x" method="GET"></form><script type="text/javascript">function goTo(fragment_identifier) { document.forms.x.action=fragment_identifier;x.submit();} goTo("#foo");</script>). This action is scriptable and it will always add an entry to Safari's history. (One significant downside of this workaround is that will not work if there are any parameters in the page's URL - if the example I mentioned is used on a page whose URL is " index.html?lorum=ipsum", Safari will try to load "index.html#foo")
  2. The second problem isn't quite as easy to tackle. I found that the only script variable that Safari will reliably change when the "Back" button is pressed is document.body.scrollTop.
    To make this variable usable, I had to take control of it away from the user. This was done by applying the "overflow:hidden;" style to document.body, as well as adding some script to make it maintain the correct scrollTop during drag and drop events.
    The actual page is loaded in an IFRAME with a style of "position:fixed;width:100%;height:100%;top:0px;left:0px;border:0px;". Safari's support of the "position:fixed" style, which prevents that IFRAME from moving as (the parent) page is scrolled, allows the IFRAME to always be positioned correctly to completely fill the browser window.
    Whenever a change needs to be made to the fragment identifier, first an <a name="(…)"></a> is dynamically created at a unique, specific vertical position on the page (using an absolute-positioned DIV and spacer <img>s), and its vertical position is stored for future lookup. Next, a form is submitted to this fragment identifier (as described in the workaround for the first problem). This causes an entry to be added to Safari's history, and whenever the user returns to that entry (by clicking "Back"), Safari will scroll back to that <a> tag. A script can detect this by monitoring document.body.scrollTop, and can find the name of the fragment identifier the user has returned to by looking up the document.body.scrollTop in its list of <a> vertical positions (a spacer <img> with height=100% is added to the end of the bottom anchor so that scrollTop will be usable to find <a> positions at the last "screen" of the page).
    This all works great..until the user leaves the page to go to another site. If they click "Back" to return the page, all of the <a> tags have been lost, and so is the ability to accurately determine the correct location.hash. I solved this problem by keeping a hidden <textarea> updated with all of this information. If the user leaves the page then comes back, this information can be reloaded from the cached data in the <textarea> when they come back.
©2006 dbloom