Repository: browserstate/history.js Branch: master Commit: 8e4c08323f4d Files: 117 Total size: 7.9 MB Directory structure: gitextract_qkioeboz/ ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── HISTORY.md ├── LICENSE.md ├── README.md ├── bower.json ├── buildr-uncompressed.coffee ├── buildr.coffee ├── component.json ├── demo/ │ ├── bcherry-orig.html │ ├── bcherry.html │ ├── chrome.html │ ├── index.html │ ├── native-auto.html │ ├── native.html │ ├── navigator.html │ ├── safari.html │ └── unicode.html ├── package.json ├── scripts/ │ ├── bundled/ │ │ ├── html4+html5/ │ │ │ ├── dojo.history.js │ │ │ ├── extjs.history.js │ │ │ ├── jquery.history.js │ │ │ ├── mootools.history.js │ │ │ ├── native.history.js │ │ │ ├── right.history.js │ │ │ └── zepto.history.js │ │ └── html5/ │ │ ├── dojo.history.js │ │ ├── extjs.history.js │ │ ├── jquery.history.js │ │ ├── mootools.history.js │ │ ├── native.history.js │ │ ├── right.history.js │ │ └── zepto.history.js │ ├── bundled-uncompressed/ │ │ ├── html4+html5/ │ │ │ ├── dojo.history.js │ │ │ ├── extjs.history.js │ │ │ ├── jquery.history.js │ │ │ ├── mootools.history.js │ │ │ ├── native.history.js │ │ │ ├── right.history.js │ │ │ └── zepto.history.js │ │ └── html5/ │ │ ├── dojo.history.js │ │ ├── extjs.history.js │ │ ├── jquery.history.js │ │ ├── mootools.history.js │ │ ├── native.history.js │ │ ├── right.history.js │ │ └── zepto.history.js │ ├── compressed/ │ │ ├── history.adapter.dojo.js │ │ ├── history.adapter.extjs.js │ │ ├── history.adapter.jquery.js │ │ ├── history.adapter.mootools.js │ │ ├── history.adapter.native.js │ │ ├── history.adapter.right.js │ │ ├── history.adapter.zepto.js │ │ ├── history.html4.js │ │ ├── history.js │ │ └── json2.js │ └── uncompressed/ │ ├── history.adapter.dojo.js │ ├── history.adapter.extjs.js │ ├── history.adapter.jquery.js │ ├── history.adapter.mootools.js │ ├── history.adapter.native.js │ ├── history.adapter.right.js │ ├── history.adapter.zepto.js │ ├── history.html4.js │ ├── history.js │ └── json2.js ├── tests/ │ ├── .htaccess │ ├── html4+html5.dojo.html │ ├── html4+html5.extjs.html │ ├── html4+html5.jquery.html │ ├── html4+html5.mootools.html │ ├── html4+html5.native.html │ ├── html4+html5.right.html │ ├── html4+html5.zepto.html │ ├── html5.dojo.html │ ├── html5.extjs.html │ ├── html5.jquery.html │ ├── html5.mootools.html │ ├── html5.native.html │ ├── html5.right.html │ ├── html5.zepto.html │ ├── image.php │ ├── index.html │ └── tests.js ├── tests.src/ │ ├── _header.php │ ├── all.php │ ├── each.php │ └── index.php └── vendor/ ├── dojo.js ├── extjs.js ├── jquery.js ├── mootools.js ├── qunit/ │ ├── .gitignore │ ├── AUTHORS.txt │ ├── History.md │ ├── README.md │ ├── grunt.js │ ├── package.json │ ├── qunit/ │ │ ├── .jshintrc │ │ ├── qunit.css │ │ └── qunit.js │ └── test/ │ ├── .jshintrc │ ├── async.html │ ├── async.js │ ├── deepEqual.js │ ├── headless.html │ ├── index.html │ ├── logs.html │ ├── logs.js │ ├── narwhal-test.js │ ├── node-test.js │ ├── same.js │ ├── swarminject.js │ └── test.js ├── right.js └── zepto.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ github: [balupton] patreon: bevry open_collective: bevry ko_fi: balupton liberapay: bevry custom: ['https://bevry.me/fund'] ================================================ FILE: .gitignore ================================================ .build /node_modules /.idea npm-debug.log ================================================ FILE: HISTORY.md ================================================ ## History - v1.8b2 - June 22 2013 - Introduced uncompressed bundled files #287 - Copied component.json to bower.json #291 and used right tag #286 - Fixed wrong argument passed to History.getHash() #305, #297, #292 (@Izkata) - v1.8b1 - May 31 2013 - Fixed "encoded string automatically unencoded on html5 browsers" #282, #236, #272 - v1.8a4 - February 13 2013 - Fixed coffee script warnings (ExtJS & Dojo adapter, IE 6 iFrame) - Updated qUnit to release 1.11.0, jQuery to release 1.9.1 - v1.8a3 - February 5 2013 - Added tests for Dojo and ExtJS - Changed setting of title and url - Testing status - All HTML5 Native Adapter fail in all browsers on Test 10 - All other HTML5 tests work fine - All other HTML4 tests fail in Test 4 (in IE) - v1.8a2 - January 21 2013 - Fixed hashchange / statechange triggers: e.g. if a user in a HTML5 browser clicks a link, statechange is fired, in a HTML4 browser only hashchange, but state has also changed - UTF8 / url encoding / decoding tested and so resolved: #64, #107, #108, #162, #177, #187, #205, #207, #210, #228, #251 - Fixed #244 - v1.8a1 - January 19 2013 - Pass options to `init()` as json is now supported - Added unicode demo - Introduced `getCurrentIndex()`, so you can get previous state by `History.getStateByIndex(getCurrentIndex()-1)` - Fixed HTML4 endless loop when url starts with a slash (e.g. /welcome/hello) #239, #232, #185 - Bundled and compressed all scripts - Responsed (and therefore solved) or old issues from balupton repository: #250, #242, #238, #234, #224, #213, #206, #195, #183, #181, #180, #179, #175, #168 - Fixed (or merged and therefore fixed) old issues from balupton repository: #248, #239, #236, #235, #229, #221, #220, #218, #215, #214, #212, #203, #191, #189, #172, #171, #166 - Feedback for UTF8 / url encode issues necessary, related problems (thanks to riyad) - #64, #107, #108, #162, #177, #187, #205, #207, #210, #228 - v1.7.2 - January 18 2013 - Updated project README - Integrated ExtJS Adapter (thanks to @seanadkinson!) - Merged from forks - added option to force no suid (@hrunting, @sbearcsiro) - provide a consistent URI-encoded document.location.href (@hrunting, @sbearcsiro) - Change History.getHash to return consistent hash, ala History.getLocationHref (@sbearcsiro) - Fix an issue where HTML4 replaceState wasn't firing when SUIDs were disabled and the url didn't change (@sbearcsiro) - Make extractId ignore the fragment if one is present (normally the hash is passed without an fragment) (@sbearcsiro) - Remove all encoding / decoding of URLs except when creating or extracting a fragment, based on the assumption that all inputs to replaceState / pushState are appropriately encoded (@sbearcsiro) - Change escapeHash/unescapeHash methods to use encodeURI/decodeURI instead of window.escape/unescape (@sbearcsiro) - Fixed issue #158 (@sbearcsiro) - isEmptyObject should use hasOwnProperty: prevents from always returning true if the Object.prototype is extended (@Alexander Johansson) - Add potential fix for IE8 not returning full hashed url from getLocationHref when hash contains encoded characters (@sbearcsiro) - Match current W3C popState event semantics for HTML4 (@STRML) - Added History.options.html4Mode for easier debugging (@gigafied) - Added History.options.delayInit (Boolean). (@gigafied) - Added error testing and quota relief for sessionStorage.setItem (@jamie-pate) - Fix IE 6 HTTPS warning (@Daniel15) - Fixed bug in html4 pushState function which left History in a busy state (@joelarson4) - Disable session storage if it's present but not working, thanks to @paulschreiber (@sbearcsiro) - Add Lakin Wecker's dojo adapter (@sbearcsiro) - Add dojo 1.8 tests (@sbearcsiro) - Change dojo adapter to not use dojo events, it seems to break other parts of dojo (@sbearcsiro) - Removes stray spaces so that the build script can run. (@billmag) - fixed an issue in Safari's Private Browsing mode where setItem throws an exception (@billmag) - Adds better error handling for the quota exceeded problem seen with the iPad. (@billmag) - Consolidated var in sessionStorage to the top of the function. Re-build compressed and bundled. (@billmag) - v1.7.1 - October 4 2011 - Added a new native adapter which is framework agnostic (can be used with, or without any framework) - Provided bundled files - Added RightJS adapter - Updated supported browser listing - Added sessionStorage support in core instead of optional Amplify.js Store support - Fixed issue with state id generation growing slower over time - Closes #104, #95, #102, #92, #81, #90, #94, #93, #91, #67, #83, #54, #45 - v1.7.0 - April 1 2011 - Added `History.enabled` property (refer to usage section). This reflects whether or not History.js is enabled for our particular browser. For instance, if we have not included support for a HTML4 browser and we are accessing through a HTML4 browser then `History.enabled` will be `false`. - Added (optional but recommended) Data Persistance and Synchronisation Support thanks to [AppendTo's](http://appendto.com/) [Amplify.js](http://amplifyjs.com/) (refer to installation and compatibility sections for details) - Made HTML5 SUIDs more transparent - [Reported](https://github.com/balupton/history.js/issues#issue/34) by [azago](https://github.com/azago) and [Mark Jaquith](http://markjaquith.com/) - Fixed Session Storage Issue - Reported by a whole bunch of different people; [one](https://github.com/balupton/history.js/issues#issue/36), [two](https://github.com/balupton/history.js/issues#issue/37), [three](http://getsatisfaction.com/balupton/topics/history_js_1_6_losing_state_after_manual_page_reload) - Fixed URL Encoding Issue - [Reported](https://github.com/balupton/history.js/issues/#issue/33) by [Rob Madole](http://robmadole.com/) - Disabled support for IE6,7,8 when using the Prototype Adapter (there is nothing we can do about this, it is due to a bug in the prototype library) - [Reported](https://github.com/balupton/history.js/issues#issue/39) by [Sindre Wimberger](http://sindre.at/) - URLs in the State Hashes for HTML4 Browsers are now even shorter - [Discussion](https://github.com/balupton/history.js/issues#issue/28) - Fixed a issue with the MooTools Adapter and JSON with IE7 and IE8 - v1.6.0 - March 22 2011 - Added Zepto adapter thanks to [Matt Garrett](http://twitter.com/#!/matthewgarrett) - The readme now references the supported versions of the libraries we use - Updated vendors to the most recent versions. jQuery 1.5.1 and Mootools 1.3.1 - Reverted versions of Safari iOS prior to version 4.3 to be HTML4 browsers, Safari iOS 4.3 is a HTML5 browser - Refined code in History.js and its adapters - Fixed issue with extra state being inserted on Safari 5 requiring an extra click on the back button to go home - [Reported](https://github.com/balupton/history.js/issues#issue/17) by [Rob Madole](http://robmadole.com/) - Fixed issue with Safari 5 and Safari iOS 4 sometimes failing to apply the state change under busy conditions - Solution conceived with [Matt Garrett](http://twitter.com/matthewgarrett) - Fixed issue with HTML4 browsers requiring a query-string in the urls of states - [Reported](https://github.com/balupton/history.js/issues#issue/26) by [azago](https://github.com/azago) - Fixed issue with HTML4 browsers requiring title in the states in order to use state data - [Reported](https://github.com/balupton/history.js/issues#issue/25) by [Jonathan McLaughlin](http://system-werks.com/) - Fixed issue with HTML4 browsers failing is a state is pushed/replaced twice in a row - [Reported](https://github.com/balupton/history.js/issues#issue/17) by [Joey Baker](http://byjoeybaker.com/) - **B/C BREAK:** The `statechange` event now only fires if the state has changed; it no longer fires on page initialisation. This is following the [Firefox 4 History API Changes](http://hacks.mozilla.org/2011/03/history-api-changes-in-firefox-4/) which we agree with - this breaks standard, but makes more sense. - v1.5.0 - February 12 2011 - Moved to UglifyJS instead of Google Closure - Split HTML4 functionality from HTML5 functionality - Installation details have changed (the filenames are different) - v1.4.1 - February 10 2011 - Added HTML History API Support for Safari 5 and Safari iOS 4.2.1 - Cleaned code a bit (mostly with unit tests) - v1.4.0 - February 10 2011 - Unit Testing now uses [QUnit](http://docs.jquery.com/Qunit) - Corrected Safari 5 Support - Now uses queues instead of timeouts - This means the API works exactly as expected, no more need to wrap calls in timeouts - Included a Subscribe Form in the Demo for Version Updates via Email - Small updates to Documentation - v1.3.1 - February 4 2011 - Improved Documentation - v1.3.0 - January 31 2011 - Support for cleaner HTML4 States - v1.2.1 - January 30 2011 - Fixed History.log always being called - [reported by dlee](https://github.com/balupton/history.js/issues/#issue/2) - Re-Added `History.go(index)` support - v1.2.0 - January 25 2011 - Support for HTML4 States in HTML5 Browsers (added test) - Updates of Documentation - v1.1.0 - January 24 2011 - Developed a series of automated test cases - Fixed issue with traditional anchors - Fixed issue with differing replaceState functionality in HTML4 Browsers - Fixed issue with Google Chrome artefacts being carried over to the initial state - Provided `onstatechange` and `onanchorchange` events - v1.0.0 - January 22 2011 - Supported `History.pushState` and `History.replaceState` degradation - Supported jQuery, MooTools and Prototype Frameworks ================================================ FILE: LICENSE.md ================================================ # License Copyright © 2014+ Bevry Pty Ltd (http://bevry.me)
Copyright © 2011-2013 Benjamin Lupton (http://balupton.com) ## The New BSD License Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - Neither the name of Benjamin Arthur Lupton nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ Memo ================== While there are some [legitimate bugs and differences in state handling even in modern browsers](https://github.com/browserstate/history.js/wiki/The-State-of-the-HTML5-History-API), they are relatively small enough now that you can just use the native HTML5 History API. If you intend to support legacy browsers, then History.js is your bet. This notice is here as History.js does not receive enough funding to be maintained, so it exists only in legacy condition for legacy browsers. Perhaps it still works for modern browsers, but it could really do with maintenance. Maintenance is very difficult as the library requires manual testing in HTML5 and HTML4 modes, and for each adapter, and for each browser. So that means `2^(# of adapters)^(# of browsers and their versions)` tests that need to be run by a human. Tests need to be run by a human as certain failures require browser interactions, such as navigating from the test suite to a different domain and back again, or clicking the physical back buttons, or checking if the physical back buttons actually work. This takes a lot of time. Despite History.js being one of the most popular JavaScript libraries there is, and has been used by even multi-million-user companies in its time - the reality of economy and company practices seems to be that companies prefer to fork their own internal versions and fix locally with their own devs rather than fund open-source maintainers what they would pay their own devs to make things better for everyone, including themselves, which would be cheaper - but no, that would require too many tiers of company approval that don't understand the need. As such, if you are an open-source developer, I'd recommend just working on open-source projects that are paid for by your own consulting work or your own company (e.g. every successful open-source project). As otherwise, when they become popular, you better hope they are easily maintainable and testable, otherwise the cost of maintenance is higher than the free time of the maintainers. So with all that said, this repo still exists for archival purposes, legacy browsers, and a hub for anarachistic issue & fork maintenance. Cheers, Benjamin Lupton, founder of Bevry, creator of History.js Welcome to History.js
v1.8b2, June 22 2013 ================== Slack community badge Patreon donate button Gratipay donate button Flattr donate button PayPal donate button Bitcoin donate button Wishlist browse button ## News - 22/06/2013: Beta 2 of v1.8 is released. Fixes and uncompressed bundled files. - 31/05/2013: Beta 1 of v1.8 is released. Fixes. - 14/02/2013: Alpha 4 of v1.8 is released. Fixes. - 05/02/2013: Alpha 3 of v1.8 is released. Tests updated. - 21/01/2013: Alpha 2 of v1.8 is released. Correct statechange behaviour. - 19/01/2013: Alpha 1 of v1.8 is released. Started to categorize old balupton's issues. ### History [See the `HISTORY.md` file for a detailed list of features, changes, solved issues and bugs](https://github.com/browserstate/history.js/blob/master/HISTORY.md#files) ### Involve Please create an issue if something doesn't work or if there is a browser specific bug. I'll try to fix it as soon as possible. Please send me your Pull requests if you have a nice solution! I'm also going to review old issues in balupton's repository and try to solve them too. ## Aims - Follow the [HTML5 History API](https://developer.mozilla.org/en/DOM/Manipulating_the_browser_history) as much as possible - Provide a cross-compatible experience for all HTML5 Browsers (they all implement the HTML5 History API a little bit differently causing different behaviours and sometimes bugs - History.js fixes this ensuring the experience is as expected / the same / great throughout the HTML5 browsers) - Provide a backwards-compatible experience for all HTML4 Browsers using a hash-fallback (including continued support for the HTML5 History API's `data`, `title`, `pushState` and `replaceState`) with the option to [remove HTML4 support if it is not right for your application](https://github.com/browserstate/history.js/wiki/Intelligent-State-Handling) - Provide a forwards-compatible experience for HTML4 States to HTML5 States (so if a hash-fallbacked url is accessed by a HTML5 browser it is naturally transformed into its non-hashed url equivalent) - Provide support for as many javascript frameworks as possible via adapters; especially [Dojo](http://dojotoolkit.org/), [ExtJS](http://www.sencha.com/), [jQuery](http://jquery.com/), [MooTools](http://mootools.net/), [Right.js](http://rightjs.org/) and [Zepto](http://zeptojs.com/). ## Quick Install ### Via Ajaxify Script To ajaxify your entire website with the HTML5 History API, History.js and jQuery the [Ajaxify script](https://github.com/browserstate/ajaxify) is all you need. It's that easy. ### Via Ajaxify Extension If you don't have access to your server, or just want to try out the Ajaxify script first, you can install the [History.js It! Google Chrome Extension](https://github.com/browserstate/historyjsit.crx) to try out History.js via Ajaxify on select websites without actually installing History.js/Ajaxify on your server. ### Via Ruby On Rails Gem If you are using Rails, then the easiest way for you to try History.js would be to use [Wiselinks](https://github.com/igor-alexandrov/wiselinks) gem. Wiselinks integrates into Rails application and allows you to start using History.js with three lines of code. ## Direct Install ### Working with History.js directly ``` javascript (function(window,undefined){ // Bind to StateChange Event History.Adapter.bind(window,'statechange',function(){ // Note: We are using statechange instead of popstate var State = History.getState(); // Note: We are using History.getState() instead of event.state }); // Change our States History.pushState({state:1}, "State 1", "?state=1"); // logs {state:1}, "State 1", "?state=1" History.pushState({state:2}, "State 2", "?state=2"); // logs {state:2}, "State 2", "?state=2" History.replaceState({state:3}, "State 3", "?state=3"); // logs {state:3}, "State 3", "?state=3" History.pushState(null, null, "?state=4"); // logs {}, '', "?state=4" History.back(); // logs {state:3}, "State 3", "?state=3" History.back(); // logs {state:1}, "State 1", "?state=1" History.back(); // logs {}, "Home Page", "?" History.go(2); // logs {state:3}, "State 3", "?state=3" })(window); ``` ### How would the above operations look in a HTML5 Browser? 1. www.mysite.com 1. www.mysite.com/?state=1 1. www.mysite.com/?state=2 1. www.mysite.com/?state=3 1. www.mysite.com/?state=4 1. www.mysite.com/?state=3 1. www.mysite.com/?state=1 1. www.mysite.com 1. www.mysite.com/?state=3 > Note: These urls also work in HTML4 browsers and Search Engines. So no need for the hashbang (`#!`) fragment-identifier that google ["recommends"](https://github.com/browserstate/history.js/wiki/Intelligent-State-Handling). ### How would they look in a HTML4 Browser? 1. www.mysite.com 1. www.mysite.com/#?state=1&_suid=1 1. www.mysite.com/#?state=2&_suid=2 1. www.mysite.com/#?state=3&_suid=3 1. www.mysite.com/#?state=4 1. www.mysite.com/#?state=3&_suid=3 1. www.mysite.com/#?state=1&_suid=1 1. www.mysite.com 1. www.mysite.com/#?state=3&_suid=3 > Note 1: These urls also work in HTML5 browsers - we use `replaceState` to transform these HTML4 states into their HTML5 equivalents so the user won't even notice :-) > > Note 2: These urls will be automatically url-encoded in IE6 to prevent certain browser-specific bugs. > > Note 3: Support for HTML4 browsers (this hash fallback) is optional [- why supporting HTML4 browsers could be either good or bad based on my app's use cases](https://github.com/browserstate/history.js/wiki/Intelligent-State-Handling) ### What's the deal with the SUIDs used in the HTML4 States? - SUIDs (State Unique Identifiers) are used when we utilise a `title` and/or `data` in our state. Adding a SUID allows us to associate particular states with data and titles while keeping the urls as simple as possible (don't worry it's all tested, working and a lot smarter than I'm making it out to be). - If you aren't utilising `title` or `data` then we don't even include a SUID (as there is no need for it) - as seen by State 4 above :-) - We also shrink the urls to make sure that the smallest url will be used. For instance we will adjust `http://www.mysite.com/#http://www.mysite.com/projects/History.js` to become `http://www.mysite.com/#/projects/History.js` automatically. (again tested, working, and smarter). - It works with domains, subdomains, subdirectories, whatever - doesn't matter where you put it. It's smart. - Safari 5 will also have a SUID appended to the URL, it is entirely transparent but just a visible side-effect. It is required to fix a bug with Safari 5. ### Is there a working demo? - Sure is, give it a download and navigate to the demo directory in your browser :-) - If you are after something a bit more adventurous than a end-user demo, open up the tests directory in your browser and editor - it'll rock your world and show all the vast use cases that History.js supports. ## Download & Installation - Download History.js and upload it to your webserver. Download links: [tar.gz](https://github.com/browserstate/history.js/tarball/master) or [zip](https://github.com/browserstate/history.js/zipball/master) - Include History.js - For [Dojo](http://dojotoolkit.org/) v1.8+ ``` html ``` - For [ExtJs](http://www.sencha.com/) v1.8+ ``` html ``` - For [jQuery](http://jquery.com/) v1.3+ ``` html ``` - For [Mootools](http://mootools.net/) v1.3+ ``` html ``` - For [Right.js](http://rightjs.org/) v2.2+ ``` html ``` - For [Zepto](http://zeptojs.com/) v0.5+ ``` html ``` - For everything else ``` html ``` > Note: If you want to only support HTML5 Browsers and not HTML4 Browsers (so no hash fallback support) then just change the `/html4+html5/` part in the urls to just `/html5/`. See [Why supporting HTML4 browsers could be either good or bad based on my app's use cases](https://github.com/browserstate/history.js/wiki/Intelligent-State-Handling) ## Get Updates - For Commit RSS/Atom Updates: - You can subscribe via the [GitHub Commit Atom Feed](http://feeds.feedburner.com/historyjs) - For GitHub News Feed Updates: - You can click the "watch" button up the top right of History.js's [GitHub Project Page](https://github.com/browserstate/history.js) ## Get Support - History.js is maintained by people like you. If you find a bug, report it to the [GitHub Issue Tracker](https://github.com/browserstate/history.js/issues). If you've fixed a bug submit a [Pull Request](https://github.com/browserstate/history.js/pulls) and add your fork to the [Network Wiki Page](https://github.com/browserstate/history.js/wiki/Network). - If you would like paid support and trainings, or have job offers, then refer to the [Network Wiki Page](https://github.com/browserstate/history.js/wiki/Network). If you are qualified with History.js, then be sure to add your details to that page too. - If your company uses History.js on your projects, and would like to see it grow and prosper (better documentation, bugfixes, upgrades, maintenance, etc.) and would love to become a corporate sponsor then do email sponsor@bevry.me - If you would like free support for History.js, then [post your question](http://stackoverflow.com/questions/ask) on [Stackoverflow](http://stackoverflow.com/about) and be sure to use the `history.js` tag when asking your question. - If you've created a website that uses History.js, or know of one, be sure to add it to the [Showcase Wiki Page](https://github.com/browserstate/history.js/wiki/Showcase). - If you'd love to +1 or like this project, then be sure to tweet about it and click the "watch" button up the top of its [Project Page](https://github.com/browserstate/history.js). - For anything else, refer to the [History.js GitHub Wiki Site](https://github.com/browserstate/history.js/wiki). Thanks! every bit of help really does make a difference! ## Browsers: Tested and Working In ### HTML5 Browsers - Firefox 4+ - Chrome 8+ - Opera 11.5+ - Safari 5.0+ - Safari iOS 4.3+ ### HTML4 Browsers - IE 6, 7, 8, 9, (10) - Firefox 3 - Opera 10, 11.0 - Safari 4 - Safari iOS 4.2, 4.1, 4.0, 3.2 ## Exposed API ### Functions #### States - `History.pushState(data,title,url)`
Pushes a new state to the browser; `data` can be null or an object, `title` can be null or a string, `url` must be a string - `History.replaceState(data,title,url)`
Replaces the existing state with a new state to the browser; `data` can be null or an object, `title` can be null or a string, `url` must be a string - `History.getState()`
Gets the current state of the browser, returns an object with `data`, `title` and `url` - `History.getStateByIndex`
Gets a state by the index - `History.getCurrentIndex`
Gets the current index - `History.getHash()`
Gets the current hash of the browser #### Adapter - `History.Adapter.bind(element,event,callback)`
A framework independent event binder, you may either use this or your framework's native event binder. - `History.Adapter.trigger(element,event)`
A framework independent event trigger, you may either use this or your framework's native event trigger. - `History.Adapter.onDomLoad(callback)`
A framework independent onDomLoad binder, you may either use this or your framework's native onDomLoad binder. #### Navigation - `History.back()`
Go back once through the history (same as hitting the browser's back button) - `History.forward()`
Go forward once through the history (same as hitting the browser's forward button) - `History.go(X)`
If X is negative go back through history X times, if X is positive go forwards through history X times #### Debug - `History.log(...)`
Logs messages to the console, the log element, and fallbacks to alert if neither of those two exist - `History.debug(...)`
Same as `History.log` but only runs if `History.options.debug === true` ### Options - `History.options.hashChangeInterval`
How long should the interval be before hashchange checks - `History.options.safariPollInterval`
How long should the interval be before safari poll checks - `History.options.doubleCheckInterval`
How long should the interval be before we perform a double check - `History.options.disableSuid`
Force History not to append suid - `History.options.storeInterval`
How long should we wait between store calls - `History.options.busyDelay`
How long should we wait between busy events - `History.options.debug`
If true will enable debug messages to be logged - `History.options.initialTitle`
What is the title of the initial state - `History.options.html4Mode`
If true, will force HTMl4 mode (hashtags) - `History.options.delayInit`
Want to override default options and call init manually. ### Events - `window.onstatechange`
Fired when the state of the page changes (does not include hash changes) - `window.onanchorchange`
Fired when the anchor of the page changes (does not include state hashes) ## Known Issues - Opera 11 fails to create history entries when under stressful loads (events fire perfectly, just the history events fail) - there is nothing we can do about this - Mercury iOS fails to apply url changes (hashes and HTML5 History API states) - there is nothing we can do about this ## Notes on Compatibility - History.js **solves** the following browser bugs: - HTML5 Browsers - Chrome 8 sometimes does not contain the correct state data when traversing back to the initial state - Safari 5, Safari iOS 4 and Firefox 3 and 4 do not fire the `onhashchange` event when the page is loaded with a hash - Safari 5 and Safari iOS 4 do not fire the `onpopstate` event when the hash has changed unlike the other browsers - Safari 5 and Safari iOS 4 fail to return to the correct state once a hash is replaced by a `replaceState` call / [bug report](https://bugs.webkit.org/show_bug.cgi?id=56249) - Safari 5 and Safari iOS 4 sometimes fail to apply the state change under busy conditions / [bug report](https://bugs.webkit.org/show_bug.cgi?id=42940) - Google Chrome 8,9,10 and Firefox 4 prior to the RC will always fire `onpopstate` once the page has loaded / [change recommendation](http://hacks.mozilla.org/2011/03/history-api-changes-in-firefox-4/) - Safari iOS 4.0, 4.1, 4.2 have a working HTML5 History API - although the actual back buttons of the browsers do not work, therefore we treat them as HTML4 browsers - None of the HTML5 browsers actually utilise the `title` argument to the `pushState` and `replaceState` calls - HTML4 Browsers - Old browsers like MSIE 6,7 and Firefox 2 do not have a `onhashchange` event - MSIE 6 and 7 sometimes do not apply a hash even it was told to (requiring a second call to the apply function) - Non-Opera HTML4 browsers sometimes do not apply the hash when the hash is not `urlencoded` - All Browsers - State data and titles do not persist once the site is left and then returned (includes page refreshes) - State titles are never applied to the `document.title` - ReplaceState functionality is emulated in HTML4 browsers by discarding the replaced state, so when the discarded state is accessed it is skipped using the appropriate `History.back()` / `History.forward()` call - Data persistance and synchronisation works like so: Every second or so, the SUIDs and URLs of the states will synchronise between the store and the local session. When a new session opens a familiar state (via the SUID or the URL) and it is not found locally then it will attempt to load the last known stored state with that information. - URLs will be unescaped to the maximum, so for instance the URL `?key=a%20b%252c` will become `?key=a b c`. This is to ensure consistency between browser url encodings. - Changing the hash of the page causes `onpopstate` to fire (this is expected/standard functionality). To ensure correct compatibility between HTML5 and HTML4 browsers the following events have been created: - `window.onstatechange`: this is the same as the `onpopstate` event except it does not fire for traditional anchors - `window.onanchorchange`: this is the same as the `onhashchange` event except it does not fire for states ## License Licensed under the [New BSD License](http://opensource.org/licenses/BSD-3-Clause)
Copyright © 2014+ [Bevry Pty Ltd](http://bevry.me)
Copyright © 2011-2013 [Benjamin Arthur Lupton](http://balupton.com) For support see the [Getting Support](#get-support) section. ================================================ FILE: bower.json ================================================ { "name": "history.js", "version": "1.8.0" } ================================================ FILE: buildr-uncompressed.coffee ================================================ # Requires buildr = require 'buildr' util = require 'util' # Options options = watch: false compress: false # Configs configs = standard: # Options name: 'standard' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Checking checkScripts: true jshintOptions: browser: true laxbreak: true boss: true undef: true onevar: true strict: true noarg: true # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false other: [ # ----------------------------- # Dojo Toolkit { # Options name: 'html4+html5+dojo' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'json2.js' 'history.adapter.dojo.js' 'history.html4.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled-uncompressed/html4+html5/dojo.history.js' } { # Options name: 'html5+dojo' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'history.adapter.dojo.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled-uncompressed/html5/dojo.history.js' } # ----------------------------- # ExtJS { # Options name: 'html4+html5+extjs' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'json2.js' 'history.adapter.extjs.js' 'history.html4.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled-uncompressed/html4+html5/extjs.history.js' } { # Options name: 'html5+extjs' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'history.adapter.extjs.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled-uncompressed/html5/extjs.history.js' } # ----------------------------- # JQUERY { # Options name: 'html4+html5+jquery' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'json2.js' 'history.adapter.jquery.js' 'history.html4.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled-uncompressed/html4+html5/jquery.history.js' } { # Options name: 'html5+jquery' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'history.adapter.jquery.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled-uncompressed/html5/jquery.history.js' } # ----------------------------- # MOOTOOLS { # Options name: 'html4+html5+mootools' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'json2.js' 'history.adapter.mootools.js' 'history.html4.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled-uncompressed/html4+html5/mootools.history.js' } { # Options name: 'html5+mootools' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'history.adapter.mootools.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled-uncompressed/html5/mootools.history.js' } # ----------------------------- # NATIVE { # Options name: 'html4+html5+native' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'json2.js' 'history.adapter.native.js' 'history.html4.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled-uncompressed/html4+html5/native.history.js' } { # Options name: 'html5+native' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'history.adapter.native.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled-uncompressed/html5/native.history.js' } # ----------------------------- # RIGHT.JS { # Options name: 'html4+html5+right' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'json2.js' 'history.adapter.right.js' 'history.html4.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled-uncompressed/html4+html5/right.history.js' } { # Options name: 'html5+right' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'history.adapter.right.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled-uncompressed/html5/right.history.js' } # ----------------------------- # ZEPTO { # Options name: 'html4+html5+zepto' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'json2.js' 'history.adapter.zepto.js' 'history.html4.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled-uncompressed/html4+html5/zepto.history.js' } { # Options name: 'html5+zepto' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'history.adapter.zepto.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled-uncompressed/html5/zepto.history.js' } ] # Standard standardConfig = configs.standard standardConfig.successHandler = -> for config in configs.other buildrInstance = buildr.createInstance config buildrInstance.process() # Process standardBuildr = buildr.createInstance configs.standard standardBuildr.process() ================================================ FILE: buildr.coffee ================================================ # Requires buildr = require 'buildr' util = require 'util' # Options options = watch: false compress: true # Configs configs = standard: # Options name: 'standard' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' outPath: __dirname+'/scripts/compressed' # Checking checkScripts: true jshintOptions: browser: true laxbreak: true boss: true undef: true onevar: true strict: true noarg: true # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false other: [ # ----------------------------- # Dojo Toolkit { # Options name: 'html4+html5+dojo' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'json2.js' 'history.adapter.dojo.js' 'history.html4.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled/html4+html5/dojo.history.js' } { # Options name: 'html5+dojo' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'history.adapter.dojo.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled/html5/dojo.history.js' } # ----------------------------- # ExtJS { # Options name: 'html4+html5+extjs' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'json2.js' 'history.adapter.extjs.js' 'history.html4.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled/html4+html5/extjs.history.js' } { # Options name: 'html5+extjs' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'history.adapter.extjs.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled/html5/extjs.history.js' } # ----------------------------- # JQUERY { # Options name: 'html4+html5+jquery' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'json2.js' 'history.adapter.jquery.js' 'history.html4.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled/html4+html5/jquery.history.js' } { # Options name: 'html5+jquery' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'history.adapter.jquery.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled/html5/jquery.history.js' } # ----------------------------- # MOOTOOLS { # Options name: 'html4+html5+mootools' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'json2.js' 'history.adapter.mootools.js' 'history.html4.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled/html4+html5/mootools.history.js' } { # Options name: 'html5+mootools' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'history.adapter.mootools.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled/html5/mootools.history.js' } # ----------------------------- # NATIVE { # Options name: 'html4+html5+native' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'json2.js' 'history.adapter.native.js' 'history.html4.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled/html4+html5/native.history.js' } { # Options name: 'html5+native' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'history.adapter.native.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled/html5/native.history.js' } # ----------------------------- # RIGHT.JS { # Options name: 'html4+html5+right' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'json2.js' 'history.adapter.right.js' 'history.html4.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled/html4+html5/right.history.js' } { # Options name: 'html5+right' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'history.adapter.right.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled/html5/right.history.js' } # ----------------------------- # ZEPTO { # Options name: 'html4+html5+zepto' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'json2.js' 'history.adapter.zepto.js' 'history.html4.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled/html4+html5/zepto.history.js' } { # Options name: 'html5+zepto' watch: options.watch # Paths srcPath: __dirname+'/scripts/uncompressed' # Compression (without outPath only the generated bundle files are compressed) compressScripts: options.compress # Array or true or false # Order scriptsOrder: [ 'history.adapter.zepto.js' 'history.js' ] # Bundling bundleScriptPath: __dirname+'/scripts/bundled/html5/zepto.history.js' } ] # Standard standardConfig = configs.standard standardConfig.successHandler = -> for config in configs.other buildrInstance = buildr.createInstance config buildrInstance.process() # Process standardBuildr = buildr.createInstance configs.standard standardBuildr.process() ================================================ FILE: component.json ================================================ { "name": "history.js", "version": "1.8.0" } ================================================ FILE: demo/bcherry-orig.html ================================================ WebKit is Dropping HTML5 "popstate" Events

There's a bug in the HTML5 "popstate" event, as implemented in WebKit (Safari and Chrome). View this page in one of those browsers. Your browser has had history entries added from #0 to #19 (you should start at #19). Hitting back/forward will navigate through these. On each URL, the large number above should reflect the hash value. If you hit back/forward quickly, you'll notice that your number gets out of sync with the URL. This is because WebKit is dropping popstate events (they are not firing). It seems to happen when outbound network requests are in progress when the user navigates in their browser happens. In this case, your browser is downloading an image that takes 1s to serve on every popstate, so you'll have to wait 1s between backs/forwards to have the feature work correctly. You could also cause constant network traffic by putting an image download in a setInterval, in which case your popstate events will never fire. This implementation simulates an AJAX application that makes a network request when you navigate between URLs using pushState/popstate. View the source for more info.

This was filed as Bug 42940 with WebKit on July 24, 2010. The Firefox 4 beta does not have this bug, which is good news.

This is put together by Ben Cherry. Ben is a front-end engineer at Twitter, and you can follow him at @bcherry.

================================================ FILE: demo/bcherry.html ================================================ WebKit is Dropping HTML5 "popstate" Events

There's a bug in the HTML5 "popstate" event, as implemented in WebKit (Safari and Chrome). View this page in one of those browsers. Your browser has had history entries added from #0 to #19 (you should start at #19). Hitting back/forward will navigate through these. On each URL, the large number above should reflect the hash value. If you hit back/forward quickly, you'll notice that your number gets out of sync with the URL. This is because WebKit is dropping popstate events (they are not firing). It seems to happen when outbound network requests are in progress when the user navigates in their browser happens. In this case, your browser is downloading an image that takes 1s to serve on every popstate, so you'll have to wait 1s between backs/forwards to have the feature work correctly. You could also cause constant network traffic by putting an image download in a setInterval, in which case your popstate events will never fire. This implementation simulates an AJAX application that makes a network request when you navigate between URLs using pushState/popstate. View the source for more info.

This was filed as Bug 42940 with WebKit on July 24, 2010. The Firefox 4 beta does not have this bug, which is good news.

This is put together by Ben Cherry. Ben is a front-end engineer at Twitter, and you can follow him at @bcherry.

This bug was fixed in History.js by Benjamin Lupton. Benjamin is a freelance web 2.0 consultant, and you can follow him at @balupton.

================================================ FILE: demo/chrome.html ================================================ Chrome History API Data Artifact

This demo demonstrates an issue with Google Chrome versions 8-10 (possibly 11) where if you push a state with data, then do history.back to the initial state, the event.state will contain the pushed states data instead of being null.

Note: The issue requires a clean history list, as such this should always be opened in a new tab/window where there are no prior history items.

Reported by Benjamin Lupton author of History.js

================================================ FILE: demo/index.html ================================================ History.js

History.js

History.js gracefully supports the HTML5 History/State APIs (pushState, replaceState, onPopState) in all browsers. Including continued support for data, titles, replaceState. Supports jQuery, MooTools and Prototype. For HTML5 browsers this means that you can modify the URL directly, without needing to use hashes anymore. For HTML4 browsers it will revert back to using the old onhashchange functionality.

Click through the buttons in order and you'll get the results demonstrated in the README.md file.

================================================ FILE: demo/native-auto.html ================================================ ================================================ FILE: demo/native.html ================================================ HTML5 History API Demo
================================================ FILE: demo/navigator.html ================================================ Navigator Output ================================================ FILE: demo/safari.html ================================================ Safari Hash ReplaceState History Traversal Bug

This demo demonstrates an issue with Safari 5.0.4 (6533.20.27) handing of hashes and replace state. When a hash is set, and then replaced using replaceState the history list are then broken, when traversing back the hash does not change.

Note: The issue requires a clean history list, as such this should always be opened in a new tab/window where there are no prior history items.

Reported by Benjamin Lupton author of History.js

================================================ FILE: demo/unicode.html ================================================ History.js

History.js

History.js gracefully supports unicode.

================================================ FILE: package.json ================================================ { "name": "history.js", "version": "1.8.0", "description": "History.js gracefully supports the HTML5 History/State APIs (pushState, replaceState, onPopState) in all browsers. Including continued support for data, titles, replaceState. Supports jQuery, MooTools and Prototype. For HTML5 browsers this means that you can modify the URL directly, without needing to use hashes anymore. For HTML4 browsers it will revert back to using the old onhashchange functionality.", "homepage": "https://github.com/browserstate/history.js", "keywords": [ "javascript", "html5 history api", "hashchange", "popstate", "pushstate", "replacestate", "hashes", "hashbang" ], "author": { "name": "Benjamin Lupton", "email": "b@lupton.cc", "web": "http://balupton.com" }, "maintainers": [ { "name": "Benjamin Lupton", "email": "b@lupton.cc", "web": "http://balupton.com" }, { "name": "Andreas Bernhard", "email": "andreas@bernhard.im", "web": "http://www.bs-infosys.com" } ], "contributors": [ { "name": "Benjamin Lupton", "email": "b@lupton.cc", "web": "http://balupton.com" }, { "name": "Andreas Bernhard", "email": "andreas@bernhard.im", "web": "http://www.bs-infosys.com" } ], "bugs": { "web": "https://github.com/browserstate/history.js/issues" }, "licenses": [ { "type": "New-BSD", "url": "http://creativecommons.org/licenses/BSD/" } ], "repository": { "type": "git", "url": "http://github.com/browserstate/history.js.git" }, "dependencies": { "buildr": "0.8.x" }, "engines": { }, "directories": { "out": "./scripts/compressed", "src": "./scripts/uncompressed" } } ================================================ FILE: scripts/bundled/html4+html5/dojo.history.js ================================================ typeof JSON!="object"&&(JSON={}),function(){"use strict";function f(e){return e<10?"0"+e:e}function quote(e){return escapable.lastIndex=0,escapable.test(e)?'"'+e.replace(escapable,function(e){var t=meta[e];return typeof t=="string"?t:"\\u"+("0000"+e.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+e+'"'}function str(e,t){var n,r,i,s,o=gap,u,a=t[e];a&&typeof a=="object"&&typeof a.toJSON=="function"&&(a=a.toJSON(e)),typeof rep=="function"&&(a=rep.call(t,e,a));switch(typeof a){case"string":return quote(a);case"number":return isFinite(a)?String(a):"null";case"boolean":case"null":return String(a);case"object":if(!a)return"null";gap+=indent,u=[];if(Object.prototype.toString.apply(a)==="[object Array]"){s=a.length;for(n=0;n")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window) ================================================ FILE: scripts/bundled/html4+html5/extjs.history.js ================================================ typeof JSON!="object"&&(JSON={}),function(){"use strict";function f(e){return e<10?"0"+e:e}function quote(e){return escapable.lastIndex=0,escapable.test(e)?'"'+e.replace(escapable,function(e){var t=meta[e];return typeof t=="string"?t:"\\u"+("0000"+e.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+e+'"'}function str(e,t){var n,r,i,s,o=gap,u,a=t[e];a&&typeof a=="object"&&typeof a.toJSON=="function"&&(a=a.toJSON(e)),typeof rep=="function"&&(a=rep.call(t,e,a));switch(typeof a){case"string":return quote(a);case"number":return isFinite(a)?String(a):"null";case"boolean":case"null":return String(a);case"object":if(!a)return"null";gap+=indent,u=[];if(Object.prototype.toString.apply(a)==="[object Array]"){s=a.length;for(n=0;n")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window) ================================================ FILE: scripts/bundled/html4+html5/jquery.history.js ================================================ typeof JSON!="object"&&(JSON={}),function(){"use strict";function f(e){return e<10?"0"+e:e}function quote(e){return escapable.lastIndex=0,escapable.test(e)?'"'+e.replace(escapable,function(e){var t=meta[e];return typeof t=="string"?t:"\\u"+("0000"+e.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+e+'"'}function str(e,t){var n,r,i,s,o=gap,u,a=t[e];a&&typeof a=="object"&&typeof a.toJSON=="function"&&(a=a.toJSON(e)),typeof rep=="function"&&(a=rep.call(t,e,a));switch(typeof a){case"string":return quote(a);case"number":return isFinite(a)?String(a):"null";case"boolean":case"null":return String(a);case"object":if(!a)return"null";gap+=indent,u=[];if(Object.prototype.toString.apply(a)==="[object Array]"){s=a.length;for(n=0;n")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window) ================================================ FILE: scripts/bundled/html4+html5/mootools.history.js ================================================ typeof JSON!="object"&&(JSON={}),function(){"use strict";function f(e){return e<10?"0"+e:e}function quote(e){return escapable.lastIndex=0,escapable.test(e)?'"'+e.replace(escapable,function(e){var t=meta[e];return typeof t=="string"?t:"\\u"+("0000"+e.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+e+'"'}function str(e,t){var n,r,i,s,o=gap,u,a=t[e];a&&typeof a=="object"&&typeof a.toJSON=="function"&&(a=a.toJSON(e)),typeof rep=="function"&&(a=rep.call(t,e,a));switch(typeof a){case"string":return quote(a);case"number":return isFinite(a)?String(a):"null";case"boolean":case"null":return String(a);case"object":if(!a)return"null";gap+=indent,u=[];if(Object.prototype.toString.apply(a)==="[object Array]"){s=a.length;for(n=0;n")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window) ================================================ FILE: scripts/bundled/html4+html5/native.history.js ================================================ typeof JSON!="object"&&(JSON={}),function(){"use strict";function f(e){return e<10?"0"+e:e}function quote(e){return escapable.lastIndex=0,escapable.test(e)?'"'+e.replace(escapable,function(e){var t=meta[e];return typeof t=="string"?t:"\\u"+("0000"+e.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+e+'"'}function str(e,t){var n,r,i,s,o=gap,u,a=t[e];a&&typeof a=="object"&&typeof a.toJSON=="function"&&(a=a.toJSON(e)),typeof rep=="function"&&(a=rep.call(t,e,a));switch(typeof a){case"string":return quote(a);case"number":return isFinite(a)?String(a):"null";case"boolean":case"null":return String(a);case"object":if(!a)return"null";gap+=indent,u=[];if(Object.prototype.toString.apply(a)==="[object Array]"){s=a.length;for(n=0;n")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window) ================================================ FILE: scripts/bundled/html4+html5/right.history.js ================================================ typeof JSON!="object"&&(JSON={}),function(){"use strict";function f(e){return e<10?"0"+e:e}function quote(e){return escapable.lastIndex=0,escapable.test(e)?'"'+e.replace(escapable,function(e){var t=meta[e];return typeof t=="string"?t:"\\u"+("0000"+e.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+e+'"'}function str(e,t){var n,r,i,s,o=gap,u,a=t[e];a&&typeof a=="object"&&typeof a.toJSON=="function"&&(a=a.toJSON(e)),typeof rep=="function"&&(a=rep.call(t,e,a));switch(typeof a){case"string":return quote(a);case"number":return isFinite(a)?String(a):"null";case"boolean":case"null":return String(a);case"object":if(!a)return"null";gap+=indent,u=[];if(Object.prototype.toString.apply(a)==="[object Array]"){s=a.length;for(n=0;n")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window) ================================================ FILE: scripts/bundled/html4+html5/zepto.history.js ================================================ typeof JSON!="object"&&(JSON={}),function(){"use strict";function f(e){return e<10?"0"+e:e}function quote(e){return escapable.lastIndex=0,escapable.test(e)?'"'+e.replace(escapable,function(e){var t=meta[e];return typeof t=="string"?t:"\\u"+("0000"+e.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+e+'"'}function str(e,t){var n,r,i,s,o=gap,u,a=t[e];a&&typeof a=="object"&&typeof a.toJSON=="function"&&(a=a.toJSON(e)),typeof rep=="function"&&(a=rep.call(t,e,a));switch(typeof a){case"string":return quote(a);case"number":return isFinite(a)?String(a):"null";case"boolean":case"null":return String(a);case"object":if(!a)return"null";gap+=indent,u=[];if(Object.prototype.toString.apply(a)==="[object Array]"){s=a.length;for(n=0;n")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window) ================================================ FILE: scripts/bundled/html5/dojo.history.js ================================================ (function(e,t){"use strict";var n=e.History=e.History||{},r=e.require;if(typeof n.Adapter!="undefined")throw new Error("History.js Adapter has already been loaded...");n.Adapter={handlers:{},_uid:1,uid:function(e){return e._uid||(e._uid=n.Adapter._uid++)},bind:function(e,t,r){var i=n.Adapter.uid(e);n.Adapter.handlers[i]=n.Adapter.handlers[i]||{},n.Adapter.handlers[i][t]=n.Adapter.handlers[i][t]||[],n.Adapter.handlers[i][t].push(r),e["on"+t]=function(e,t){return function(r){n.Adapter.trigger(e,t,r)}}(e,t)},trigger:function(e,t,r){r=r||{};var i=n.Adapter.uid(e),s,o;n.Adapter.handlers[i]=n.Adapter.handlers[i]||{},n.Adapter.handlers[i][t]=n.Adapter.handlers[i][t]||[];for(s=0,o=n.Adapter.handlers[i][t].length;s")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window) ================================================ FILE: scripts/bundled/html5/extjs.history.js ================================================ (function(e,t){"use strict";var n=e.History=e.History||{},r=e.Ext;e.JSON={stringify:r.JSON.encode,parse:r.JSON.decode};if(typeof n.Adapter!="undefined")throw new Error("History.js Adapter has already been loaded...");n.Adapter={observables:{},bind:function(e,t,n,i){r.EventManager.addListener(e,t,n,i);var s=r.id(e,"history-"),o=this.observables[s];o||(o=r.create("Ext.util.Observable"),this.observables[s]=o),o.on(t,n,i)},trigger:function(e,t,n){var i=r.id(e,"history-"),s=this.observables[i];s&&s.fireEvent(t,n)},extractEventData:function(e,n,r){var i=n&&n.browserEvent&&n.browserEvent[e]||r&&r[e]||t;return i},onDomLoad:function(e){r.onReady(e)}},typeof n.init!="undefined"&&n.init()})(window),function(e,t){"use strict";var n=e.console||t,r=e.document,i=e.navigator,s=!1,o=e.setTimeout,u=e.clearTimeout,a=e.setInterval,f=e.clearInterval,l=e.JSON,c=e.alert,h=e.History=e.History||{},p=e.history;try{s=e.sessionStorage,s.setItem("TEST","1"),s.removeItem("TEST")}catch(d){s=!1}l.stringify=l.stringify||l.encode,l.parse=l.parse||l.decode;if(typeof h.init!="undefined")throw new Error("History.js Core has already been loaded...");h.init=function(e){return typeof h.Adapter=="undefined"?!1:(typeof h.initCore!="undefined"&&h.initCore(),typeof h.initHtml4!="undefined"&&h.initHtml4(),!0)},h.initCore=function(d){if(typeof h.initCore.initialized!="undefined")return!1;h.initCore.initialized=!0,h.options=h.options||{},h.options.hashChangeInterval=h.options.hashChangeInterval||100,h.options.safariPollInterval=h.options.safariPollInterval||500,h.options.doubleCheckInterval=h.options.doubleCheckInterval||500,h.options.disableSuid=h.options.disableSuid||!1,h.options.storeInterval=h.options.storeInterval||1e3,h.options.busyDelay=h.options.busyDelay||250,h.options.debug=h.options.debug||!1,h.options.initialTitle=h.options.initialTitle||r.title,h.options.html4Mode=h.options.html4Mode||!1,h.options.delayInit=h.options.delayInit||!1,h.intervalList=[],h.clearAllIntervals=function(){var e,t=h.intervalList;if(typeof t!="undefined"&&t!==null){for(e=0;e")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window) ================================================ FILE: scripts/bundled/html5/jquery.history.js ================================================ (function(e,t){"use strict";var n=e.History=e.History||{},r=e.jQuery;if(typeof n.Adapter!="undefined")throw new Error("History.js Adapter has already been loaded...");n.Adapter={bind:function(e,t,n){r(e).bind(t,n)},trigger:function(e,t,n){r(e).trigger(t,n)},extractEventData:function(e,n,r){var i=n&&n.originalEvent&&n.originalEvent[e]||r&&r[e]||t;return i},onDomLoad:function(e){r(e)}},typeof n.init!="undefined"&&n.init()})(window),function(e,t){"use strict";var n=e.console||t,r=e.document,i=e.navigator,s=!1,o=e.setTimeout,u=e.clearTimeout,a=e.setInterval,f=e.clearInterval,l=e.JSON,c=e.alert,h=e.History=e.History||{},p=e.history;try{s=e.sessionStorage,s.setItem("TEST","1"),s.removeItem("TEST")}catch(d){s=!1}l.stringify=l.stringify||l.encode,l.parse=l.parse||l.decode;if(typeof h.init!="undefined")throw new Error("History.js Core has already been loaded...");h.init=function(e){return typeof h.Adapter=="undefined"?!1:(typeof h.initCore!="undefined"&&h.initCore(),typeof h.initHtml4!="undefined"&&h.initHtml4(),!0)},h.initCore=function(d){if(typeof h.initCore.initialized!="undefined")return!1;h.initCore.initialized=!0,h.options=h.options||{},h.options.hashChangeInterval=h.options.hashChangeInterval||100,h.options.safariPollInterval=h.options.safariPollInterval||500,h.options.doubleCheckInterval=h.options.doubleCheckInterval||500,h.options.disableSuid=h.options.disableSuid||!1,h.options.storeInterval=h.options.storeInterval||1e3,h.options.busyDelay=h.options.busyDelay||250,h.options.debug=h.options.debug||!1,h.options.initialTitle=h.options.initialTitle||r.title,h.options.html4Mode=h.options.html4Mode||!1,h.options.delayInit=h.options.delayInit||!1,h.intervalList=[],h.clearAllIntervals=function(){var e,t=h.intervalList;if(typeof t!="undefined"&&t!==null){for(e=0;e")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window) ================================================ FILE: scripts/bundled/html5/mootools.history.js ================================================ (function(e,t){"use strict";var n=e.History=e.History||{},r=e.MooTools,i=e.Element;if(typeof n.Adapter!="undefined")throw new Error("History.js Adapter has already been loaded...");Object.append(i.NativeEvents,{popstate:2,hashchange:2}),n.Adapter={bind:function(e,t,n){var r=typeof e=="string"?document.id(e):e;r.addEvent(t,n)},trigger:function(e,t,n){var r=typeof e=="string"?document.id(e):e;r.fireEvent(t,n)},extractEventData:function(e,n){var r=n&&n.event&&n.event[e]||n&&n[e]||t;return r},onDomLoad:function(t){e.addEvent("domready",t)}},typeof n.init!="undefined"&&n.init()})(window),function(e,t){"use strict";var n=e.console||t,r=e.document,i=e.navigator,s=!1,o=e.setTimeout,u=e.clearTimeout,a=e.setInterval,f=e.clearInterval,l=e.JSON,c=e.alert,h=e.History=e.History||{},p=e.history;try{s=e.sessionStorage,s.setItem("TEST","1"),s.removeItem("TEST")}catch(d){s=!1}l.stringify=l.stringify||l.encode,l.parse=l.parse||l.decode;if(typeof h.init!="undefined")throw new Error("History.js Core has already been loaded...");h.init=function(e){return typeof h.Adapter=="undefined"?!1:(typeof h.initCore!="undefined"&&h.initCore(),typeof h.initHtml4!="undefined"&&h.initHtml4(),!0)},h.initCore=function(d){if(typeof h.initCore.initialized!="undefined")return!1;h.initCore.initialized=!0,h.options=h.options||{},h.options.hashChangeInterval=h.options.hashChangeInterval||100,h.options.safariPollInterval=h.options.safariPollInterval||500,h.options.doubleCheckInterval=h.options.doubleCheckInterval||500,h.options.disableSuid=h.options.disableSuid||!1,h.options.storeInterval=h.options.storeInterval||1e3,h.options.busyDelay=h.options.busyDelay||250,h.options.debug=h.options.debug||!1,h.options.initialTitle=h.options.initialTitle||r.title,h.options.html4Mode=h.options.html4Mode||!1,h.options.delayInit=h.options.delayInit||!1,h.intervalList=[],h.clearAllIntervals=function(){var e,t=h.intervalList;if(typeof t!="undefined"&&t!==null){for(e=0;e")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window) ================================================ FILE: scripts/bundled/html5/native.history.js ================================================ (function(e,t){"use strict";var n=e.History=e.History||{};if(typeof n.Adapter!="undefined")throw new Error("History.js Adapter has already been loaded...");n.Adapter={handlers:{},_uid:1,uid:function(e){return e._uid||(e._uid=n.Adapter._uid++)},bind:function(e,t,r){var i=n.Adapter.uid(e);n.Adapter.handlers[i]=n.Adapter.handlers[i]||{},n.Adapter.handlers[i][t]=n.Adapter.handlers[i][t]||[],n.Adapter.handlers[i][t].push(r),e["on"+t]=function(e,t){return function(r){n.Adapter.trigger(e,t,r)}}(e,t)},trigger:function(e,t,r){r=r||{};var i=n.Adapter.uid(e),s,o;n.Adapter.handlers[i]=n.Adapter.handlers[i]||{},n.Adapter.handlers[i][t]=n.Adapter.handlers[i][t]||[];for(s=0,o=n.Adapter.handlers[i][t].length;s")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window) ================================================ FILE: scripts/bundled/html5/right.history.js ================================================ (function(e,t){"use strict";var n=e.History=e.History||{},r=e.document,i=e.RightJS,s=i.$;if(typeof n.Adapter!="undefined")throw new Error("History.js Adapter has already been loaded...");n.Adapter={bind:function(e,t,n){s(e).on(t,n)},trigger:function(e,t,n){s(e).fire(t,n)},extractEventData:function(e,n){var r=n&&n._&&n._[e]||t;return r},onDomLoad:function(e){s(r).onReady(e)}},typeof n.init!="undefined"&&n.init()})(window),function(e,t){"use strict";var n=e.console||t,r=e.document,i=e.navigator,s=!1,o=e.setTimeout,u=e.clearTimeout,a=e.setInterval,f=e.clearInterval,l=e.JSON,c=e.alert,h=e.History=e.History||{},p=e.history;try{s=e.sessionStorage,s.setItem("TEST","1"),s.removeItem("TEST")}catch(d){s=!1}l.stringify=l.stringify||l.encode,l.parse=l.parse||l.decode;if(typeof h.init!="undefined")throw new Error("History.js Core has already been loaded...");h.init=function(e){return typeof h.Adapter=="undefined"?!1:(typeof h.initCore!="undefined"&&h.initCore(),typeof h.initHtml4!="undefined"&&h.initHtml4(),!0)},h.initCore=function(d){if(typeof h.initCore.initialized!="undefined")return!1;h.initCore.initialized=!0,h.options=h.options||{},h.options.hashChangeInterval=h.options.hashChangeInterval||100,h.options.safariPollInterval=h.options.safariPollInterval||500,h.options.doubleCheckInterval=h.options.doubleCheckInterval||500,h.options.disableSuid=h.options.disableSuid||!1,h.options.storeInterval=h.options.storeInterval||1e3,h.options.busyDelay=h.options.busyDelay||250,h.options.debug=h.options.debug||!1,h.options.initialTitle=h.options.initialTitle||r.title,h.options.html4Mode=h.options.html4Mode||!1,h.options.delayInit=h.options.delayInit||!1,h.intervalList=[],h.clearAllIntervals=function(){var e,t=h.intervalList;if(typeof t!="undefined"&&t!==null){for(e=0;e")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window) ================================================ FILE: scripts/bundled/html5/zepto.history.js ================================================ (function(e,t){"use strict";var n=e.History=e.History||{},r=e.Zepto;if(typeof n.Adapter!="undefined")throw new Error("History.js Adapter has already been loaded...");n.Adapter={bind:function(e,t,n){(new r(e)).bind(t,n)},trigger:function(e,t){(new r(e)).trigger(t)},extractEventData:function(e,n){var r=n&&n[e]||t;return r},onDomLoad:function(e){new r(e)}},typeof n.init!="undefined"&&n.init()})(window),function(e,t){"use strict";var n=e.console||t,r=e.document,i=e.navigator,s=!1,o=e.setTimeout,u=e.clearTimeout,a=e.setInterval,f=e.clearInterval,l=e.JSON,c=e.alert,h=e.History=e.History||{},p=e.history;try{s=e.sessionStorage,s.setItem("TEST","1"),s.removeItem("TEST")}catch(d){s=!1}l.stringify=l.stringify||l.encode,l.parse=l.parse||l.decode;if(typeof h.init!="undefined")throw new Error("History.js Core has already been loaded...");h.init=function(e){return typeof h.Adapter=="undefined"?!1:(typeof h.initCore!="undefined"&&h.initCore(),typeof h.initHtml4!="undefined"&&h.initHtml4(),!0)},h.initCore=function(d){if(typeof h.initCore.initialized!="undefined")return!1;h.initCore.initialized=!0,h.options=h.options||{},h.options.hashChangeInterval=h.options.hashChangeInterval||100,h.options.safariPollInterval=h.options.safariPollInterval||500,h.options.doubleCheckInterval=h.options.doubleCheckInterval||500,h.options.disableSuid=h.options.disableSuid||!1,h.options.storeInterval=h.options.storeInterval||1e3,h.options.busyDelay=h.options.busyDelay||250,h.options.debug=h.options.debug||!1,h.options.initialTitle=h.options.initialTitle||r.title,h.options.html4Mode=h.options.html4Mode||!1,h.options.delayInit=h.options.delayInit||!1,h.intervalList=[],h.clearAllIntervals=function(){var e,t=h.intervalList;if(typeof t!="undefined"&&t!==null){for(e=0;e")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window) ================================================ FILE: scripts/bundled-uncompressed/html4+html5/dojo.history.js ================================================ /* json2.js 2012-10-08 Public Domain. NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. See http://www.JSON.org/js.html This code should be minified before deployment. See http://javascript.crockford.com/jsmin.html USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO NOT CONTROL. This file creates a global JSON object containing two methods: stringify and parse. JSON.stringify(value, replacer, space) value any JavaScript value, usually an object or array. replacer an optional parameter that determines how object values are stringified for objects. It can be a function or an array of strings. space an optional parameter that specifies the indentation of nested structures. If it is omitted, the text will be packed without extra whitespace. If it is a number, it will specify the number of spaces to indent at each level. If it is a string (such as '\t' or ' '), it contains the characters used to indent at each level. This method produces a JSON text from a JavaScript value. When an object value is found, if the object contains a toJSON method, its toJSON method will be called and the result will be stringified. A toJSON method does not serialize: it returns the value represented by the name/value pair that should be serialized, or undefined if nothing should be serialized. The toJSON method will be passed the key associated with the value, and this will be bound to the value For example, this would serialize Dates as ISO strings. Date.prototype.toJSON = function (key) { function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } return this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z'; }; You can provide an optional replacer method. It will be passed the key and value of each member, with this bound to the containing object. The value that is returned from your method will be serialized. If your method returns undefined, then the member will be excluded from the serialization. If the replacer parameter is an array of strings, then it will be used to select the members to be serialized. It filters the results such that only members with keys listed in the replacer array are stringified. Values that do not have JSON representations, such as undefined or functions, will not be serialized. Such values in objects will be dropped; in arrays they will be replaced with null. You can use a replacer function to replace those with JSON values. JSON.stringify(undefined) returns undefined. The optional space parameter produces a stringification of the value that is filled with line breaks and indentation to make it easier to read. If the space parameter is a non-empty string, then that string will be used for indentation. If the space parameter is a number, then the indentation will be that many spaces. Example: text = JSON.stringify(['e', {pluribus: 'unum'}]); // text is '["e",{"pluribus":"unum"}]' text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' text = JSON.stringify([new Date()], function (key, value) { return this[key] instanceof Date ? 'Date(' + this[key] + ')' : value; }); // text is '["Date(---current time---)"]' JSON.parse(text, reviver) This method parses a JSON text to produce an object or array. It can throw a SyntaxError exception. The optional reviver parameter is a function that can filter and transform the results. It receives each of the keys and values, and its return value is used instead of the original value. If it returns what it received, then the structure is not modified. If it returns undefined then the member is deleted. Example: // Parse the text. Values that look like ISO date strings will // be converted to Date objects. myData = JSON.parse(text, function (key, value) { var a; if (typeof value === 'string') { a = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); if (a) { return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6])); } } return value; }); myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { var d; if (typeof value === 'string' && value.slice(0, 5) === 'Date(' && value.slice(-1) === ')') { d = new Date(value.slice(5, -1)); if (d) { return d; } } return value; }); This is a reference implementation. You are free to copy, modify, or redistribute. */ /*jslint evil: true, regexp: true */ /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, lastIndex, length, parse, prototype, push, replace, slice, stringify, test, toJSON, toString, valueOf */ // Create a JSON object only if one does not already exist. We create the // methods in a closure to avoid creating global variables. if (typeof JSON !== 'object') { JSON = {}; } (function () { 'use strict'; function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } if (typeof Date.prototype.toJSON !== 'function') { Date.prototype.toJSON = function (key) { return isFinite(this.valueOf()) ? this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z' : null; }; String.prototype.toJSON = Number.prototype.toJSON = Boolean.prototype.toJSON = function (key) { return this.valueOf(); }; } var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, gap, indent, meta = { // table of character substitutions '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\' }, rep; function quote(string) { // If the string contains no control characters, no quote characters, and no // backslash characters, then we can safely slap some quotes around it. // Otherwise we must also replace the offending characters with safe escape // sequences. escapable.lastIndex = 0; return escapable.test(string) ? '"' + string.replace(escapable, function (a) { var c = meta[a]; return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }) + '"' : '"' + string + '"'; } function str(key, holder) { // Produce a string from holder[key]. var i, // The loop counter. k, // The member key. v, // The member value. length, mind = gap, partial, value = holder[key]; // If the value has a toJSON method, call it to obtain a replacement value. if (value && typeof value === 'object' && typeof value.toJSON === 'function') { value = value.toJSON(key); } // If we were called with a replacer function, then call the replacer to // obtain a replacement value. if (typeof rep === 'function') { value = rep.call(holder, key, value); } // What happens next depends on the value's type. switch (typeof value) { case 'string': return quote(value); case 'number': // JSON numbers must be finite. Encode non-finite numbers as null. return isFinite(value) ? String(value) : 'null'; case 'boolean': case 'null': // If the value is a boolean or null, convert it to a string. Note: // typeof null does not produce 'null'. The case is included here in // the remote chance that this gets fixed someday. return String(value); // If the type is 'object', we might be dealing with an object or an array or // null. case 'object': // Due to a specification blunder in ECMAScript, typeof null is 'object', // so watch out for that case. if (!value) { return 'null'; } // Make an array to hold the partial results of stringifying this object value. gap += indent; partial = []; // Is the value an array? if (Object.prototype.toString.apply(value) === '[object Array]') { // The value is an array. Stringify every element. Use null as a placeholder // for non-JSON values. length = value.length; for (i = 0; i < length; i += 1) { partial[i] = str(i, value) || 'null'; } // Join all of the elements together, separated with commas, and wrap them in // brackets. v = partial.length === 0 ? '[]' : gap ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : '[' + partial.join(',') + ']'; gap = mind; return v; } // If the replacer is an array, use it to select the members to be stringified. if (rep && typeof rep === 'object') { length = rep.length; for (i = 0; i < length; i += 1) { if (typeof rep[i] === 'string') { k = rep[i]; v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } else { // Otherwise, iterate through all of the keys in the object. for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } // Join all of the member texts together, separated with commas, // and wrap them in braces. v = partial.length === 0 ? '{}' : gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : '{' + partial.join(',') + '}'; gap = mind; return v; } } // If the JSON object does not yet have a stringify method, give it one. if (typeof JSON.stringify !== 'function') { JSON.stringify = function (value, replacer, space) { // The stringify method takes a value and an optional replacer, and an optional // space parameter, and returns a JSON text. The replacer can be a function // that can replace values, or an array of strings that will select the keys. // A default replacer method can be provided. Use of the space parameter can // produce text that is more easily readable. var i; gap = ''; indent = ''; // If the space parameter is a number, make an indent string containing that // many spaces. if (typeof space === 'number') { for (i = 0; i < space; i += 1) { indent += ' '; } // If the space parameter is a string, it will be used as the indent string. } else if (typeof space === 'string') { indent = space; } // If there is a replacer, it must be a function or an array. // Otherwise, throw an error. rep = replacer; if (replacer && typeof replacer !== 'function' && (typeof replacer !== 'object' || typeof replacer.length !== 'number')) { throw new Error('JSON.stringify'); } // Make a fake root object containing our value under the key of ''. // Return the result of stringifying the value. return str('', {'': value}); }; } // If the JSON object does not yet have a parse method, give it one. if (typeof JSON.parse !== 'function') { JSON.parse = function (text, reviver) { // The parse method takes a text and an optional reviver function, and returns // a JavaScript value if the text is a valid JSON text. var j; function walk(holder, key) { // The walk method is used to recursively walk the resulting structure so // that modifications can be made. var k, v, value = holder[key]; if (value && typeof value === 'object') { for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = walk(value, k); if (v !== undefined) { value[k] = v; } else { delete value[k]; } } } } return reviver.call(holder, key, value); } // Parsing happens in four stages. In the first stage, we replace certain // Unicode characters with escape sequences. JavaScript handles many characters // incorrectly, either silently deleting them, or treating them as line endings. text = String(text); cx.lastIndex = 0; if (cx.test(text)) { text = text.replace(cx, function (a) { return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }); } // In the second stage, we run the text against regular expressions that look // for non-JSON patterns. We are especially concerned with '()' and 'new' // because they can cause invocation, and '=' because it can cause mutation. // But just to be safe, we want to reject all unexpected forms. // We split the second stage into 4 regexp operations in order to work around // crippling inefficiencies in IE's and Safari's regexp engines. First we // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we // replace all simple value tokens with ']' characters. Third, we delete all // open brackets that follow a colon or comma or that begin the text. Finally, // we look to see that the remaining characters are only whitespace or ']' or // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. if (/^[\],:{}\s]*$/ .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { // In the third stage we use the eval function to compile the text into a // JavaScript structure. The '{' operator is subject to a syntactic ambiguity // in JavaScript: it can begin a block or an object literal. We wrap the text // in parens to eliminate the ambiguity. j = eval('(' + text + ')'); // In the optional fourth stage, we recursively walk the new structure, passing // each name/value pair to a reviver function for possible transformation. return typeof reviver === 'function' ? walk({'': j}, '') : j; } // If the text is not JSON parseable, then a SyntaxError is thrown. throw new SyntaxError('JSON.parse'); }; } }());/** * History.js Dojo Adapter * * Essentially the same as the native adapter but uses dojo/ready for the dom load callback. * * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ // Closure (function(window,undefined){ "use strict"; // Localise Globals var History = window.History = window.History||{}, require = window.require; // Check Existence if ( typeof History.Adapter !== 'undefined' ) { throw new Error('History.js Adapter has already been loaded...'); } // Add the Adapter History.Adapter = { /** * History.Adapter.handlers[uid][eventName] = Array */ handlers: {}, /** * History.Adapter._uid * The current element unique identifier */ _uid: 1, /** * History.Adapter.uid(element) * @param {Element} element * @return {String} uid */ uid: function(element){ return element._uid || (element._uid = History.Adapter._uid++); }, /** * History.Adapter.bind(el,event,callback) * @param {Element} element * @param {String} eventName - custom and standard events * @param {Function} callback * @return */ bind: function(element,eventName,callback){ // Prepare var uid = History.Adapter.uid(element); // Apply Listener History.Adapter.handlers[uid] = History.Adapter.handlers[uid] || {}; History.Adapter.handlers[uid][eventName] = History.Adapter.handlers[uid][eventName] || []; History.Adapter.handlers[uid][eventName].push(callback); // Bind Global Listener element['on'+eventName] = (function(element,eventName){ return function(event){ History.Adapter.trigger(element,eventName,event); }; })(element,eventName); }, /** * History.Adapter.trigger(el,event) * @param {Element} element * @param {String} eventName - custom and standard events * @param {Object} event - a object of event data * @return */ trigger: function(element,eventName,event){ // Prepare event = event || {}; var uid = History.Adapter.uid(element), i,n; // Apply Listener History.Adapter.handlers[uid] = History.Adapter.handlers[uid] || {}; History.Adapter.handlers[uid][eventName] = History.Adapter.handlers[uid][eventName] || []; // Fire Listeners for ( i=0,n=History.Adapter.handlers[uid][eventName].length; i * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window,undefined){ "use strict"; // ======================================================================== // Initialise // Localise Globals var document = window.document, // Make sure we are using the correct document setTimeout = window.setTimeout||setTimeout, clearTimeout = window.clearTimeout||clearTimeout, setInterval = window.setInterval||setInterval, History = window.History = window.History||{}; // Public History Object // Check Existence if ( typeof History.initHtml4 !== 'undefined' ) { throw new Error('History.js HTML4 Support has already been loaded...'); } // ======================================================================== // Initialise HTML4 Support // Initialise HTML4 Support History.initHtml4 = function(){ // Initialise if ( typeof History.initHtml4.initialized !== 'undefined' ) { // Already Loaded return false; } else { History.initHtml4.initialized = true; } // ==================================================================== // Properties /** * History.enabled * Is History enabled? */ History.enabled = true; // ==================================================================== // Hash Storage /** * History.savedHashes * Store the hashes in an array */ History.savedHashes = []; /** * History.isLastHash(newHash) * Checks if the hash is the last hash * @param {string} newHash * @return {boolean} true */ History.isLastHash = function(newHash){ // Prepare var oldHash = History.getHashByIndex(), isLast; // Check isLast = newHash === oldHash; // Return isLast return isLast; }; /** * History.isHashEqual(newHash, oldHash) * Checks to see if two hashes are functionally equal * @param {string} newHash * @param {string} oldHash * @return {boolean} true */ History.isHashEqual = function(newHash, oldHash){ newHash = encodeURIComponent(newHash).replace(/%25/g, "%"); oldHash = encodeURIComponent(oldHash).replace(/%25/g, "%"); return newHash === oldHash; }; /** * History.saveHash(newHash) * Push a Hash * @param {string} newHash * @return {boolean} true */ History.saveHash = function(newHash){ // Check Hash if ( History.isLastHash(newHash) ) { return false; } // Push the Hash History.savedHashes.push(newHash); // Return true return true; }; /** * History.getHashByIndex() * Gets a hash by the index * @param {integer} index * @return {string} */ History.getHashByIndex = function(index){ // Prepare var hash = null; // Handle if ( typeof index === 'undefined' ) { // Get the last inserted hash = History.savedHashes[History.savedHashes.length-1]; } else if ( index < 0 ) { // Get from the end hash = History.savedHashes[History.savedHashes.length+index]; } else { // Get from the beginning hash = History.savedHashes[index]; } // Return hash return hash; }; // ==================================================================== // Discarded States /** * History.discardedHashes * A hashed array of discarded hashes */ History.discardedHashes = {}; /** * History.discardedStates * A hashed array of discarded states */ History.discardedStates = {}; /** * History.discardState(State) * Discards the state by ignoring it through History * @param {object} State * @return {true} */ History.discardState = function(discardedState,forwardState,backState){ //History.debug('History.discardState', arguments); // Prepare var discardedStateHash = History.getHashByState(discardedState), discardObject; // Create Discard Object discardObject = { 'discardedState': discardedState, 'backState': backState, 'forwardState': forwardState }; // Add to DiscardedStates History.discardedStates[discardedStateHash] = discardObject; // Return true return true; }; /** * History.discardHash(hash) * Discards the hash by ignoring it through History * @param {string} hash * @return {true} */ History.discardHash = function(discardedHash,forwardState,backState){ //History.debug('History.discardState', arguments); // Create Discard Object var discardObject = { 'discardedHash': discardedHash, 'backState': backState, 'forwardState': forwardState }; // Add to discardedHash History.discardedHashes[discardedHash] = discardObject; // Return true return true; }; /** * History.discardedState(State) * Checks to see if the state is discarded * @param {object} State * @return {bool} */ History.discardedState = function(State){ // Prepare var StateHash = History.getHashByState(State), discarded; // Check discarded = History.discardedStates[StateHash]||false; // Return true return discarded; }; /** * History.discardedHash(hash) * Checks to see if the state is discarded * @param {string} State * @return {bool} */ History.discardedHash = function(hash){ // Check var discarded = History.discardedHashes[hash]||false; // Return true return discarded; }; /** * History.recycleState(State) * Allows a discarded state to be used again * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.recycleState = function(State){ //History.debug('History.recycleState', arguments); // Prepare var StateHash = History.getHashByState(State); // Remove from DiscardedStates if ( History.discardedState(State) ) { delete History.discardedStates[StateHash]; } // Return true return true; }; // ==================================================================== // HTML4 HashChange Support if ( History.emulated.hashChange ) { /* * We must emulate the HTML4 HashChange Support by manually checking for hash changes */ /** * History.hashChangeInit() * Init the HashChange Emulation */ History.hashChangeInit = function(){ // Define our Checker Function History.checkerFunction = null; // Define some variables that will help in our checker function var lastDocumentHash = '', iframeId, iframe, lastIframeHash, checkerRunning, startedWithHash = Boolean(History.getHash()); // Handle depending on the browser if ( History.isInternetExplorer() ) { // IE6 and IE7 // We need to use an iframe to emulate the back and forward buttons // Create iFrame iframeId = 'historyjs-iframe'; iframe = document.createElement('iframe'); // Adjust iFarme // IE 6 requires iframe to have a src on HTTPS pages, otherwise it will throw a // "This page contains both secure and nonsecure items" warning. iframe.setAttribute('id', iframeId); iframe.setAttribute('src', '#'); iframe.style.display = 'none'; // Append iFrame document.body.appendChild(iframe); // Create initial history entry iframe.contentWindow.document.open(); iframe.contentWindow.document.close(); // Define some variables that will help in our checker function lastIframeHash = ''; checkerRunning = false; // Define the checker function History.checkerFunction = function(){ // Check Running if ( checkerRunning ) { return false; } // Update Running checkerRunning = true; // Fetch var documentHash = History.getHash(), iframeHash = History.getHash(iframe.contentWindow.document); // The Document Hash has changed (application caused) if ( documentHash !== lastDocumentHash ) { // Equalise lastDocumentHash = documentHash; // Create a history entry in the iframe if ( iframeHash !== documentHash ) { //History.debug('hashchange.checker: iframe hash change', 'documentHash (new):', documentHash, 'iframeHash (old):', iframeHash); // Equalise lastIframeHash = iframeHash = documentHash; // Create History Entry iframe.contentWindow.document.open(); iframe.contentWindow.document.close(); // Update the iframe's hash iframe.contentWindow.document.location.hash = History.escapeHash(documentHash); } // Trigger Hashchange Event History.Adapter.trigger(window,'hashchange'); } // The iFrame Hash has changed (back button caused) else if ( iframeHash !== lastIframeHash ) { //History.debug('hashchange.checker: iframe hash out of sync', 'iframeHash (new):', iframeHash, 'documentHash (old):', documentHash); // Equalise lastIframeHash = iframeHash; // If there is no iframe hash that means we're at the original // iframe state. // And if there was a hash on the original request, the original // iframe state was replaced instantly, so skip this state and take // the user back to where they came from. if (startedWithHash && iframeHash === '') { History.back(); } else { // Update the Hash History.setHash(iframeHash,false); } } // Reset Running checkerRunning = false; // Return true return true; }; } else { // We are not IE // Firefox 1 or 2, Opera // Define the checker function History.checkerFunction = function(){ // Prepare var documentHash = History.getHash()||''; // The Document Hash has changed (application caused) if ( documentHash !== lastDocumentHash ) { // Equalise lastDocumentHash = documentHash; // Trigger Hashchange Event History.Adapter.trigger(window,'hashchange'); } // Return true return true; }; } // Apply the checker function History.intervalList.push(setInterval(History.checkerFunction, History.options.hashChangeInterval)); // Done return true; }; // History.hashChangeInit // Bind hashChangeInit History.Adapter.onDomLoad(History.hashChangeInit); } // History.emulated.hashChange // ==================================================================== // HTML5 State Support // Non-Native pushState Implementation if ( History.emulated.pushState ) { /* * We must emulate the HTML5 State Management by using HTML4 HashChange */ /** * History.onHashChange(event) * Trigger HTML5's window.onpopstate via HTML4 HashChange Support */ History.onHashChange = function(event){ //History.debug('History.onHashChange', arguments); // Prepare var currentUrl = ((event && event.newURL) || History.getLocationHref()), currentHash = History.getHashByUrl(currentUrl), currentState = null, currentStateHash = null, currentStateHashExits = null, discardObject; // Check if we are the same state if ( History.isLastHash(currentHash) ) { // There has been no change (just the page's hash has finally propagated) //History.debug('History.onHashChange: no change'); History.busy(false); return false; } // Reset the double check History.doubleCheckComplete(); // Store our location for use in detecting back/forward direction History.saveHash(currentHash); // Expand Hash if ( currentHash && History.isTraditionalAnchor(currentHash) ) { //History.debug('History.onHashChange: traditional anchor', currentHash); // Traditional Anchor Hash History.Adapter.trigger(window,'anchorchange'); History.busy(false); return false; } // Create State currentState = History.extractState(History.getFullUrl(currentHash||History.getLocationHref()),true); // Check if we are the same state if ( History.isLastSavedState(currentState) ) { //History.debug('History.onHashChange: no change'); // There has been no change (just the page's hash has finally propagated) History.busy(false); return false; } // Create the state Hash currentStateHash = History.getHashByState(currentState); // Check if we are DiscardedState discardObject = History.discardedState(currentState); if ( discardObject ) { // Ignore this state as it has been discarded and go back to the state before it if ( History.getHashByIndex(-2) === History.getHashByState(discardObject.forwardState) ) { // We are going backwards //History.debug('History.onHashChange: go backwards'); History.back(false); } else { // We are going forwards //History.debug('History.onHashChange: go forwards'); History.forward(false); } return false; } // Push the new HTML5 State //History.debug('History.onHashChange: success hashchange'); History.pushState(currentState.data,currentState.title,encodeURI(currentState.url),false); // End onHashChange closure return true; }; History.Adapter.bind(window,'hashchange',History.onHashChange); /** * History.pushState(data,title,url) * Add a new State to the history object, become it, and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.pushState = function(data,title,url,queue){ //History.debug('History.pushState: called', arguments); // We assume that the URL passed in is URI-encoded, but this makes // sure that it's fully URI encoded; any '%'s that are encoded are // converted back into '%'s url = encodeURI(url).replace(/%25/g, "%"); // Check the State if ( History.getHashByUrl(url) ) { throw new Error('History.js does not support states with fragment-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.pushState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.pushState, args: arguments, queue: queue }); return false; } // Make Busy History.busy(true); // Fetch the State Object var newState = History.createStateObject(data,title,url), newStateHash = History.getHashByState(newState), oldState = History.getState(false), oldStateHash = History.getHashByState(oldState), html4Hash = History.getHash(), wasExpected = History.expectedStateId == newState.id; // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Recycle the State History.recycleState(newState); // Force update of the title History.setTitle(newState); // Check if we are the same State if ( newStateHash === oldStateHash ) { //History.debug('History.pushState: no change', newStateHash); History.busy(false); return false; } // Update HTML5 State History.saveState(newState); // Fire HTML5 Event if(!wasExpected) History.Adapter.trigger(window,'statechange'); // Update HTML4 Hash if ( !History.isHashEqual(newStateHash, html4Hash) && !History.isHashEqual(newStateHash, History.getShortUrl(History.getLocationHref())) ) { History.setHash(newStateHash,false); } History.busy(false); // End pushState closure return true; }; /** * History.replaceState(data,title,url) * Replace the State and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.replaceState = function(data,title,url,queue){ //History.debug('History.replaceState: called', arguments); // We assume that the URL passed in is URI-encoded, but this makes // sure that it's fully URI encoded; any '%'s that are encoded are // converted back into '%'s url = encodeURI(url).replace(/%25/g, "%"); // Check the State if ( History.getHashByUrl(url) ) { throw new Error('History.js does not support states with fragment-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.replaceState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.replaceState, args: arguments, queue: queue }); return false; } // Make Busy History.busy(true); // Fetch the State Objects var newState = History.createStateObject(data,title,url), newStateHash = History.getHashByState(newState), oldState = History.getState(false), oldStateHash = History.getHashByState(oldState), previousState = History.getStateByIndex(-2); // Discard Old State History.discardState(oldState,newState,previousState); // If the url hasn't changed, just store and save the state // and fire a statechange event to be consistent with the // html 5 api if ( newStateHash === oldStateHash ) { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Recycle the State History.recycleState(newState); // Force update of the title History.setTitle(newState); // Update HTML5 State History.saveState(newState); // Fire HTML5 Event //History.debug('History.pushState: trigger popstate'); History.Adapter.trigger(window,'statechange'); History.busy(false); } else { // Alias to PushState History.pushState(newState.data,newState.title,newState.url,false); } // End replaceState closure return true; }; } // History.emulated.pushState // ==================================================================== // Initialise // Non-Native pushState Implementation if ( History.emulated.pushState ) { /** * Ensure initial state is handled correctly */ if ( History.getHash() && !History.emulated.hashChange ) { History.Adapter.onDomLoad(function(){ History.Adapter.trigger(window,'hashchange'); }); } } // History.emulated.pushState }; // History.initHtml4 // Try to Initialise History if ( typeof History.init !== 'undefined' ) { History.init(); } })(window); /** * History.js Core * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window,undefined){ "use strict"; // ======================================================================== // Initialise // Localise Globals var console = window.console||undefined, // Prevent a JSLint complain document = window.document, // Make sure we are using the correct document navigator = window.navigator, // Make sure we are using the correct navigator sessionStorage = false, // sessionStorage setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, setInterval = window.setInterval, clearInterval = window.clearInterval, JSON = window.JSON, alert = window.alert, History = window.History = window.History||{}, // Public History Object history = window.history; // Old History Object try { sessionStorage = window.sessionStorage; // This will throw an exception in some browsers when cookies/localStorage are explicitly disabled (i.e. Chrome) sessionStorage.setItem('TEST', '1'); sessionStorage.removeItem('TEST'); } catch(e) { sessionStorage = false; } // MooTools Compatibility JSON.stringify = JSON.stringify||JSON.encode; JSON.parse = JSON.parse||JSON.decode; // Check Existence if ( typeof History.init !== 'undefined' ) { throw new Error('History.js Core has already been loaded...'); } // Initialise History History.init = function(options){ // Check Load Status of Adapter if ( typeof History.Adapter === 'undefined' ) { return false; } // Check Load Status of Core if ( typeof History.initCore !== 'undefined' ) { History.initCore(); } // Check Load Status of HTML4 Support if ( typeof History.initHtml4 !== 'undefined' ) { History.initHtml4(); } // Return true return true; }; // ======================================================================== // Initialise Core // Initialise Core History.initCore = function(options){ // Initialise if ( typeof History.initCore.initialized !== 'undefined' ) { // Already Loaded return false; } else { History.initCore.initialized = true; } // ==================================================================== // Options /** * History.options * Configurable options */ History.options = History.options||{}; /** * History.options.hashChangeInterval * How long should the interval be before hashchange checks */ History.options.hashChangeInterval = History.options.hashChangeInterval || 100; /** * History.options.safariPollInterval * How long should the interval be before safari poll checks */ History.options.safariPollInterval = History.options.safariPollInterval || 500; /** * History.options.doubleCheckInterval * How long should the interval be before we perform a double check */ History.options.doubleCheckInterval = History.options.doubleCheckInterval || 500; /** * History.options.disableSuid * Force History not to append suid */ History.options.disableSuid = History.options.disableSuid || false; /** * History.options.storeInterval * How long should we wait between store calls */ History.options.storeInterval = History.options.storeInterval || 1000; /** * History.options.busyDelay * How long should we wait between busy events */ History.options.busyDelay = History.options.busyDelay || 250; /** * History.options.debug * If true will enable debug messages to be logged */ History.options.debug = History.options.debug || false; /** * History.options.initialTitle * What is the title of the initial state */ History.options.initialTitle = History.options.initialTitle || document.title; /** * History.options.html4Mode * If true, will force HTMl4 mode (hashtags) */ History.options.html4Mode = History.options.html4Mode || false; /** * History.options.delayInit * Want to override default options and call init manually. */ History.options.delayInit = History.options.delayInit || false; // ==================================================================== // Interval record /** * History.intervalList * List of intervals set, to be cleared when document is unloaded. */ History.intervalList = []; /** * History.clearAllIntervals * Clears all setInterval instances. */ History.clearAllIntervals = function(){ var i, il = History.intervalList; if (typeof il !== "undefined" && il !== null) { for (i = 0; i < il.length; i++) { clearInterval(il[i]); } History.intervalList = null; } }; // ==================================================================== // Debug /** * History.debug(message,...) * Logs the passed arguments if debug enabled */ History.debug = function(){ if ( (History.options.debug||false) ) { History.log.apply(History,arguments); } }; /** * History.log(message,...) * Logs the passed arguments */ History.log = function(){ // Prepare var consoleExists = !(typeof console === 'undefined' || typeof console.log === 'undefined' || typeof console.log.apply === 'undefined'), textarea = document.getElementById('log'), message, i,n, args,arg ; // Write to Console if ( consoleExists ) { args = Array.prototype.slice.call(arguments); message = args.shift(); if ( typeof console.debug !== 'undefined' ) { console.debug.apply(console,[message,args]); } else { console.log.apply(console,[message,args]); } } else { message = ("\n"+arguments[0]+"\n"); } // Write to log for ( i=1,n=arguments.length; i * @author James Padolsey */ History.getInternetExplorerMajorVersion = function(){ var result = History.getInternetExplorerMajorVersion.cached = (typeof History.getInternetExplorerMajorVersion.cached !== 'undefined') ? History.getInternetExplorerMajorVersion.cached : (function(){ var v = 3, div = document.createElement('div'), all = div.getElementsByTagName('i'); while ( (div.innerHTML = '') && all[0] ) {} return (v > 4) ? v : false; })() ; return result; }; /** * History.isInternetExplorer() * Are we using Internet Explorer? * @return {boolean} * @license Public Domain * @author Benjamin Arthur Lupton */ History.isInternetExplorer = function(){ var result = History.isInternetExplorer.cached = (typeof History.isInternetExplorer.cached !== 'undefined') ? History.isInternetExplorer.cached : Boolean(History.getInternetExplorerMajorVersion()) ; return result; }; /** * History.emulated * Which features require emulating? */ if (History.options.html4Mode) { History.emulated = { pushState : true, hashChange: true }; } else { History.emulated = { pushState: !Boolean( window.history && window.history.pushState && window.history.replaceState && !( (/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i).test(navigator.userAgent) /* disable for versions of iOS before version 4.3 (8F190) */ || (/AppleWebKit\/5([0-2]|3[0-2])/i).test(navigator.userAgent) /* disable for the mercury iOS browser, or at least older versions of the webkit engine */ ) ), hashChange: Boolean( !(('onhashchange' in window) || ('onhashchange' in document)) || (History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8) ) }; } /** * History.enabled * Is History enabled? */ History.enabled = !History.emulated.pushState; /** * History.bugs * Which bugs are present */ History.bugs = { /** * Safari 5 and Safari iOS 4 fail to return to the correct state once a hash is replaced by a `replaceState` call * https://bugs.webkit.org/show_bug.cgi?id=56249 */ setHash: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * Safari 5 and Safari iOS 4 sometimes fail to apply the state change under busy conditions * https://bugs.webkit.org/show_bug.cgi?id=42940 */ safariPoll: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * MSIE 6 and 7 sometimes do not apply a hash even it was told to (requiring a second call to the apply function) */ ieDoubleCheck: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8), /** * MSIE 6 requires the entire hash to be encoded for the hashes to trigger the onHashChange event */ hashEscape: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 7) }; /** * History.isEmptyObject(obj) * Checks to see if the Object is Empty * @param {Object} obj * @return {boolean} */ History.isEmptyObject = function(obj) { for ( var name in obj ) { if ( obj.hasOwnProperty(name) ) { return false; } } return true; }; /** * History.cloneObject(obj) * Clones a object and eliminate all references to the original contexts * @param {Object} obj * @return {Object} */ History.cloneObject = function(obj) { var hash,newObj; if ( obj ) { hash = JSON.stringify(obj); newObj = JSON.parse(hash); } else { newObj = {}; } return newObj; }; // ==================================================================== // URL Helpers /** * History.getRootUrl() * Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com" * @return {String} rootUrl */ History.getRootUrl = function(){ // Create var rootUrl = document.location.protocol+'//'+(document.location.hostname||document.location.host); if ( document.location.port||false ) { rootUrl += ':'+document.location.port; } rootUrl += '/'; // Return return rootUrl; }; /** * History.getBaseHref() * Fetches the `href` attribute of the `` element if it exists * @return {String} baseHref */ History.getBaseHref = function(){ // Create var baseElements = document.getElementsByTagName('base'), baseElement = null, baseHref = ''; // Test for Base Element if ( baseElements.length === 1 ) { // Prepare for Base Element baseElement = baseElements[0]; baseHref = baseElement.href.replace(/[^\/]+$/,''); } // Adjust trailing slash baseHref = baseHref.replace(/\/+$/,''); if ( baseHref ) baseHref += '/'; // Return return baseHref; }; /** * History.getBaseUrl() * Fetches the baseHref or basePageUrl or rootUrl (whichever one exists first) * @return {String} baseUrl */ History.getBaseUrl = function(){ // Create var baseUrl = History.getBaseHref()||History.getBasePageUrl()||History.getRootUrl(); // Return return baseUrl; }; /** * History.getPageUrl() * Fetches the URL of the current page * @return {String} pageUrl */ History.getPageUrl = function(){ // Fetch var State = History.getState(false,false), stateUrl = (State||{}).url||History.getLocationHref(), pageUrl; // Create pageUrl = stateUrl.replace(/\/+$/,'').replace(/[^\/]+$/,function(part,index,string){ return (/\./).test(part) ? part : part+'/'; }); // Return return pageUrl; }; /** * History.getBasePageUrl() * Fetches the Url of the directory of the current page * @return {String} basePageUrl */ History.getBasePageUrl = function(){ // Create var basePageUrl = (History.getLocationHref()).replace(/[#\?].*/,'').replace(/[^\/]+$/,function(part,index,string){ return (/[^\/]$/).test(part) ? '' : part; }).replace(/\/+$/,'')+'/'; // Return return basePageUrl; }; /** * History.getFullUrl(url) * Ensures that we have an absolute URL and not a relative URL * @param {string} url * @param {Boolean} allowBaseHref * @return {string} fullUrl */ History.getFullUrl = function(url,allowBaseHref){ // Prepare var fullUrl = url, firstChar = url.substring(0,1); allowBaseHref = (typeof allowBaseHref === 'undefined') ? true : allowBaseHref; // Check if ( /[a-z]+\:\/\//.test(url) ) { // Full URL } else if ( firstChar === '/' ) { // Root URL fullUrl = History.getRootUrl()+url.replace(/^\/+/,''); } else if ( firstChar === '#' ) { // Anchor URL fullUrl = History.getPageUrl().replace(/#.*/,'')+url; } else if ( firstChar === '?' ) { // Query URL fullUrl = History.getPageUrl().replace(/[\?#].*/,'')+url; } else { // Relative URL if ( allowBaseHref ) { fullUrl = History.getBaseUrl()+url.replace(/^(\.\/)+/,''); } else { fullUrl = History.getBasePageUrl()+url.replace(/^(\.\/)+/,''); } // We have an if condition above as we do not want hashes // which are relative to the baseHref in our URLs // as if the baseHref changes, then all our bookmarks // would now point to different locations // whereas the basePageUrl will always stay the same } // Return return fullUrl.replace(/\#$/,''); }; /** * History.getShortUrl(url) * Ensures that we have a relative URL and not a absolute URL * @param {string} url * @return {string} url */ History.getShortUrl = function(url){ // Prepare var shortUrl = url, baseUrl = History.getBaseUrl(), rootUrl = History.getRootUrl(); // Trim baseUrl if ( History.emulated.pushState ) { // We are in a if statement as when pushState is not emulated // The actual url these short urls are relative to can change // So within the same session, we the url may end up somewhere different shortUrl = shortUrl.replace(baseUrl,''); } // Trim rootUrl shortUrl = shortUrl.replace(rootUrl,'/'); // Ensure we can still detect it as a state if ( History.isTraditionalAnchor(shortUrl) ) { shortUrl = './'+shortUrl; } // Clean It shortUrl = shortUrl.replace(/^(\.\/)+/g,'./').replace(/\#$/,''); // Return return shortUrl; }; /** * History.getLocationHref(document) * Returns a normalized version of document.location.href * accounting for browser inconsistencies, etc. * * This URL will be URI-encoded and will include the hash * * @param {object} document * @return {string} url */ History.getLocationHref = function(doc) { doc = doc || document; // most of the time, this will be true if (doc.URL === doc.location.href) return doc.location.href; // some versions of webkit URI-decode document.location.href // but they leave document.URL in an encoded state if (doc.location.href === decodeURIComponent(doc.URL)) return doc.URL; // FF 3.6 only updates document.URL when a page is reloaded // document.location.href is updated correctly if (doc.location.hash && decodeURIComponent(doc.location.href.replace(/^[^#]+/, "")) === doc.location.hash) return doc.location.href; if (doc.URL.indexOf('#') == -1 && doc.location.href.indexOf('#') != -1) return doc.location.href; return doc.URL || doc.location.href; }; // ==================================================================== // State Storage /** * History.store * The store for all session specific data */ History.store = {}; /** * History.idToState * 1-1: State ID to State Object */ History.idToState = History.idToState||{}; /** * History.stateToId * 1-1: State String to State ID */ History.stateToId = History.stateToId||{}; /** * History.urlToId * 1-1: State URL to State ID */ History.urlToId = History.urlToId||{}; /** * History.storedStates * Store the states in an array */ History.storedStates = History.storedStates||[]; /** * History.savedStates * Saved the states in an array */ History.savedStates = History.savedStates||[]; /** * History.noramlizeStore() * Noramlize the store by adding necessary values */ History.normalizeStore = function(){ History.store.idToState = History.store.idToState||{}; History.store.urlToId = History.store.urlToId||{}; History.store.stateToId = History.store.stateToId||{}; }; /** * History.getState() * Get an object containing the data, title and url of the current state * @param {Boolean} friendly * @param {Boolean} create * @return {Object} State */ History.getState = function(friendly,create){ // Prepare if ( typeof friendly === 'undefined' ) { friendly = true; } if ( typeof create === 'undefined' ) { create = true; } // Fetch var State = History.getLastSavedState(); // Create if ( !State && create ) { State = History.createStateObject(); } // Adjust if ( friendly ) { State = History.cloneObject(State); State.url = State.cleanUrl||State.url; } // Return return State; }; /** * History.getIdByState(State) * Gets a ID for a State * @param {State} newState * @return {String} id */ History.getIdByState = function(newState){ // Fetch ID var id = History.extractId(newState.url), str; if ( !id ) { // Find ID via State String str = History.getStateString(newState); if ( typeof History.stateToId[str] !== 'undefined' ) { id = History.stateToId[str]; } else if ( typeof History.store.stateToId[str] !== 'undefined' ) { id = History.store.stateToId[str]; } else { // Generate a new ID while ( true ) { id = (new Date()).getTime() + String(Math.random()).replace(/\D/g,''); if ( typeof History.idToState[id] === 'undefined' && typeof History.store.idToState[id] === 'undefined' ) { break; } } // Apply the new State to the ID History.stateToId[str] = id; History.idToState[id] = newState; } } // Return ID return id; }; /** * History.normalizeState(State) * Expands a State Object * @param {object} State * @return {object} */ History.normalizeState = function(oldState){ // Variables var newState, dataNotEmpty; // Prepare if ( !oldState || (typeof oldState !== 'object') ) { oldState = {}; } // Check if ( typeof oldState.normalized !== 'undefined' ) { return oldState; } // Adjust if ( !oldState.data || (typeof oldState.data !== 'object') ) { oldState.data = {}; } // ---------------------------------------------------------------- // Create newState = {}; newState.normalized = true; newState.title = oldState.title||''; newState.url = History.getFullUrl(oldState.url?oldState.url:(History.getLocationHref())); newState.hash = History.getShortUrl(newState.url); newState.data = History.cloneObject(oldState.data); // Fetch ID newState.id = History.getIdByState(newState); // ---------------------------------------------------------------- // Clean the URL newState.cleanUrl = newState.url.replace(/\??\&_suid.*/,''); newState.url = newState.cleanUrl; // Check to see if we have more than just a url dataNotEmpty = !History.isEmptyObject(newState.data); // Apply if ( (newState.title || dataNotEmpty) && History.options.disableSuid !== true ) { // Add ID to Hash newState.hash = History.getShortUrl(newState.url).replace(/\??\&_suid.*/,''); if ( !/\?/.test(newState.hash) ) { newState.hash += '?'; } newState.hash += '&_suid='+newState.id; } // Create the Hashed URL newState.hashedUrl = History.getFullUrl(newState.hash); // ---------------------------------------------------------------- // Update the URL if we have a duplicate if ( (History.emulated.pushState || History.bugs.safariPoll) && History.hasUrlDuplicate(newState) ) { newState.url = newState.hashedUrl; } // ---------------------------------------------------------------- // Return return newState; }; /** * History.createStateObject(data,title,url) * Creates a object based on the data, title and url state params * @param {object} data * @param {string} title * @param {string} url * @return {object} */ History.createStateObject = function(data,title,url){ // Hashify var State = { 'data': data, 'title': title, 'url': url }; // Expand the State State = History.normalizeState(State); // Return object return State; }; /** * History.getStateById(id) * Get a state by it's UID * @param {String} id */ History.getStateById = function(id){ // Prepare id = String(id); // Retrieve var State = History.idToState[id] || History.store.idToState[id] || undefined; // Return State return State; }; /** * Get a State's String * @param {State} passedState */ History.getStateString = function(passedState){ // Prepare var State, cleanedState, str; // Fetch State = History.normalizeState(passedState); // Clean cleanedState = { data: State.data, title: passedState.title, url: passedState.url }; // Fetch str = JSON.stringify(cleanedState); // Return return str; }; /** * Get a State's ID * @param {State} passedState * @return {String} id */ History.getStateId = function(passedState){ // Prepare var State, id; // Fetch State = History.normalizeState(passedState); // Fetch id = State.id; // Return return id; }; /** * History.getHashByState(State) * Creates a Hash for the State Object * @param {State} passedState * @return {String} hash */ History.getHashByState = function(passedState){ // Prepare var State, hash; // Fetch State = History.normalizeState(passedState); // Hash hash = State.hash; // Return return hash; }; /** * History.extractId(url_or_hash) * Get a State ID by it's URL or Hash * @param {string} url_or_hash * @return {string} id */ History.extractId = function ( url_or_hash ) { // Prepare var id,parts,url, tmp; // Extract // If the URL has a #, use the id from before the # if (url_or_hash.indexOf('#') != -1) { tmp = url_or_hash.split("#")[0]; } else { tmp = url_or_hash; } parts = /(.*)\&_suid=([0-9]+)$/.exec(tmp); url = parts ? (parts[1]||url_or_hash) : url_or_hash; id = parts ? String(parts[2]||'') : ''; // Return return id||false; }; /** * History.isTraditionalAnchor * Checks to see if the url is a traditional anchor or not * @param {String} url_or_hash * @return {Boolean} */ History.isTraditionalAnchor = function(url_or_hash){ // Check var isTraditional = !(/[\/\?\.]/.test(url_or_hash)); // Return return isTraditional; }; /** * History.extractState * Get a State by it's URL or Hash * @param {String} url_or_hash * @return {State|null} */ History.extractState = function(url_or_hash,create){ // Prepare var State = null, id, url; create = create||false; // Fetch SUID id = History.extractId(url_or_hash); if ( id ) { State = History.getStateById(id); } // Fetch SUID returned no State if ( !State ) { // Fetch URL url = History.getFullUrl(url_or_hash); // Check URL id = History.getIdByUrl(url)||false; if ( id ) { State = History.getStateById(id); } // Create State if ( !State && create && !History.isTraditionalAnchor(url_or_hash) ) { State = History.createStateObject(null,null,url); } } // Return return State; }; /** * History.getIdByUrl() * Get a State ID by a State URL */ History.getIdByUrl = function(url){ // Fetch var id = History.urlToId[url] || History.store.urlToId[url] || undefined; // Return return id; }; /** * History.getLastSavedState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastSavedState = function(){ return History.savedStates[History.savedStates.length-1]||undefined; }; /** * History.getLastStoredState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastStoredState = function(){ return History.storedStates[History.storedStates.length-1]||undefined; }; /** * History.hasUrlDuplicate * Checks if a Url will have a url conflict * @param {Object} newState * @return {Boolean} hasDuplicate */ History.hasUrlDuplicate = function(newState) { // Prepare var hasDuplicate = false, oldState; // Fetch oldState = History.extractState(newState.url); // Check hasDuplicate = oldState && oldState.id !== newState.id; // Return return hasDuplicate; }; /** * History.storeState * Store a State * @param {Object} newState * @return {Object} newState */ History.storeState = function(newState){ // Store the State History.urlToId[newState.url] = newState.id; // Push the State History.storedStates.push(History.cloneObject(newState)); // Return newState return newState; }; /** * History.isLastSavedState(newState) * Tests to see if the state is the last state * @param {Object} newState * @return {boolean} isLast */ History.isLastSavedState = function(newState){ // Prepare var isLast = false, newId, oldState, oldId; // Check if ( History.savedStates.length ) { newId = newState.id; oldState = History.getLastSavedState(); oldId = oldState.id; // Check isLast = (newId === oldId); } // Return return isLast; }; /** * History.saveState * Push a State * @param {Object} newState * @return {boolean} changed */ History.saveState = function(newState){ // Check Hash if ( History.isLastSavedState(newState) ) { return false; } // Push the State History.savedStates.push(History.cloneObject(newState)); // Return true return true; }; /** * History.getStateByIndex() * Gets a state by the index * @param {integer} index * @return {Object} */ History.getStateByIndex = function(index){ // Prepare var State = null; // Handle if ( typeof index === 'undefined' ) { // Get the last inserted State = History.savedStates[History.savedStates.length-1]; } else if ( index < 0 ) { // Get from the end State = History.savedStates[History.savedStates.length+index]; } else { // Get from the beginning State = History.savedStates[index]; } // Return State return State; }; /** * History.getCurrentIndex() * Gets the current index * @return (integer) */ History.getCurrentIndex = function(){ // Prepare var index = null; // No states saved if(History.savedStates.length < 1) { index = 0; } else { index = History.savedStates.length-1; } return index; }; // ==================================================================== // Hash Helpers /** * History.getHash() * @param {Location=} location * Gets the current document hash * Note: unlike location.hash, this is guaranteed to return the escaped hash in all browsers * @return {string} */ History.getHash = function(doc){ var url = History.getLocationHref(doc), hash; hash = History.getHashByUrl(url); return hash; }; /** * History.unescapeHash() * normalize and Unescape a Hash * @param {String} hash * @return {string} */ History.unescapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Unescape hash result = decodeURIComponent(result); // Return result return result; }; /** * History.normalizeHash() * normalize a hash across browsers * @return {string} */ History.normalizeHash = function(hash){ // Prepare var result = hash.replace(/[^#]*#/,'').replace(/#.*/, ''); // Return result return result; }; /** * History.setHash(hash) * Sets the document hash * @param {string} hash * @return {History} */ History.setHash = function(hash,queue){ // Prepare var State, pageUrl; // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.setHash: we must wait', arguments); History.pushQueue({ scope: History, callback: History.setHash, args: arguments, queue: queue }); return false; } // Log //History.debug('History.setHash: called',hash); // Make Busy + Continue History.busy(true); // Check if hash is a state State = History.extractState(hash,true); if ( State && !History.emulated.pushState ) { // Hash is a state so skip the setHash //History.debug('History.setHash: Hash is a state so skipping the hash set with a direct pushState call',arguments); // PushState History.pushState(State.data,State.title,State.url,false); } else if ( History.getHash() !== hash ) { // Hash is a proper hash, so apply it // Handle browser bugs if ( History.bugs.setHash ) { // Fix Safari Bug https://bugs.webkit.org/show_bug.cgi?id=56249 // Fetch the base page pageUrl = History.getPageUrl(); // Safari hash apply History.pushState(null,null,pageUrl+'#'+hash,false); } else { // Normal hash apply document.location.hash = hash; } } // Chain return History; }; /** * History.escape() * normalize and Escape a Hash * @return {string} */ History.escapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Escape hash result = window.encodeURIComponent(result); // IE6 Escape Bug if ( !History.bugs.hashEscape ) { // Restore common parts result = result .replace(/\%21/g,'!') .replace(/\%26/g,'&') .replace(/\%3D/g,'=') .replace(/\%3F/g,'?'); } // Return result return result; }; /** * History.getHashByUrl(url) * Extracts the Hash from a URL * @param {string} url * @return {string} url */ History.getHashByUrl = function(url){ // Extract the hash var hash = String(url) .replace(/([^#]*)#?([^#]*)#?(.*)/, '$2') ; // Unescape hash hash = History.unescapeHash(hash); // Return hash return hash; }; /** * History.setTitle(title) * Applies the title to the document * @param {State} newState * @return {Boolean} */ History.setTitle = function(newState){ // Prepare var title = newState.title, firstState; // Initial if ( !title ) { firstState = History.getStateByIndex(0); if ( firstState && firstState.url === newState.url ) { title = firstState.title||History.options.initialTitle; } } // Apply try { document.getElementsByTagName('title')[0].innerHTML = title.replace('<','<').replace('>','>').replace(' & ',' & '); } catch ( Exception ) { } document.title = title; // Chain return History; }; // ==================================================================== // Queueing /** * History.queues * The list of queues to use * First In, First Out */ History.queues = []; /** * History.busy(value) * @param {boolean} value [optional] * @return {boolean} busy */ History.busy = function(value){ // Apply if ( typeof value !== 'undefined' ) { //History.debug('History.busy: changing ['+(History.busy.flag||false)+'] to ['+(value||false)+']', History.queues.length); History.busy.flag = value; } // Default else if ( typeof History.busy.flag === 'undefined' ) { History.busy.flag = false; } // Queue if ( !History.busy.flag ) { // Execute the next item in the queue clearTimeout(History.busy.timeout); var fireNext = function(){ var i, queue, item; if ( History.busy.flag ) return; for ( i=History.queues.length-1; i >= 0; --i ) { queue = History.queues[i]; if ( queue.length === 0 ) continue; item = queue.shift(); History.fireQueueItem(item); History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } }; History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } // Return return History.busy.flag; }; /** * History.busy.flag */ History.busy.flag = false; /** * History.fireQueueItem(item) * Fire a Queue Item * @param {Object} item * @return {Mixed} result */ History.fireQueueItem = function(item){ return item.callback.apply(item.scope||History,item.args||[]); }; /** * History.pushQueue(callback,args) * Add an item to the queue * @param {Object} item [scope,callback,args,queue] */ History.pushQueue = function(item){ // Prepare the queue History.queues[item.queue||0] = History.queues[item.queue||0]||[]; // Add to the queue History.queues[item.queue||0].push(item); // Chain return History; }; /** * History.queue (item,queue), (func,queue), (func), (item) * Either firs the item now if not busy, or adds it to the queue */ History.queue = function(item,queue){ // Prepare if ( typeof item === 'function' ) { item = { callback: item }; } if ( typeof queue !== 'undefined' ) { item.queue = queue; } // Handle if ( History.busy() ) { History.pushQueue(item); } else { History.fireQueueItem(item); } // Chain return History; }; /** * History.clearQueue() * Clears the Queue */ History.clearQueue = function(){ History.busy.flag = false; History.queues = []; return History; }; // ==================================================================== // IE Bug Fix /** * History.stateChanged * States whether or not the state has changed since the last double check was initialised */ History.stateChanged = false; /** * History.doubleChecker * Contains the timeout used for the double checks */ History.doubleChecker = false; /** * History.doubleCheckComplete() * Complete a double check * @return {History} */ History.doubleCheckComplete = function(){ // Update History.stateChanged = true; // Clear History.doubleCheckClear(); // Chain return History; }; /** * History.doubleCheckClear() * Clear a double check * @return {History} */ History.doubleCheckClear = function(){ // Clear if ( History.doubleChecker ) { clearTimeout(History.doubleChecker); History.doubleChecker = false; } // Chain return History; }; /** * History.doubleCheck() * Create a double check * @return {History} */ History.doubleCheck = function(tryAgain){ // Reset History.stateChanged = false; History.doubleCheckClear(); // Fix IE6,IE7 bug where calling history.back or history.forward does not actually change the hash (whereas doing it manually does) // Fix Safari 5 bug where sometimes the state does not change: https://bugs.webkit.org/show_bug.cgi?id=42940 if ( History.bugs.ieDoubleCheck ) { // Apply Check History.doubleChecker = setTimeout( function(){ History.doubleCheckClear(); if ( !History.stateChanged ) { //History.debug('History.doubleCheck: State has not yet changed, trying again', arguments); // Re-Attempt tryAgain(); } return true; }, History.options.doubleCheckInterval ); } // Chain return History; }; // ==================================================================== // Safari Bug Fix /** * History.safariStatePoll() * Poll the current state * @return {History} */ History.safariStatePoll = function(){ // Poll the URL // Get the Last State which has the new URL var urlState = History.extractState(History.getLocationHref()), newState; // Check for a difference if ( !History.isLastSavedState(urlState) ) { newState = urlState; } else { return; } // Check if we have a state with that url // If not create it if ( !newState ) { //History.debug('History.safariStatePoll: new'); newState = History.createStateObject(); } // Apply the New State //History.debug('History.safariStatePoll: trigger'); History.Adapter.trigger(window,'popstate'); // Chain return History; }; // ==================================================================== // State Aliases /** * History.back(queue) * Send the browser history back one item * @param {Integer} queue [optional] */ History.back = function(queue){ //History.debug('History.back: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.back: we must wait', arguments); History.pushQueue({ scope: History, callback: History.back, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.back(false); }); // Go back history.go(-1); // End back closure return true; }; /** * History.forward(queue) * Send the browser history forward one item * @param {Integer} queue [optional] */ History.forward = function(queue){ //History.debug('History.forward: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.forward: we must wait', arguments); History.pushQueue({ scope: History, callback: History.forward, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.forward(false); }); // Go forward history.go(1); // End forward closure return true; }; /** * History.go(index,queue) * Send the browser history back or forward index times * @param {Integer} queue [optional] */ History.go = function(index,queue){ //History.debug('History.go: called', arguments); // Prepare var i; // Handle if ( index > 0 ) { // Forward for ( i=1; i<=index; ++i ) { History.forward(queue); } } else if ( index < 0 ) { // Backward for ( i=-1; i>=index; --i ) { History.back(queue); } } else { throw new Error('History.go: History.go requires a positive or negative integer passed.'); } // Chain return History; }; // ==================================================================== // HTML5 State Support // Non-Native pushState Implementation if ( History.emulated.pushState ) { /* * Provide Skeleton for HTML4 Browsers */ // Prepare var emptyFunction = function(){}; History.pushState = History.pushState||emptyFunction; History.replaceState = History.replaceState||emptyFunction; } // History.emulated.pushState // Native pushState Implementation else { /* * Use native HTML5 History API Implementation */ /** * History.onPopState(event,extra) * Refresh the Current State */ History.onPopState = function(event,extra){ // Prepare var stateId = false, newState = false, currentHash, currentState; // Reset the double check History.doubleCheckComplete(); // Check for a Hash, and handle apporiatly currentHash = History.getHash(); if ( currentHash ) { // Expand Hash currentState = History.extractState(currentHash||History.getLocationHref(),true); if ( currentState ) { // We were able to parse it, it must be a State! // Let's forward to replaceState //History.debug('History.onPopState: state anchor', currentHash, currentState); History.replaceState(currentState.data, currentState.title, currentState.url, false); } else { // Traditional Anchor //History.debug('History.onPopState: traditional anchor', currentHash); History.Adapter.trigger(window,'anchorchange'); History.busy(false); } // We don't care for hashes History.expectedStateId = false; return false; } // Ensure stateId = History.Adapter.extractEventData('state',event,extra) || false; // Fetch State if ( stateId ) { // Vanilla: Back/forward button was used newState = History.getStateById(stateId); } else if ( History.expectedStateId ) { // Vanilla: A new state was pushed, and popstate was called manually newState = History.getStateById(History.expectedStateId); } else { // Initial State newState = History.extractState(History.getLocationHref()); } // The State did not exist in our store if ( !newState ) { // Regenerate the State newState = History.createStateObject(null,null,History.getLocationHref()); } // Clean History.expectedStateId = false; // Check if we are the same state if ( History.isLastSavedState(newState) ) { // There has been no change (just the page's hash has finally propagated) //History.debug('History.onPopState: no change', newState, History.savedStates); History.busy(false); return false; } // Store the State History.storeState(newState); History.saveState(newState); // Force update of the title History.setTitle(newState); // Fire Our Event History.Adapter.trigger(window,'statechange'); History.busy(false); // Return true return true; }; History.Adapter.bind(window,'popstate',History.onPopState); /** * History.pushState(data,title,url) * Add a new State to the history object, become it, and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.pushState = function(data,title,url,queue){ //History.debug('History.pushState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.pushState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.pushState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.pushState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End pushState closure return true; }; /** * History.replaceState(data,title,url) * Replace the State and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.replaceState = function(data,title,url,queue){ //History.debug('History.replaceState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.replaceState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.replaceState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.replaceState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End replaceState closure return true; }; } // !History.emulated.pushState // ==================================================================== // Initialise /** * Load the Store */ if ( sessionStorage ) { // Fetch try { History.store = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { History.store = {}; } // Normalize History.normalizeStore(); } else { // Default Load History.store = {}; History.normalizeStore(); } /** * Clear Intervals on exit to prevent memory leaks */ History.Adapter.bind(window,"unload",History.clearAllIntervals); /** * Create the initial State */ History.saveState(History.storeState(History.extractState(History.getLocationHref(),true))); /** * Bind for Saving Store */ if ( sessionStorage ) { // When the page is closed History.onUnload = function(){ // Prepare var currentStore, item, currentStoreString; // Fetch try { currentStore = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { currentStore = {}; } // Ensure currentStore.idToState = currentStore.idToState || {}; currentStore.urlToId = currentStore.urlToId || {}; currentStore.stateToId = currentStore.stateToId || {}; // Sync for ( item in History.idToState ) { if ( !History.idToState.hasOwnProperty(item) ) { continue; } currentStore.idToState[item] = History.idToState[item]; } for ( item in History.urlToId ) { if ( !History.urlToId.hasOwnProperty(item) ) { continue; } currentStore.urlToId[item] = History.urlToId[item]; } for ( item in History.stateToId ) { if ( !History.stateToId.hasOwnProperty(item) ) { continue; } currentStore.stateToId[item] = History.stateToId[item]; } // Update History.store = currentStore; History.normalizeStore(); // In Safari, going into Private Browsing mode causes the // Session Storage object to still exist but if you try and use // or set any property/function of it it throws the exception // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to // add something to storage that exceeded the quota." infinitely // every second. currentStoreString = JSON.stringify(currentStore); try { // Store sessionStorage.setItem('History.store', currentStoreString); } catch (e) { if (e.code === DOMException.QUOTA_EXCEEDED_ERR) { if (sessionStorage.length) { // Workaround for a bug seen on iPads. Sometimes the quota exceeded error comes up and simply // removing/resetting the storage can work. sessionStorage.removeItem('History.store'); sessionStorage.setItem('History.store', currentStoreString); } else { // Otherwise, we're probably private browsing in Safari, so we'll ignore the exception. } } else { throw e; } } }; // For Internet Explorer History.intervalList.push(setInterval(History.onUnload,History.options.storeInterval)); // For Other Browsers History.Adapter.bind(window,'beforeunload',History.onUnload); History.Adapter.bind(window,'unload',History.onUnload); // Both are enabled for consistency } // Non-Native pushState Implementation if ( !History.emulated.pushState ) { // Be aware, the following is only for native pushState implementations // If you are wanting to include something for all browsers // Then include it above this if block /** * Setup Safari Fix */ if ( History.bugs.safariPoll ) { History.intervalList.push(setInterval(History.safariStatePoll, History.options.safariPollInterval)); } /** * Ensure Cross Browser Compatibility */ if ( navigator.vendor === 'Apple Computer, Inc.' || (navigator.appCodeName||'') === 'Mozilla' ) { /** * Fix Safari HashChange Issue */ // Setup Alias History.Adapter.bind(window,'hashchange',function(){ History.Adapter.trigger(window,'popstate'); }); // Initialise Alias if ( History.getHash() ) { History.Adapter.onDomLoad(function(){ History.Adapter.trigger(window,'hashchange'); }); } } } // !History.emulated.pushState }; // History.initCore // Try to Initialise History if (!History.options || !History.options.delayInit) { History.init(); } })(window); ================================================ FILE: scripts/bundled-uncompressed/html4+html5/extjs.history.js ================================================ /* json2.js 2012-10-08 Public Domain. NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. See http://www.JSON.org/js.html This code should be minified before deployment. See http://javascript.crockford.com/jsmin.html USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO NOT CONTROL. This file creates a global JSON object containing two methods: stringify and parse. JSON.stringify(value, replacer, space) value any JavaScript value, usually an object or array. replacer an optional parameter that determines how object values are stringified for objects. It can be a function or an array of strings. space an optional parameter that specifies the indentation of nested structures. If it is omitted, the text will be packed without extra whitespace. If it is a number, it will specify the number of spaces to indent at each level. If it is a string (such as '\t' or ' '), it contains the characters used to indent at each level. This method produces a JSON text from a JavaScript value. When an object value is found, if the object contains a toJSON method, its toJSON method will be called and the result will be stringified. A toJSON method does not serialize: it returns the value represented by the name/value pair that should be serialized, or undefined if nothing should be serialized. The toJSON method will be passed the key associated with the value, and this will be bound to the value For example, this would serialize Dates as ISO strings. Date.prototype.toJSON = function (key) { function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } return this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z'; }; You can provide an optional replacer method. It will be passed the key and value of each member, with this bound to the containing object. The value that is returned from your method will be serialized. If your method returns undefined, then the member will be excluded from the serialization. If the replacer parameter is an array of strings, then it will be used to select the members to be serialized. It filters the results such that only members with keys listed in the replacer array are stringified. Values that do not have JSON representations, such as undefined or functions, will not be serialized. Such values in objects will be dropped; in arrays they will be replaced with null. You can use a replacer function to replace those with JSON values. JSON.stringify(undefined) returns undefined. The optional space parameter produces a stringification of the value that is filled with line breaks and indentation to make it easier to read. If the space parameter is a non-empty string, then that string will be used for indentation. If the space parameter is a number, then the indentation will be that many spaces. Example: text = JSON.stringify(['e', {pluribus: 'unum'}]); // text is '["e",{"pluribus":"unum"}]' text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' text = JSON.stringify([new Date()], function (key, value) { return this[key] instanceof Date ? 'Date(' + this[key] + ')' : value; }); // text is '["Date(---current time---)"]' JSON.parse(text, reviver) This method parses a JSON text to produce an object or array. It can throw a SyntaxError exception. The optional reviver parameter is a function that can filter and transform the results. It receives each of the keys and values, and its return value is used instead of the original value. If it returns what it received, then the structure is not modified. If it returns undefined then the member is deleted. Example: // Parse the text. Values that look like ISO date strings will // be converted to Date objects. myData = JSON.parse(text, function (key, value) { var a; if (typeof value === 'string') { a = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); if (a) { return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6])); } } return value; }); myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { var d; if (typeof value === 'string' && value.slice(0, 5) === 'Date(' && value.slice(-1) === ')') { d = new Date(value.slice(5, -1)); if (d) { return d; } } return value; }); This is a reference implementation. You are free to copy, modify, or redistribute. */ /*jslint evil: true, regexp: true */ /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, lastIndex, length, parse, prototype, push, replace, slice, stringify, test, toJSON, toString, valueOf */ // Create a JSON object only if one does not already exist. We create the // methods in a closure to avoid creating global variables. if (typeof JSON !== 'object') { JSON = {}; } (function () { 'use strict'; function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } if (typeof Date.prototype.toJSON !== 'function') { Date.prototype.toJSON = function (key) { return isFinite(this.valueOf()) ? this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z' : null; }; String.prototype.toJSON = Number.prototype.toJSON = Boolean.prototype.toJSON = function (key) { return this.valueOf(); }; } var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, gap, indent, meta = { // table of character substitutions '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\' }, rep; function quote(string) { // If the string contains no control characters, no quote characters, and no // backslash characters, then we can safely slap some quotes around it. // Otherwise we must also replace the offending characters with safe escape // sequences. escapable.lastIndex = 0; return escapable.test(string) ? '"' + string.replace(escapable, function (a) { var c = meta[a]; return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }) + '"' : '"' + string + '"'; } function str(key, holder) { // Produce a string from holder[key]. var i, // The loop counter. k, // The member key. v, // The member value. length, mind = gap, partial, value = holder[key]; // If the value has a toJSON method, call it to obtain a replacement value. if (value && typeof value === 'object' && typeof value.toJSON === 'function') { value = value.toJSON(key); } // If we were called with a replacer function, then call the replacer to // obtain a replacement value. if (typeof rep === 'function') { value = rep.call(holder, key, value); } // What happens next depends on the value's type. switch (typeof value) { case 'string': return quote(value); case 'number': // JSON numbers must be finite. Encode non-finite numbers as null. return isFinite(value) ? String(value) : 'null'; case 'boolean': case 'null': // If the value is a boolean or null, convert it to a string. Note: // typeof null does not produce 'null'. The case is included here in // the remote chance that this gets fixed someday. return String(value); // If the type is 'object', we might be dealing with an object or an array or // null. case 'object': // Due to a specification blunder in ECMAScript, typeof null is 'object', // so watch out for that case. if (!value) { return 'null'; } // Make an array to hold the partial results of stringifying this object value. gap += indent; partial = []; // Is the value an array? if (Object.prototype.toString.apply(value) === '[object Array]') { // The value is an array. Stringify every element. Use null as a placeholder // for non-JSON values. length = value.length; for (i = 0; i < length; i += 1) { partial[i] = str(i, value) || 'null'; } // Join all of the elements together, separated with commas, and wrap them in // brackets. v = partial.length === 0 ? '[]' : gap ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : '[' + partial.join(',') + ']'; gap = mind; return v; } // If the replacer is an array, use it to select the members to be stringified. if (rep && typeof rep === 'object') { length = rep.length; for (i = 0; i < length; i += 1) { if (typeof rep[i] === 'string') { k = rep[i]; v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } else { // Otherwise, iterate through all of the keys in the object. for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } // Join all of the member texts together, separated with commas, // and wrap them in braces. v = partial.length === 0 ? '{}' : gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : '{' + partial.join(',') + '}'; gap = mind; return v; } } // If the JSON object does not yet have a stringify method, give it one. if (typeof JSON.stringify !== 'function') { JSON.stringify = function (value, replacer, space) { // The stringify method takes a value and an optional replacer, and an optional // space parameter, and returns a JSON text. The replacer can be a function // that can replace values, or an array of strings that will select the keys. // A default replacer method can be provided. Use of the space parameter can // produce text that is more easily readable. var i; gap = ''; indent = ''; // If the space parameter is a number, make an indent string containing that // many spaces. if (typeof space === 'number') { for (i = 0; i < space; i += 1) { indent += ' '; } // If the space parameter is a string, it will be used as the indent string. } else if (typeof space === 'string') { indent = space; } // If there is a replacer, it must be a function or an array. // Otherwise, throw an error. rep = replacer; if (replacer && typeof replacer !== 'function' && (typeof replacer !== 'object' || typeof replacer.length !== 'number')) { throw new Error('JSON.stringify'); } // Make a fake root object containing our value under the key of ''. // Return the result of stringifying the value. return str('', {'': value}); }; } // If the JSON object does not yet have a parse method, give it one. if (typeof JSON.parse !== 'function') { JSON.parse = function (text, reviver) { // The parse method takes a text and an optional reviver function, and returns // a JavaScript value if the text is a valid JSON text. var j; function walk(holder, key) { // The walk method is used to recursively walk the resulting structure so // that modifications can be made. var k, v, value = holder[key]; if (value && typeof value === 'object') { for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = walk(value, k); if (v !== undefined) { value[k] = v; } else { delete value[k]; } } } } return reviver.call(holder, key, value); } // Parsing happens in four stages. In the first stage, we replace certain // Unicode characters with escape sequences. JavaScript handles many characters // incorrectly, either silently deleting them, or treating them as line endings. text = String(text); cx.lastIndex = 0; if (cx.test(text)) { text = text.replace(cx, function (a) { return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }); } // In the second stage, we run the text against regular expressions that look // for non-JSON patterns. We are especially concerned with '()' and 'new' // because they can cause invocation, and '=' because it can cause mutation. // But just to be safe, we want to reject all unexpected forms. // We split the second stage into 4 regexp operations in order to work around // crippling inefficiencies in IE's and Safari's regexp engines. First we // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we // replace all simple value tokens with ']' characters. Third, we delete all // open brackets that follow a colon or comma or that begin the text. Finally, // we look to see that the remaining characters are only whitespace or ']' or // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. if (/^[\],:{}\s]*$/ .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { // In the third stage we use the eval function to compile the text into a // JavaScript structure. The '{' operator is subject to a syntactic ambiguity // in JavaScript: it can begin a block or an object literal. We wrap the text // in parens to eliminate the ambiguity. j = eval('(' + text + ')'); // In the optional fourth stage, we recursively walk the new structure, passing // each name/value pair to a reviver function for possible transformation. return typeof reviver === 'function' ? walk({'': j}, '') : j; } // If the text is not JSON parseable, then a SyntaxError is thrown. throw new SyntaxError('JSON.parse'); }; } }());/** * History.js ExtJS Adapter * @author Sean Adkinson * @copyright 2012 Sean Adkinson * @license New BSD License */ // Closure (function(window,undefined){ "use strict"; // Localise Globals var History = window.History = window.History||{}, Ext = window.Ext; window.JSON = { stringify: Ext.JSON.encode, parse: Ext.JSON.decode }; // Check Existence if ( typeof History.Adapter !== 'undefined' ) { throw new Error('History.js Adapter has already been loaded...'); } // Add the Adapter History.Adapter = { observables: {}, /** * History.Adapter.bind(el,event,callback) * @param {Element|string} el * @param {string} event - custom and standard events * @param {function} callback * @param {Object} scope * @return {void} */ bind: function(element,eventName,callback,scope){ Ext.EventManager.addListener(element, eventName, callback, scope); //bind an observable to the element that will let us "trigger" events on it var id = Ext.id(element, 'history-'), observable = this.observables[id]; if (!observable) { observable = Ext.create('Ext.util.Observable'); this.observables[id] = observable; } observable.on(eventName, callback, scope); }, /** * History.Adapter.trigger(el,event) * @param {Element|string} el * @param {string} event - custom and standard events * @param {Object=} extra - a object of extra event data (optional) * @return {void} */ trigger: function(element,eventName,extra){ var id = Ext.id(element, 'history-'), observable = this.observables[id]; if (observable) { observable.fireEvent(eventName, extra); } }, /** * History.Adapter.extractEventData(key,event,extra) * @param {string} key - key for the event data to extract * @param {string} event - custom and standard events * @param {Object=} extra - a object of extra event data (optional) * @return {mixed} */ extractEventData: function(key,event,extra){ var result = (event && event.browserEvent && event.browserEvent[key]) || (extra && extra[key]) || undefined; return result; }, /** * History.Adapter.onDomLoad(callback) * @param {function} callback * @return {void} */ onDomLoad: function(callback) { Ext.onReady(callback); } }; // Try and Initialise History if ( typeof History.init !== 'undefined' ) { History.init(); } })(window);/** * History.js HTML4 Support * Depends on the HTML5 Support * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window,undefined){ "use strict"; // ======================================================================== // Initialise // Localise Globals var document = window.document, // Make sure we are using the correct document setTimeout = window.setTimeout||setTimeout, clearTimeout = window.clearTimeout||clearTimeout, setInterval = window.setInterval||setInterval, History = window.History = window.History||{}; // Public History Object // Check Existence if ( typeof History.initHtml4 !== 'undefined' ) { throw new Error('History.js HTML4 Support has already been loaded...'); } // ======================================================================== // Initialise HTML4 Support // Initialise HTML4 Support History.initHtml4 = function(){ // Initialise if ( typeof History.initHtml4.initialized !== 'undefined' ) { // Already Loaded return false; } else { History.initHtml4.initialized = true; } // ==================================================================== // Properties /** * History.enabled * Is History enabled? */ History.enabled = true; // ==================================================================== // Hash Storage /** * History.savedHashes * Store the hashes in an array */ History.savedHashes = []; /** * History.isLastHash(newHash) * Checks if the hash is the last hash * @param {string} newHash * @return {boolean} true */ History.isLastHash = function(newHash){ // Prepare var oldHash = History.getHashByIndex(), isLast; // Check isLast = newHash === oldHash; // Return isLast return isLast; }; /** * History.isHashEqual(newHash, oldHash) * Checks to see if two hashes are functionally equal * @param {string} newHash * @param {string} oldHash * @return {boolean} true */ History.isHashEqual = function(newHash, oldHash){ newHash = encodeURIComponent(newHash).replace(/%25/g, "%"); oldHash = encodeURIComponent(oldHash).replace(/%25/g, "%"); return newHash === oldHash; }; /** * History.saveHash(newHash) * Push a Hash * @param {string} newHash * @return {boolean} true */ History.saveHash = function(newHash){ // Check Hash if ( History.isLastHash(newHash) ) { return false; } // Push the Hash History.savedHashes.push(newHash); // Return true return true; }; /** * History.getHashByIndex() * Gets a hash by the index * @param {integer} index * @return {string} */ History.getHashByIndex = function(index){ // Prepare var hash = null; // Handle if ( typeof index === 'undefined' ) { // Get the last inserted hash = History.savedHashes[History.savedHashes.length-1]; } else if ( index < 0 ) { // Get from the end hash = History.savedHashes[History.savedHashes.length+index]; } else { // Get from the beginning hash = History.savedHashes[index]; } // Return hash return hash; }; // ==================================================================== // Discarded States /** * History.discardedHashes * A hashed array of discarded hashes */ History.discardedHashes = {}; /** * History.discardedStates * A hashed array of discarded states */ History.discardedStates = {}; /** * History.discardState(State) * Discards the state by ignoring it through History * @param {object} State * @return {true} */ History.discardState = function(discardedState,forwardState,backState){ //History.debug('History.discardState', arguments); // Prepare var discardedStateHash = History.getHashByState(discardedState), discardObject; // Create Discard Object discardObject = { 'discardedState': discardedState, 'backState': backState, 'forwardState': forwardState }; // Add to DiscardedStates History.discardedStates[discardedStateHash] = discardObject; // Return true return true; }; /** * History.discardHash(hash) * Discards the hash by ignoring it through History * @param {string} hash * @return {true} */ History.discardHash = function(discardedHash,forwardState,backState){ //History.debug('History.discardState', arguments); // Create Discard Object var discardObject = { 'discardedHash': discardedHash, 'backState': backState, 'forwardState': forwardState }; // Add to discardedHash History.discardedHashes[discardedHash] = discardObject; // Return true return true; }; /** * History.discardedState(State) * Checks to see if the state is discarded * @param {object} State * @return {bool} */ History.discardedState = function(State){ // Prepare var StateHash = History.getHashByState(State), discarded; // Check discarded = History.discardedStates[StateHash]||false; // Return true return discarded; }; /** * History.discardedHash(hash) * Checks to see if the state is discarded * @param {string} State * @return {bool} */ History.discardedHash = function(hash){ // Check var discarded = History.discardedHashes[hash]||false; // Return true return discarded; }; /** * History.recycleState(State) * Allows a discarded state to be used again * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.recycleState = function(State){ //History.debug('History.recycleState', arguments); // Prepare var StateHash = History.getHashByState(State); // Remove from DiscardedStates if ( History.discardedState(State) ) { delete History.discardedStates[StateHash]; } // Return true return true; }; // ==================================================================== // HTML4 HashChange Support if ( History.emulated.hashChange ) { /* * We must emulate the HTML4 HashChange Support by manually checking for hash changes */ /** * History.hashChangeInit() * Init the HashChange Emulation */ History.hashChangeInit = function(){ // Define our Checker Function History.checkerFunction = null; // Define some variables that will help in our checker function var lastDocumentHash = '', iframeId, iframe, lastIframeHash, checkerRunning, startedWithHash = Boolean(History.getHash()); // Handle depending on the browser if ( History.isInternetExplorer() ) { // IE6 and IE7 // We need to use an iframe to emulate the back and forward buttons // Create iFrame iframeId = 'historyjs-iframe'; iframe = document.createElement('iframe'); // Adjust iFarme // IE 6 requires iframe to have a src on HTTPS pages, otherwise it will throw a // "This page contains both secure and nonsecure items" warning. iframe.setAttribute('id', iframeId); iframe.setAttribute('src', '#'); iframe.style.display = 'none'; // Append iFrame document.body.appendChild(iframe); // Create initial history entry iframe.contentWindow.document.open(); iframe.contentWindow.document.close(); // Define some variables that will help in our checker function lastIframeHash = ''; checkerRunning = false; // Define the checker function History.checkerFunction = function(){ // Check Running if ( checkerRunning ) { return false; } // Update Running checkerRunning = true; // Fetch var documentHash = History.getHash(), iframeHash = History.getHash(iframe.contentWindow.document); // The Document Hash has changed (application caused) if ( documentHash !== lastDocumentHash ) { // Equalise lastDocumentHash = documentHash; // Create a history entry in the iframe if ( iframeHash !== documentHash ) { //History.debug('hashchange.checker: iframe hash change', 'documentHash (new):', documentHash, 'iframeHash (old):', iframeHash); // Equalise lastIframeHash = iframeHash = documentHash; // Create History Entry iframe.contentWindow.document.open(); iframe.contentWindow.document.close(); // Update the iframe's hash iframe.contentWindow.document.location.hash = History.escapeHash(documentHash); } // Trigger Hashchange Event History.Adapter.trigger(window,'hashchange'); } // The iFrame Hash has changed (back button caused) else if ( iframeHash !== lastIframeHash ) { //History.debug('hashchange.checker: iframe hash out of sync', 'iframeHash (new):', iframeHash, 'documentHash (old):', documentHash); // Equalise lastIframeHash = iframeHash; // If there is no iframe hash that means we're at the original // iframe state. // And if there was a hash on the original request, the original // iframe state was replaced instantly, so skip this state and take // the user back to where they came from. if (startedWithHash && iframeHash === '') { History.back(); } else { // Update the Hash History.setHash(iframeHash,false); } } // Reset Running checkerRunning = false; // Return true return true; }; } else { // We are not IE // Firefox 1 or 2, Opera // Define the checker function History.checkerFunction = function(){ // Prepare var documentHash = History.getHash()||''; // The Document Hash has changed (application caused) if ( documentHash !== lastDocumentHash ) { // Equalise lastDocumentHash = documentHash; // Trigger Hashchange Event History.Adapter.trigger(window,'hashchange'); } // Return true return true; }; } // Apply the checker function History.intervalList.push(setInterval(History.checkerFunction, History.options.hashChangeInterval)); // Done return true; }; // History.hashChangeInit // Bind hashChangeInit History.Adapter.onDomLoad(History.hashChangeInit); } // History.emulated.hashChange // ==================================================================== // HTML5 State Support // Non-Native pushState Implementation if ( History.emulated.pushState ) { /* * We must emulate the HTML5 State Management by using HTML4 HashChange */ /** * History.onHashChange(event) * Trigger HTML5's window.onpopstate via HTML4 HashChange Support */ History.onHashChange = function(event){ //History.debug('History.onHashChange', arguments); // Prepare var currentUrl = ((event && event.newURL) || History.getLocationHref()), currentHash = History.getHashByUrl(currentUrl), currentState = null, currentStateHash = null, currentStateHashExits = null, discardObject; // Check if we are the same state if ( History.isLastHash(currentHash) ) { // There has been no change (just the page's hash has finally propagated) //History.debug('History.onHashChange: no change'); History.busy(false); return false; } // Reset the double check History.doubleCheckComplete(); // Store our location for use in detecting back/forward direction History.saveHash(currentHash); // Expand Hash if ( currentHash && History.isTraditionalAnchor(currentHash) ) { //History.debug('History.onHashChange: traditional anchor', currentHash); // Traditional Anchor Hash History.Adapter.trigger(window,'anchorchange'); History.busy(false); return false; } // Create State currentState = History.extractState(History.getFullUrl(currentHash||History.getLocationHref()),true); // Check if we are the same state if ( History.isLastSavedState(currentState) ) { //History.debug('History.onHashChange: no change'); // There has been no change (just the page's hash has finally propagated) History.busy(false); return false; } // Create the state Hash currentStateHash = History.getHashByState(currentState); // Check if we are DiscardedState discardObject = History.discardedState(currentState); if ( discardObject ) { // Ignore this state as it has been discarded and go back to the state before it if ( History.getHashByIndex(-2) === History.getHashByState(discardObject.forwardState) ) { // We are going backwards //History.debug('History.onHashChange: go backwards'); History.back(false); } else { // We are going forwards //History.debug('History.onHashChange: go forwards'); History.forward(false); } return false; } // Push the new HTML5 State //History.debug('History.onHashChange: success hashchange'); History.pushState(currentState.data,currentState.title,encodeURI(currentState.url),false); // End onHashChange closure return true; }; History.Adapter.bind(window,'hashchange',History.onHashChange); /** * History.pushState(data,title,url) * Add a new State to the history object, become it, and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.pushState = function(data,title,url,queue){ //History.debug('History.pushState: called', arguments); // We assume that the URL passed in is URI-encoded, but this makes // sure that it's fully URI encoded; any '%'s that are encoded are // converted back into '%'s url = encodeURI(url).replace(/%25/g, "%"); // Check the State if ( History.getHashByUrl(url) ) { throw new Error('History.js does not support states with fragment-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.pushState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.pushState, args: arguments, queue: queue }); return false; } // Make Busy History.busy(true); // Fetch the State Object var newState = History.createStateObject(data,title,url), newStateHash = History.getHashByState(newState), oldState = History.getState(false), oldStateHash = History.getHashByState(oldState), html4Hash = History.getHash(), wasExpected = History.expectedStateId == newState.id; // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Recycle the State History.recycleState(newState); // Force update of the title History.setTitle(newState); // Check if we are the same State if ( newStateHash === oldStateHash ) { //History.debug('History.pushState: no change', newStateHash); History.busy(false); return false; } // Update HTML5 State History.saveState(newState); // Fire HTML5 Event if(!wasExpected) History.Adapter.trigger(window,'statechange'); // Update HTML4 Hash if ( !History.isHashEqual(newStateHash, html4Hash) && !History.isHashEqual(newStateHash, History.getShortUrl(History.getLocationHref())) ) { History.setHash(newStateHash,false); } History.busy(false); // End pushState closure return true; }; /** * History.replaceState(data,title,url) * Replace the State and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.replaceState = function(data,title,url,queue){ //History.debug('History.replaceState: called', arguments); // We assume that the URL passed in is URI-encoded, but this makes // sure that it's fully URI encoded; any '%'s that are encoded are // converted back into '%'s url = encodeURI(url).replace(/%25/g, "%"); // Check the State if ( History.getHashByUrl(url) ) { throw new Error('History.js does not support states with fragment-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.replaceState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.replaceState, args: arguments, queue: queue }); return false; } // Make Busy History.busy(true); // Fetch the State Objects var newState = History.createStateObject(data,title,url), newStateHash = History.getHashByState(newState), oldState = History.getState(false), oldStateHash = History.getHashByState(oldState), previousState = History.getStateByIndex(-2); // Discard Old State History.discardState(oldState,newState,previousState); // If the url hasn't changed, just store and save the state // and fire a statechange event to be consistent with the // html 5 api if ( newStateHash === oldStateHash ) { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Recycle the State History.recycleState(newState); // Force update of the title History.setTitle(newState); // Update HTML5 State History.saveState(newState); // Fire HTML5 Event //History.debug('History.pushState: trigger popstate'); History.Adapter.trigger(window,'statechange'); History.busy(false); } else { // Alias to PushState History.pushState(newState.data,newState.title,newState.url,false); } // End replaceState closure return true; }; } // History.emulated.pushState // ==================================================================== // Initialise // Non-Native pushState Implementation if ( History.emulated.pushState ) { /** * Ensure initial state is handled correctly */ if ( History.getHash() && !History.emulated.hashChange ) { History.Adapter.onDomLoad(function(){ History.Adapter.trigger(window,'hashchange'); }); } } // History.emulated.pushState }; // History.initHtml4 // Try to Initialise History if ( typeof History.init !== 'undefined' ) { History.init(); } })(window); /** * History.js Core * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window,undefined){ "use strict"; // ======================================================================== // Initialise // Localise Globals var console = window.console||undefined, // Prevent a JSLint complain document = window.document, // Make sure we are using the correct document navigator = window.navigator, // Make sure we are using the correct navigator sessionStorage = false, // sessionStorage setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, setInterval = window.setInterval, clearInterval = window.clearInterval, JSON = window.JSON, alert = window.alert, History = window.History = window.History||{}, // Public History Object history = window.history; // Old History Object try { sessionStorage = window.sessionStorage; // This will throw an exception in some browsers when cookies/localStorage are explicitly disabled (i.e. Chrome) sessionStorage.setItem('TEST', '1'); sessionStorage.removeItem('TEST'); } catch(e) { sessionStorage = false; } // MooTools Compatibility JSON.stringify = JSON.stringify||JSON.encode; JSON.parse = JSON.parse||JSON.decode; // Check Existence if ( typeof History.init !== 'undefined' ) { throw new Error('History.js Core has already been loaded...'); } // Initialise History History.init = function(options){ // Check Load Status of Adapter if ( typeof History.Adapter === 'undefined' ) { return false; } // Check Load Status of Core if ( typeof History.initCore !== 'undefined' ) { History.initCore(); } // Check Load Status of HTML4 Support if ( typeof History.initHtml4 !== 'undefined' ) { History.initHtml4(); } // Return true return true; }; // ======================================================================== // Initialise Core // Initialise Core History.initCore = function(options){ // Initialise if ( typeof History.initCore.initialized !== 'undefined' ) { // Already Loaded return false; } else { History.initCore.initialized = true; } // ==================================================================== // Options /** * History.options * Configurable options */ History.options = History.options||{}; /** * History.options.hashChangeInterval * How long should the interval be before hashchange checks */ History.options.hashChangeInterval = History.options.hashChangeInterval || 100; /** * History.options.safariPollInterval * How long should the interval be before safari poll checks */ History.options.safariPollInterval = History.options.safariPollInterval || 500; /** * History.options.doubleCheckInterval * How long should the interval be before we perform a double check */ History.options.doubleCheckInterval = History.options.doubleCheckInterval || 500; /** * History.options.disableSuid * Force History not to append suid */ History.options.disableSuid = History.options.disableSuid || false; /** * History.options.storeInterval * How long should we wait between store calls */ History.options.storeInterval = History.options.storeInterval || 1000; /** * History.options.busyDelay * How long should we wait between busy events */ History.options.busyDelay = History.options.busyDelay || 250; /** * History.options.debug * If true will enable debug messages to be logged */ History.options.debug = History.options.debug || false; /** * History.options.initialTitle * What is the title of the initial state */ History.options.initialTitle = History.options.initialTitle || document.title; /** * History.options.html4Mode * If true, will force HTMl4 mode (hashtags) */ History.options.html4Mode = History.options.html4Mode || false; /** * History.options.delayInit * Want to override default options and call init manually. */ History.options.delayInit = History.options.delayInit || false; // ==================================================================== // Interval record /** * History.intervalList * List of intervals set, to be cleared when document is unloaded. */ History.intervalList = []; /** * History.clearAllIntervals * Clears all setInterval instances. */ History.clearAllIntervals = function(){ var i, il = History.intervalList; if (typeof il !== "undefined" && il !== null) { for (i = 0; i < il.length; i++) { clearInterval(il[i]); } History.intervalList = null; } }; // ==================================================================== // Debug /** * History.debug(message,...) * Logs the passed arguments if debug enabled */ History.debug = function(){ if ( (History.options.debug||false) ) { History.log.apply(History,arguments); } }; /** * History.log(message,...) * Logs the passed arguments */ History.log = function(){ // Prepare var consoleExists = !(typeof console === 'undefined' || typeof console.log === 'undefined' || typeof console.log.apply === 'undefined'), textarea = document.getElementById('log'), message, i,n, args,arg ; // Write to Console if ( consoleExists ) { args = Array.prototype.slice.call(arguments); message = args.shift(); if ( typeof console.debug !== 'undefined' ) { console.debug.apply(console,[message,args]); } else { console.log.apply(console,[message,args]); } } else { message = ("\n"+arguments[0]+"\n"); } // Write to log for ( i=1,n=arguments.length; i * @author James Padolsey */ History.getInternetExplorerMajorVersion = function(){ var result = History.getInternetExplorerMajorVersion.cached = (typeof History.getInternetExplorerMajorVersion.cached !== 'undefined') ? History.getInternetExplorerMajorVersion.cached : (function(){ var v = 3, div = document.createElement('div'), all = div.getElementsByTagName('i'); while ( (div.innerHTML = '') && all[0] ) {} return (v > 4) ? v : false; })() ; return result; }; /** * History.isInternetExplorer() * Are we using Internet Explorer? * @return {boolean} * @license Public Domain * @author Benjamin Arthur Lupton */ History.isInternetExplorer = function(){ var result = History.isInternetExplorer.cached = (typeof History.isInternetExplorer.cached !== 'undefined') ? History.isInternetExplorer.cached : Boolean(History.getInternetExplorerMajorVersion()) ; return result; }; /** * History.emulated * Which features require emulating? */ if (History.options.html4Mode) { History.emulated = { pushState : true, hashChange: true }; } else { History.emulated = { pushState: !Boolean( window.history && window.history.pushState && window.history.replaceState && !( (/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i).test(navigator.userAgent) /* disable for versions of iOS before version 4.3 (8F190) */ || (/AppleWebKit\/5([0-2]|3[0-2])/i).test(navigator.userAgent) /* disable for the mercury iOS browser, or at least older versions of the webkit engine */ ) ), hashChange: Boolean( !(('onhashchange' in window) || ('onhashchange' in document)) || (History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8) ) }; } /** * History.enabled * Is History enabled? */ History.enabled = !History.emulated.pushState; /** * History.bugs * Which bugs are present */ History.bugs = { /** * Safari 5 and Safari iOS 4 fail to return to the correct state once a hash is replaced by a `replaceState` call * https://bugs.webkit.org/show_bug.cgi?id=56249 */ setHash: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * Safari 5 and Safari iOS 4 sometimes fail to apply the state change under busy conditions * https://bugs.webkit.org/show_bug.cgi?id=42940 */ safariPoll: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * MSIE 6 and 7 sometimes do not apply a hash even it was told to (requiring a second call to the apply function) */ ieDoubleCheck: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8), /** * MSIE 6 requires the entire hash to be encoded for the hashes to trigger the onHashChange event */ hashEscape: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 7) }; /** * History.isEmptyObject(obj) * Checks to see if the Object is Empty * @param {Object} obj * @return {boolean} */ History.isEmptyObject = function(obj) { for ( var name in obj ) { if ( obj.hasOwnProperty(name) ) { return false; } } return true; }; /** * History.cloneObject(obj) * Clones a object and eliminate all references to the original contexts * @param {Object} obj * @return {Object} */ History.cloneObject = function(obj) { var hash,newObj; if ( obj ) { hash = JSON.stringify(obj); newObj = JSON.parse(hash); } else { newObj = {}; } return newObj; }; // ==================================================================== // URL Helpers /** * History.getRootUrl() * Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com" * @return {String} rootUrl */ History.getRootUrl = function(){ // Create var rootUrl = document.location.protocol+'//'+(document.location.hostname||document.location.host); if ( document.location.port||false ) { rootUrl += ':'+document.location.port; } rootUrl += '/'; // Return return rootUrl; }; /** * History.getBaseHref() * Fetches the `href` attribute of the `` element if it exists * @return {String} baseHref */ History.getBaseHref = function(){ // Create var baseElements = document.getElementsByTagName('base'), baseElement = null, baseHref = ''; // Test for Base Element if ( baseElements.length === 1 ) { // Prepare for Base Element baseElement = baseElements[0]; baseHref = baseElement.href.replace(/[^\/]+$/,''); } // Adjust trailing slash baseHref = baseHref.replace(/\/+$/,''); if ( baseHref ) baseHref += '/'; // Return return baseHref; }; /** * History.getBaseUrl() * Fetches the baseHref or basePageUrl or rootUrl (whichever one exists first) * @return {String} baseUrl */ History.getBaseUrl = function(){ // Create var baseUrl = History.getBaseHref()||History.getBasePageUrl()||History.getRootUrl(); // Return return baseUrl; }; /** * History.getPageUrl() * Fetches the URL of the current page * @return {String} pageUrl */ History.getPageUrl = function(){ // Fetch var State = History.getState(false,false), stateUrl = (State||{}).url||History.getLocationHref(), pageUrl; // Create pageUrl = stateUrl.replace(/\/+$/,'').replace(/[^\/]+$/,function(part,index,string){ return (/\./).test(part) ? part : part+'/'; }); // Return return pageUrl; }; /** * History.getBasePageUrl() * Fetches the Url of the directory of the current page * @return {String} basePageUrl */ History.getBasePageUrl = function(){ // Create var basePageUrl = (History.getLocationHref()).replace(/[#\?].*/,'').replace(/[^\/]+$/,function(part,index,string){ return (/[^\/]$/).test(part) ? '' : part; }).replace(/\/+$/,'')+'/'; // Return return basePageUrl; }; /** * History.getFullUrl(url) * Ensures that we have an absolute URL and not a relative URL * @param {string} url * @param {Boolean} allowBaseHref * @return {string} fullUrl */ History.getFullUrl = function(url,allowBaseHref){ // Prepare var fullUrl = url, firstChar = url.substring(0,1); allowBaseHref = (typeof allowBaseHref === 'undefined') ? true : allowBaseHref; // Check if ( /[a-z]+\:\/\//.test(url) ) { // Full URL } else if ( firstChar === '/' ) { // Root URL fullUrl = History.getRootUrl()+url.replace(/^\/+/,''); } else if ( firstChar === '#' ) { // Anchor URL fullUrl = History.getPageUrl().replace(/#.*/,'')+url; } else if ( firstChar === '?' ) { // Query URL fullUrl = History.getPageUrl().replace(/[\?#].*/,'')+url; } else { // Relative URL if ( allowBaseHref ) { fullUrl = History.getBaseUrl()+url.replace(/^(\.\/)+/,''); } else { fullUrl = History.getBasePageUrl()+url.replace(/^(\.\/)+/,''); } // We have an if condition above as we do not want hashes // which are relative to the baseHref in our URLs // as if the baseHref changes, then all our bookmarks // would now point to different locations // whereas the basePageUrl will always stay the same } // Return return fullUrl.replace(/\#$/,''); }; /** * History.getShortUrl(url) * Ensures that we have a relative URL and not a absolute URL * @param {string} url * @return {string} url */ History.getShortUrl = function(url){ // Prepare var shortUrl = url, baseUrl = History.getBaseUrl(), rootUrl = History.getRootUrl(); // Trim baseUrl if ( History.emulated.pushState ) { // We are in a if statement as when pushState is not emulated // The actual url these short urls are relative to can change // So within the same session, we the url may end up somewhere different shortUrl = shortUrl.replace(baseUrl,''); } // Trim rootUrl shortUrl = shortUrl.replace(rootUrl,'/'); // Ensure we can still detect it as a state if ( History.isTraditionalAnchor(shortUrl) ) { shortUrl = './'+shortUrl; } // Clean It shortUrl = shortUrl.replace(/^(\.\/)+/g,'./').replace(/\#$/,''); // Return return shortUrl; }; /** * History.getLocationHref(document) * Returns a normalized version of document.location.href * accounting for browser inconsistencies, etc. * * This URL will be URI-encoded and will include the hash * * @param {object} document * @return {string} url */ History.getLocationHref = function(doc) { doc = doc || document; // most of the time, this will be true if (doc.URL === doc.location.href) return doc.location.href; // some versions of webkit URI-decode document.location.href // but they leave document.URL in an encoded state if (doc.location.href === decodeURIComponent(doc.URL)) return doc.URL; // FF 3.6 only updates document.URL when a page is reloaded // document.location.href is updated correctly if (doc.location.hash && decodeURIComponent(doc.location.href.replace(/^[^#]+/, "")) === doc.location.hash) return doc.location.href; if (doc.URL.indexOf('#') == -1 && doc.location.href.indexOf('#') != -1) return doc.location.href; return doc.URL || doc.location.href; }; // ==================================================================== // State Storage /** * History.store * The store for all session specific data */ History.store = {}; /** * History.idToState * 1-1: State ID to State Object */ History.idToState = History.idToState||{}; /** * History.stateToId * 1-1: State String to State ID */ History.stateToId = History.stateToId||{}; /** * History.urlToId * 1-1: State URL to State ID */ History.urlToId = History.urlToId||{}; /** * History.storedStates * Store the states in an array */ History.storedStates = History.storedStates||[]; /** * History.savedStates * Saved the states in an array */ History.savedStates = History.savedStates||[]; /** * History.noramlizeStore() * Noramlize the store by adding necessary values */ History.normalizeStore = function(){ History.store.idToState = History.store.idToState||{}; History.store.urlToId = History.store.urlToId||{}; History.store.stateToId = History.store.stateToId||{}; }; /** * History.getState() * Get an object containing the data, title and url of the current state * @param {Boolean} friendly * @param {Boolean} create * @return {Object} State */ History.getState = function(friendly,create){ // Prepare if ( typeof friendly === 'undefined' ) { friendly = true; } if ( typeof create === 'undefined' ) { create = true; } // Fetch var State = History.getLastSavedState(); // Create if ( !State && create ) { State = History.createStateObject(); } // Adjust if ( friendly ) { State = History.cloneObject(State); State.url = State.cleanUrl||State.url; } // Return return State; }; /** * History.getIdByState(State) * Gets a ID for a State * @param {State} newState * @return {String} id */ History.getIdByState = function(newState){ // Fetch ID var id = History.extractId(newState.url), str; if ( !id ) { // Find ID via State String str = History.getStateString(newState); if ( typeof History.stateToId[str] !== 'undefined' ) { id = History.stateToId[str]; } else if ( typeof History.store.stateToId[str] !== 'undefined' ) { id = History.store.stateToId[str]; } else { // Generate a new ID while ( true ) { id = (new Date()).getTime() + String(Math.random()).replace(/\D/g,''); if ( typeof History.idToState[id] === 'undefined' && typeof History.store.idToState[id] === 'undefined' ) { break; } } // Apply the new State to the ID History.stateToId[str] = id; History.idToState[id] = newState; } } // Return ID return id; }; /** * History.normalizeState(State) * Expands a State Object * @param {object} State * @return {object} */ History.normalizeState = function(oldState){ // Variables var newState, dataNotEmpty; // Prepare if ( !oldState || (typeof oldState !== 'object') ) { oldState = {}; } // Check if ( typeof oldState.normalized !== 'undefined' ) { return oldState; } // Adjust if ( !oldState.data || (typeof oldState.data !== 'object') ) { oldState.data = {}; } // ---------------------------------------------------------------- // Create newState = {}; newState.normalized = true; newState.title = oldState.title||''; newState.url = History.getFullUrl(oldState.url?oldState.url:(History.getLocationHref())); newState.hash = History.getShortUrl(newState.url); newState.data = History.cloneObject(oldState.data); // Fetch ID newState.id = History.getIdByState(newState); // ---------------------------------------------------------------- // Clean the URL newState.cleanUrl = newState.url.replace(/\??\&_suid.*/,''); newState.url = newState.cleanUrl; // Check to see if we have more than just a url dataNotEmpty = !History.isEmptyObject(newState.data); // Apply if ( (newState.title || dataNotEmpty) && History.options.disableSuid !== true ) { // Add ID to Hash newState.hash = History.getShortUrl(newState.url).replace(/\??\&_suid.*/,''); if ( !/\?/.test(newState.hash) ) { newState.hash += '?'; } newState.hash += '&_suid='+newState.id; } // Create the Hashed URL newState.hashedUrl = History.getFullUrl(newState.hash); // ---------------------------------------------------------------- // Update the URL if we have a duplicate if ( (History.emulated.pushState || History.bugs.safariPoll) && History.hasUrlDuplicate(newState) ) { newState.url = newState.hashedUrl; } // ---------------------------------------------------------------- // Return return newState; }; /** * History.createStateObject(data,title,url) * Creates a object based on the data, title and url state params * @param {object} data * @param {string} title * @param {string} url * @return {object} */ History.createStateObject = function(data,title,url){ // Hashify var State = { 'data': data, 'title': title, 'url': url }; // Expand the State State = History.normalizeState(State); // Return object return State; }; /** * History.getStateById(id) * Get a state by it's UID * @param {String} id */ History.getStateById = function(id){ // Prepare id = String(id); // Retrieve var State = History.idToState[id] || History.store.idToState[id] || undefined; // Return State return State; }; /** * Get a State's String * @param {State} passedState */ History.getStateString = function(passedState){ // Prepare var State, cleanedState, str; // Fetch State = History.normalizeState(passedState); // Clean cleanedState = { data: State.data, title: passedState.title, url: passedState.url }; // Fetch str = JSON.stringify(cleanedState); // Return return str; }; /** * Get a State's ID * @param {State} passedState * @return {String} id */ History.getStateId = function(passedState){ // Prepare var State, id; // Fetch State = History.normalizeState(passedState); // Fetch id = State.id; // Return return id; }; /** * History.getHashByState(State) * Creates a Hash for the State Object * @param {State} passedState * @return {String} hash */ History.getHashByState = function(passedState){ // Prepare var State, hash; // Fetch State = History.normalizeState(passedState); // Hash hash = State.hash; // Return return hash; }; /** * History.extractId(url_or_hash) * Get a State ID by it's URL or Hash * @param {string} url_or_hash * @return {string} id */ History.extractId = function ( url_or_hash ) { // Prepare var id,parts,url, tmp; // Extract // If the URL has a #, use the id from before the # if (url_or_hash.indexOf('#') != -1) { tmp = url_or_hash.split("#")[0]; } else { tmp = url_or_hash; } parts = /(.*)\&_suid=([0-9]+)$/.exec(tmp); url = parts ? (parts[1]||url_or_hash) : url_or_hash; id = parts ? String(parts[2]||'') : ''; // Return return id||false; }; /** * History.isTraditionalAnchor * Checks to see if the url is a traditional anchor or not * @param {String} url_or_hash * @return {Boolean} */ History.isTraditionalAnchor = function(url_or_hash){ // Check var isTraditional = !(/[\/\?\.]/.test(url_or_hash)); // Return return isTraditional; }; /** * History.extractState * Get a State by it's URL or Hash * @param {String} url_or_hash * @return {State|null} */ History.extractState = function(url_or_hash,create){ // Prepare var State = null, id, url; create = create||false; // Fetch SUID id = History.extractId(url_or_hash); if ( id ) { State = History.getStateById(id); } // Fetch SUID returned no State if ( !State ) { // Fetch URL url = History.getFullUrl(url_or_hash); // Check URL id = History.getIdByUrl(url)||false; if ( id ) { State = History.getStateById(id); } // Create State if ( !State && create && !History.isTraditionalAnchor(url_or_hash) ) { State = History.createStateObject(null,null,url); } } // Return return State; }; /** * History.getIdByUrl() * Get a State ID by a State URL */ History.getIdByUrl = function(url){ // Fetch var id = History.urlToId[url] || History.store.urlToId[url] || undefined; // Return return id; }; /** * History.getLastSavedState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastSavedState = function(){ return History.savedStates[History.savedStates.length-1]||undefined; }; /** * History.getLastStoredState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastStoredState = function(){ return History.storedStates[History.storedStates.length-1]||undefined; }; /** * History.hasUrlDuplicate * Checks if a Url will have a url conflict * @param {Object} newState * @return {Boolean} hasDuplicate */ History.hasUrlDuplicate = function(newState) { // Prepare var hasDuplicate = false, oldState; // Fetch oldState = History.extractState(newState.url); // Check hasDuplicate = oldState && oldState.id !== newState.id; // Return return hasDuplicate; }; /** * History.storeState * Store a State * @param {Object} newState * @return {Object} newState */ History.storeState = function(newState){ // Store the State History.urlToId[newState.url] = newState.id; // Push the State History.storedStates.push(History.cloneObject(newState)); // Return newState return newState; }; /** * History.isLastSavedState(newState) * Tests to see if the state is the last state * @param {Object} newState * @return {boolean} isLast */ History.isLastSavedState = function(newState){ // Prepare var isLast = false, newId, oldState, oldId; // Check if ( History.savedStates.length ) { newId = newState.id; oldState = History.getLastSavedState(); oldId = oldState.id; // Check isLast = (newId === oldId); } // Return return isLast; }; /** * History.saveState * Push a State * @param {Object} newState * @return {boolean} changed */ History.saveState = function(newState){ // Check Hash if ( History.isLastSavedState(newState) ) { return false; } // Push the State History.savedStates.push(History.cloneObject(newState)); // Return true return true; }; /** * History.getStateByIndex() * Gets a state by the index * @param {integer} index * @return {Object} */ History.getStateByIndex = function(index){ // Prepare var State = null; // Handle if ( typeof index === 'undefined' ) { // Get the last inserted State = History.savedStates[History.savedStates.length-1]; } else if ( index < 0 ) { // Get from the end State = History.savedStates[History.savedStates.length+index]; } else { // Get from the beginning State = History.savedStates[index]; } // Return State return State; }; /** * History.getCurrentIndex() * Gets the current index * @return (integer) */ History.getCurrentIndex = function(){ // Prepare var index = null; // No states saved if(History.savedStates.length < 1) { index = 0; } else { index = History.savedStates.length-1; } return index; }; // ==================================================================== // Hash Helpers /** * History.getHash() * @param {Location=} location * Gets the current document hash * Note: unlike location.hash, this is guaranteed to return the escaped hash in all browsers * @return {string} */ History.getHash = function(doc){ var url = History.getLocationHref(doc), hash; hash = History.getHashByUrl(url); return hash; }; /** * History.unescapeHash() * normalize and Unescape a Hash * @param {String} hash * @return {string} */ History.unescapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Unescape hash result = decodeURIComponent(result); // Return result return result; }; /** * History.normalizeHash() * normalize a hash across browsers * @return {string} */ History.normalizeHash = function(hash){ // Prepare var result = hash.replace(/[^#]*#/,'').replace(/#.*/, ''); // Return result return result; }; /** * History.setHash(hash) * Sets the document hash * @param {string} hash * @return {History} */ History.setHash = function(hash,queue){ // Prepare var State, pageUrl; // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.setHash: we must wait', arguments); History.pushQueue({ scope: History, callback: History.setHash, args: arguments, queue: queue }); return false; } // Log //History.debug('History.setHash: called',hash); // Make Busy + Continue History.busy(true); // Check if hash is a state State = History.extractState(hash,true); if ( State && !History.emulated.pushState ) { // Hash is a state so skip the setHash //History.debug('History.setHash: Hash is a state so skipping the hash set with a direct pushState call',arguments); // PushState History.pushState(State.data,State.title,State.url,false); } else if ( History.getHash() !== hash ) { // Hash is a proper hash, so apply it // Handle browser bugs if ( History.bugs.setHash ) { // Fix Safari Bug https://bugs.webkit.org/show_bug.cgi?id=56249 // Fetch the base page pageUrl = History.getPageUrl(); // Safari hash apply History.pushState(null,null,pageUrl+'#'+hash,false); } else { // Normal hash apply document.location.hash = hash; } } // Chain return History; }; /** * History.escape() * normalize and Escape a Hash * @return {string} */ History.escapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Escape hash result = window.encodeURIComponent(result); // IE6 Escape Bug if ( !History.bugs.hashEscape ) { // Restore common parts result = result .replace(/\%21/g,'!') .replace(/\%26/g,'&') .replace(/\%3D/g,'=') .replace(/\%3F/g,'?'); } // Return result return result; }; /** * History.getHashByUrl(url) * Extracts the Hash from a URL * @param {string} url * @return {string} url */ History.getHashByUrl = function(url){ // Extract the hash var hash = String(url) .replace(/([^#]*)#?([^#]*)#?(.*)/, '$2') ; // Unescape hash hash = History.unescapeHash(hash); // Return hash return hash; }; /** * History.setTitle(title) * Applies the title to the document * @param {State} newState * @return {Boolean} */ History.setTitle = function(newState){ // Prepare var title = newState.title, firstState; // Initial if ( !title ) { firstState = History.getStateByIndex(0); if ( firstState && firstState.url === newState.url ) { title = firstState.title||History.options.initialTitle; } } // Apply try { document.getElementsByTagName('title')[0].innerHTML = title.replace('<','<').replace('>','>').replace(' & ',' & '); } catch ( Exception ) { } document.title = title; // Chain return History; }; // ==================================================================== // Queueing /** * History.queues * The list of queues to use * First In, First Out */ History.queues = []; /** * History.busy(value) * @param {boolean} value [optional] * @return {boolean} busy */ History.busy = function(value){ // Apply if ( typeof value !== 'undefined' ) { //History.debug('History.busy: changing ['+(History.busy.flag||false)+'] to ['+(value||false)+']', History.queues.length); History.busy.flag = value; } // Default else if ( typeof History.busy.flag === 'undefined' ) { History.busy.flag = false; } // Queue if ( !History.busy.flag ) { // Execute the next item in the queue clearTimeout(History.busy.timeout); var fireNext = function(){ var i, queue, item; if ( History.busy.flag ) return; for ( i=History.queues.length-1; i >= 0; --i ) { queue = History.queues[i]; if ( queue.length === 0 ) continue; item = queue.shift(); History.fireQueueItem(item); History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } }; History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } // Return return History.busy.flag; }; /** * History.busy.flag */ History.busy.flag = false; /** * History.fireQueueItem(item) * Fire a Queue Item * @param {Object} item * @return {Mixed} result */ History.fireQueueItem = function(item){ return item.callback.apply(item.scope||History,item.args||[]); }; /** * History.pushQueue(callback,args) * Add an item to the queue * @param {Object} item [scope,callback,args,queue] */ History.pushQueue = function(item){ // Prepare the queue History.queues[item.queue||0] = History.queues[item.queue||0]||[]; // Add to the queue History.queues[item.queue||0].push(item); // Chain return History; }; /** * History.queue (item,queue), (func,queue), (func), (item) * Either firs the item now if not busy, or adds it to the queue */ History.queue = function(item,queue){ // Prepare if ( typeof item === 'function' ) { item = { callback: item }; } if ( typeof queue !== 'undefined' ) { item.queue = queue; } // Handle if ( History.busy() ) { History.pushQueue(item); } else { History.fireQueueItem(item); } // Chain return History; }; /** * History.clearQueue() * Clears the Queue */ History.clearQueue = function(){ History.busy.flag = false; History.queues = []; return History; }; // ==================================================================== // IE Bug Fix /** * History.stateChanged * States whether or not the state has changed since the last double check was initialised */ History.stateChanged = false; /** * History.doubleChecker * Contains the timeout used for the double checks */ History.doubleChecker = false; /** * History.doubleCheckComplete() * Complete a double check * @return {History} */ History.doubleCheckComplete = function(){ // Update History.stateChanged = true; // Clear History.doubleCheckClear(); // Chain return History; }; /** * History.doubleCheckClear() * Clear a double check * @return {History} */ History.doubleCheckClear = function(){ // Clear if ( History.doubleChecker ) { clearTimeout(History.doubleChecker); History.doubleChecker = false; } // Chain return History; }; /** * History.doubleCheck() * Create a double check * @return {History} */ History.doubleCheck = function(tryAgain){ // Reset History.stateChanged = false; History.doubleCheckClear(); // Fix IE6,IE7 bug where calling history.back or history.forward does not actually change the hash (whereas doing it manually does) // Fix Safari 5 bug where sometimes the state does not change: https://bugs.webkit.org/show_bug.cgi?id=42940 if ( History.bugs.ieDoubleCheck ) { // Apply Check History.doubleChecker = setTimeout( function(){ History.doubleCheckClear(); if ( !History.stateChanged ) { //History.debug('History.doubleCheck: State has not yet changed, trying again', arguments); // Re-Attempt tryAgain(); } return true; }, History.options.doubleCheckInterval ); } // Chain return History; }; // ==================================================================== // Safari Bug Fix /** * History.safariStatePoll() * Poll the current state * @return {History} */ History.safariStatePoll = function(){ // Poll the URL // Get the Last State which has the new URL var urlState = History.extractState(History.getLocationHref()), newState; // Check for a difference if ( !History.isLastSavedState(urlState) ) { newState = urlState; } else { return; } // Check if we have a state with that url // If not create it if ( !newState ) { //History.debug('History.safariStatePoll: new'); newState = History.createStateObject(); } // Apply the New State //History.debug('History.safariStatePoll: trigger'); History.Adapter.trigger(window,'popstate'); // Chain return History; }; // ==================================================================== // State Aliases /** * History.back(queue) * Send the browser history back one item * @param {Integer} queue [optional] */ History.back = function(queue){ //History.debug('History.back: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.back: we must wait', arguments); History.pushQueue({ scope: History, callback: History.back, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.back(false); }); // Go back history.go(-1); // End back closure return true; }; /** * History.forward(queue) * Send the browser history forward one item * @param {Integer} queue [optional] */ History.forward = function(queue){ //History.debug('History.forward: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.forward: we must wait', arguments); History.pushQueue({ scope: History, callback: History.forward, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.forward(false); }); // Go forward history.go(1); // End forward closure return true; }; /** * History.go(index,queue) * Send the browser history back or forward index times * @param {Integer} queue [optional] */ History.go = function(index,queue){ //History.debug('History.go: called', arguments); // Prepare var i; // Handle if ( index > 0 ) { // Forward for ( i=1; i<=index; ++i ) { History.forward(queue); } } else if ( index < 0 ) { // Backward for ( i=-1; i>=index; --i ) { History.back(queue); } } else { throw new Error('History.go: History.go requires a positive or negative integer passed.'); } // Chain return History; }; // ==================================================================== // HTML5 State Support // Non-Native pushState Implementation if ( History.emulated.pushState ) { /* * Provide Skeleton for HTML4 Browsers */ // Prepare var emptyFunction = function(){}; History.pushState = History.pushState||emptyFunction; History.replaceState = History.replaceState||emptyFunction; } // History.emulated.pushState // Native pushState Implementation else { /* * Use native HTML5 History API Implementation */ /** * History.onPopState(event,extra) * Refresh the Current State */ History.onPopState = function(event,extra){ // Prepare var stateId = false, newState = false, currentHash, currentState; // Reset the double check History.doubleCheckComplete(); // Check for a Hash, and handle apporiatly currentHash = History.getHash(); if ( currentHash ) { // Expand Hash currentState = History.extractState(currentHash||History.getLocationHref(),true); if ( currentState ) { // We were able to parse it, it must be a State! // Let's forward to replaceState //History.debug('History.onPopState: state anchor', currentHash, currentState); History.replaceState(currentState.data, currentState.title, currentState.url, false); } else { // Traditional Anchor //History.debug('History.onPopState: traditional anchor', currentHash); History.Adapter.trigger(window,'anchorchange'); History.busy(false); } // We don't care for hashes History.expectedStateId = false; return false; } // Ensure stateId = History.Adapter.extractEventData('state',event,extra) || false; // Fetch State if ( stateId ) { // Vanilla: Back/forward button was used newState = History.getStateById(stateId); } else if ( History.expectedStateId ) { // Vanilla: A new state was pushed, and popstate was called manually newState = History.getStateById(History.expectedStateId); } else { // Initial State newState = History.extractState(History.getLocationHref()); } // The State did not exist in our store if ( !newState ) { // Regenerate the State newState = History.createStateObject(null,null,History.getLocationHref()); } // Clean History.expectedStateId = false; // Check if we are the same state if ( History.isLastSavedState(newState) ) { // There has been no change (just the page's hash has finally propagated) //History.debug('History.onPopState: no change', newState, History.savedStates); History.busy(false); return false; } // Store the State History.storeState(newState); History.saveState(newState); // Force update of the title History.setTitle(newState); // Fire Our Event History.Adapter.trigger(window,'statechange'); History.busy(false); // Return true return true; }; History.Adapter.bind(window,'popstate',History.onPopState); /** * History.pushState(data,title,url) * Add a new State to the history object, become it, and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.pushState = function(data,title,url,queue){ //History.debug('History.pushState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.pushState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.pushState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.pushState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End pushState closure return true; }; /** * History.replaceState(data,title,url) * Replace the State and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.replaceState = function(data,title,url,queue){ //History.debug('History.replaceState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.replaceState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.replaceState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.replaceState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End replaceState closure return true; }; } // !History.emulated.pushState // ==================================================================== // Initialise /** * Load the Store */ if ( sessionStorage ) { // Fetch try { History.store = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { History.store = {}; } // Normalize History.normalizeStore(); } else { // Default Load History.store = {}; History.normalizeStore(); } /** * Clear Intervals on exit to prevent memory leaks */ History.Adapter.bind(window,"unload",History.clearAllIntervals); /** * Create the initial State */ History.saveState(History.storeState(History.extractState(History.getLocationHref(),true))); /** * Bind for Saving Store */ if ( sessionStorage ) { // When the page is closed History.onUnload = function(){ // Prepare var currentStore, item, currentStoreString; // Fetch try { currentStore = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { currentStore = {}; } // Ensure currentStore.idToState = currentStore.idToState || {}; currentStore.urlToId = currentStore.urlToId || {}; currentStore.stateToId = currentStore.stateToId || {}; // Sync for ( item in History.idToState ) { if ( !History.idToState.hasOwnProperty(item) ) { continue; } currentStore.idToState[item] = History.idToState[item]; } for ( item in History.urlToId ) { if ( !History.urlToId.hasOwnProperty(item) ) { continue; } currentStore.urlToId[item] = History.urlToId[item]; } for ( item in History.stateToId ) { if ( !History.stateToId.hasOwnProperty(item) ) { continue; } currentStore.stateToId[item] = History.stateToId[item]; } // Update History.store = currentStore; History.normalizeStore(); // In Safari, going into Private Browsing mode causes the // Session Storage object to still exist but if you try and use // or set any property/function of it it throws the exception // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to // add something to storage that exceeded the quota." infinitely // every second. currentStoreString = JSON.stringify(currentStore); try { // Store sessionStorage.setItem('History.store', currentStoreString); } catch (e) { if (e.code === DOMException.QUOTA_EXCEEDED_ERR) { if (sessionStorage.length) { // Workaround for a bug seen on iPads. Sometimes the quota exceeded error comes up and simply // removing/resetting the storage can work. sessionStorage.removeItem('History.store'); sessionStorage.setItem('History.store', currentStoreString); } else { // Otherwise, we're probably private browsing in Safari, so we'll ignore the exception. } } else { throw e; } } }; // For Internet Explorer History.intervalList.push(setInterval(History.onUnload,History.options.storeInterval)); // For Other Browsers History.Adapter.bind(window,'beforeunload',History.onUnload); History.Adapter.bind(window,'unload',History.onUnload); // Both are enabled for consistency } // Non-Native pushState Implementation if ( !History.emulated.pushState ) { // Be aware, the following is only for native pushState implementations // If you are wanting to include something for all browsers // Then include it above this if block /** * Setup Safari Fix */ if ( History.bugs.safariPoll ) { History.intervalList.push(setInterval(History.safariStatePoll, History.options.safariPollInterval)); } /** * Ensure Cross Browser Compatibility */ if ( navigator.vendor === 'Apple Computer, Inc.' || (navigator.appCodeName||'') === 'Mozilla' ) { /** * Fix Safari HashChange Issue */ // Setup Alias History.Adapter.bind(window,'hashchange',function(){ History.Adapter.trigger(window,'popstate'); }); // Initialise Alias if ( History.getHash() ) { History.Adapter.onDomLoad(function(){ History.Adapter.trigger(window,'hashchange'); }); } } } // !History.emulated.pushState }; // History.initCore // Try to Initialise History if (!History.options || !History.options.delayInit) { History.init(); } })(window); ================================================ FILE: scripts/bundled-uncompressed/html4+html5/jquery.history.js ================================================ /* json2.js 2012-10-08 Public Domain. NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. See http://www.JSON.org/js.html This code should be minified before deployment. See http://javascript.crockford.com/jsmin.html USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO NOT CONTROL. This file creates a global JSON object containing two methods: stringify and parse. JSON.stringify(value, replacer, space) value any JavaScript value, usually an object or array. replacer an optional parameter that determines how object values are stringified for objects. It can be a function or an array of strings. space an optional parameter that specifies the indentation of nested structures. If it is omitted, the text will be packed without extra whitespace. If it is a number, it will specify the number of spaces to indent at each level. If it is a string (such as '\t' or ' '), it contains the characters used to indent at each level. This method produces a JSON text from a JavaScript value. When an object value is found, if the object contains a toJSON method, its toJSON method will be called and the result will be stringified. A toJSON method does not serialize: it returns the value represented by the name/value pair that should be serialized, or undefined if nothing should be serialized. The toJSON method will be passed the key associated with the value, and this will be bound to the value For example, this would serialize Dates as ISO strings. Date.prototype.toJSON = function (key) { function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } return this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z'; }; You can provide an optional replacer method. It will be passed the key and value of each member, with this bound to the containing object. The value that is returned from your method will be serialized. If your method returns undefined, then the member will be excluded from the serialization. If the replacer parameter is an array of strings, then it will be used to select the members to be serialized. It filters the results such that only members with keys listed in the replacer array are stringified. Values that do not have JSON representations, such as undefined or functions, will not be serialized. Such values in objects will be dropped; in arrays they will be replaced with null. You can use a replacer function to replace those with JSON values. JSON.stringify(undefined) returns undefined. The optional space parameter produces a stringification of the value that is filled with line breaks and indentation to make it easier to read. If the space parameter is a non-empty string, then that string will be used for indentation. If the space parameter is a number, then the indentation will be that many spaces. Example: text = JSON.stringify(['e', {pluribus: 'unum'}]); // text is '["e",{"pluribus":"unum"}]' text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' text = JSON.stringify([new Date()], function (key, value) { return this[key] instanceof Date ? 'Date(' + this[key] + ')' : value; }); // text is '["Date(---current time---)"]' JSON.parse(text, reviver) This method parses a JSON text to produce an object or array. It can throw a SyntaxError exception. The optional reviver parameter is a function that can filter and transform the results. It receives each of the keys and values, and its return value is used instead of the original value. If it returns what it received, then the structure is not modified. If it returns undefined then the member is deleted. Example: // Parse the text. Values that look like ISO date strings will // be converted to Date objects. myData = JSON.parse(text, function (key, value) { var a; if (typeof value === 'string') { a = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); if (a) { return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6])); } } return value; }); myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { var d; if (typeof value === 'string' && value.slice(0, 5) === 'Date(' && value.slice(-1) === ')') { d = new Date(value.slice(5, -1)); if (d) { return d; } } return value; }); This is a reference implementation. You are free to copy, modify, or redistribute. */ /*jslint evil: true, regexp: true */ /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, lastIndex, length, parse, prototype, push, replace, slice, stringify, test, toJSON, toString, valueOf */ // Create a JSON object only if one does not already exist. We create the // methods in a closure to avoid creating global variables. if (typeof JSON !== 'object') { JSON = {}; } (function () { 'use strict'; function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } if (typeof Date.prototype.toJSON !== 'function') { Date.prototype.toJSON = function (key) { return isFinite(this.valueOf()) ? this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z' : null; }; String.prototype.toJSON = Number.prototype.toJSON = Boolean.prototype.toJSON = function (key) { return this.valueOf(); }; } var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, gap, indent, meta = { // table of character substitutions '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\' }, rep; function quote(string) { // If the string contains no control characters, no quote characters, and no // backslash characters, then we can safely slap some quotes around it. // Otherwise we must also replace the offending characters with safe escape // sequences. escapable.lastIndex = 0; return escapable.test(string) ? '"' + string.replace(escapable, function (a) { var c = meta[a]; return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }) + '"' : '"' + string + '"'; } function str(key, holder) { // Produce a string from holder[key]. var i, // The loop counter. k, // The member key. v, // The member value. length, mind = gap, partial, value = holder[key]; // If the value has a toJSON method, call it to obtain a replacement value. if (value && typeof value === 'object' && typeof value.toJSON === 'function') { value = value.toJSON(key); } // If we were called with a replacer function, then call the replacer to // obtain a replacement value. if (typeof rep === 'function') { value = rep.call(holder, key, value); } // What happens next depends on the value's type. switch (typeof value) { case 'string': return quote(value); case 'number': // JSON numbers must be finite. Encode non-finite numbers as null. return isFinite(value) ? String(value) : 'null'; case 'boolean': case 'null': // If the value is a boolean or null, convert it to a string. Note: // typeof null does not produce 'null'. The case is included here in // the remote chance that this gets fixed someday. return String(value); // If the type is 'object', we might be dealing with an object or an array or // null. case 'object': // Due to a specification blunder in ECMAScript, typeof null is 'object', // so watch out for that case. if (!value) { return 'null'; } // Make an array to hold the partial results of stringifying this object value. gap += indent; partial = []; // Is the value an array? if (Object.prototype.toString.apply(value) === '[object Array]') { // The value is an array. Stringify every element. Use null as a placeholder // for non-JSON values. length = value.length; for (i = 0; i < length; i += 1) { partial[i] = str(i, value) || 'null'; } // Join all of the elements together, separated with commas, and wrap them in // brackets. v = partial.length === 0 ? '[]' : gap ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : '[' + partial.join(',') + ']'; gap = mind; return v; } // If the replacer is an array, use it to select the members to be stringified. if (rep && typeof rep === 'object') { length = rep.length; for (i = 0; i < length; i += 1) { if (typeof rep[i] === 'string') { k = rep[i]; v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } else { // Otherwise, iterate through all of the keys in the object. for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } // Join all of the member texts together, separated with commas, // and wrap them in braces. v = partial.length === 0 ? '{}' : gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : '{' + partial.join(',') + '}'; gap = mind; return v; } } // If the JSON object does not yet have a stringify method, give it one. if (typeof JSON.stringify !== 'function') { JSON.stringify = function (value, replacer, space) { // The stringify method takes a value and an optional replacer, and an optional // space parameter, and returns a JSON text. The replacer can be a function // that can replace values, or an array of strings that will select the keys. // A default replacer method can be provided. Use of the space parameter can // produce text that is more easily readable. var i; gap = ''; indent = ''; // If the space parameter is a number, make an indent string containing that // many spaces. if (typeof space === 'number') { for (i = 0; i < space; i += 1) { indent += ' '; } // If the space parameter is a string, it will be used as the indent string. } else if (typeof space === 'string') { indent = space; } // If there is a replacer, it must be a function or an array. // Otherwise, throw an error. rep = replacer; if (replacer && typeof replacer !== 'function' && (typeof replacer !== 'object' || typeof replacer.length !== 'number')) { throw new Error('JSON.stringify'); } // Make a fake root object containing our value under the key of ''. // Return the result of stringifying the value. return str('', {'': value}); }; } // If the JSON object does not yet have a parse method, give it one. if (typeof JSON.parse !== 'function') { JSON.parse = function (text, reviver) { // The parse method takes a text and an optional reviver function, and returns // a JavaScript value if the text is a valid JSON text. var j; function walk(holder, key) { // The walk method is used to recursively walk the resulting structure so // that modifications can be made. var k, v, value = holder[key]; if (value && typeof value === 'object') { for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = walk(value, k); if (v !== undefined) { value[k] = v; } else { delete value[k]; } } } } return reviver.call(holder, key, value); } // Parsing happens in four stages. In the first stage, we replace certain // Unicode characters with escape sequences. JavaScript handles many characters // incorrectly, either silently deleting them, or treating them as line endings. text = String(text); cx.lastIndex = 0; if (cx.test(text)) { text = text.replace(cx, function (a) { return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }); } // In the second stage, we run the text against regular expressions that look // for non-JSON patterns. We are especially concerned with '()' and 'new' // because they can cause invocation, and '=' because it can cause mutation. // But just to be safe, we want to reject all unexpected forms. // We split the second stage into 4 regexp operations in order to work around // crippling inefficiencies in IE's and Safari's regexp engines. First we // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we // replace all simple value tokens with ']' characters. Third, we delete all // open brackets that follow a colon or comma or that begin the text. Finally, // we look to see that the remaining characters are only whitespace or ']' or // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. if (/^[\],:{}\s]*$/ .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { // In the third stage we use the eval function to compile the text into a // JavaScript structure. The '{' operator is subject to a syntactic ambiguity // in JavaScript: it can begin a block or an object literal. We wrap the text // in parens to eliminate the ambiguity. j = eval('(' + text + ')'); // In the optional fourth stage, we recursively walk the new structure, passing // each name/value pair to a reviver function for possible transformation. return typeof reviver === 'function' ? walk({'': j}, '') : j; } // If the text is not JSON parseable, then a SyntaxError is thrown. throw new SyntaxError('JSON.parse'); }; } }());/** * History.js jQuery Adapter * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ // Closure (function(window,undefined){ "use strict"; // Localise Globals var History = window.History = window.History||{}, jQuery = window.jQuery; // Check Existence if ( typeof History.Adapter !== 'undefined' ) { throw new Error('History.js Adapter has already been loaded...'); } // Add the Adapter History.Adapter = { /** * History.Adapter.bind(el,event,callback) * @param {Element|string} el * @param {string} event - custom and standard events * @param {function} callback * @return {void} */ bind: function(el,event,callback){ jQuery(el).bind(event,callback); }, /** * History.Adapter.trigger(el,event) * @param {Element|string} el * @param {string} event - custom and standard events * @param {Object=} extra - a object of extra event data (optional) * @return {void} */ trigger: function(el,event,extra){ jQuery(el).trigger(event,extra); }, /** * History.Adapter.extractEventData(key,event,extra) * @param {string} key - key for the event data to extract * @param {string} event - custom and standard events * @param {Object=} extra - a object of extra event data (optional) * @return {mixed} */ extractEventData: function(key,event,extra){ // jQuery Native then jQuery Custom var result = (event && event.originalEvent && event.originalEvent[key]) || (extra && extra[key]) || undefined; // Return return result; }, /** * History.Adapter.onDomLoad(callback) * @param {function} callback * @return {void} */ onDomLoad: function(callback) { jQuery(callback); } }; // Try and Initialise History if ( typeof History.init !== 'undefined' ) { History.init(); } })(window); /** * History.js HTML4 Support * Depends on the HTML5 Support * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window,undefined){ "use strict"; // ======================================================================== // Initialise // Localise Globals var document = window.document, // Make sure we are using the correct document setTimeout = window.setTimeout||setTimeout, clearTimeout = window.clearTimeout||clearTimeout, setInterval = window.setInterval||setInterval, History = window.History = window.History||{}; // Public History Object // Check Existence if ( typeof History.initHtml4 !== 'undefined' ) { throw new Error('History.js HTML4 Support has already been loaded...'); } // ======================================================================== // Initialise HTML4 Support // Initialise HTML4 Support History.initHtml4 = function(){ // Initialise if ( typeof History.initHtml4.initialized !== 'undefined' ) { // Already Loaded return false; } else { History.initHtml4.initialized = true; } // ==================================================================== // Properties /** * History.enabled * Is History enabled? */ History.enabled = true; // ==================================================================== // Hash Storage /** * History.savedHashes * Store the hashes in an array */ History.savedHashes = []; /** * History.isLastHash(newHash) * Checks if the hash is the last hash * @param {string} newHash * @return {boolean} true */ History.isLastHash = function(newHash){ // Prepare var oldHash = History.getHashByIndex(), isLast; // Check isLast = newHash === oldHash; // Return isLast return isLast; }; /** * History.isHashEqual(newHash, oldHash) * Checks to see if two hashes are functionally equal * @param {string} newHash * @param {string} oldHash * @return {boolean} true */ History.isHashEqual = function(newHash, oldHash){ newHash = encodeURIComponent(newHash).replace(/%25/g, "%"); oldHash = encodeURIComponent(oldHash).replace(/%25/g, "%"); return newHash === oldHash; }; /** * History.saveHash(newHash) * Push a Hash * @param {string} newHash * @return {boolean} true */ History.saveHash = function(newHash){ // Check Hash if ( History.isLastHash(newHash) ) { return false; } // Push the Hash History.savedHashes.push(newHash); // Return true return true; }; /** * History.getHashByIndex() * Gets a hash by the index * @param {integer} index * @return {string} */ History.getHashByIndex = function(index){ // Prepare var hash = null; // Handle if ( typeof index === 'undefined' ) { // Get the last inserted hash = History.savedHashes[History.savedHashes.length-1]; } else if ( index < 0 ) { // Get from the end hash = History.savedHashes[History.savedHashes.length+index]; } else { // Get from the beginning hash = History.savedHashes[index]; } // Return hash return hash; }; // ==================================================================== // Discarded States /** * History.discardedHashes * A hashed array of discarded hashes */ History.discardedHashes = {}; /** * History.discardedStates * A hashed array of discarded states */ History.discardedStates = {}; /** * History.discardState(State) * Discards the state by ignoring it through History * @param {object} State * @return {true} */ History.discardState = function(discardedState,forwardState,backState){ //History.debug('History.discardState', arguments); // Prepare var discardedStateHash = History.getHashByState(discardedState), discardObject; // Create Discard Object discardObject = { 'discardedState': discardedState, 'backState': backState, 'forwardState': forwardState }; // Add to DiscardedStates History.discardedStates[discardedStateHash] = discardObject; // Return true return true; }; /** * History.discardHash(hash) * Discards the hash by ignoring it through History * @param {string} hash * @return {true} */ History.discardHash = function(discardedHash,forwardState,backState){ //History.debug('History.discardState', arguments); // Create Discard Object var discardObject = { 'discardedHash': discardedHash, 'backState': backState, 'forwardState': forwardState }; // Add to discardedHash History.discardedHashes[discardedHash] = discardObject; // Return true return true; }; /** * History.discardedState(State) * Checks to see if the state is discarded * @param {object} State * @return {bool} */ History.discardedState = function(State){ // Prepare var StateHash = History.getHashByState(State), discarded; // Check discarded = History.discardedStates[StateHash]||false; // Return true return discarded; }; /** * History.discardedHash(hash) * Checks to see if the state is discarded * @param {string} State * @return {bool} */ History.discardedHash = function(hash){ // Check var discarded = History.discardedHashes[hash]||false; // Return true return discarded; }; /** * History.recycleState(State) * Allows a discarded state to be used again * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.recycleState = function(State){ //History.debug('History.recycleState', arguments); // Prepare var StateHash = History.getHashByState(State); // Remove from DiscardedStates if ( History.discardedState(State) ) { delete History.discardedStates[StateHash]; } // Return true return true; }; // ==================================================================== // HTML4 HashChange Support if ( History.emulated.hashChange ) { /* * We must emulate the HTML4 HashChange Support by manually checking for hash changes */ /** * History.hashChangeInit() * Init the HashChange Emulation */ History.hashChangeInit = function(){ // Define our Checker Function History.checkerFunction = null; // Define some variables that will help in our checker function var lastDocumentHash = '', iframeId, iframe, lastIframeHash, checkerRunning, startedWithHash = Boolean(History.getHash()); // Handle depending on the browser if ( History.isInternetExplorer() ) { // IE6 and IE7 // We need to use an iframe to emulate the back and forward buttons // Create iFrame iframeId = 'historyjs-iframe'; iframe = document.createElement('iframe'); // Adjust iFarme // IE 6 requires iframe to have a src on HTTPS pages, otherwise it will throw a // "This page contains both secure and nonsecure items" warning. iframe.setAttribute('id', iframeId); iframe.setAttribute('src', '#'); iframe.style.display = 'none'; // Append iFrame document.body.appendChild(iframe); // Create initial history entry iframe.contentWindow.document.open(); iframe.contentWindow.document.close(); // Define some variables that will help in our checker function lastIframeHash = ''; checkerRunning = false; // Define the checker function History.checkerFunction = function(){ // Check Running if ( checkerRunning ) { return false; } // Update Running checkerRunning = true; // Fetch var documentHash = History.getHash(), iframeHash = History.getHash(iframe.contentWindow.document); // The Document Hash has changed (application caused) if ( documentHash !== lastDocumentHash ) { // Equalise lastDocumentHash = documentHash; // Create a history entry in the iframe if ( iframeHash !== documentHash ) { //History.debug('hashchange.checker: iframe hash change', 'documentHash (new):', documentHash, 'iframeHash (old):', iframeHash); // Equalise lastIframeHash = iframeHash = documentHash; // Create History Entry iframe.contentWindow.document.open(); iframe.contentWindow.document.close(); // Update the iframe's hash iframe.contentWindow.document.location.hash = History.escapeHash(documentHash); } // Trigger Hashchange Event History.Adapter.trigger(window,'hashchange'); } // The iFrame Hash has changed (back button caused) else if ( iframeHash !== lastIframeHash ) { //History.debug('hashchange.checker: iframe hash out of sync', 'iframeHash (new):', iframeHash, 'documentHash (old):', documentHash); // Equalise lastIframeHash = iframeHash; // If there is no iframe hash that means we're at the original // iframe state. // And if there was a hash on the original request, the original // iframe state was replaced instantly, so skip this state and take // the user back to where they came from. if (startedWithHash && iframeHash === '') { History.back(); } else { // Update the Hash History.setHash(iframeHash,false); } } // Reset Running checkerRunning = false; // Return true return true; }; } else { // We are not IE // Firefox 1 or 2, Opera // Define the checker function History.checkerFunction = function(){ // Prepare var documentHash = History.getHash()||''; // The Document Hash has changed (application caused) if ( documentHash !== lastDocumentHash ) { // Equalise lastDocumentHash = documentHash; // Trigger Hashchange Event History.Adapter.trigger(window,'hashchange'); } // Return true return true; }; } // Apply the checker function History.intervalList.push(setInterval(History.checkerFunction, History.options.hashChangeInterval)); // Done return true; }; // History.hashChangeInit // Bind hashChangeInit History.Adapter.onDomLoad(History.hashChangeInit); } // History.emulated.hashChange // ==================================================================== // HTML5 State Support // Non-Native pushState Implementation if ( History.emulated.pushState ) { /* * We must emulate the HTML5 State Management by using HTML4 HashChange */ /** * History.onHashChange(event) * Trigger HTML5's window.onpopstate via HTML4 HashChange Support */ History.onHashChange = function(event){ //History.debug('History.onHashChange', arguments); // Prepare var currentUrl = ((event && event.newURL) || History.getLocationHref()), currentHash = History.getHashByUrl(currentUrl), currentState = null, currentStateHash = null, currentStateHashExits = null, discardObject; // Check if we are the same state if ( History.isLastHash(currentHash) ) { // There has been no change (just the page's hash has finally propagated) //History.debug('History.onHashChange: no change'); History.busy(false); return false; } // Reset the double check History.doubleCheckComplete(); // Store our location for use in detecting back/forward direction History.saveHash(currentHash); // Expand Hash if ( currentHash && History.isTraditionalAnchor(currentHash) ) { //History.debug('History.onHashChange: traditional anchor', currentHash); // Traditional Anchor Hash History.Adapter.trigger(window,'anchorchange'); History.busy(false); return false; } // Create State currentState = History.extractState(History.getFullUrl(currentHash||History.getLocationHref()),true); // Check if we are the same state if ( History.isLastSavedState(currentState) ) { //History.debug('History.onHashChange: no change'); // There has been no change (just the page's hash has finally propagated) History.busy(false); return false; } // Create the state Hash currentStateHash = History.getHashByState(currentState); // Check if we are DiscardedState discardObject = History.discardedState(currentState); if ( discardObject ) { // Ignore this state as it has been discarded and go back to the state before it if ( History.getHashByIndex(-2) === History.getHashByState(discardObject.forwardState) ) { // We are going backwards //History.debug('History.onHashChange: go backwards'); History.back(false); } else { // We are going forwards //History.debug('History.onHashChange: go forwards'); History.forward(false); } return false; } // Push the new HTML5 State //History.debug('History.onHashChange: success hashchange'); History.pushState(currentState.data,currentState.title,encodeURI(currentState.url),false); // End onHashChange closure return true; }; History.Adapter.bind(window,'hashchange',History.onHashChange); /** * History.pushState(data,title,url) * Add a new State to the history object, become it, and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.pushState = function(data,title,url,queue){ //History.debug('History.pushState: called', arguments); // We assume that the URL passed in is URI-encoded, but this makes // sure that it's fully URI encoded; any '%'s that are encoded are // converted back into '%'s url = encodeURI(url).replace(/%25/g, "%"); // Check the State if ( History.getHashByUrl(url) ) { throw new Error('History.js does not support states with fragment-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.pushState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.pushState, args: arguments, queue: queue }); return false; } // Make Busy History.busy(true); // Fetch the State Object var newState = History.createStateObject(data,title,url), newStateHash = History.getHashByState(newState), oldState = History.getState(false), oldStateHash = History.getHashByState(oldState), html4Hash = History.getHash(), wasExpected = History.expectedStateId == newState.id; // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Recycle the State History.recycleState(newState); // Force update of the title History.setTitle(newState); // Check if we are the same State if ( newStateHash === oldStateHash ) { //History.debug('History.pushState: no change', newStateHash); History.busy(false); return false; } // Update HTML5 State History.saveState(newState); // Fire HTML5 Event if(!wasExpected) History.Adapter.trigger(window,'statechange'); // Update HTML4 Hash if ( !History.isHashEqual(newStateHash, html4Hash) && !History.isHashEqual(newStateHash, History.getShortUrl(History.getLocationHref())) ) { History.setHash(newStateHash,false); } History.busy(false); // End pushState closure return true; }; /** * History.replaceState(data,title,url) * Replace the State and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.replaceState = function(data,title,url,queue){ //History.debug('History.replaceState: called', arguments); // We assume that the URL passed in is URI-encoded, but this makes // sure that it's fully URI encoded; any '%'s that are encoded are // converted back into '%'s url = encodeURI(url).replace(/%25/g, "%"); // Check the State if ( History.getHashByUrl(url) ) { throw new Error('History.js does not support states with fragment-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.replaceState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.replaceState, args: arguments, queue: queue }); return false; } // Make Busy History.busy(true); // Fetch the State Objects var newState = History.createStateObject(data,title,url), newStateHash = History.getHashByState(newState), oldState = History.getState(false), oldStateHash = History.getHashByState(oldState), previousState = History.getStateByIndex(-2); // Discard Old State History.discardState(oldState,newState,previousState); // If the url hasn't changed, just store and save the state // and fire a statechange event to be consistent with the // html 5 api if ( newStateHash === oldStateHash ) { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Recycle the State History.recycleState(newState); // Force update of the title History.setTitle(newState); // Update HTML5 State History.saveState(newState); // Fire HTML5 Event //History.debug('History.pushState: trigger popstate'); History.Adapter.trigger(window,'statechange'); History.busy(false); } else { // Alias to PushState History.pushState(newState.data,newState.title,newState.url,false); } // End replaceState closure return true; }; } // History.emulated.pushState // ==================================================================== // Initialise // Non-Native pushState Implementation if ( History.emulated.pushState ) { /** * Ensure initial state is handled correctly */ if ( History.getHash() && !History.emulated.hashChange ) { History.Adapter.onDomLoad(function(){ History.Adapter.trigger(window,'hashchange'); }); } } // History.emulated.pushState }; // History.initHtml4 // Try to Initialise History if ( typeof History.init !== 'undefined' ) { History.init(); } })(window); /** * History.js Core * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window,undefined){ "use strict"; // ======================================================================== // Initialise // Localise Globals var console = window.console||undefined, // Prevent a JSLint complain document = window.document, // Make sure we are using the correct document navigator = window.navigator, // Make sure we are using the correct navigator sessionStorage = false, // sessionStorage setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, setInterval = window.setInterval, clearInterval = window.clearInterval, JSON = window.JSON, alert = window.alert, History = window.History = window.History||{}, // Public History Object history = window.history; // Old History Object try { sessionStorage = window.sessionStorage; // This will throw an exception in some browsers when cookies/localStorage are explicitly disabled (i.e. Chrome) sessionStorage.setItem('TEST', '1'); sessionStorage.removeItem('TEST'); } catch(e) { sessionStorage = false; } // MooTools Compatibility JSON.stringify = JSON.stringify||JSON.encode; JSON.parse = JSON.parse||JSON.decode; // Check Existence if ( typeof History.init !== 'undefined' ) { throw new Error('History.js Core has already been loaded...'); } // Initialise History History.init = function(options){ // Check Load Status of Adapter if ( typeof History.Adapter === 'undefined' ) { return false; } // Check Load Status of Core if ( typeof History.initCore !== 'undefined' ) { History.initCore(); } // Check Load Status of HTML4 Support if ( typeof History.initHtml4 !== 'undefined' ) { History.initHtml4(); } // Return true return true; }; // ======================================================================== // Initialise Core // Initialise Core History.initCore = function(options){ // Initialise if ( typeof History.initCore.initialized !== 'undefined' ) { // Already Loaded return false; } else { History.initCore.initialized = true; } // ==================================================================== // Options /** * History.options * Configurable options */ History.options = History.options||{}; /** * History.options.hashChangeInterval * How long should the interval be before hashchange checks */ History.options.hashChangeInterval = History.options.hashChangeInterval || 100; /** * History.options.safariPollInterval * How long should the interval be before safari poll checks */ History.options.safariPollInterval = History.options.safariPollInterval || 500; /** * History.options.doubleCheckInterval * How long should the interval be before we perform a double check */ History.options.doubleCheckInterval = History.options.doubleCheckInterval || 500; /** * History.options.disableSuid * Force History not to append suid */ History.options.disableSuid = History.options.disableSuid || false; /** * History.options.storeInterval * How long should we wait between store calls */ History.options.storeInterval = History.options.storeInterval || 1000; /** * History.options.busyDelay * How long should we wait between busy events */ History.options.busyDelay = History.options.busyDelay || 250; /** * History.options.debug * If true will enable debug messages to be logged */ History.options.debug = History.options.debug || false; /** * History.options.initialTitle * What is the title of the initial state */ History.options.initialTitle = History.options.initialTitle || document.title; /** * History.options.html4Mode * If true, will force HTMl4 mode (hashtags) */ History.options.html4Mode = History.options.html4Mode || false; /** * History.options.delayInit * Want to override default options and call init manually. */ History.options.delayInit = History.options.delayInit || false; // ==================================================================== // Interval record /** * History.intervalList * List of intervals set, to be cleared when document is unloaded. */ History.intervalList = []; /** * History.clearAllIntervals * Clears all setInterval instances. */ History.clearAllIntervals = function(){ var i, il = History.intervalList; if (typeof il !== "undefined" && il !== null) { for (i = 0; i < il.length; i++) { clearInterval(il[i]); } History.intervalList = null; } }; // ==================================================================== // Debug /** * History.debug(message,...) * Logs the passed arguments if debug enabled */ History.debug = function(){ if ( (History.options.debug||false) ) { History.log.apply(History,arguments); } }; /** * History.log(message,...) * Logs the passed arguments */ History.log = function(){ // Prepare var consoleExists = !(typeof console === 'undefined' || typeof console.log === 'undefined' || typeof console.log.apply === 'undefined'), textarea = document.getElementById('log'), message, i,n, args,arg ; // Write to Console if ( consoleExists ) { args = Array.prototype.slice.call(arguments); message = args.shift(); if ( typeof console.debug !== 'undefined' ) { console.debug.apply(console,[message,args]); } else { console.log.apply(console,[message,args]); } } else { message = ("\n"+arguments[0]+"\n"); } // Write to log for ( i=1,n=arguments.length; i * @author James Padolsey */ History.getInternetExplorerMajorVersion = function(){ var result = History.getInternetExplorerMajorVersion.cached = (typeof History.getInternetExplorerMajorVersion.cached !== 'undefined') ? History.getInternetExplorerMajorVersion.cached : (function(){ var v = 3, div = document.createElement('div'), all = div.getElementsByTagName('i'); while ( (div.innerHTML = '') && all[0] ) {} return (v > 4) ? v : false; })() ; return result; }; /** * History.isInternetExplorer() * Are we using Internet Explorer? * @return {boolean} * @license Public Domain * @author Benjamin Arthur Lupton */ History.isInternetExplorer = function(){ var result = History.isInternetExplorer.cached = (typeof History.isInternetExplorer.cached !== 'undefined') ? History.isInternetExplorer.cached : Boolean(History.getInternetExplorerMajorVersion()) ; return result; }; /** * History.emulated * Which features require emulating? */ if (History.options.html4Mode) { History.emulated = { pushState : true, hashChange: true }; } else { History.emulated = { pushState: !Boolean( window.history && window.history.pushState && window.history.replaceState && !( (/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i).test(navigator.userAgent) /* disable for versions of iOS before version 4.3 (8F190) */ || (/AppleWebKit\/5([0-2]|3[0-2])/i).test(navigator.userAgent) /* disable for the mercury iOS browser, or at least older versions of the webkit engine */ ) ), hashChange: Boolean( !(('onhashchange' in window) || ('onhashchange' in document)) || (History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8) ) }; } /** * History.enabled * Is History enabled? */ History.enabled = !History.emulated.pushState; /** * History.bugs * Which bugs are present */ History.bugs = { /** * Safari 5 and Safari iOS 4 fail to return to the correct state once a hash is replaced by a `replaceState` call * https://bugs.webkit.org/show_bug.cgi?id=56249 */ setHash: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * Safari 5 and Safari iOS 4 sometimes fail to apply the state change under busy conditions * https://bugs.webkit.org/show_bug.cgi?id=42940 */ safariPoll: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * MSIE 6 and 7 sometimes do not apply a hash even it was told to (requiring a second call to the apply function) */ ieDoubleCheck: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8), /** * MSIE 6 requires the entire hash to be encoded for the hashes to trigger the onHashChange event */ hashEscape: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 7) }; /** * History.isEmptyObject(obj) * Checks to see if the Object is Empty * @param {Object} obj * @return {boolean} */ History.isEmptyObject = function(obj) { for ( var name in obj ) { if ( obj.hasOwnProperty(name) ) { return false; } } return true; }; /** * History.cloneObject(obj) * Clones a object and eliminate all references to the original contexts * @param {Object} obj * @return {Object} */ History.cloneObject = function(obj) { var hash,newObj; if ( obj ) { hash = JSON.stringify(obj); newObj = JSON.parse(hash); } else { newObj = {}; } return newObj; }; // ==================================================================== // URL Helpers /** * History.getRootUrl() * Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com" * @return {String} rootUrl */ History.getRootUrl = function(){ // Create var rootUrl = document.location.protocol+'//'+(document.location.hostname||document.location.host); if ( document.location.port||false ) { rootUrl += ':'+document.location.port; } rootUrl += '/'; // Return return rootUrl; }; /** * History.getBaseHref() * Fetches the `href` attribute of the `` element if it exists * @return {String} baseHref */ History.getBaseHref = function(){ // Create var baseElements = document.getElementsByTagName('base'), baseElement = null, baseHref = ''; // Test for Base Element if ( baseElements.length === 1 ) { // Prepare for Base Element baseElement = baseElements[0]; baseHref = baseElement.href.replace(/[^\/]+$/,''); } // Adjust trailing slash baseHref = baseHref.replace(/\/+$/,''); if ( baseHref ) baseHref += '/'; // Return return baseHref; }; /** * History.getBaseUrl() * Fetches the baseHref or basePageUrl or rootUrl (whichever one exists first) * @return {String} baseUrl */ History.getBaseUrl = function(){ // Create var baseUrl = History.getBaseHref()||History.getBasePageUrl()||History.getRootUrl(); // Return return baseUrl; }; /** * History.getPageUrl() * Fetches the URL of the current page * @return {String} pageUrl */ History.getPageUrl = function(){ // Fetch var State = History.getState(false,false), stateUrl = (State||{}).url||History.getLocationHref(), pageUrl; // Create pageUrl = stateUrl.replace(/\/+$/,'').replace(/[^\/]+$/,function(part,index,string){ return (/\./).test(part) ? part : part+'/'; }); // Return return pageUrl; }; /** * History.getBasePageUrl() * Fetches the Url of the directory of the current page * @return {String} basePageUrl */ History.getBasePageUrl = function(){ // Create var basePageUrl = (History.getLocationHref()).replace(/[#\?].*/,'').replace(/[^\/]+$/,function(part,index,string){ return (/[^\/]$/).test(part) ? '' : part; }).replace(/\/+$/,'')+'/'; // Return return basePageUrl; }; /** * History.getFullUrl(url) * Ensures that we have an absolute URL and not a relative URL * @param {string} url * @param {Boolean} allowBaseHref * @return {string} fullUrl */ History.getFullUrl = function(url,allowBaseHref){ // Prepare var fullUrl = url, firstChar = url.substring(0,1); allowBaseHref = (typeof allowBaseHref === 'undefined') ? true : allowBaseHref; // Check if ( /[a-z]+\:\/\//.test(url) ) { // Full URL } else if ( firstChar === '/' ) { // Root URL fullUrl = History.getRootUrl()+url.replace(/^\/+/,''); } else if ( firstChar === '#' ) { // Anchor URL fullUrl = History.getPageUrl().replace(/#.*/,'')+url; } else if ( firstChar === '?' ) { // Query URL fullUrl = History.getPageUrl().replace(/[\?#].*/,'')+url; } else { // Relative URL if ( allowBaseHref ) { fullUrl = History.getBaseUrl()+url.replace(/^(\.\/)+/,''); } else { fullUrl = History.getBasePageUrl()+url.replace(/^(\.\/)+/,''); } // We have an if condition above as we do not want hashes // which are relative to the baseHref in our URLs // as if the baseHref changes, then all our bookmarks // would now point to different locations // whereas the basePageUrl will always stay the same } // Return return fullUrl.replace(/\#$/,''); }; /** * History.getShortUrl(url) * Ensures that we have a relative URL and not a absolute URL * @param {string} url * @return {string} url */ History.getShortUrl = function(url){ // Prepare var shortUrl = url, baseUrl = History.getBaseUrl(), rootUrl = History.getRootUrl(); // Trim baseUrl if ( History.emulated.pushState ) { // We are in a if statement as when pushState is not emulated // The actual url these short urls are relative to can change // So within the same session, we the url may end up somewhere different shortUrl = shortUrl.replace(baseUrl,''); } // Trim rootUrl shortUrl = shortUrl.replace(rootUrl,'/'); // Ensure we can still detect it as a state if ( History.isTraditionalAnchor(shortUrl) ) { shortUrl = './'+shortUrl; } // Clean It shortUrl = shortUrl.replace(/^(\.\/)+/g,'./').replace(/\#$/,''); // Return return shortUrl; }; /** * History.getLocationHref(document) * Returns a normalized version of document.location.href * accounting for browser inconsistencies, etc. * * This URL will be URI-encoded and will include the hash * * @param {object} document * @return {string} url */ History.getLocationHref = function(doc) { doc = doc || document; // most of the time, this will be true if (doc.URL === doc.location.href) return doc.location.href; // some versions of webkit URI-decode document.location.href // but they leave document.URL in an encoded state if (doc.location.href === decodeURIComponent(doc.URL)) return doc.URL; // FF 3.6 only updates document.URL when a page is reloaded // document.location.href is updated correctly if (doc.location.hash && decodeURIComponent(doc.location.href.replace(/^[^#]+/, "")) === doc.location.hash) return doc.location.href; if (doc.URL.indexOf('#') == -1 && doc.location.href.indexOf('#') != -1) return doc.location.href; return doc.URL || doc.location.href; }; // ==================================================================== // State Storage /** * History.store * The store for all session specific data */ History.store = {}; /** * History.idToState * 1-1: State ID to State Object */ History.idToState = History.idToState||{}; /** * History.stateToId * 1-1: State String to State ID */ History.stateToId = History.stateToId||{}; /** * History.urlToId * 1-1: State URL to State ID */ History.urlToId = History.urlToId||{}; /** * History.storedStates * Store the states in an array */ History.storedStates = History.storedStates||[]; /** * History.savedStates * Saved the states in an array */ History.savedStates = History.savedStates||[]; /** * History.noramlizeStore() * Noramlize the store by adding necessary values */ History.normalizeStore = function(){ History.store.idToState = History.store.idToState||{}; History.store.urlToId = History.store.urlToId||{}; History.store.stateToId = History.store.stateToId||{}; }; /** * History.getState() * Get an object containing the data, title and url of the current state * @param {Boolean} friendly * @param {Boolean} create * @return {Object} State */ History.getState = function(friendly,create){ // Prepare if ( typeof friendly === 'undefined' ) { friendly = true; } if ( typeof create === 'undefined' ) { create = true; } // Fetch var State = History.getLastSavedState(); // Create if ( !State && create ) { State = History.createStateObject(); } // Adjust if ( friendly ) { State = History.cloneObject(State); State.url = State.cleanUrl||State.url; } // Return return State; }; /** * History.getIdByState(State) * Gets a ID for a State * @param {State} newState * @return {String} id */ History.getIdByState = function(newState){ // Fetch ID var id = History.extractId(newState.url), str; if ( !id ) { // Find ID via State String str = History.getStateString(newState); if ( typeof History.stateToId[str] !== 'undefined' ) { id = History.stateToId[str]; } else if ( typeof History.store.stateToId[str] !== 'undefined' ) { id = History.store.stateToId[str]; } else { // Generate a new ID while ( true ) { id = (new Date()).getTime() + String(Math.random()).replace(/\D/g,''); if ( typeof History.idToState[id] === 'undefined' && typeof History.store.idToState[id] === 'undefined' ) { break; } } // Apply the new State to the ID History.stateToId[str] = id; History.idToState[id] = newState; } } // Return ID return id; }; /** * History.normalizeState(State) * Expands a State Object * @param {object} State * @return {object} */ History.normalizeState = function(oldState){ // Variables var newState, dataNotEmpty; // Prepare if ( !oldState || (typeof oldState !== 'object') ) { oldState = {}; } // Check if ( typeof oldState.normalized !== 'undefined' ) { return oldState; } // Adjust if ( !oldState.data || (typeof oldState.data !== 'object') ) { oldState.data = {}; } // ---------------------------------------------------------------- // Create newState = {}; newState.normalized = true; newState.title = oldState.title||''; newState.url = History.getFullUrl(oldState.url?oldState.url:(History.getLocationHref())); newState.hash = History.getShortUrl(newState.url); newState.data = History.cloneObject(oldState.data); // Fetch ID newState.id = History.getIdByState(newState); // ---------------------------------------------------------------- // Clean the URL newState.cleanUrl = newState.url.replace(/\??\&_suid.*/,''); newState.url = newState.cleanUrl; // Check to see if we have more than just a url dataNotEmpty = !History.isEmptyObject(newState.data); // Apply if ( (newState.title || dataNotEmpty) && History.options.disableSuid !== true ) { // Add ID to Hash newState.hash = History.getShortUrl(newState.url).replace(/\??\&_suid.*/,''); if ( !/\?/.test(newState.hash) ) { newState.hash += '?'; } newState.hash += '&_suid='+newState.id; } // Create the Hashed URL newState.hashedUrl = History.getFullUrl(newState.hash); // ---------------------------------------------------------------- // Update the URL if we have a duplicate if ( (History.emulated.pushState || History.bugs.safariPoll) && History.hasUrlDuplicate(newState) ) { newState.url = newState.hashedUrl; } // ---------------------------------------------------------------- // Return return newState; }; /** * History.createStateObject(data,title,url) * Creates a object based on the data, title and url state params * @param {object} data * @param {string} title * @param {string} url * @return {object} */ History.createStateObject = function(data,title,url){ // Hashify var State = { 'data': data, 'title': title, 'url': url }; // Expand the State State = History.normalizeState(State); // Return object return State; }; /** * History.getStateById(id) * Get a state by it's UID * @param {String} id */ History.getStateById = function(id){ // Prepare id = String(id); // Retrieve var State = History.idToState[id] || History.store.idToState[id] || undefined; // Return State return State; }; /** * Get a State's String * @param {State} passedState */ History.getStateString = function(passedState){ // Prepare var State, cleanedState, str; // Fetch State = History.normalizeState(passedState); // Clean cleanedState = { data: State.data, title: passedState.title, url: passedState.url }; // Fetch str = JSON.stringify(cleanedState); // Return return str; }; /** * Get a State's ID * @param {State} passedState * @return {String} id */ History.getStateId = function(passedState){ // Prepare var State, id; // Fetch State = History.normalizeState(passedState); // Fetch id = State.id; // Return return id; }; /** * History.getHashByState(State) * Creates a Hash for the State Object * @param {State} passedState * @return {String} hash */ History.getHashByState = function(passedState){ // Prepare var State, hash; // Fetch State = History.normalizeState(passedState); // Hash hash = State.hash; // Return return hash; }; /** * History.extractId(url_or_hash) * Get a State ID by it's URL or Hash * @param {string} url_or_hash * @return {string} id */ History.extractId = function ( url_or_hash ) { // Prepare var id,parts,url, tmp; // Extract // If the URL has a #, use the id from before the # if (url_or_hash.indexOf('#') != -1) { tmp = url_or_hash.split("#")[0]; } else { tmp = url_or_hash; } parts = /(.*)\&_suid=([0-9]+)$/.exec(tmp); url = parts ? (parts[1]||url_or_hash) : url_or_hash; id = parts ? String(parts[2]||'') : ''; // Return return id||false; }; /** * History.isTraditionalAnchor * Checks to see if the url is a traditional anchor or not * @param {String} url_or_hash * @return {Boolean} */ History.isTraditionalAnchor = function(url_or_hash){ // Check var isTraditional = !(/[\/\?\.]/.test(url_or_hash)); // Return return isTraditional; }; /** * History.extractState * Get a State by it's URL or Hash * @param {String} url_or_hash * @return {State|null} */ History.extractState = function(url_or_hash,create){ // Prepare var State = null, id, url; create = create||false; // Fetch SUID id = History.extractId(url_or_hash); if ( id ) { State = History.getStateById(id); } // Fetch SUID returned no State if ( !State ) { // Fetch URL url = History.getFullUrl(url_or_hash); // Check URL id = History.getIdByUrl(url)||false; if ( id ) { State = History.getStateById(id); } // Create State if ( !State && create && !History.isTraditionalAnchor(url_or_hash) ) { State = History.createStateObject(null,null,url); } } // Return return State; }; /** * History.getIdByUrl() * Get a State ID by a State URL */ History.getIdByUrl = function(url){ // Fetch var id = History.urlToId[url] || History.store.urlToId[url] || undefined; // Return return id; }; /** * History.getLastSavedState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastSavedState = function(){ return History.savedStates[History.savedStates.length-1]||undefined; }; /** * History.getLastStoredState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastStoredState = function(){ return History.storedStates[History.storedStates.length-1]||undefined; }; /** * History.hasUrlDuplicate * Checks if a Url will have a url conflict * @param {Object} newState * @return {Boolean} hasDuplicate */ History.hasUrlDuplicate = function(newState) { // Prepare var hasDuplicate = false, oldState; // Fetch oldState = History.extractState(newState.url); // Check hasDuplicate = oldState && oldState.id !== newState.id; // Return return hasDuplicate; }; /** * History.storeState * Store a State * @param {Object} newState * @return {Object} newState */ History.storeState = function(newState){ // Store the State History.urlToId[newState.url] = newState.id; // Push the State History.storedStates.push(History.cloneObject(newState)); // Return newState return newState; }; /** * History.isLastSavedState(newState) * Tests to see if the state is the last state * @param {Object} newState * @return {boolean} isLast */ History.isLastSavedState = function(newState){ // Prepare var isLast = false, newId, oldState, oldId; // Check if ( History.savedStates.length ) { newId = newState.id; oldState = History.getLastSavedState(); oldId = oldState.id; // Check isLast = (newId === oldId); } // Return return isLast; }; /** * History.saveState * Push a State * @param {Object} newState * @return {boolean} changed */ History.saveState = function(newState){ // Check Hash if ( History.isLastSavedState(newState) ) { return false; } // Push the State History.savedStates.push(History.cloneObject(newState)); // Return true return true; }; /** * History.getStateByIndex() * Gets a state by the index * @param {integer} index * @return {Object} */ History.getStateByIndex = function(index){ // Prepare var State = null; // Handle if ( typeof index === 'undefined' ) { // Get the last inserted State = History.savedStates[History.savedStates.length-1]; } else if ( index < 0 ) { // Get from the end State = History.savedStates[History.savedStates.length+index]; } else { // Get from the beginning State = History.savedStates[index]; } // Return State return State; }; /** * History.getCurrentIndex() * Gets the current index * @return (integer) */ History.getCurrentIndex = function(){ // Prepare var index = null; // No states saved if(History.savedStates.length < 1) { index = 0; } else { index = History.savedStates.length-1; } return index; }; // ==================================================================== // Hash Helpers /** * History.getHash() * @param {Location=} location * Gets the current document hash * Note: unlike location.hash, this is guaranteed to return the escaped hash in all browsers * @return {string} */ History.getHash = function(doc){ var url = History.getLocationHref(doc), hash; hash = History.getHashByUrl(url); return hash; }; /** * History.unescapeHash() * normalize and Unescape a Hash * @param {String} hash * @return {string} */ History.unescapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Unescape hash result = decodeURIComponent(result); // Return result return result; }; /** * History.normalizeHash() * normalize a hash across browsers * @return {string} */ History.normalizeHash = function(hash){ // Prepare var result = hash.replace(/[^#]*#/,'').replace(/#.*/, ''); // Return result return result; }; /** * History.setHash(hash) * Sets the document hash * @param {string} hash * @return {History} */ History.setHash = function(hash,queue){ // Prepare var State, pageUrl; // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.setHash: we must wait', arguments); History.pushQueue({ scope: History, callback: History.setHash, args: arguments, queue: queue }); return false; } // Log //History.debug('History.setHash: called',hash); // Make Busy + Continue History.busy(true); // Check if hash is a state State = History.extractState(hash,true); if ( State && !History.emulated.pushState ) { // Hash is a state so skip the setHash //History.debug('History.setHash: Hash is a state so skipping the hash set with a direct pushState call',arguments); // PushState History.pushState(State.data,State.title,State.url,false); } else if ( History.getHash() !== hash ) { // Hash is a proper hash, so apply it // Handle browser bugs if ( History.bugs.setHash ) { // Fix Safari Bug https://bugs.webkit.org/show_bug.cgi?id=56249 // Fetch the base page pageUrl = History.getPageUrl(); // Safari hash apply History.pushState(null,null,pageUrl+'#'+hash,false); } else { // Normal hash apply document.location.hash = hash; } } // Chain return History; }; /** * History.escape() * normalize and Escape a Hash * @return {string} */ History.escapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Escape hash result = window.encodeURIComponent(result); // IE6 Escape Bug if ( !History.bugs.hashEscape ) { // Restore common parts result = result .replace(/\%21/g,'!') .replace(/\%26/g,'&') .replace(/\%3D/g,'=') .replace(/\%3F/g,'?'); } // Return result return result; }; /** * History.getHashByUrl(url) * Extracts the Hash from a URL * @param {string} url * @return {string} url */ History.getHashByUrl = function(url){ // Extract the hash var hash = String(url) .replace(/([^#]*)#?([^#]*)#?(.*)/, '$2') ; // Unescape hash hash = History.unescapeHash(hash); // Return hash return hash; }; /** * History.setTitle(title) * Applies the title to the document * @param {State} newState * @return {Boolean} */ History.setTitle = function(newState){ // Prepare var title = newState.title, firstState; // Initial if ( !title ) { firstState = History.getStateByIndex(0); if ( firstState && firstState.url === newState.url ) { title = firstState.title||History.options.initialTitle; } } // Apply try { document.getElementsByTagName('title')[0].innerHTML = title.replace('<','<').replace('>','>').replace(' & ',' & '); } catch ( Exception ) { } document.title = title; // Chain return History; }; // ==================================================================== // Queueing /** * History.queues * The list of queues to use * First In, First Out */ History.queues = []; /** * History.busy(value) * @param {boolean} value [optional] * @return {boolean} busy */ History.busy = function(value){ // Apply if ( typeof value !== 'undefined' ) { //History.debug('History.busy: changing ['+(History.busy.flag||false)+'] to ['+(value||false)+']', History.queues.length); History.busy.flag = value; } // Default else if ( typeof History.busy.flag === 'undefined' ) { History.busy.flag = false; } // Queue if ( !History.busy.flag ) { // Execute the next item in the queue clearTimeout(History.busy.timeout); var fireNext = function(){ var i, queue, item; if ( History.busy.flag ) return; for ( i=History.queues.length-1; i >= 0; --i ) { queue = History.queues[i]; if ( queue.length === 0 ) continue; item = queue.shift(); History.fireQueueItem(item); History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } }; History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } // Return return History.busy.flag; }; /** * History.busy.flag */ History.busy.flag = false; /** * History.fireQueueItem(item) * Fire a Queue Item * @param {Object} item * @return {Mixed} result */ History.fireQueueItem = function(item){ return item.callback.apply(item.scope||History,item.args||[]); }; /** * History.pushQueue(callback,args) * Add an item to the queue * @param {Object} item [scope,callback,args,queue] */ History.pushQueue = function(item){ // Prepare the queue History.queues[item.queue||0] = History.queues[item.queue||0]||[]; // Add to the queue History.queues[item.queue||0].push(item); // Chain return History; }; /** * History.queue (item,queue), (func,queue), (func), (item) * Either firs the item now if not busy, or adds it to the queue */ History.queue = function(item,queue){ // Prepare if ( typeof item === 'function' ) { item = { callback: item }; } if ( typeof queue !== 'undefined' ) { item.queue = queue; } // Handle if ( History.busy() ) { History.pushQueue(item); } else { History.fireQueueItem(item); } // Chain return History; }; /** * History.clearQueue() * Clears the Queue */ History.clearQueue = function(){ History.busy.flag = false; History.queues = []; return History; }; // ==================================================================== // IE Bug Fix /** * History.stateChanged * States whether or not the state has changed since the last double check was initialised */ History.stateChanged = false; /** * History.doubleChecker * Contains the timeout used for the double checks */ History.doubleChecker = false; /** * History.doubleCheckComplete() * Complete a double check * @return {History} */ History.doubleCheckComplete = function(){ // Update History.stateChanged = true; // Clear History.doubleCheckClear(); // Chain return History; }; /** * History.doubleCheckClear() * Clear a double check * @return {History} */ History.doubleCheckClear = function(){ // Clear if ( History.doubleChecker ) { clearTimeout(History.doubleChecker); History.doubleChecker = false; } // Chain return History; }; /** * History.doubleCheck() * Create a double check * @return {History} */ History.doubleCheck = function(tryAgain){ // Reset History.stateChanged = false; History.doubleCheckClear(); // Fix IE6,IE7 bug where calling history.back or history.forward does not actually change the hash (whereas doing it manually does) // Fix Safari 5 bug where sometimes the state does not change: https://bugs.webkit.org/show_bug.cgi?id=42940 if ( History.bugs.ieDoubleCheck ) { // Apply Check History.doubleChecker = setTimeout( function(){ History.doubleCheckClear(); if ( !History.stateChanged ) { //History.debug('History.doubleCheck: State has not yet changed, trying again', arguments); // Re-Attempt tryAgain(); } return true; }, History.options.doubleCheckInterval ); } // Chain return History; }; // ==================================================================== // Safari Bug Fix /** * History.safariStatePoll() * Poll the current state * @return {History} */ History.safariStatePoll = function(){ // Poll the URL // Get the Last State which has the new URL var urlState = History.extractState(History.getLocationHref()), newState; // Check for a difference if ( !History.isLastSavedState(urlState) ) { newState = urlState; } else { return; } // Check if we have a state with that url // If not create it if ( !newState ) { //History.debug('History.safariStatePoll: new'); newState = History.createStateObject(); } // Apply the New State //History.debug('History.safariStatePoll: trigger'); History.Adapter.trigger(window,'popstate'); // Chain return History; }; // ==================================================================== // State Aliases /** * History.back(queue) * Send the browser history back one item * @param {Integer} queue [optional] */ History.back = function(queue){ //History.debug('History.back: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.back: we must wait', arguments); History.pushQueue({ scope: History, callback: History.back, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.back(false); }); // Go back history.go(-1); // End back closure return true; }; /** * History.forward(queue) * Send the browser history forward one item * @param {Integer} queue [optional] */ History.forward = function(queue){ //History.debug('History.forward: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.forward: we must wait', arguments); History.pushQueue({ scope: History, callback: History.forward, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.forward(false); }); // Go forward history.go(1); // End forward closure return true; }; /** * History.go(index,queue) * Send the browser history back or forward index times * @param {Integer} queue [optional] */ History.go = function(index,queue){ //History.debug('History.go: called', arguments); // Prepare var i; // Handle if ( index > 0 ) { // Forward for ( i=1; i<=index; ++i ) { History.forward(queue); } } else if ( index < 0 ) { // Backward for ( i=-1; i>=index; --i ) { History.back(queue); } } else { throw new Error('History.go: History.go requires a positive or negative integer passed.'); } // Chain return History; }; // ==================================================================== // HTML5 State Support // Non-Native pushState Implementation if ( History.emulated.pushState ) { /* * Provide Skeleton for HTML4 Browsers */ // Prepare var emptyFunction = function(){}; History.pushState = History.pushState||emptyFunction; History.replaceState = History.replaceState||emptyFunction; } // History.emulated.pushState // Native pushState Implementation else { /* * Use native HTML5 History API Implementation */ /** * History.onPopState(event,extra) * Refresh the Current State */ History.onPopState = function(event,extra){ // Prepare var stateId = false, newState = false, currentHash, currentState; // Reset the double check History.doubleCheckComplete(); // Check for a Hash, and handle apporiatly currentHash = History.getHash(); if ( currentHash ) { // Expand Hash currentState = History.extractState(currentHash||History.getLocationHref(),true); if ( currentState ) { // We were able to parse it, it must be a State! // Let's forward to replaceState //History.debug('History.onPopState: state anchor', currentHash, currentState); History.replaceState(currentState.data, currentState.title, currentState.url, false); } else { // Traditional Anchor //History.debug('History.onPopState: traditional anchor', currentHash); History.Adapter.trigger(window,'anchorchange'); History.busy(false); } // We don't care for hashes History.expectedStateId = false; return false; } // Ensure stateId = History.Adapter.extractEventData('state',event,extra) || false; // Fetch State if ( stateId ) { // Vanilla: Back/forward button was used newState = History.getStateById(stateId); } else if ( History.expectedStateId ) { // Vanilla: A new state was pushed, and popstate was called manually newState = History.getStateById(History.expectedStateId); } else { // Initial State newState = History.extractState(History.getLocationHref()); } // The State did not exist in our store if ( !newState ) { // Regenerate the State newState = History.createStateObject(null,null,History.getLocationHref()); } // Clean History.expectedStateId = false; // Check if we are the same state if ( History.isLastSavedState(newState) ) { // There has been no change (just the page's hash has finally propagated) //History.debug('History.onPopState: no change', newState, History.savedStates); History.busy(false); return false; } // Store the State History.storeState(newState); History.saveState(newState); // Force update of the title History.setTitle(newState); // Fire Our Event History.Adapter.trigger(window,'statechange'); History.busy(false); // Return true return true; }; History.Adapter.bind(window,'popstate',History.onPopState); /** * History.pushState(data,title,url) * Add a new State to the history object, become it, and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.pushState = function(data,title,url,queue){ //History.debug('History.pushState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.pushState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.pushState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.pushState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End pushState closure return true; }; /** * History.replaceState(data,title,url) * Replace the State and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.replaceState = function(data,title,url,queue){ //History.debug('History.replaceState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.replaceState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.replaceState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.replaceState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End replaceState closure return true; }; } // !History.emulated.pushState // ==================================================================== // Initialise /** * Load the Store */ if ( sessionStorage ) { // Fetch try { History.store = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { History.store = {}; } // Normalize History.normalizeStore(); } else { // Default Load History.store = {}; History.normalizeStore(); } /** * Clear Intervals on exit to prevent memory leaks */ History.Adapter.bind(window,"unload",History.clearAllIntervals); /** * Create the initial State */ History.saveState(History.storeState(History.extractState(History.getLocationHref(),true))); /** * Bind for Saving Store */ if ( sessionStorage ) { // When the page is closed History.onUnload = function(){ // Prepare var currentStore, item, currentStoreString; // Fetch try { currentStore = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { currentStore = {}; } // Ensure currentStore.idToState = currentStore.idToState || {}; currentStore.urlToId = currentStore.urlToId || {}; currentStore.stateToId = currentStore.stateToId || {}; // Sync for ( item in History.idToState ) { if ( !History.idToState.hasOwnProperty(item) ) { continue; } currentStore.idToState[item] = History.idToState[item]; } for ( item in History.urlToId ) { if ( !History.urlToId.hasOwnProperty(item) ) { continue; } currentStore.urlToId[item] = History.urlToId[item]; } for ( item in History.stateToId ) { if ( !History.stateToId.hasOwnProperty(item) ) { continue; } currentStore.stateToId[item] = History.stateToId[item]; } // Update History.store = currentStore; History.normalizeStore(); // In Safari, going into Private Browsing mode causes the // Session Storage object to still exist but if you try and use // or set any property/function of it it throws the exception // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to // add something to storage that exceeded the quota." infinitely // every second. currentStoreString = JSON.stringify(currentStore); try { // Store sessionStorage.setItem('History.store', currentStoreString); } catch (e) { if (e.code === DOMException.QUOTA_EXCEEDED_ERR) { if (sessionStorage.length) { // Workaround for a bug seen on iPads. Sometimes the quota exceeded error comes up and simply // removing/resetting the storage can work. sessionStorage.removeItem('History.store'); sessionStorage.setItem('History.store', currentStoreString); } else { // Otherwise, we're probably private browsing in Safari, so we'll ignore the exception. } } else { throw e; } } }; // For Internet Explorer History.intervalList.push(setInterval(History.onUnload,History.options.storeInterval)); // For Other Browsers History.Adapter.bind(window,'beforeunload',History.onUnload); History.Adapter.bind(window,'unload',History.onUnload); // Both are enabled for consistency } // Non-Native pushState Implementation if ( !History.emulated.pushState ) { // Be aware, the following is only for native pushState implementations // If you are wanting to include something for all browsers // Then include it above this if block /** * Setup Safari Fix */ if ( History.bugs.safariPoll ) { History.intervalList.push(setInterval(History.safariStatePoll, History.options.safariPollInterval)); } /** * Ensure Cross Browser Compatibility */ if ( navigator.vendor === 'Apple Computer, Inc.' || (navigator.appCodeName||'') === 'Mozilla' ) { /** * Fix Safari HashChange Issue */ // Setup Alias History.Adapter.bind(window,'hashchange',function(){ History.Adapter.trigger(window,'popstate'); }); // Initialise Alias if ( History.getHash() ) { History.Adapter.onDomLoad(function(){ History.Adapter.trigger(window,'hashchange'); }); } } } // !History.emulated.pushState }; // History.initCore // Try to Initialise History if (!History.options || !History.options.delayInit) { History.init(); } })(window); ================================================ FILE: scripts/bundled-uncompressed/html4+html5/mootools.history.js ================================================ /* json2.js 2012-10-08 Public Domain. NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. See http://www.JSON.org/js.html This code should be minified before deployment. See http://javascript.crockford.com/jsmin.html USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO NOT CONTROL. This file creates a global JSON object containing two methods: stringify and parse. JSON.stringify(value, replacer, space) value any JavaScript value, usually an object or array. replacer an optional parameter that determines how object values are stringified for objects. It can be a function or an array of strings. space an optional parameter that specifies the indentation of nested structures. If it is omitted, the text will be packed without extra whitespace. If it is a number, it will specify the number of spaces to indent at each level. If it is a string (such as '\t' or ' '), it contains the characters used to indent at each level. This method produces a JSON text from a JavaScript value. When an object value is found, if the object contains a toJSON method, its toJSON method will be called and the result will be stringified. A toJSON method does not serialize: it returns the value represented by the name/value pair that should be serialized, or undefined if nothing should be serialized. The toJSON method will be passed the key associated with the value, and this will be bound to the value For example, this would serialize Dates as ISO strings. Date.prototype.toJSON = function (key) { function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } return this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z'; }; You can provide an optional replacer method. It will be passed the key and value of each member, with this bound to the containing object. The value that is returned from your method will be serialized. If your method returns undefined, then the member will be excluded from the serialization. If the replacer parameter is an array of strings, then it will be used to select the members to be serialized. It filters the results such that only members with keys listed in the replacer array are stringified. Values that do not have JSON representations, such as undefined or functions, will not be serialized. Such values in objects will be dropped; in arrays they will be replaced with null. You can use a replacer function to replace those with JSON values. JSON.stringify(undefined) returns undefined. The optional space parameter produces a stringification of the value that is filled with line breaks and indentation to make it easier to read. If the space parameter is a non-empty string, then that string will be used for indentation. If the space parameter is a number, then the indentation will be that many spaces. Example: text = JSON.stringify(['e', {pluribus: 'unum'}]); // text is '["e",{"pluribus":"unum"}]' text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' text = JSON.stringify([new Date()], function (key, value) { return this[key] instanceof Date ? 'Date(' + this[key] + ')' : value; }); // text is '["Date(---current time---)"]' JSON.parse(text, reviver) This method parses a JSON text to produce an object or array. It can throw a SyntaxError exception. The optional reviver parameter is a function that can filter and transform the results. It receives each of the keys and values, and its return value is used instead of the original value. If it returns what it received, then the structure is not modified. If it returns undefined then the member is deleted. Example: // Parse the text. Values that look like ISO date strings will // be converted to Date objects. myData = JSON.parse(text, function (key, value) { var a; if (typeof value === 'string') { a = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); if (a) { return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6])); } } return value; }); myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { var d; if (typeof value === 'string' && value.slice(0, 5) === 'Date(' && value.slice(-1) === ')') { d = new Date(value.slice(5, -1)); if (d) { return d; } } return value; }); This is a reference implementation. You are free to copy, modify, or redistribute. */ /*jslint evil: true, regexp: true */ /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, lastIndex, length, parse, prototype, push, replace, slice, stringify, test, toJSON, toString, valueOf */ // Create a JSON object only if one does not already exist. We create the // methods in a closure to avoid creating global variables. if (typeof JSON !== 'object') { JSON = {}; } (function () { 'use strict'; function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } if (typeof Date.prototype.toJSON !== 'function') { Date.prototype.toJSON = function (key) { return isFinite(this.valueOf()) ? this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z' : null; }; String.prototype.toJSON = Number.prototype.toJSON = Boolean.prototype.toJSON = function (key) { return this.valueOf(); }; } var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, gap, indent, meta = { // table of character substitutions '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\' }, rep; function quote(string) { // If the string contains no control characters, no quote characters, and no // backslash characters, then we can safely slap some quotes around it. // Otherwise we must also replace the offending characters with safe escape // sequences. escapable.lastIndex = 0; return escapable.test(string) ? '"' + string.replace(escapable, function (a) { var c = meta[a]; return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }) + '"' : '"' + string + '"'; } function str(key, holder) { // Produce a string from holder[key]. var i, // The loop counter. k, // The member key. v, // The member value. length, mind = gap, partial, value = holder[key]; // If the value has a toJSON method, call it to obtain a replacement value. if (value && typeof value === 'object' && typeof value.toJSON === 'function') { value = value.toJSON(key); } // If we were called with a replacer function, then call the replacer to // obtain a replacement value. if (typeof rep === 'function') { value = rep.call(holder, key, value); } // What happens next depends on the value's type. switch (typeof value) { case 'string': return quote(value); case 'number': // JSON numbers must be finite. Encode non-finite numbers as null. return isFinite(value) ? String(value) : 'null'; case 'boolean': case 'null': // If the value is a boolean or null, convert it to a string. Note: // typeof null does not produce 'null'. The case is included here in // the remote chance that this gets fixed someday. return String(value); // If the type is 'object', we might be dealing with an object or an array or // null. case 'object': // Due to a specification blunder in ECMAScript, typeof null is 'object', // so watch out for that case. if (!value) { return 'null'; } // Make an array to hold the partial results of stringifying this object value. gap += indent; partial = []; // Is the value an array? if (Object.prototype.toString.apply(value) === '[object Array]') { // The value is an array. Stringify every element. Use null as a placeholder // for non-JSON values. length = value.length; for (i = 0; i < length; i += 1) { partial[i] = str(i, value) || 'null'; } // Join all of the elements together, separated with commas, and wrap them in // brackets. v = partial.length === 0 ? '[]' : gap ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : '[' + partial.join(',') + ']'; gap = mind; return v; } // If the replacer is an array, use it to select the members to be stringified. if (rep && typeof rep === 'object') { length = rep.length; for (i = 0; i < length; i += 1) { if (typeof rep[i] === 'string') { k = rep[i]; v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } else { // Otherwise, iterate through all of the keys in the object. for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } // Join all of the member texts together, separated with commas, // and wrap them in braces. v = partial.length === 0 ? '{}' : gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : '{' + partial.join(',') + '}'; gap = mind; return v; } } // If the JSON object does not yet have a stringify method, give it one. if (typeof JSON.stringify !== 'function') { JSON.stringify = function (value, replacer, space) { // The stringify method takes a value and an optional replacer, and an optional // space parameter, and returns a JSON text. The replacer can be a function // that can replace values, or an array of strings that will select the keys. // A default replacer method can be provided. Use of the space parameter can // produce text that is more easily readable. var i; gap = ''; indent = ''; // If the space parameter is a number, make an indent string containing that // many spaces. if (typeof space === 'number') { for (i = 0; i < space; i += 1) { indent += ' '; } // If the space parameter is a string, it will be used as the indent string. } else if (typeof space === 'string') { indent = space; } // If there is a replacer, it must be a function or an array. // Otherwise, throw an error. rep = replacer; if (replacer && typeof replacer !== 'function' && (typeof replacer !== 'object' || typeof replacer.length !== 'number')) { throw new Error('JSON.stringify'); } // Make a fake root object containing our value under the key of ''. // Return the result of stringifying the value. return str('', {'': value}); }; } // If the JSON object does not yet have a parse method, give it one. if (typeof JSON.parse !== 'function') { JSON.parse = function (text, reviver) { // The parse method takes a text and an optional reviver function, and returns // a JavaScript value if the text is a valid JSON text. var j; function walk(holder, key) { // The walk method is used to recursively walk the resulting structure so // that modifications can be made. var k, v, value = holder[key]; if (value && typeof value === 'object') { for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = walk(value, k); if (v !== undefined) { value[k] = v; } else { delete value[k]; } } } } return reviver.call(holder, key, value); } // Parsing happens in four stages. In the first stage, we replace certain // Unicode characters with escape sequences. JavaScript handles many characters // incorrectly, either silently deleting them, or treating them as line endings. text = String(text); cx.lastIndex = 0; if (cx.test(text)) { text = text.replace(cx, function (a) { return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }); } // In the second stage, we run the text against regular expressions that look // for non-JSON patterns. We are especially concerned with '()' and 'new' // because they can cause invocation, and '=' because it can cause mutation. // But just to be safe, we want to reject all unexpected forms. // We split the second stage into 4 regexp operations in order to work around // crippling inefficiencies in IE's and Safari's regexp engines. First we // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we // replace all simple value tokens with ']' characters. Third, we delete all // open brackets that follow a colon or comma or that begin the text. Finally, // we look to see that the remaining characters are only whitespace or ']' or // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. if (/^[\],:{}\s]*$/ .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { // In the third stage we use the eval function to compile the text into a // JavaScript structure. The '{' operator is subject to a syntactic ambiguity // in JavaScript: it can begin a block or an object literal. We wrap the text // in parens to eliminate the ambiguity. j = eval('(' + text + ')'); // In the optional fourth stage, we recursively walk the new structure, passing // each name/value pair to a reviver function for possible transformation. return typeof reviver === 'function' ? walk({'': j}, '') : j; } // If the text is not JSON parseable, then a SyntaxError is thrown. throw new SyntaxError('JSON.parse'); }; } }());/** * History.js MooTools Adapter * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ // Closure (function(window,undefined){ "use strict"; // Localise Globals var History = window.History = window.History||{}, MooTools = window.MooTools, Element = window.Element; // Check Existence if ( typeof History.Adapter !== 'undefined' ) { throw new Error('History.js Adapter has already been loaded...'); } // Make MooTools aware of History.js Events Object.append(Element.NativeEvents,{ 'popstate':2, 'hashchange':2 }); // Add the Adapter History.Adapter = { /** * History.Adapter.bind(el,event,callback) * @param {Element|string} el * @param {string} event - custom and standard events * @param {function} callback * @return {void} */ bind: function(el,event,callback){ var El = typeof el === 'string' ? document.id(el) : el; El.addEvent(event,callback); }, /** * History.Adapter.trigger(el,event) * @param {Element|string} el * @param {string} event - custom and standard events * @param {Object=} extra - a object of extra event data (optional) * @return void */ trigger: function(el,event,extra){ var El = typeof el === 'string' ? document.id(el) : el; El.fireEvent(event,extra); }, /** * History.Adapter.extractEventData(key,event,extra) * @param {string} key - key for the event data to extract * @param {string} event - custom and standard events * @return {mixed} */ extractEventData: function(key,event){ // MooTools Native then MooTools Custom var result = (event && event.event && event.event[key]) || (event && event[key]) || undefined; // Return return result; }, /** * History.Adapter.onDomLoad(callback) * @param {function} callback * @return {void} */ onDomLoad: function(callback) { window.addEvent('domready',callback); } }; // Try and Initialise History if ( typeof History.init !== 'undefined' ) { History.init(); } })(window); /** * History.js HTML4 Support * Depends on the HTML5 Support * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window,undefined){ "use strict"; // ======================================================================== // Initialise // Localise Globals var document = window.document, // Make sure we are using the correct document setTimeout = window.setTimeout||setTimeout, clearTimeout = window.clearTimeout||clearTimeout, setInterval = window.setInterval||setInterval, History = window.History = window.History||{}; // Public History Object // Check Existence if ( typeof History.initHtml4 !== 'undefined' ) { throw new Error('History.js HTML4 Support has already been loaded...'); } // ======================================================================== // Initialise HTML4 Support // Initialise HTML4 Support History.initHtml4 = function(){ // Initialise if ( typeof History.initHtml4.initialized !== 'undefined' ) { // Already Loaded return false; } else { History.initHtml4.initialized = true; } // ==================================================================== // Properties /** * History.enabled * Is History enabled? */ History.enabled = true; // ==================================================================== // Hash Storage /** * History.savedHashes * Store the hashes in an array */ History.savedHashes = []; /** * History.isLastHash(newHash) * Checks if the hash is the last hash * @param {string} newHash * @return {boolean} true */ History.isLastHash = function(newHash){ // Prepare var oldHash = History.getHashByIndex(), isLast; // Check isLast = newHash === oldHash; // Return isLast return isLast; }; /** * History.isHashEqual(newHash, oldHash) * Checks to see if two hashes are functionally equal * @param {string} newHash * @param {string} oldHash * @return {boolean} true */ History.isHashEqual = function(newHash, oldHash){ newHash = encodeURIComponent(newHash).replace(/%25/g, "%"); oldHash = encodeURIComponent(oldHash).replace(/%25/g, "%"); return newHash === oldHash; }; /** * History.saveHash(newHash) * Push a Hash * @param {string} newHash * @return {boolean} true */ History.saveHash = function(newHash){ // Check Hash if ( History.isLastHash(newHash) ) { return false; } // Push the Hash History.savedHashes.push(newHash); // Return true return true; }; /** * History.getHashByIndex() * Gets a hash by the index * @param {integer} index * @return {string} */ History.getHashByIndex = function(index){ // Prepare var hash = null; // Handle if ( typeof index === 'undefined' ) { // Get the last inserted hash = History.savedHashes[History.savedHashes.length-1]; } else if ( index < 0 ) { // Get from the end hash = History.savedHashes[History.savedHashes.length+index]; } else { // Get from the beginning hash = History.savedHashes[index]; } // Return hash return hash; }; // ==================================================================== // Discarded States /** * History.discardedHashes * A hashed array of discarded hashes */ History.discardedHashes = {}; /** * History.discardedStates * A hashed array of discarded states */ History.discardedStates = {}; /** * History.discardState(State) * Discards the state by ignoring it through History * @param {object} State * @return {true} */ History.discardState = function(discardedState,forwardState,backState){ //History.debug('History.discardState', arguments); // Prepare var discardedStateHash = History.getHashByState(discardedState), discardObject; // Create Discard Object discardObject = { 'discardedState': discardedState, 'backState': backState, 'forwardState': forwardState }; // Add to DiscardedStates History.discardedStates[discardedStateHash] = discardObject; // Return true return true; }; /** * History.discardHash(hash) * Discards the hash by ignoring it through History * @param {string} hash * @return {true} */ History.discardHash = function(discardedHash,forwardState,backState){ //History.debug('History.discardState', arguments); // Create Discard Object var discardObject = { 'discardedHash': discardedHash, 'backState': backState, 'forwardState': forwardState }; // Add to discardedHash History.discardedHashes[discardedHash] = discardObject; // Return true return true; }; /** * History.discardedState(State) * Checks to see if the state is discarded * @param {object} State * @return {bool} */ History.discardedState = function(State){ // Prepare var StateHash = History.getHashByState(State), discarded; // Check discarded = History.discardedStates[StateHash]||false; // Return true return discarded; }; /** * History.discardedHash(hash) * Checks to see if the state is discarded * @param {string} State * @return {bool} */ History.discardedHash = function(hash){ // Check var discarded = History.discardedHashes[hash]||false; // Return true return discarded; }; /** * History.recycleState(State) * Allows a discarded state to be used again * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.recycleState = function(State){ //History.debug('History.recycleState', arguments); // Prepare var StateHash = History.getHashByState(State); // Remove from DiscardedStates if ( History.discardedState(State) ) { delete History.discardedStates[StateHash]; } // Return true return true; }; // ==================================================================== // HTML4 HashChange Support if ( History.emulated.hashChange ) { /* * We must emulate the HTML4 HashChange Support by manually checking for hash changes */ /** * History.hashChangeInit() * Init the HashChange Emulation */ History.hashChangeInit = function(){ // Define our Checker Function History.checkerFunction = null; // Define some variables that will help in our checker function var lastDocumentHash = '', iframeId, iframe, lastIframeHash, checkerRunning, startedWithHash = Boolean(History.getHash()); // Handle depending on the browser if ( History.isInternetExplorer() ) { // IE6 and IE7 // We need to use an iframe to emulate the back and forward buttons // Create iFrame iframeId = 'historyjs-iframe'; iframe = document.createElement('iframe'); // Adjust iFarme // IE 6 requires iframe to have a src on HTTPS pages, otherwise it will throw a // "This page contains both secure and nonsecure items" warning. iframe.setAttribute('id', iframeId); iframe.setAttribute('src', '#'); iframe.style.display = 'none'; // Append iFrame document.body.appendChild(iframe); // Create initial history entry iframe.contentWindow.document.open(); iframe.contentWindow.document.close(); // Define some variables that will help in our checker function lastIframeHash = ''; checkerRunning = false; // Define the checker function History.checkerFunction = function(){ // Check Running if ( checkerRunning ) { return false; } // Update Running checkerRunning = true; // Fetch var documentHash = History.getHash(), iframeHash = History.getHash(iframe.contentWindow.document); // The Document Hash has changed (application caused) if ( documentHash !== lastDocumentHash ) { // Equalise lastDocumentHash = documentHash; // Create a history entry in the iframe if ( iframeHash !== documentHash ) { //History.debug('hashchange.checker: iframe hash change', 'documentHash (new):', documentHash, 'iframeHash (old):', iframeHash); // Equalise lastIframeHash = iframeHash = documentHash; // Create History Entry iframe.contentWindow.document.open(); iframe.contentWindow.document.close(); // Update the iframe's hash iframe.contentWindow.document.location.hash = History.escapeHash(documentHash); } // Trigger Hashchange Event History.Adapter.trigger(window,'hashchange'); } // The iFrame Hash has changed (back button caused) else if ( iframeHash !== lastIframeHash ) { //History.debug('hashchange.checker: iframe hash out of sync', 'iframeHash (new):', iframeHash, 'documentHash (old):', documentHash); // Equalise lastIframeHash = iframeHash; // If there is no iframe hash that means we're at the original // iframe state. // And if there was a hash on the original request, the original // iframe state was replaced instantly, so skip this state and take // the user back to where they came from. if (startedWithHash && iframeHash === '') { History.back(); } else { // Update the Hash History.setHash(iframeHash,false); } } // Reset Running checkerRunning = false; // Return true return true; }; } else { // We are not IE // Firefox 1 or 2, Opera // Define the checker function History.checkerFunction = function(){ // Prepare var documentHash = History.getHash()||''; // The Document Hash has changed (application caused) if ( documentHash !== lastDocumentHash ) { // Equalise lastDocumentHash = documentHash; // Trigger Hashchange Event History.Adapter.trigger(window,'hashchange'); } // Return true return true; }; } // Apply the checker function History.intervalList.push(setInterval(History.checkerFunction, History.options.hashChangeInterval)); // Done return true; }; // History.hashChangeInit // Bind hashChangeInit History.Adapter.onDomLoad(History.hashChangeInit); } // History.emulated.hashChange // ==================================================================== // HTML5 State Support // Non-Native pushState Implementation if ( History.emulated.pushState ) { /* * We must emulate the HTML5 State Management by using HTML4 HashChange */ /** * History.onHashChange(event) * Trigger HTML5's window.onpopstate via HTML4 HashChange Support */ History.onHashChange = function(event){ //History.debug('History.onHashChange', arguments); // Prepare var currentUrl = ((event && event.newURL) || History.getLocationHref()), currentHash = History.getHashByUrl(currentUrl), currentState = null, currentStateHash = null, currentStateHashExits = null, discardObject; // Check if we are the same state if ( History.isLastHash(currentHash) ) { // There has been no change (just the page's hash has finally propagated) //History.debug('History.onHashChange: no change'); History.busy(false); return false; } // Reset the double check History.doubleCheckComplete(); // Store our location for use in detecting back/forward direction History.saveHash(currentHash); // Expand Hash if ( currentHash && History.isTraditionalAnchor(currentHash) ) { //History.debug('History.onHashChange: traditional anchor', currentHash); // Traditional Anchor Hash History.Adapter.trigger(window,'anchorchange'); History.busy(false); return false; } // Create State currentState = History.extractState(History.getFullUrl(currentHash||History.getLocationHref()),true); // Check if we are the same state if ( History.isLastSavedState(currentState) ) { //History.debug('History.onHashChange: no change'); // There has been no change (just the page's hash has finally propagated) History.busy(false); return false; } // Create the state Hash currentStateHash = History.getHashByState(currentState); // Check if we are DiscardedState discardObject = History.discardedState(currentState); if ( discardObject ) { // Ignore this state as it has been discarded and go back to the state before it if ( History.getHashByIndex(-2) === History.getHashByState(discardObject.forwardState) ) { // We are going backwards //History.debug('History.onHashChange: go backwards'); History.back(false); } else { // We are going forwards //History.debug('History.onHashChange: go forwards'); History.forward(false); } return false; } // Push the new HTML5 State //History.debug('History.onHashChange: success hashchange'); History.pushState(currentState.data,currentState.title,encodeURI(currentState.url),false); // End onHashChange closure return true; }; History.Adapter.bind(window,'hashchange',History.onHashChange); /** * History.pushState(data,title,url) * Add a new State to the history object, become it, and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.pushState = function(data,title,url,queue){ //History.debug('History.pushState: called', arguments); // We assume that the URL passed in is URI-encoded, but this makes // sure that it's fully URI encoded; any '%'s that are encoded are // converted back into '%'s url = encodeURI(url).replace(/%25/g, "%"); // Check the State if ( History.getHashByUrl(url) ) { throw new Error('History.js does not support states with fragment-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.pushState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.pushState, args: arguments, queue: queue }); return false; } // Make Busy History.busy(true); // Fetch the State Object var newState = History.createStateObject(data,title,url), newStateHash = History.getHashByState(newState), oldState = History.getState(false), oldStateHash = History.getHashByState(oldState), html4Hash = History.getHash(), wasExpected = History.expectedStateId == newState.id; // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Recycle the State History.recycleState(newState); // Force update of the title History.setTitle(newState); // Check if we are the same State if ( newStateHash === oldStateHash ) { //History.debug('History.pushState: no change', newStateHash); History.busy(false); return false; } // Update HTML5 State History.saveState(newState); // Fire HTML5 Event if(!wasExpected) History.Adapter.trigger(window,'statechange'); // Update HTML4 Hash if ( !History.isHashEqual(newStateHash, html4Hash) && !History.isHashEqual(newStateHash, History.getShortUrl(History.getLocationHref())) ) { History.setHash(newStateHash,false); } History.busy(false); // End pushState closure return true; }; /** * History.replaceState(data,title,url) * Replace the State and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.replaceState = function(data,title,url,queue){ //History.debug('History.replaceState: called', arguments); // We assume that the URL passed in is URI-encoded, but this makes // sure that it's fully URI encoded; any '%'s that are encoded are // converted back into '%'s url = encodeURI(url).replace(/%25/g, "%"); // Check the State if ( History.getHashByUrl(url) ) { throw new Error('History.js does not support states with fragment-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.replaceState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.replaceState, args: arguments, queue: queue }); return false; } // Make Busy History.busy(true); // Fetch the State Objects var newState = History.createStateObject(data,title,url), newStateHash = History.getHashByState(newState), oldState = History.getState(false), oldStateHash = History.getHashByState(oldState), previousState = History.getStateByIndex(-2); // Discard Old State History.discardState(oldState,newState,previousState); // If the url hasn't changed, just store and save the state // and fire a statechange event to be consistent with the // html 5 api if ( newStateHash === oldStateHash ) { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Recycle the State History.recycleState(newState); // Force update of the title History.setTitle(newState); // Update HTML5 State History.saveState(newState); // Fire HTML5 Event //History.debug('History.pushState: trigger popstate'); History.Adapter.trigger(window,'statechange'); History.busy(false); } else { // Alias to PushState History.pushState(newState.data,newState.title,newState.url,false); } // End replaceState closure return true; }; } // History.emulated.pushState // ==================================================================== // Initialise // Non-Native pushState Implementation if ( History.emulated.pushState ) { /** * Ensure initial state is handled correctly */ if ( History.getHash() && !History.emulated.hashChange ) { History.Adapter.onDomLoad(function(){ History.Adapter.trigger(window,'hashchange'); }); } } // History.emulated.pushState }; // History.initHtml4 // Try to Initialise History if ( typeof History.init !== 'undefined' ) { History.init(); } })(window); /** * History.js Core * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window,undefined){ "use strict"; // ======================================================================== // Initialise // Localise Globals var console = window.console||undefined, // Prevent a JSLint complain document = window.document, // Make sure we are using the correct document navigator = window.navigator, // Make sure we are using the correct navigator sessionStorage = false, // sessionStorage setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, setInterval = window.setInterval, clearInterval = window.clearInterval, JSON = window.JSON, alert = window.alert, History = window.History = window.History||{}, // Public History Object history = window.history; // Old History Object try { sessionStorage = window.sessionStorage; // This will throw an exception in some browsers when cookies/localStorage are explicitly disabled (i.e. Chrome) sessionStorage.setItem('TEST', '1'); sessionStorage.removeItem('TEST'); } catch(e) { sessionStorage = false; } // MooTools Compatibility JSON.stringify = JSON.stringify||JSON.encode; JSON.parse = JSON.parse||JSON.decode; // Check Existence if ( typeof History.init !== 'undefined' ) { throw new Error('History.js Core has already been loaded...'); } // Initialise History History.init = function(options){ // Check Load Status of Adapter if ( typeof History.Adapter === 'undefined' ) { return false; } // Check Load Status of Core if ( typeof History.initCore !== 'undefined' ) { History.initCore(); } // Check Load Status of HTML4 Support if ( typeof History.initHtml4 !== 'undefined' ) { History.initHtml4(); } // Return true return true; }; // ======================================================================== // Initialise Core // Initialise Core History.initCore = function(options){ // Initialise if ( typeof History.initCore.initialized !== 'undefined' ) { // Already Loaded return false; } else { History.initCore.initialized = true; } // ==================================================================== // Options /** * History.options * Configurable options */ History.options = History.options||{}; /** * History.options.hashChangeInterval * How long should the interval be before hashchange checks */ History.options.hashChangeInterval = History.options.hashChangeInterval || 100; /** * History.options.safariPollInterval * How long should the interval be before safari poll checks */ History.options.safariPollInterval = History.options.safariPollInterval || 500; /** * History.options.doubleCheckInterval * How long should the interval be before we perform a double check */ History.options.doubleCheckInterval = History.options.doubleCheckInterval || 500; /** * History.options.disableSuid * Force History not to append suid */ History.options.disableSuid = History.options.disableSuid || false; /** * History.options.storeInterval * How long should we wait between store calls */ History.options.storeInterval = History.options.storeInterval || 1000; /** * History.options.busyDelay * How long should we wait between busy events */ History.options.busyDelay = History.options.busyDelay || 250; /** * History.options.debug * If true will enable debug messages to be logged */ History.options.debug = History.options.debug || false; /** * History.options.initialTitle * What is the title of the initial state */ History.options.initialTitle = History.options.initialTitle || document.title; /** * History.options.html4Mode * If true, will force HTMl4 mode (hashtags) */ History.options.html4Mode = History.options.html4Mode || false; /** * History.options.delayInit * Want to override default options and call init manually. */ History.options.delayInit = History.options.delayInit || false; // ==================================================================== // Interval record /** * History.intervalList * List of intervals set, to be cleared when document is unloaded. */ History.intervalList = []; /** * History.clearAllIntervals * Clears all setInterval instances. */ History.clearAllIntervals = function(){ var i, il = History.intervalList; if (typeof il !== "undefined" && il !== null) { for (i = 0; i < il.length; i++) { clearInterval(il[i]); } History.intervalList = null; } }; // ==================================================================== // Debug /** * History.debug(message,...) * Logs the passed arguments if debug enabled */ History.debug = function(){ if ( (History.options.debug||false) ) { History.log.apply(History,arguments); } }; /** * History.log(message,...) * Logs the passed arguments */ History.log = function(){ // Prepare var consoleExists = !(typeof console === 'undefined' || typeof console.log === 'undefined' || typeof console.log.apply === 'undefined'), textarea = document.getElementById('log'), message, i,n, args,arg ; // Write to Console if ( consoleExists ) { args = Array.prototype.slice.call(arguments); message = args.shift(); if ( typeof console.debug !== 'undefined' ) { console.debug.apply(console,[message,args]); } else { console.log.apply(console,[message,args]); } } else { message = ("\n"+arguments[0]+"\n"); } // Write to log for ( i=1,n=arguments.length; i * @author James Padolsey */ History.getInternetExplorerMajorVersion = function(){ var result = History.getInternetExplorerMajorVersion.cached = (typeof History.getInternetExplorerMajorVersion.cached !== 'undefined') ? History.getInternetExplorerMajorVersion.cached : (function(){ var v = 3, div = document.createElement('div'), all = div.getElementsByTagName('i'); while ( (div.innerHTML = '') && all[0] ) {} return (v > 4) ? v : false; })() ; return result; }; /** * History.isInternetExplorer() * Are we using Internet Explorer? * @return {boolean} * @license Public Domain * @author Benjamin Arthur Lupton */ History.isInternetExplorer = function(){ var result = History.isInternetExplorer.cached = (typeof History.isInternetExplorer.cached !== 'undefined') ? History.isInternetExplorer.cached : Boolean(History.getInternetExplorerMajorVersion()) ; return result; }; /** * History.emulated * Which features require emulating? */ if (History.options.html4Mode) { History.emulated = { pushState : true, hashChange: true }; } else { History.emulated = { pushState: !Boolean( window.history && window.history.pushState && window.history.replaceState && !( (/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i).test(navigator.userAgent) /* disable for versions of iOS before version 4.3 (8F190) */ || (/AppleWebKit\/5([0-2]|3[0-2])/i).test(navigator.userAgent) /* disable for the mercury iOS browser, or at least older versions of the webkit engine */ ) ), hashChange: Boolean( !(('onhashchange' in window) || ('onhashchange' in document)) || (History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8) ) }; } /** * History.enabled * Is History enabled? */ History.enabled = !History.emulated.pushState; /** * History.bugs * Which bugs are present */ History.bugs = { /** * Safari 5 and Safari iOS 4 fail to return to the correct state once a hash is replaced by a `replaceState` call * https://bugs.webkit.org/show_bug.cgi?id=56249 */ setHash: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * Safari 5 and Safari iOS 4 sometimes fail to apply the state change under busy conditions * https://bugs.webkit.org/show_bug.cgi?id=42940 */ safariPoll: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * MSIE 6 and 7 sometimes do not apply a hash even it was told to (requiring a second call to the apply function) */ ieDoubleCheck: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8), /** * MSIE 6 requires the entire hash to be encoded for the hashes to trigger the onHashChange event */ hashEscape: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 7) }; /** * History.isEmptyObject(obj) * Checks to see if the Object is Empty * @param {Object} obj * @return {boolean} */ History.isEmptyObject = function(obj) { for ( var name in obj ) { if ( obj.hasOwnProperty(name) ) { return false; } } return true; }; /** * History.cloneObject(obj) * Clones a object and eliminate all references to the original contexts * @param {Object} obj * @return {Object} */ History.cloneObject = function(obj) { var hash,newObj; if ( obj ) { hash = JSON.stringify(obj); newObj = JSON.parse(hash); } else { newObj = {}; } return newObj; }; // ==================================================================== // URL Helpers /** * History.getRootUrl() * Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com" * @return {String} rootUrl */ History.getRootUrl = function(){ // Create var rootUrl = document.location.protocol+'//'+(document.location.hostname||document.location.host); if ( document.location.port||false ) { rootUrl += ':'+document.location.port; } rootUrl += '/'; // Return return rootUrl; }; /** * History.getBaseHref() * Fetches the `href` attribute of the `` element if it exists * @return {String} baseHref */ History.getBaseHref = function(){ // Create var baseElements = document.getElementsByTagName('base'), baseElement = null, baseHref = ''; // Test for Base Element if ( baseElements.length === 1 ) { // Prepare for Base Element baseElement = baseElements[0]; baseHref = baseElement.href.replace(/[^\/]+$/,''); } // Adjust trailing slash baseHref = baseHref.replace(/\/+$/,''); if ( baseHref ) baseHref += '/'; // Return return baseHref; }; /** * History.getBaseUrl() * Fetches the baseHref or basePageUrl or rootUrl (whichever one exists first) * @return {String} baseUrl */ History.getBaseUrl = function(){ // Create var baseUrl = History.getBaseHref()||History.getBasePageUrl()||History.getRootUrl(); // Return return baseUrl; }; /** * History.getPageUrl() * Fetches the URL of the current page * @return {String} pageUrl */ History.getPageUrl = function(){ // Fetch var State = History.getState(false,false), stateUrl = (State||{}).url||History.getLocationHref(), pageUrl; // Create pageUrl = stateUrl.replace(/\/+$/,'').replace(/[^\/]+$/,function(part,index,string){ return (/\./).test(part) ? part : part+'/'; }); // Return return pageUrl; }; /** * History.getBasePageUrl() * Fetches the Url of the directory of the current page * @return {String} basePageUrl */ History.getBasePageUrl = function(){ // Create var basePageUrl = (History.getLocationHref()).replace(/[#\?].*/,'').replace(/[^\/]+$/,function(part,index,string){ return (/[^\/]$/).test(part) ? '' : part; }).replace(/\/+$/,'')+'/'; // Return return basePageUrl; }; /** * History.getFullUrl(url) * Ensures that we have an absolute URL and not a relative URL * @param {string} url * @param {Boolean} allowBaseHref * @return {string} fullUrl */ History.getFullUrl = function(url,allowBaseHref){ // Prepare var fullUrl = url, firstChar = url.substring(0,1); allowBaseHref = (typeof allowBaseHref === 'undefined') ? true : allowBaseHref; // Check if ( /[a-z]+\:\/\//.test(url) ) { // Full URL } else if ( firstChar === '/' ) { // Root URL fullUrl = History.getRootUrl()+url.replace(/^\/+/,''); } else if ( firstChar === '#' ) { // Anchor URL fullUrl = History.getPageUrl().replace(/#.*/,'')+url; } else if ( firstChar === '?' ) { // Query URL fullUrl = History.getPageUrl().replace(/[\?#].*/,'')+url; } else { // Relative URL if ( allowBaseHref ) { fullUrl = History.getBaseUrl()+url.replace(/^(\.\/)+/,''); } else { fullUrl = History.getBasePageUrl()+url.replace(/^(\.\/)+/,''); } // We have an if condition above as we do not want hashes // which are relative to the baseHref in our URLs // as if the baseHref changes, then all our bookmarks // would now point to different locations // whereas the basePageUrl will always stay the same } // Return return fullUrl.replace(/\#$/,''); }; /** * History.getShortUrl(url) * Ensures that we have a relative URL and not a absolute URL * @param {string} url * @return {string} url */ History.getShortUrl = function(url){ // Prepare var shortUrl = url, baseUrl = History.getBaseUrl(), rootUrl = History.getRootUrl(); // Trim baseUrl if ( History.emulated.pushState ) { // We are in a if statement as when pushState is not emulated // The actual url these short urls are relative to can change // So within the same session, we the url may end up somewhere different shortUrl = shortUrl.replace(baseUrl,''); } // Trim rootUrl shortUrl = shortUrl.replace(rootUrl,'/'); // Ensure we can still detect it as a state if ( History.isTraditionalAnchor(shortUrl) ) { shortUrl = './'+shortUrl; } // Clean It shortUrl = shortUrl.replace(/^(\.\/)+/g,'./').replace(/\#$/,''); // Return return shortUrl; }; /** * History.getLocationHref(document) * Returns a normalized version of document.location.href * accounting for browser inconsistencies, etc. * * This URL will be URI-encoded and will include the hash * * @param {object} document * @return {string} url */ History.getLocationHref = function(doc) { doc = doc || document; // most of the time, this will be true if (doc.URL === doc.location.href) return doc.location.href; // some versions of webkit URI-decode document.location.href // but they leave document.URL in an encoded state if (doc.location.href === decodeURIComponent(doc.URL)) return doc.URL; // FF 3.6 only updates document.URL when a page is reloaded // document.location.href is updated correctly if (doc.location.hash && decodeURIComponent(doc.location.href.replace(/^[^#]+/, "")) === doc.location.hash) return doc.location.href; if (doc.URL.indexOf('#') == -1 && doc.location.href.indexOf('#') != -1) return doc.location.href; return doc.URL || doc.location.href; }; // ==================================================================== // State Storage /** * History.store * The store for all session specific data */ History.store = {}; /** * History.idToState * 1-1: State ID to State Object */ History.idToState = History.idToState||{}; /** * History.stateToId * 1-1: State String to State ID */ History.stateToId = History.stateToId||{}; /** * History.urlToId * 1-1: State URL to State ID */ History.urlToId = History.urlToId||{}; /** * History.storedStates * Store the states in an array */ History.storedStates = History.storedStates||[]; /** * History.savedStates * Saved the states in an array */ History.savedStates = History.savedStates||[]; /** * History.noramlizeStore() * Noramlize the store by adding necessary values */ History.normalizeStore = function(){ History.store.idToState = History.store.idToState||{}; History.store.urlToId = History.store.urlToId||{}; History.store.stateToId = History.store.stateToId||{}; }; /** * History.getState() * Get an object containing the data, title and url of the current state * @param {Boolean} friendly * @param {Boolean} create * @return {Object} State */ History.getState = function(friendly,create){ // Prepare if ( typeof friendly === 'undefined' ) { friendly = true; } if ( typeof create === 'undefined' ) { create = true; } // Fetch var State = History.getLastSavedState(); // Create if ( !State && create ) { State = History.createStateObject(); } // Adjust if ( friendly ) { State = History.cloneObject(State); State.url = State.cleanUrl||State.url; } // Return return State; }; /** * History.getIdByState(State) * Gets a ID for a State * @param {State} newState * @return {String} id */ History.getIdByState = function(newState){ // Fetch ID var id = History.extractId(newState.url), str; if ( !id ) { // Find ID via State String str = History.getStateString(newState); if ( typeof History.stateToId[str] !== 'undefined' ) { id = History.stateToId[str]; } else if ( typeof History.store.stateToId[str] !== 'undefined' ) { id = History.store.stateToId[str]; } else { // Generate a new ID while ( true ) { id = (new Date()).getTime() + String(Math.random()).replace(/\D/g,''); if ( typeof History.idToState[id] === 'undefined' && typeof History.store.idToState[id] === 'undefined' ) { break; } } // Apply the new State to the ID History.stateToId[str] = id; History.idToState[id] = newState; } } // Return ID return id; }; /** * History.normalizeState(State) * Expands a State Object * @param {object} State * @return {object} */ History.normalizeState = function(oldState){ // Variables var newState, dataNotEmpty; // Prepare if ( !oldState || (typeof oldState !== 'object') ) { oldState = {}; } // Check if ( typeof oldState.normalized !== 'undefined' ) { return oldState; } // Adjust if ( !oldState.data || (typeof oldState.data !== 'object') ) { oldState.data = {}; } // ---------------------------------------------------------------- // Create newState = {}; newState.normalized = true; newState.title = oldState.title||''; newState.url = History.getFullUrl(oldState.url?oldState.url:(History.getLocationHref())); newState.hash = History.getShortUrl(newState.url); newState.data = History.cloneObject(oldState.data); // Fetch ID newState.id = History.getIdByState(newState); // ---------------------------------------------------------------- // Clean the URL newState.cleanUrl = newState.url.replace(/\??\&_suid.*/,''); newState.url = newState.cleanUrl; // Check to see if we have more than just a url dataNotEmpty = !History.isEmptyObject(newState.data); // Apply if ( (newState.title || dataNotEmpty) && History.options.disableSuid !== true ) { // Add ID to Hash newState.hash = History.getShortUrl(newState.url).replace(/\??\&_suid.*/,''); if ( !/\?/.test(newState.hash) ) { newState.hash += '?'; } newState.hash += '&_suid='+newState.id; } // Create the Hashed URL newState.hashedUrl = History.getFullUrl(newState.hash); // ---------------------------------------------------------------- // Update the URL if we have a duplicate if ( (History.emulated.pushState || History.bugs.safariPoll) && History.hasUrlDuplicate(newState) ) { newState.url = newState.hashedUrl; } // ---------------------------------------------------------------- // Return return newState; }; /** * History.createStateObject(data,title,url) * Creates a object based on the data, title and url state params * @param {object} data * @param {string} title * @param {string} url * @return {object} */ History.createStateObject = function(data,title,url){ // Hashify var State = { 'data': data, 'title': title, 'url': url }; // Expand the State State = History.normalizeState(State); // Return object return State; }; /** * History.getStateById(id) * Get a state by it's UID * @param {String} id */ History.getStateById = function(id){ // Prepare id = String(id); // Retrieve var State = History.idToState[id] || History.store.idToState[id] || undefined; // Return State return State; }; /** * Get a State's String * @param {State} passedState */ History.getStateString = function(passedState){ // Prepare var State, cleanedState, str; // Fetch State = History.normalizeState(passedState); // Clean cleanedState = { data: State.data, title: passedState.title, url: passedState.url }; // Fetch str = JSON.stringify(cleanedState); // Return return str; }; /** * Get a State's ID * @param {State} passedState * @return {String} id */ History.getStateId = function(passedState){ // Prepare var State, id; // Fetch State = History.normalizeState(passedState); // Fetch id = State.id; // Return return id; }; /** * History.getHashByState(State) * Creates a Hash for the State Object * @param {State} passedState * @return {String} hash */ History.getHashByState = function(passedState){ // Prepare var State, hash; // Fetch State = History.normalizeState(passedState); // Hash hash = State.hash; // Return return hash; }; /** * History.extractId(url_or_hash) * Get a State ID by it's URL or Hash * @param {string} url_or_hash * @return {string} id */ History.extractId = function ( url_or_hash ) { // Prepare var id,parts,url, tmp; // Extract // If the URL has a #, use the id from before the # if (url_or_hash.indexOf('#') != -1) { tmp = url_or_hash.split("#")[0]; } else { tmp = url_or_hash; } parts = /(.*)\&_suid=([0-9]+)$/.exec(tmp); url = parts ? (parts[1]||url_or_hash) : url_or_hash; id = parts ? String(parts[2]||'') : ''; // Return return id||false; }; /** * History.isTraditionalAnchor * Checks to see if the url is a traditional anchor or not * @param {String} url_or_hash * @return {Boolean} */ History.isTraditionalAnchor = function(url_or_hash){ // Check var isTraditional = !(/[\/\?\.]/.test(url_or_hash)); // Return return isTraditional; }; /** * History.extractState * Get a State by it's URL or Hash * @param {String} url_or_hash * @return {State|null} */ History.extractState = function(url_or_hash,create){ // Prepare var State = null, id, url; create = create||false; // Fetch SUID id = History.extractId(url_or_hash); if ( id ) { State = History.getStateById(id); } // Fetch SUID returned no State if ( !State ) { // Fetch URL url = History.getFullUrl(url_or_hash); // Check URL id = History.getIdByUrl(url)||false; if ( id ) { State = History.getStateById(id); } // Create State if ( !State && create && !History.isTraditionalAnchor(url_or_hash) ) { State = History.createStateObject(null,null,url); } } // Return return State; }; /** * History.getIdByUrl() * Get a State ID by a State URL */ History.getIdByUrl = function(url){ // Fetch var id = History.urlToId[url] || History.store.urlToId[url] || undefined; // Return return id; }; /** * History.getLastSavedState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastSavedState = function(){ return History.savedStates[History.savedStates.length-1]||undefined; }; /** * History.getLastStoredState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastStoredState = function(){ return History.storedStates[History.storedStates.length-1]||undefined; }; /** * History.hasUrlDuplicate * Checks if a Url will have a url conflict * @param {Object} newState * @return {Boolean} hasDuplicate */ History.hasUrlDuplicate = function(newState) { // Prepare var hasDuplicate = false, oldState; // Fetch oldState = History.extractState(newState.url); // Check hasDuplicate = oldState && oldState.id !== newState.id; // Return return hasDuplicate; }; /** * History.storeState * Store a State * @param {Object} newState * @return {Object} newState */ History.storeState = function(newState){ // Store the State History.urlToId[newState.url] = newState.id; // Push the State History.storedStates.push(History.cloneObject(newState)); // Return newState return newState; }; /** * History.isLastSavedState(newState) * Tests to see if the state is the last state * @param {Object} newState * @return {boolean} isLast */ History.isLastSavedState = function(newState){ // Prepare var isLast = false, newId, oldState, oldId; // Check if ( History.savedStates.length ) { newId = newState.id; oldState = History.getLastSavedState(); oldId = oldState.id; // Check isLast = (newId === oldId); } // Return return isLast; }; /** * History.saveState * Push a State * @param {Object} newState * @return {boolean} changed */ History.saveState = function(newState){ // Check Hash if ( History.isLastSavedState(newState) ) { return false; } // Push the State History.savedStates.push(History.cloneObject(newState)); // Return true return true; }; /** * History.getStateByIndex() * Gets a state by the index * @param {integer} index * @return {Object} */ History.getStateByIndex = function(index){ // Prepare var State = null; // Handle if ( typeof index === 'undefined' ) { // Get the last inserted State = History.savedStates[History.savedStates.length-1]; } else if ( index < 0 ) { // Get from the end State = History.savedStates[History.savedStates.length+index]; } else { // Get from the beginning State = History.savedStates[index]; } // Return State return State; }; /** * History.getCurrentIndex() * Gets the current index * @return (integer) */ History.getCurrentIndex = function(){ // Prepare var index = null; // No states saved if(History.savedStates.length < 1) { index = 0; } else { index = History.savedStates.length-1; } return index; }; // ==================================================================== // Hash Helpers /** * History.getHash() * @param {Location=} location * Gets the current document hash * Note: unlike location.hash, this is guaranteed to return the escaped hash in all browsers * @return {string} */ History.getHash = function(doc){ var url = History.getLocationHref(doc), hash; hash = History.getHashByUrl(url); return hash; }; /** * History.unescapeHash() * normalize and Unescape a Hash * @param {String} hash * @return {string} */ History.unescapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Unescape hash result = decodeURIComponent(result); // Return result return result; }; /** * History.normalizeHash() * normalize a hash across browsers * @return {string} */ History.normalizeHash = function(hash){ // Prepare var result = hash.replace(/[^#]*#/,'').replace(/#.*/, ''); // Return result return result; }; /** * History.setHash(hash) * Sets the document hash * @param {string} hash * @return {History} */ History.setHash = function(hash,queue){ // Prepare var State, pageUrl; // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.setHash: we must wait', arguments); History.pushQueue({ scope: History, callback: History.setHash, args: arguments, queue: queue }); return false; } // Log //History.debug('History.setHash: called',hash); // Make Busy + Continue History.busy(true); // Check if hash is a state State = History.extractState(hash,true); if ( State && !History.emulated.pushState ) { // Hash is a state so skip the setHash //History.debug('History.setHash: Hash is a state so skipping the hash set with a direct pushState call',arguments); // PushState History.pushState(State.data,State.title,State.url,false); } else if ( History.getHash() !== hash ) { // Hash is a proper hash, so apply it // Handle browser bugs if ( History.bugs.setHash ) { // Fix Safari Bug https://bugs.webkit.org/show_bug.cgi?id=56249 // Fetch the base page pageUrl = History.getPageUrl(); // Safari hash apply History.pushState(null,null,pageUrl+'#'+hash,false); } else { // Normal hash apply document.location.hash = hash; } } // Chain return History; }; /** * History.escape() * normalize and Escape a Hash * @return {string} */ History.escapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Escape hash result = window.encodeURIComponent(result); // IE6 Escape Bug if ( !History.bugs.hashEscape ) { // Restore common parts result = result .replace(/\%21/g,'!') .replace(/\%26/g,'&') .replace(/\%3D/g,'=') .replace(/\%3F/g,'?'); } // Return result return result; }; /** * History.getHashByUrl(url) * Extracts the Hash from a URL * @param {string} url * @return {string} url */ History.getHashByUrl = function(url){ // Extract the hash var hash = String(url) .replace(/([^#]*)#?([^#]*)#?(.*)/, '$2') ; // Unescape hash hash = History.unescapeHash(hash); // Return hash return hash; }; /** * History.setTitle(title) * Applies the title to the document * @param {State} newState * @return {Boolean} */ History.setTitle = function(newState){ // Prepare var title = newState.title, firstState; // Initial if ( !title ) { firstState = History.getStateByIndex(0); if ( firstState && firstState.url === newState.url ) { title = firstState.title||History.options.initialTitle; } } // Apply try { document.getElementsByTagName('title')[0].innerHTML = title.replace('<','<').replace('>','>').replace(' & ',' & '); } catch ( Exception ) { } document.title = title; // Chain return History; }; // ==================================================================== // Queueing /** * History.queues * The list of queues to use * First In, First Out */ History.queues = []; /** * History.busy(value) * @param {boolean} value [optional] * @return {boolean} busy */ History.busy = function(value){ // Apply if ( typeof value !== 'undefined' ) { //History.debug('History.busy: changing ['+(History.busy.flag||false)+'] to ['+(value||false)+']', History.queues.length); History.busy.flag = value; } // Default else if ( typeof History.busy.flag === 'undefined' ) { History.busy.flag = false; } // Queue if ( !History.busy.flag ) { // Execute the next item in the queue clearTimeout(History.busy.timeout); var fireNext = function(){ var i, queue, item; if ( History.busy.flag ) return; for ( i=History.queues.length-1; i >= 0; --i ) { queue = History.queues[i]; if ( queue.length === 0 ) continue; item = queue.shift(); History.fireQueueItem(item); History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } }; History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } // Return return History.busy.flag; }; /** * History.busy.flag */ History.busy.flag = false; /** * History.fireQueueItem(item) * Fire a Queue Item * @param {Object} item * @return {Mixed} result */ History.fireQueueItem = function(item){ return item.callback.apply(item.scope||History,item.args||[]); }; /** * History.pushQueue(callback,args) * Add an item to the queue * @param {Object} item [scope,callback,args,queue] */ History.pushQueue = function(item){ // Prepare the queue History.queues[item.queue||0] = History.queues[item.queue||0]||[]; // Add to the queue History.queues[item.queue||0].push(item); // Chain return History; }; /** * History.queue (item,queue), (func,queue), (func), (item) * Either firs the item now if not busy, or adds it to the queue */ History.queue = function(item,queue){ // Prepare if ( typeof item === 'function' ) { item = { callback: item }; } if ( typeof queue !== 'undefined' ) { item.queue = queue; } // Handle if ( History.busy() ) { History.pushQueue(item); } else { History.fireQueueItem(item); } // Chain return History; }; /** * History.clearQueue() * Clears the Queue */ History.clearQueue = function(){ History.busy.flag = false; History.queues = []; return History; }; // ==================================================================== // IE Bug Fix /** * History.stateChanged * States whether or not the state has changed since the last double check was initialised */ History.stateChanged = false; /** * History.doubleChecker * Contains the timeout used for the double checks */ History.doubleChecker = false; /** * History.doubleCheckComplete() * Complete a double check * @return {History} */ History.doubleCheckComplete = function(){ // Update History.stateChanged = true; // Clear History.doubleCheckClear(); // Chain return History; }; /** * History.doubleCheckClear() * Clear a double check * @return {History} */ History.doubleCheckClear = function(){ // Clear if ( History.doubleChecker ) { clearTimeout(History.doubleChecker); History.doubleChecker = false; } // Chain return History; }; /** * History.doubleCheck() * Create a double check * @return {History} */ History.doubleCheck = function(tryAgain){ // Reset History.stateChanged = false; History.doubleCheckClear(); // Fix IE6,IE7 bug where calling history.back or history.forward does not actually change the hash (whereas doing it manually does) // Fix Safari 5 bug where sometimes the state does not change: https://bugs.webkit.org/show_bug.cgi?id=42940 if ( History.bugs.ieDoubleCheck ) { // Apply Check History.doubleChecker = setTimeout( function(){ History.doubleCheckClear(); if ( !History.stateChanged ) { //History.debug('History.doubleCheck: State has not yet changed, trying again', arguments); // Re-Attempt tryAgain(); } return true; }, History.options.doubleCheckInterval ); } // Chain return History; }; // ==================================================================== // Safari Bug Fix /** * History.safariStatePoll() * Poll the current state * @return {History} */ History.safariStatePoll = function(){ // Poll the URL // Get the Last State which has the new URL var urlState = History.extractState(History.getLocationHref()), newState; // Check for a difference if ( !History.isLastSavedState(urlState) ) { newState = urlState; } else { return; } // Check if we have a state with that url // If not create it if ( !newState ) { //History.debug('History.safariStatePoll: new'); newState = History.createStateObject(); } // Apply the New State //History.debug('History.safariStatePoll: trigger'); History.Adapter.trigger(window,'popstate'); // Chain return History; }; // ==================================================================== // State Aliases /** * History.back(queue) * Send the browser history back one item * @param {Integer} queue [optional] */ History.back = function(queue){ //History.debug('History.back: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.back: we must wait', arguments); History.pushQueue({ scope: History, callback: History.back, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.back(false); }); // Go back history.go(-1); // End back closure return true; }; /** * History.forward(queue) * Send the browser history forward one item * @param {Integer} queue [optional] */ History.forward = function(queue){ //History.debug('History.forward: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.forward: we must wait', arguments); History.pushQueue({ scope: History, callback: History.forward, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.forward(false); }); // Go forward history.go(1); // End forward closure return true; }; /** * History.go(index,queue) * Send the browser history back or forward index times * @param {Integer} queue [optional] */ History.go = function(index,queue){ //History.debug('History.go: called', arguments); // Prepare var i; // Handle if ( index > 0 ) { // Forward for ( i=1; i<=index; ++i ) { History.forward(queue); } } else if ( index < 0 ) { // Backward for ( i=-1; i>=index; --i ) { History.back(queue); } } else { throw new Error('History.go: History.go requires a positive or negative integer passed.'); } // Chain return History; }; // ==================================================================== // HTML5 State Support // Non-Native pushState Implementation if ( History.emulated.pushState ) { /* * Provide Skeleton for HTML4 Browsers */ // Prepare var emptyFunction = function(){}; History.pushState = History.pushState||emptyFunction; History.replaceState = History.replaceState||emptyFunction; } // History.emulated.pushState // Native pushState Implementation else { /* * Use native HTML5 History API Implementation */ /** * History.onPopState(event,extra) * Refresh the Current State */ History.onPopState = function(event,extra){ // Prepare var stateId = false, newState = false, currentHash, currentState; // Reset the double check History.doubleCheckComplete(); // Check for a Hash, and handle apporiatly currentHash = History.getHash(); if ( currentHash ) { // Expand Hash currentState = History.extractState(currentHash||History.getLocationHref(),true); if ( currentState ) { // We were able to parse it, it must be a State! // Let's forward to replaceState //History.debug('History.onPopState: state anchor', currentHash, currentState); History.replaceState(currentState.data, currentState.title, currentState.url, false); } else { // Traditional Anchor //History.debug('History.onPopState: traditional anchor', currentHash); History.Adapter.trigger(window,'anchorchange'); History.busy(false); } // We don't care for hashes History.expectedStateId = false; return false; } // Ensure stateId = History.Adapter.extractEventData('state',event,extra) || false; // Fetch State if ( stateId ) { // Vanilla: Back/forward button was used newState = History.getStateById(stateId); } else if ( History.expectedStateId ) { // Vanilla: A new state was pushed, and popstate was called manually newState = History.getStateById(History.expectedStateId); } else { // Initial State newState = History.extractState(History.getLocationHref()); } // The State did not exist in our store if ( !newState ) { // Regenerate the State newState = History.createStateObject(null,null,History.getLocationHref()); } // Clean History.expectedStateId = false; // Check if we are the same state if ( History.isLastSavedState(newState) ) { // There has been no change (just the page's hash has finally propagated) //History.debug('History.onPopState: no change', newState, History.savedStates); History.busy(false); return false; } // Store the State History.storeState(newState); History.saveState(newState); // Force update of the title History.setTitle(newState); // Fire Our Event History.Adapter.trigger(window,'statechange'); History.busy(false); // Return true return true; }; History.Adapter.bind(window,'popstate',History.onPopState); /** * History.pushState(data,title,url) * Add a new State to the history object, become it, and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.pushState = function(data,title,url,queue){ //History.debug('History.pushState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.pushState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.pushState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.pushState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End pushState closure return true; }; /** * History.replaceState(data,title,url) * Replace the State and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.replaceState = function(data,title,url,queue){ //History.debug('History.replaceState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.replaceState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.replaceState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.replaceState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End replaceState closure return true; }; } // !History.emulated.pushState // ==================================================================== // Initialise /** * Load the Store */ if ( sessionStorage ) { // Fetch try { History.store = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { History.store = {}; } // Normalize History.normalizeStore(); } else { // Default Load History.store = {}; History.normalizeStore(); } /** * Clear Intervals on exit to prevent memory leaks */ History.Adapter.bind(window,"unload",History.clearAllIntervals); /** * Create the initial State */ History.saveState(History.storeState(History.extractState(History.getLocationHref(),true))); /** * Bind for Saving Store */ if ( sessionStorage ) { // When the page is closed History.onUnload = function(){ // Prepare var currentStore, item, currentStoreString; // Fetch try { currentStore = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { currentStore = {}; } // Ensure currentStore.idToState = currentStore.idToState || {}; currentStore.urlToId = currentStore.urlToId || {}; currentStore.stateToId = currentStore.stateToId || {}; // Sync for ( item in History.idToState ) { if ( !History.idToState.hasOwnProperty(item) ) { continue; } currentStore.idToState[item] = History.idToState[item]; } for ( item in History.urlToId ) { if ( !History.urlToId.hasOwnProperty(item) ) { continue; } currentStore.urlToId[item] = History.urlToId[item]; } for ( item in History.stateToId ) { if ( !History.stateToId.hasOwnProperty(item) ) { continue; } currentStore.stateToId[item] = History.stateToId[item]; } // Update History.store = currentStore; History.normalizeStore(); // In Safari, going into Private Browsing mode causes the // Session Storage object to still exist but if you try and use // or set any property/function of it it throws the exception // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to // add something to storage that exceeded the quota." infinitely // every second. currentStoreString = JSON.stringify(currentStore); try { // Store sessionStorage.setItem('History.store', currentStoreString); } catch (e) { if (e.code === DOMException.QUOTA_EXCEEDED_ERR) { if (sessionStorage.length) { // Workaround for a bug seen on iPads. Sometimes the quota exceeded error comes up and simply // removing/resetting the storage can work. sessionStorage.removeItem('History.store'); sessionStorage.setItem('History.store', currentStoreString); } else { // Otherwise, we're probably private browsing in Safari, so we'll ignore the exception. } } else { throw e; } } }; // For Internet Explorer History.intervalList.push(setInterval(History.onUnload,History.options.storeInterval)); // For Other Browsers History.Adapter.bind(window,'beforeunload',History.onUnload); History.Adapter.bind(window,'unload',History.onUnload); // Both are enabled for consistency } // Non-Native pushState Implementation if ( !History.emulated.pushState ) { // Be aware, the following is only for native pushState implementations // If you are wanting to include something for all browsers // Then include it above this if block /** * Setup Safari Fix */ if ( History.bugs.safariPoll ) { History.intervalList.push(setInterval(History.safariStatePoll, History.options.safariPollInterval)); } /** * Ensure Cross Browser Compatibility */ if ( navigator.vendor === 'Apple Computer, Inc.' || (navigator.appCodeName||'') === 'Mozilla' ) { /** * Fix Safari HashChange Issue */ // Setup Alias History.Adapter.bind(window,'hashchange',function(){ History.Adapter.trigger(window,'popstate'); }); // Initialise Alias if ( History.getHash() ) { History.Adapter.onDomLoad(function(){ History.Adapter.trigger(window,'hashchange'); }); } } } // !History.emulated.pushState }; // History.initCore // Try to Initialise History if (!History.options || !History.options.delayInit) { History.init(); } })(window); ================================================ FILE: scripts/bundled-uncompressed/html4+html5/native.history.js ================================================ /* json2.js 2012-10-08 Public Domain. NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. See http://www.JSON.org/js.html This code should be minified before deployment. See http://javascript.crockford.com/jsmin.html USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO NOT CONTROL. This file creates a global JSON object containing two methods: stringify and parse. JSON.stringify(value, replacer, space) value any JavaScript value, usually an object or array. replacer an optional parameter that determines how object values are stringified for objects. It can be a function or an array of strings. space an optional parameter that specifies the indentation of nested structures. If it is omitted, the text will be packed without extra whitespace. If it is a number, it will specify the number of spaces to indent at each level. If it is a string (such as '\t' or ' '), it contains the characters used to indent at each level. This method produces a JSON text from a JavaScript value. When an object value is found, if the object contains a toJSON method, its toJSON method will be called and the result will be stringified. A toJSON method does not serialize: it returns the value represented by the name/value pair that should be serialized, or undefined if nothing should be serialized. The toJSON method will be passed the key associated with the value, and this will be bound to the value For example, this would serialize Dates as ISO strings. Date.prototype.toJSON = function (key) { function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } return this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z'; }; You can provide an optional replacer method. It will be passed the key and value of each member, with this bound to the containing object. The value that is returned from your method will be serialized. If your method returns undefined, then the member will be excluded from the serialization. If the replacer parameter is an array of strings, then it will be used to select the members to be serialized. It filters the results such that only members with keys listed in the replacer array are stringified. Values that do not have JSON representations, such as undefined or functions, will not be serialized. Such values in objects will be dropped; in arrays they will be replaced with null. You can use a replacer function to replace those with JSON values. JSON.stringify(undefined) returns undefined. The optional space parameter produces a stringification of the value that is filled with line breaks and indentation to make it easier to read. If the space parameter is a non-empty string, then that string will be used for indentation. If the space parameter is a number, then the indentation will be that many spaces. Example: text = JSON.stringify(['e', {pluribus: 'unum'}]); // text is '["e",{"pluribus":"unum"}]' text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' text = JSON.stringify([new Date()], function (key, value) { return this[key] instanceof Date ? 'Date(' + this[key] + ')' : value; }); // text is '["Date(---current time---)"]' JSON.parse(text, reviver) This method parses a JSON text to produce an object or array. It can throw a SyntaxError exception. The optional reviver parameter is a function that can filter and transform the results. It receives each of the keys and values, and its return value is used instead of the original value. If it returns what it received, then the structure is not modified. If it returns undefined then the member is deleted. Example: // Parse the text. Values that look like ISO date strings will // be converted to Date objects. myData = JSON.parse(text, function (key, value) { var a; if (typeof value === 'string') { a = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); if (a) { return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6])); } } return value; }); myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { var d; if (typeof value === 'string' && value.slice(0, 5) === 'Date(' && value.slice(-1) === ')') { d = new Date(value.slice(5, -1)); if (d) { return d; } } return value; }); This is a reference implementation. You are free to copy, modify, or redistribute. */ /*jslint evil: true, regexp: true */ /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, lastIndex, length, parse, prototype, push, replace, slice, stringify, test, toJSON, toString, valueOf */ // Create a JSON object only if one does not already exist. We create the // methods in a closure to avoid creating global variables. if (typeof JSON !== 'object') { JSON = {}; } (function () { 'use strict'; function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } if (typeof Date.prototype.toJSON !== 'function') { Date.prototype.toJSON = function (key) { return isFinite(this.valueOf()) ? this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z' : null; }; String.prototype.toJSON = Number.prototype.toJSON = Boolean.prototype.toJSON = function (key) { return this.valueOf(); }; } var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, gap, indent, meta = { // table of character substitutions '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\' }, rep; function quote(string) { // If the string contains no control characters, no quote characters, and no // backslash characters, then we can safely slap some quotes around it. // Otherwise we must also replace the offending characters with safe escape // sequences. escapable.lastIndex = 0; return escapable.test(string) ? '"' + string.replace(escapable, function (a) { var c = meta[a]; return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }) + '"' : '"' + string + '"'; } function str(key, holder) { // Produce a string from holder[key]. var i, // The loop counter. k, // The member key. v, // The member value. length, mind = gap, partial, value = holder[key]; // If the value has a toJSON method, call it to obtain a replacement value. if (value && typeof value === 'object' && typeof value.toJSON === 'function') { value = value.toJSON(key); } // If we were called with a replacer function, then call the replacer to // obtain a replacement value. if (typeof rep === 'function') { value = rep.call(holder, key, value); } // What happens next depends on the value's type. switch (typeof value) { case 'string': return quote(value); case 'number': // JSON numbers must be finite. Encode non-finite numbers as null. return isFinite(value) ? String(value) : 'null'; case 'boolean': case 'null': // If the value is a boolean or null, convert it to a string. Note: // typeof null does not produce 'null'. The case is included here in // the remote chance that this gets fixed someday. return String(value); // If the type is 'object', we might be dealing with an object or an array or // null. case 'object': // Due to a specification blunder in ECMAScript, typeof null is 'object', // so watch out for that case. if (!value) { return 'null'; } // Make an array to hold the partial results of stringifying this object value. gap += indent; partial = []; // Is the value an array? if (Object.prototype.toString.apply(value) === '[object Array]') { // The value is an array. Stringify every element. Use null as a placeholder // for non-JSON values. length = value.length; for (i = 0; i < length; i += 1) { partial[i] = str(i, value) || 'null'; } // Join all of the elements together, separated with commas, and wrap them in // brackets. v = partial.length === 0 ? '[]' : gap ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : '[' + partial.join(',') + ']'; gap = mind; return v; } // If the replacer is an array, use it to select the members to be stringified. if (rep && typeof rep === 'object') { length = rep.length; for (i = 0; i < length; i += 1) { if (typeof rep[i] === 'string') { k = rep[i]; v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } else { // Otherwise, iterate through all of the keys in the object. for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } // Join all of the member texts together, separated with commas, // and wrap them in braces. v = partial.length === 0 ? '{}' : gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : '{' + partial.join(',') + '}'; gap = mind; return v; } } // If the JSON object does not yet have a stringify method, give it one. if (typeof JSON.stringify !== 'function') { JSON.stringify = function (value, replacer, space) { // The stringify method takes a value and an optional replacer, and an optional // space parameter, and returns a JSON text. The replacer can be a function // that can replace values, or an array of strings that will select the keys. // A default replacer method can be provided. Use of the space parameter can // produce text that is more easily readable. var i; gap = ''; indent = ''; // If the space parameter is a number, make an indent string containing that // many spaces. if (typeof space === 'number') { for (i = 0; i < space; i += 1) { indent += ' '; } // If the space parameter is a string, it will be used as the indent string. } else if (typeof space === 'string') { indent = space; } // If there is a replacer, it must be a function or an array. // Otherwise, throw an error. rep = replacer; if (replacer && typeof replacer !== 'function' && (typeof replacer !== 'object' || typeof replacer.length !== 'number')) { throw new Error('JSON.stringify'); } // Make a fake root object containing our value under the key of ''. // Return the result of stringifying the value. return str('', {'': value}); }; } // If the JSON object does not yet have a parse method, give it one. if (typeof JSON.parse !== 'function') { JSON.parse = function (text, reviver) { // The parse method takes a text and an optional reviver function, and returns // a JavaScript value if the text is a valid JSON text. var j; function walk(holder, key) { // The walk method is used to recursively walk the resulting structure so // that modifications can be made. var k, v, value = holder[key]; if (value && typeof value === 'object') { for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = walk(value, k); if (v !== undefined) { value[k] = v; } else { delete value[k]; } } } } return reviver.call(holder, key, value); } // Parsing happens in four stages. In the first stage, we replace certain // Unicode characters with escape sequences. JavaScript handles many characters // incorrectly, either silently deleting them, or treating them as line endings. text = String(text); cx.lastIndex = 0; if (cx.test(text)) { text = text.replace(cx, function (a) { return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }); } // In the second stage, we run the text against regular expressions that look // for non-JSON patterns. We are especially concerned with '()' and 'new' // because they can cause invocation, and '=' because it can cause mutation. // But just to be safe, we want to reject all unexpected forms. // We split the second stage into 4 regexp operations in order to work around // crippling inefficiencies in IE's and Safari's regexp engines. First we // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we // replace all simple value tokens with ']' characters. Third, we delete all // open brackets that follow a colon or comma or that begin the text. Finally, // we look to see that the remaining characters are only whitespace or ']' or // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. if (/^[\],:{}\s]*$/ .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { // In the third stage we use the eval function to compile the text into a // JavaScript structure. The '{' operator is subject to a syntactic ambiguity // in JavaScript: it can begin a block or an object literal. We wrap the text // in parens to eliminate the ambiguity. j = eval('(' + text + ')'); // In the optional fourth stage, we recursively walk the new structure, passing // each name/value pair to a reviver function for possible transformation. return typeof reviver === 'function' ? walk({'': j}, '') : j; } // If the text is not JSON parseable, then a SyntaxError is thrown. throw new SyntaxError('JSON.parse'); }; } }());/** * History.js Native Adapter * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ // Closure (function(window,undefined){ "use strict"; // Localise Globals var History = window.History = window.History||{}; // Check Existence if ( typeof History.Adapter !== 'undefined' ) { throw new Error('History.js Adapter has already been loaded...'); } // Add the Adapter History.Adapter = { /** * History.Adapter.handlers[uid][eventName] = Array */ handlers: {}, /** * History.Adapter._uid * The current element unique identifier */ _uid: 1, /** * History.Adapter.uid(element) * @param {Element} element * @return {String} uid */ uid: function(element){ return element._uid || (element._uid = History.Adapter._uid++); }, /** * History.Adapter.bind(el,event,callback) * @param {Element} element * @param {String} eventName - custom and standard events * @param {Function} callback * @return */ bind: function(element,eventName,callback){ // Prepare var uid = History.Adapter.uid(element); // Apply Listener History.Adapter.handlers[uid] = History.Adapter.handlers[uid] || {}; History.Adapter.handlers[uid][eventName] = History.Adapter.handlers[uid][eventName] || []; History.Adapter.handlers[uid][eventName].push(callback); // Bind Global Listener element['on'+eventName] = (function(element,eventName){ return function(event){ History.Adapter.trigger(element,eventName,event); }; })(element,eventName); }, /** * History.Adapter.trigger(el,event) * @param {Element} element * @param {String} eventName - custom and standard events * @param {Object} event - a object of event data * @return */ trigger: function(element,eventName,event){ // Prepare event = event || {}; var uid = History.Adapter.uid(element), i,n; // Apply Listener History.Adapter.handlers[uid] = History.Adapter.handlers[uid] || {}; History.Adapter.handlers[uid][eventName] = History.Adapter.handlers[uid][eventName] || []; // Fire Listeners for ( i=0,n=History.Adapter.handlers[uid][eventName].length; i * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window,undefined){ "use strict"; // ======================================================================== // Initialise // Localise Globals var document = window.document, // Make sure we are using the correct document setTimeout = window.setTimeout||setTimeout, clearTimeout = window.clearTimeout||clearTimeout, setInterval = window.setInterval||setInterval, History = window.History = window.History||{}; // Public History Object // Check Existence if ( typeof History.initHtml4 !== 'undefined' ) { throw new Error('History.js HTML4 Support has already been loaded...'); } // ======================================================================== // Initialise HTML4 Support // Initialise HTML4 Support History.initHtml4 = function(){ // Initialise if ( typeof History.initHtml4.initialized !== 'undefined' ) { // Already Loaded return false; } else { History.initHtml4.initialized = true; } // ==================================================================== // Properties /** * History.enabled * Is History enabled? */ History.enabled = true; // ==================================================================== // Hash Storage /** * History.savedHashes * Store the hashes in an array */ History.savedHashes = []; /** * History.isLastHash(newHash) * Checks if the hash is the last hash * @param {string} newHash * @return {boolean} true */ History.isLastHash = function(newHash){ // Prepare var oldHash = History.getHashByIndex(), isLast; // Check isLast = newHash === oldHash; // Return isLast return isLast; }; /** * History.isHashEqual(newHash, oldHash) * Checks to see if two hashes are functionally equal * @param {string} newHash * @param {string} oldHash * @return {boolean} true */ History.isHashEqual = function(newHash, oldHash){ newHash = encodeURIComponent(newHash).replace(/%25/g, "%"); oldHash = encodeURIComponent(oldHash).replace(/%25/g, "%"); return newHash === oldHash; }; /** * History.saveHash(newHash) * Push a Hash * @param {string} newHash * @return {boolean} true */ History.saveHash = function(newHash){ // Check Hash if ( History.isLastHash(newHash) ) { return false; } // Push the Hash History.savedHashes.push(newHash); // Return true return true; }; /** * History.getHashByIndex() * Gets a hash by the index * @param {integer} index * @return {string} */ History.getHashByIndex = function(index){ // Prepare var hash = null; // Handle if ( typeof index === 'undefined' ) { // Get the last inserted hash = History.savedHashes[History.savedHashes.length-1]; } else if ( index < 0 ) { // Get from the end hash = History.savedHashes[History.savedHashes.length+index]; } else { // Get from the beginning hash = History.savedHashes[index]; } // Return hash return hash; }; // ==================================================================== // Discarded States /** * History.discardedHashes * A hashed array of discarded hashes */ History.discardedHashes = {}; /** * History.discardedStates * A hashed array of discarded states */ History.discardedStates = {}; /** * History.discardState(State) * Discards the state by ignoring it through History * @param {object} State * @return {true} */ History.discardState = function(discardedState,forwardState,backState){ //History.debug('History.discardState', arguments); // Prepare var discardedStateHash = History.getHashByState(discardedState), discardObject; // Create Discard Object discardObject = { 'discardedState': discardedState, 'backState': backState, 'forwardState': forwardState }; // Add to DiscardedStates History.discardedStates[discardedStateHash] = discardObject; // Return true return true; }; /** * History.discardHash(hash) * Discards the hash by ignoring it through History * @param {string} hash * @return {true} */ History.discardHash = function(discardedHash,forwardState,backState){ //History.debug('History.discardState', arguments); // Create Discard Object var discardObject = { 'discardedHash': discardedHash, 'backState': backState, 'forwardState': forwardState }; // Add to discardedHash History.discardedHashes[discardedHash] = discardObject; // Return true return true; }; /** * History.discardedState(State) * Checks to see if the state is discarded * @param {object} State * @return {bool} */ History.discardedState = function(State){ // Prepare var StateHash = History.getHashByState(State), discarded; // Check discarded = History.discardedStates[StateHash]||false; // Return true return discarded; }; /** * History.discardedHash(hash) * Checks to see if the state is discarded * @param {string} State * @return {bool} */ History.discardedHash = function(hash){ // Check var discarded = History.discardedHashes[hash]||false; // Return true return discarded; }; /** * History.recycleState(State) * Allows a discarded state to be used again * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.recycleState = function(State){ //History.debug('History.recycleState', arguments); // Prepare var StateHash = History.getHashByState(State); // Remove from DiscardedStates if ( History.discardedState(State) ) { delete History.discardedStates[StateHash]; } // Return true return true; }; // ==================================================================== // HTML4 HashChange Support if ( History.emulated.hashChange ) { /* * We must emulate the HTML4 HashChange Support by manually checking for hash changes */ /** * History.hashChangeInit() * Init the HashChange Emulation */ History.hashChangeInit = function(){ // Define our Checker Function History.checkerFunction = null; // Define some variables that will help in our checker function var lastDocumentHash = '', iframeId, iframe, lastIframeHash, checkerRunning, startedWithHash = Boolean(History.getHash()); // Handle depending on the browser if ( History.isInternetExplorer() ) { // IE6 and IE7 // We need to use an iframe to emulate the back and forward buttons // Create iFrame iframeId = 'historyjs-iframe'; iframe = document.createElement('iframe'); // Adjust iFarme // IE 6 requires iframe to have a src on HTTPS pages, otherwise it will throw a // "This page contains both secure and nonsecure items" warning. iframe.setAttribute('id', iframeId); iframe.setAttribute('src', '#'); iframe.style.display = 'none'; // Append iFrame document.body.appendChild(iframe); // Create initial history entry iframe.contentWindow.document.open(); iframe.contentWindow.document.close(); // Define some variables that will help in our checker function lastIframeHash = ''; checkerRunning = false; // Define the checker function History.checkerFunction = function(){ // Check Running if ( checkerRunning ) { return false; } // Update Running checkerRunning = true; // Fetch var documentHash = History.getHash(), iframeHash = History.getHash(iframe.contentWindow.document); // The Document Hash has changed (application caused) if ( documentHash !== lastDocumentHash ) { // Equalise lastDocumentHash = documentHash; // Create a history entry in the iframe if ( iframeHash !== documentHash ) { //History.debug('hashchange.checker: iframe hash change', 'documentHash (new):', documentHash, 'iframeHash (old):', iframeHash); // Equalise lastIframeHash = iframeHash = documentHash; // Create History Entry iframe.contentWindow.document.open(); iframe.contentWindow.document.close(); // Update the iframe's hash iframe.contentWindow.document.location.hash = History.escapeHash(documentHash); } // Trigger Hashchange Event History.Adapter.trigger(window,'hashchange'); } // The iFrame Hash has changed (back button caused) else if ( iframeHash !== lastIframeHash ) { //History.debug('hashchange.checker: iframe hash out of sync', 'iframeHash (new):', iframeHash, 'documentHash (old):', documentHash); // Equalise lastIframeHash = iframeHash; // If there is no iframe hash that means we're at the original // iframe state. // And if there was a hash on the original request, the original // iframe state was replaced instantly, so skip this state and take // the user back to where they came from. if (startedWithHash && iframeHash === '') { History.back(); } else { // Update the Hash History.setHash(iframeHash,false); } } // Reset Running checkerRunning = false; // Return true return true; }; } else { // We are not IE // Firefox 1 or 2, Opera // Define the checker function History.checkerFunction = function(){ // Prepare var documentHash = History.getHash()||''; // The Document Hash has changed (application caused) if ( documentHash !== lastDocumentHash ) { // Equalise lastDocumentHash = documentHash; // Trigger Hashchange Event History.Adapter.trigger(window,'hashchange'); } // Return true return true; }; } // Apply the checker function History.intervalList.push(setInterval(History.checkerFunction, History.options.hashChangeInterval)); // Done return true; }; // History.hashChangeInit // Bind hashChangeInit History.Adapter.onDomLoad(History.hashChangeInit); } // History.emulated.hashChange // ==================================================================== // HTML5 State Support // Non-Native pushState Implementation if ( History.emulated.pushState ) { /* * We must emulate the HTML5 State Management by using HTML4 HashChange */ /** * History.onHashChange(event) * Trigger HTML5's window.onpopstate via HTML4 HashChange Support */ History.onHashChange = function(event){ //History.debug('History.onHashChange', arguments); // Prepare var currentUrl = ((event && event.newURL) || History.getLocationHref()), currentHash = History.getHashByUrl(currentUrl), currentState = null, currentStateHash = null, currentStateHashExits = null, discardObject; // Check if we are the same state if ( History.isLastHash(currentHash) ) { // There has been no change (just the page's hash has finally propagated) //History.debug('History.onHashChange: no change'); History.busy(false); return false; } // Reset the double check History.doubleCheckComplete(); // Store our location for use in detecting back/forward direction History.saveHash(currentHash); // Expand Hash if ( currentHash && History.isTraditionalAnchor(currentHash) ) { //History.debug('History.onHashChange: traditional anchor', currentHash); // Traditional Anchor Hash History.Adapter.trigger(window,'anchorchange'); History.busy(false); return false; } // Create State currentState = History.extractState(History.getFullUrl(currentHash||History.getLocationHref()),true); // Check if we are the same state if ( History.isLastSavedState(currentState) ) { //History.debug('History.onHashChange: no change'); // There has been no change (just the page's hash has finally propagated) History.busy(false); return false; } // Create the state Hash currentStateHash = History.getHashByState(currentState); // Check if we are DiscardedState discardObject = History.discardedState(currentState); if ( discardObject ) { // Ignore this state as it has been discarded and go back to the state before it if ( History.getHashByIndex(-2) === History.getHashByState(discardObject.forwardState) ) { // We are going backwards //History.debug('History.onHashChange: go backwards'); History.back(false); } else { // We are going forwards //History.debug('History.onHashChange: go forwards'); History.forward(false); } return false; } // Push the new HTML5 State //History.debug('History.onHashChange: success hashchange'); History.pushState(currentState.data,currentState.title,encodeURI(currentState.url),false); // End onHashChange closure return true; }; History.Adapter.bind(window,'hashchange',History.onHashChange); /** * History.pushState(data,title,url) * Add a new State to the history object, become it, and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.pushState = function(data,title,url,queue){ //History.debug('History.pushState: called', arguments); // We assume that the URL passed in is URI-encoded, but this makes // sure that it's fully URI encoded; any '%'s that are encoded are // converted back into '%'s url = encodeURI(url).replace(/%25/g, "%"); // Check the State if ( History.getHashByUrl(url) ) { throw new Error('History.js does not support states with fragment-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.pushState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.pushState, args: arguments, queue: queue }); return false; } // Make Busy History.busy(true); // Fetch the State Object var newState = History.createStateObject(data,title,url), newStateHash = History.getHashByState(newState), oldState = History.getState(false), oldStateHash = History.getHashByState(oldState), html4Hash = History.getHash(), wasExpected = History.expectedStateId == newState.id; // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Recycle the State History.recycleState(newState); // Force update of the title History.setTitle(newState); // Check if we are the same State if ( newStateHash === oldStateHash ) { //History.debug('History.pushState: no change', newStateHash); History.busy(false); return false; } // Update HTML5 State History.saveState(newState); // Fire HTML5 Event if(!wasExpected) History.Adapter.trigger(window,'statechange'); // Update HTML4 Hash if ( !History.isHashEqual(newStateHash, html4Hash) && !History.isHashEqual(newStateHash, History.getShortUrl(History.getLocationHref())) ) { History.setHash(newStateHash,false); } History.busy(false); // End pushState closure return true; }; /** * History.replaceState(data,title,url) * Replace the State and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.replaceState = function(data,title,url,queue){ //History.debug('History.replaceState: called', arguments); // We assume that the URL passed in is URI-encoded, but this makes // sure that it's fully URI encoded; any '%'s that are encoded are // converted back into '%'s url = encodeURI(url).replace(/%25/g, "%"); // Check the State if ( History.getHashByUrl(url) ) { throw new Error('History.js does not support states with fragment-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.replaceState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.replaceState, args: arguments, queue: queue }); return false; } // Make Busy History.busy(true); // Fetch the State Objects var newState = History.createStateObject(data,title,url), newStateHash = History.getHashByState(newState), oldState = History.getState(false), oldStateHash = History.getHashByState(oldState), previousState = History.getStateByIndex(-2); // Discard Old State History.discardState(oldState,newState,previousState); // If the url hasn't changed, just store and save the state // and fire a statechange event to be consistent with the // html 5 api if ( newStateHash === oldStateHash ) { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Recycle the State History.recycleState(newState); // Force update of the title History.setTitle(newState); // Update HTML5 State History.saveState(newState); // Fire HTML5 Event //History.debug('History.pushState: trigger popstate'); History.Adapter.trigger(window,'statechange'); History.busy(false); } else { // Alias to PushState History.pushState(newState.data,newState.title,newState.url,false); } // End replaceState closure return true; }; } // History.emulated.pushState // ==================================================================== // Initialise // Non-Native pushState Implementation if ( History.emulated.pushState ) { /** * Ensure initial state is handled correctly */ if ( History.getHash() && !History.emulated.hashChange ) { History.Adapter.onDomLoad(function(){ History.Adapter.trigger(window,'hashchange'); }); } } // History.emulated.pushState }; // History.initHtml4 // Try to Initialise History if ( typeof History.init !== 'undefined' ) { History.init(); } })(window); /** * History.js Core * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window,undefined){ "use strict"; // ======================================================================== // Initialise // Localise Globals var console = window.console||undefined, // Prevent a JSLint complain document = window.document, // Make sure we are using the correct document navigator = window.navigator, // Make sure we are using the correct navigator sessionStorage = false, // sessionStorage setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, setInterval = window.setInterval, clearInterval = window.clearInterval, JSON = window.JSON, alert = window.alert, History = window.History = window.History||{}, // Public History Object history = window.history; // Old History Object try { sessionStorage = window.sessionStorage; // This will throw an exception in some browsers when cookies/localStorage are explicitly disabled (i.e. Chrome) sessionStorage.setItem('TEST', '1'); sessionStorage.removeItem('TEST'); } catch(e) { sessionStorage = false; } // MooTools Compatibility JSON.stringify = JSON.stringify||JSON.encode; JSON.parse = JSON.parse||JSON.decode; // Check Existence if ( typeof History.init !== 'undefined' ) { throw new Error('History.js Core has already been loaded...'); } // Initialise History History.init = function(options){ // Check Load Status of Adapter if ( typeof History.Adapter === 'undefined' ) { return false; } // Check Load Status of Core if ( typeof History.initCore !== 'undefined' ) { History.initCore(); } // Check Load Status of HTML4 Support if ( typeof History.initHtml4 !== 'undefined' ) { History.initHtml4(); } // Return true return true; }; // ======================================================================== // Initialise Core // Initialise Core History.initCore = function(options){ // Initialise if ( typeof History.initCore.initialized !== 'undefined' ) { // Already Loaded return false; } else { History.initCore.initialized = true; } // ==================================================================== // Options /** * History.options * Configurable options */ History.options = History.options||{}; /** * History.options.hashChangeInterval * How long should the interval be before hashchange checks */ History.options.hashChangeInterval = History.options.hashChangeInterval || 100; /** * History.options.safariPollInterval * How long should the interval be before safari poll checks */ History.options.safariPollInterval = History.options.safariPollInterval || 500; /** * History.options.doubleCheckInterval * How long should the interval be before we perform a double check */ History.options.doubleCheckInterval = History.options.doubleCheckInterval || 500; /** * History.options.disableSuid * Force History not to append suid */ History.options.disableSuid = History.options.disableSuid || false; /** * History.options.storeInterval * How long should we wait between store calls */ History.options.storeInterval = History.options.storeInterval || 1000; /** * History.options.busyDelay * How long should we wait between busy events */ History.options.busyDelay = History.options.busyDelay || 250; /** * History.options.debug * If true will enable debug messages to be logged */ History.options.debug = History.options.debug || false; /** * History.options.initialTitle * What is the title of the initial state */ History.options.initialTitle = History.options.initialTitle || document.title; /** * History.options.html4Mode * If true, will force HTMl4 mode (hashtags) */ History.options.html4Mode = History.options.html4Mode || false; /** * History.options.delayInit * Want to override default options and call init manually. */ History.options.delayInit = History.options.delayInit || false; // ==================================================================== // Interval record /** * History.intervalList * List of intervals set, to be cleared when document is unloaded. */ History.intervalList = []; /** * History.clearAllIntervals * Clears all setInterval instances. */ History.clearAllIntervals = function(){ var i, il = History.intervalList; if (typeof il !== "undefined" && il !== null) { for (i = 0; i < il.length; i++) { clearInterval(il[i]); } History.intervalList = null; } }; // ==================================================================== // Debug /** * History.debug(message,...) * Logs the passed arguments if debug enabled */ History.debug = function(){ if ( (History.options.debug||false) ) { History.log.apply(History,arguments); } }; /** * History.log(message,...) * Logs the passed arguments */ History.log = function(){ // Prepare var consoleExists = !(typeof console === 'undefined' || typeof console.log === 'undefined' || typeof console.log.apply === 'undefined'), textarea = document.getElementById('log'), message, i,n, args,arg ; // Write to Console if ( consoleExists ) { args = Array.prototype.slice.call(arguments); message = args.shift(); if ( typeof console.debug !== 'undefined' ) { console.debug.apply(console,[message,args]); } else { console.log.apply(console,[message,args]); } } else { message = ("\n"+arguments[0]+"\n"); } // Write to log for ( i=1,n=arguments.length; i * @author James Padolsey */ History.getInternetExplorerMajorVersion = function(){ var result = History.getInternetExplorerMajorVersion.cached = (typeof History.getInternetExplorerMajorVersion.cached !== 'undefined') ? History.getInternetExplorerMajorVersion.cached : (function(){ var v = 3, div = document.createElement('div'), all = div.getElementsByTagName('i'); while ( (div.innerHTML = '') && all[0] ) {} return (v > 4) ? v : false; })() ; return result; }; /** * History.isInternetExplorer() * Are we using Internet Explorer? * @return {boolean} * @license Public Domain * @author Benjamin Arthur Lupton */ History.isInternetExplorer = function(){ var result = History.isInternetExplorer.cached = (typeof History.isInternetExplorer.cached !== 'undefined') ? History.isInternetExplorer.cached : Boolean(History.getInternetExplorerMajorVersion()) ; return result; }; /** * History.emulated * Which features require emulating? */ if (History.options.html4Mode) { History.emulated = { pushState : true, hashChange: true }; } else { History.emulated = { pushState: !Boolean( window.history && window.history.pushState && window.history.replaceState && !( (/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i).test(navigator.userAgent) /* disable for versions of iOS before version 4.3 (8F190) */ || (/AppleWebKit\/5([0-2]|3[0-2])/i).test(navigator.userAgent) /* disable for the mercury iOS browser, or at least older versions of the webkit engine */ ) ), hashChange: Boolean( !(('onhashchange' in window) || ('onhashchange' in document)) || (History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8) ) }; } /** * History.enabled * Is History enabled? */ History.enabled = !History.emulated.pushState; /** * History.bugs * Which bugs are present */ History.bugs = { /** * Safari 5 and Safari iOS 4 fail to return to the correct state once a hash is replaced by a `replaceState` call * https://bugs.webkit.org/show_bug.cgi?id=56249 */ setHash: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * Safari 5 and Safari iOS 4 sometimes fail to apply the state change under busy conditions * https://bugs.webkit.org/show_bug.cgi?id=42940 */ safariPoll: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * MSIE 6 and 7 sometimes do not apply a hash even it was told to (requiring a second call to the apply function) */ ieDoubleCheck: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8), /** * MSIE 6 requires the entire hash to be encoded for the hashes to trigger the onHashChange event */ hashEscape: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 7) }; /** * History.isEmptyObject(obj) * Checks to see if the Object is Empty * @param {Object} obj * @return {boolean} */ History.isEmptyObject = function(obj) { for ( var name in obj ) { if ( obj.hasOwnProperty(name) ) { return false; } } return true; }; /** * History.cloneObject(obj) * Clones a object and eliminate all references to the original contexts * @param {Object} obj * @return {Object} */ History.cloneObject = function(obj) { var hash,newObj; if ( obj ) { hash = JSON.stringify(obj); newObj = JSON.parse(hash); } else { newObj = {}; } return newObj; }; // ==================================================================== // URL Helpers /** * History.getRootUrl() * Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com" * @return {String} rootUrl */ History.getRootUrl = function(){ // Create var rootUrl = document.location.protocol+'//'+(document.location.hostname||document.location.host); if ( document.location.port||false ) { rootUrl += ':'+document.location.port; } rootUrl += '/'; // Return return rootUrl; }; /** * History.getBaseHref() * Fetches the `href` attribute of the `` element if it exists * @return {String} baseHref */ History.getBaseHref = function(){ // Create var baseElements = document.getElementsByTagName('base'), baseElement = null, baseHref = ''; // Test for Base Element if ( baseElements.length === 1 ) { // Prepare for Base Element baseElement = baseElements[0]; baseHref = baseElement.href.replace(/[^\/]+$/,''); } // Adjust trailing slash baseHref = baseHref.replace(/\/+$/,''); if ( baseHref ) baseHref += '/'; // Return return baseHref; }; /** * History.getBaseUrl() * Fetches the baseHref or basePageUrl or rootUrl (whichever one exists first) * @return {String} baseUrl */ History.getBaseUrl = function(){ // Create var baseUrl = History.getBaseHref()||History.getBasePageUrl()||History.getRootUrl(); // Return return baseUrl; }; /** * History.getPageUrl() * Fetches the URL of the current page * @return {String} pageUrl */ History.getPageUrl = function(){ // Fetch var State = History.getState(false,false), stateUrl = (State||{}).url||History.getLocationHref(), pageUrl; // Create pageUrl = stateUrl.replace(/\/+$/,'').replace(/[^\/]+$/,function(part,index,string){ return (/\./).test(part) ? part : part+'/'; }); // Return return pageUrl; }; /** * History.getBasePageUrl() * Fetches the Url of the directory of the current page * @return {String} basePageUrl */ History.getBasePageUrl = function(){ // Create var basePageUrl = (History.getLocationHref()).replace(/[#\?].*/,'').replace(/[^\/]+$/,function(part,index,string){ return (/[^\/]$/).test(part) ? '' : part; }).replace(/\/+$/,'')+'/'; // Return return basePageUrl; }; /** * History.getFullUrl(url) * Ensures that we have an absolute URL and not a relative URL * @param {string} url * @param {Boolean} allowBaseHref * @return {string} fullUrl */ History.getFullUrl = function(url,allowBaseHref){ // Prepare var fullUrl = url, firstChar = url.substring(0,1); allowBaseHref = (typeof allowBaseHref === 'undefined') ? true : allowBaseHref; // Check if ( /[a-z]+\:\/\//.test(url) ) { // Full URL } else if ( firstChar === '/' ) { // Root URL fullUrl = History.getRootUrl()+url.replace(/^\/+/,''); } else if ( firstChar === '#' ) { // Anchor URL fullUrl = History.getPageUrl().replace(/#.*/,'')+url; } else if ( firstChar === '?' ) { // Query URL fullUrl = History.getPageUrl().replace(/[\?#].*/,'')+url; } else { // Relative URL if ( allowBaseHref ) { fullUrl = History.getBaseUrl()+url.replace(/^(\.\/)+/,''); } else { fullUrl = History.getBasePageUrl()+url.replace(/^(\.\/)+/,''); } // We have an if condition above as we do not want hashes // which are relative to the baseHref in our URLs // as if the baseHref changes, then all our bookmarks // would now point to different locations // whereas the basePageUrl will always stay the same } // Return return fullUrl.replace(/\#$/,''); }; /** * History.getShortUrl(url) * Ensures that we have a relative URL and not a absolute URL * @param {string} url * @return {string} url */ History.getShortUrl = function(url){ // Prepare var shortUrl = url, baseUrl = History.getBaseUrl(), rootUrl = History.getRootUrl(); // Trim baseUrl if ( History.emulated.pushState ) { // We are in a if statement as when pushState is not emulated // The actual url these short urls are relative to can change // So within the same session, we the url may end up somewhere different shortUrl = shortUrl.replace(baseUrl,''); } // Trim rootUrl shortUrl = shortUrl.replace(rootUrl,'/'); // Ensure we can still detect it as a state if ( History.isTraditionalAnchor(shortUrl) ) { shortUrl = './'+shortUrl; } // Clean It shortUrl = shortUrl.replace(/^(\.\/)+/g,'./').replace(/\#$/,''); // Return return shortUrl; }; /** * History.getLocationHref(document) * Returns a normalized version of document.location.href * accounting for browser inconsistencies, etc. * * This URL will be URI-encoded and will include the hash * * @param {object} document * @return {string} url */ History.getLocationHref = function(doc) { doc = doc || document; // most of the time, this will be true if (doc.URL === doc.location.href) return doc.location.href; // some versions of webkit URI-decode document.location.href // but they leave document.URL in an encoded state if (doc.location.href === decodeURIComponent(doc.URL)) return doc.URL; // FF 3.6 only updates document.URL when a page is reloaded // document.location.href is updated correctly if (doc.location.hash && decodeURIComponent(doc.location.href.replace(/^[^#]+/, "")) === doc.location.hash) return doc.location.href; if (doc.URL.indexOf('#') == -1 && doc.location.href.indexOf('#') != -1) return doc.location.href; return doc.URL || doc.location.href; }; // ==================================================================== // State Storage /** * History.store * The store for all session specific data */ History.store = {}; /** * History.idToState * 1-1: State ID to State Object */ History.idToState = History.idToState||{}; /** * History.stateToId * 1-1: State String to State ID */ History.stateToId = History.stateToId||{}; /** * History.urlToId * 1-1: State URL to State ID */ History.urlToId = History.urlToId||{}; /** * History.storedStates * Store the states in an array */ History.storedStates = History.storedStates||[]; /** * History.savedStates * Saved the states in an array */ History.savedStates = History.savedStates||[]; /** * History.noramlizeStore() * Noramlize the store by adding necessary values */ History.normalizeStore = function(){ History.store.idToState = History.store.idToState||{}; History.store.urlToId = History.store.urlToId||{}; History.store.stateToId = History.store.stateToId||{}; }; /** * History.getState() * Get an object containing the data, title and url of the current state * @param {Boolean} friendly * @param {Boolean} create * @return {Object} State */ History.getState = function(friendly,create){ // Prepare if ( typeof friendly === 'undefined' ) { friendly = true; } if ( typeof create === 'undefined' ) { create = true; } // Fetch var State = History.getLastSavedState(); // Create if ( !State && create ) { State = History.createStateObject(); } // Adjust if ( friendly ) { State = History.cloneObject(State); State.url = State.cleanUrl||State.url; } // Return return State; }; /** * History.getIdByState(State) * Gets a ID for a State * @param {State} newState * @return {String} id */ History.getIdByState = function(newState){ // Fetch ID var id = History.extractId(newState.url), str; if ( !id ) { // Find ID via State String str = History.getStateString(newState); if ( typeof History.stateToId[str] !== 'undefined' ) { id = History.stateToId[str]; } else if ( typeof History.store.stateToId[str] !== 'undefined' ) { id = History.store.stateToId[str]; } else { // Generate a new ID while ( true ) { id = (new Date()).getTime() + String(Math.random()).replace(/\D/g,''); if ( typeof History.idToState[id] === 'undefined' && typeof History.store.idToState[id] === 'undefined' ) { break; } } // Apply the new State to the ID History.stateToId[str] = id; History.idToState[id] = newState; } } // Return ID return id; }; /** * History.normalizeState(State) * Expands a State Object * @param {object} State * @return {object} */ History.normalizeState = function(oldState){ // Variables var newState, dataNotEmpty; // Prepare if ( !oldState || (typeof oldState !== 'object') ) { oldState = {}; } // Check if ( typeof oldState.normalized !== 'undefined' ) { return oldState; } // Adjust if ( !oldState.data || (typeof oldState.data !== 'object') ) { oldState.data = {}; } // ---------------------------------------------------------------- // Create newState = {}; newState.normalized = true; newState.title = oldState.title||''; newState.url = History.getFullUrl(oldState.url?oldState.url:(History.getLocationHref())); newState.hash = History.getShortUrl(newState.url); newState.data = History.cloneObject(oldState.data); // Fetch ID newState.id = History.getIdByState(newState); // ---------------------------------------------------------------- // Clean the URL newState.cleanUrl = newState.url.replace(/\??\&_suid.*/,''); newState.url = newState.cleanUrl; // Check to see if we have more than just a url dataNotEmpty = !History.isEmptyObject(newState.data); // Apply if ( (newState.title || dataNotEmpty) && History.options.disableSuid !== true ) { // Add ID to Hash newState.hash = History.getShortUrl(newState.url).replace(/\??\&_suid.*/,''); if ( !/\?/.test(newState.hash) ) { newState.hash += '?'; } newState.hash += '&_suid='+newState.id; } // Create the Hashed URL newState.hashedUrl = History.getFullUrl(newState.hash); // ---------------------------------------------------------------- // Update the URL if we have a duplicate if ( (History.emulated.pushState || History.bugs.safariPoll) && History.hasUrlDuplicate(newState) ) { newState.url = newState.hashedUrl; } // ---------------------------------------------------------------- // Return return newState; }; /** * History.createStateObject(data,title,url) * Creates a object based on the data, title and url state params * @param {object} data * @param {string} title * @param {string} url * @return {object} */ History.createStateObject = function(data,title,url){ // Hashify var State = { 'data': data, 'title': title, 'url': url }; // Expand the State State = History.normalizeState(State); // Return object return State; }; /** * History.getStateById(id) * Get a state by it's UID * @param {String} id */ History.getStateById = function(id){ // Prepare id = String(id); // Retrieve var State = History.idToState[id] || History.store.idToState[id] || undefined; // Return State return State; }; /** * Get a State's String * @param {State} passedState */ History.getStateString = function(passedState){ // Prepare var State, cleanedState, str; // Fetch State = History.normalizeState(passedState); // Clean cleanedState = { data: State.data, title: passedState.title, url: passedState.url }; // Fetch str = JSON.stringify(cleanedState); // Return return str; }; /** * Get a State's ID * @param {State} passedState * @return {String} id */ History.getStateId = function(passedState){ // Prepare var State, id; // Fetch State = History.normalizeState(passedState); // Fetch id = State.id; // Return return id; }; /** * History.getHashByState(State) * Creates a Hash for the State Object * @param {State} passedState * @return {String} hash */ History.getHashByState = function(passedState){ // Prepare var State, hash; // Fetch State = History.normalizeState(passedState); // Hash hash = State.hash; // Return return hash; }; /** * History.extractId(url_or_hash) * Get a State ID by it's URL or Hash * @param {string} url_or_hash * @return {string} id */ History.extractId = function ( url_or_hash ) { // Prepare var id,parts,url, tmp; // Extract // If the URL has a #, use the id from before the # if (url_or_hash.indexOf('#') != -1) { tmp = url_or_hash.split("#")[0]; } else { tmp = url_or_hash; } parts = /(.*)\&_suid=([0-9]+)$/.exec(tmp); url = parts ? (parts[1]||url_or_hash) : url_or_hash; id = parts ? String(parts[2]||'') : ''; // Return return id||false; }; /** * History.isTraditionalAnchor * Checks to see if the url is a traditional anchor or not * @param {String} url_or_hash * @return {Boolean} */ History.isTraditionalAnchor = function(url_or_hash){ // Check var isTraditional = !(/[\/\?\.]/.test(url_or_hash)); // Return return isTraditional; }; /** * History.extractState * Get a State by it's URL or Hash * @param {String} url_or_hash * @return {State|null} */ History.extractState = function(url_or_hash,create){ // Prepare var State = null, id, url; create = create||false; // Fetch SUID id = History.extractId(url_or_hash); if ( id ) { State = History.getStateById(id); } // Fetch SUID returned no State if ( !State ) { // Fetch URL url = History.getFullUrl(url_or_hash); // Check URL id = History.getIdByUrl(url)||false; if ( id ) { State = History.getStateById(id); } // Create State if ( !State && create && !History.isTraditionalAnchor(url_or_hash) ) { State = History.createStateObject(null,null,url); } } // Return return State; }; /** * History.getIdByUrl() * Get a State ID by a State URL */ History.getIdByUrl = function(url){ // Fetch var id = History.urlToId[url] || History.store.urlToId[url] || undefined; // Return return id; }; /** * History.getLastSavedState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastSavedState = function(){ return History.savedStates[History.savedStates.length-1]||undefined; }; /** * History.getLastStoredState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastStoredState = function(){ return History.storedStates[History.storedStates.length-1]||undefined; }; /** * History.hasUrlDuplicate * Checks if a Url will have a url conflict * @param {Object} newState * @return {Boolean} hasDuplicate */ History.hasUrlDuplicate = function(newState) { // Prepare var hasDuplicate = false, oldState; // Fetch oldState = History.extractState(newState.url); // Check hasDuplicate = oldState && oldState.id !== newState.id; // Return return hasDuplicate; }; /** * History.storeState * Store a State * @param {Object} newState * @return {Object} newState */ History.storeState = function(newState){ // Store the State History.urlToId[newState.url] = newState.id; // Push the State History.storedStates.push(History.cloneObject(newState)); // Return newState return newState; }; /** * History.isLastSavedState(newState) * Tests to see if the state is the last state * @param {Object} newState * @return {boolean} isLast */ History.isLastSavedState = function(newState){ // Prepare var isLast = false, newId, oldState, oldId; // Check if ( History.savedStates.length ) { newId = newState.id; oldState = History.getLastSavedState(); oldId = oldState.id; // Check isLast = (newId === oldId); } // Return return isLast; }; /** * History.saveState * Push a State * @param {Object} newState * @return {boolean} changed */ History.saveState = function(newState){ // Check Hash if ( History.isLastSavedState(newState) ) { return false; } // Push the State History.savedStates.push(History.cloneObject(newState)); // Return true return true; }; /** * History.getStateByIndex() * Gets a state by the index * @param {integer} index * @return {Object} */ History.getStateByIndex = function(index){ // Prepare var State = null; // Handle if ( typeof index === 'undefined' ) { // Get the last inserted State = History.savedStates[History.savedStates.length-1]; } else if ( index < 0 ) { // Get from the end State = History.savedStates[History.savedStates.length+index]; } else { // Get from the beginning State = History.savedStates[index]; } // Return State return State; }; /** * History.getCurrentIndex() * Gets the current index * @return (integer) */ History.getCurrentIndex = function(){ // Prepare var index = null; // No states saved if(History.savedStates.length < 1) { index = 0; } else { index = History.savedStates.length-1; } return index; }; // ==================================================================== // Hash Helpers /** * History.getHash() * @param {Location=} location * Gets the current document hash * Note: unlike location.hash, this is guaranteed to return the escaped hash in all browsers * @return {string} */ History.getHash = function(doc){ var url = History.getLocationHref(doc), hash; hash = History.getHashByUrl(url); return hash; }; /** * History.unescapeHash() * normalize and Unescape a Hash * @param {String} hash * @return {string} */ History.unescapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Unescape hash result = decodeURIComponent(result); // Return result return result; }; /** * History.normalizeHash() * normalize a hash across browsers * @return {string} */ History.normalizeHash = function(hash){ // Prepare var result = hash.replace(/[^#]*#/,'').replace(/#.*/, ''); // Return result return result; }; /** * History.setHash(hash) * Sets the document hash * @param {string} hash * @return {History} */ History.setHash = function(hash,queue){ // Prepare var State, pageUrl; // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.setHash: we must wait', arguments); History.pushQueue({ scope: History, callback: History.setHash, args: arguments, queue: queue }); return false; } // Log //History.debug('History.setHash: called',hash); // Make Busy + Continue History.busy(true); // Check if hash is a state State = History.extractState(hash,true); if ( State && !History.emulated.pushState ) { // Hash is a state so skip the setHash //History.debug('History.setHash: Hash is a state so skipping the hash set with a direct pushState call',arguments); // PushState History.pushState(State.data,State.title,State.url,false); } else if ( History.getHash() !== hash ) { // Hash is a proper hash, so apply it // Handle browser bugs if ( History.bugs.setHash ) { // Fix Safari Bug https://bugs.webkit.org/show_bug.cgi?id=56249 // Fetch the base page pageUrl = History.getPageUrl(); // Safari hash apply History.pushState(null,null,pageUrl+'#'+hash,false); } else { // Normal hash apply document.location.hash = hash; } } // Chain return History; }; /** * History.escape() * normalize and Escape a Hash * @return {string} */ History.escapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Escape hash result = window.encodeURIComponent(result); // IE6 Escape Bug if ( !History.bugs.hashEscape ) { // Restore common parts result = result .replace(/\%21/g,'!') .replace(/\%26/g,'&') .replace(/\%3D/g,'=') .replace(/\%3F/g,'?'); } // Return result return result; }; /** * History.getHashByUrl(url) * Extracts the Hash from a URL * @param {string} url * @return {string} url */ History.getHashByUrl = function(url){ // Extract the hash var hash = String(url) .replace(/([^#]*)#?([^#]*)#?(.*)/, '$2') ; // Unescape hash hash = History.unescapeHash(hash); // Return hash return hash; }; /** * History.setTitle(title) * Applies the title to the document * @param {State} newState * @return {Boolean} */ History.setTitle = function(newState){ // Prepare var title = newState.title, firstState; // Initial if ( !title ) { firstState = History.getStateByIndex(0); if ( firstState && firstState.url === newState.url ) { title = firstState.title||History.options.initialTitle; } } // Apply try { document.getElementsByTagName('title')[0].innerHTML = title.replace('<','<').replace('>','>').replace(' & ',' & '); } catch ( Exception ) { } document.title = title; // Chain return History; }; // ==================================================================== // Queueing /** * History.queues * The list of queues to use * First In, First Out */ History.queues = []; /** * History.busy(value) * @param {boolean} value [optional] * @return {boolean} busy */ History.busy = function(value){ // Apply if ( typeof value !== 'undefined' ) { //History.debug('History.busy: changing ['+(History.busy.flag||false)+'] to ['+(value||false)+']', History.queues.length); History.busy.flag = value; } // Default else if ( typeof History.busy.flag === 'undefined' ) { History.busy.flag = false; } // Queue if ( !History.busy.flag ) { // Execute the next item in the queue clearTimeout(History.busy.timeout); var fireNext = function(){ var i, queue, item; if ( History.busy.flag ) return; for ( i=History.queues.length-1; i >= 0; --i ) { queue = History.queues[i]; if ( queue.length === 0 ) continue; item = queue.shift(); History.fireQueueItem(item); History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } }; History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } // Return return History.busy.flag; }; /** * History.busy.flag */ History.busy.flag = false; /** * History.fireQueueItem(item) * Fire a Queue Item * @param {Object} item * @return {Mixed} result */ History.fireQueueItem = function(item){ return item.callback.apply(item.scope||History,item.args||[]); }; /** * History.pushQueue(callback,args) * Add an item to the queue * @param {Object} item [scope,callback,args,queue] */ History.pushQueue = function(item){ // Prepare the queue History.queues[item.queue||0] = History.queues[item.queue||0]||[]; // Add to the queue History.queues[item.queue||0].push(item); // Chain return History; }; /** * History.queue (item,queue), (func,queue), (func), (item) * Either firs the item now if not busy, or adds it to the queue */ History.queue = function(item,queue){ // Prepare if ( typeof item === 'function' ) { item = { callback: item }; } if ( typeof queue !== 'undefined' ) { item.queue = queue; } // Handle if ( History.busy() ) { History.pushQueue(item); } else { History.fireQueueItem(item); } // Chain return History; }; /** * History.clearQueue() * Clears the Queue */ History.clearQueue = function(){ History.busy.flag = false; History.queues = []; return History; }; // ==================================================================== // IE Bug Fix /** * History.stateChanged * States whether or not the state has changed since the last double check was initialised */ History.stateChanged = false; /** * History.doubleChecker * Contains the timeout used for the double checks */ History.doubleChecker = false; /** * History.doubleCheckComplete() * Complete a double check * @return {History} */ History.doubleCheckComplete = function(){ // Update History.stateChanged = true; // Clear History.doubleCheckClear(); // Chain return History; }; /** * History.doubleCheckClear() * Clear a double check * @return {History} */ History.doubleCheckClear = function(){ // Clear if ( History.doubleChecker ) { clearTimeout(History.doubleChecker); History.doubleChecker = false; } // Chain return History; }; /** * History.doubleCheck() * Create a double check * @return {History} */ History.doubleCheck = function(tryAgain){ // Reset History.stateChanged = false; History.doubleCheckClear(); // Fix IE6,IE7 bug where calling history.back or history.forward does not actually change the hash (whereas doing it manually does) // Fix Safari 5 bug where sometimes the state does not change: https://bugs.webkit.org/show_bug.cgi?id=42940 if ( History.bugs.ieDoubleCheck ) { // Apply Check History.doubleChecker = setTimeout( function(){ History.doubleCheckClear(); if ( !History.stateChanged ) { //History.debug('History.doubleCheck: State has not yet changed, trying again', arguments); // Re-Attempt tryAgain(); } return true; }, History.options.doubleCheckInterval ); } // Chain return History; }; // ==================================================================== // Safari Bug Fix /** * History.safariStatePoll() * Poll the current state * @return {History} */ History.safariStatePoll = function(){ // Poll the URL // Get the Last State which has the new URL var urlState = History.extractState(History.getLocationHref()), newState; // Check for a difference if ( !History.isLastSavedState(urlState) ) { newState = urlState; } else { return; } // Check if we have a state with that url // If not create it if ( !newState ) { //History.debug('History.safariStatePoll: new'); newState = History.createStateObject(); } // Apply the New State //History.debug('History.safariStatePoll: trigger'); History.Adapter.trigger(window,'popstate'); // Chain return History; }; // ==================================================================== // State Aliases /** * History.back(queue) * Send the browser history back one item * @param {Integer} queue [optional] */ History.back = function(queue){ //History.debug('History.back: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.back: we must wait', arguments); History.pushQueue({ scope: History, callback: History.back, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.back(false); }); // Go back history.go(-1); // End back closure return true; }; /** * History.forward(queue) * Send the browser history forward one item * @param {Integer} queue [optional] */ History.forward = function(queue){ //History.debug('History.forward: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.forward: we must wait', arguments); History.pushQueue({ scope: History, callback: History.forward, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.forward(false); }); // Go forward history.go(1); // End forward closure return true; }; /** * History.go(index,queue) * Send the browser history back or forward index times * @param {Integer} queue [optional] */ History.go = function(index,queue){ //History.debug('History.go: called', arguments); // Prepare var i; // Handle if ( index > 0 ) { // Forward for ( i=1; i<=index; ++i ) { History.forward(queue); } } else if ( index < 0 ) { // Backward for ( i=-1; i>=index; --i ) { History.back(queue); } } else { throw new Error('History.go: History.go requires a positive or negative integer passed.'); } // Chain return History; }; // ==================================================================== // HTML5 State Support // Non-Native pushState Implementation if ( History.emulated.pushState ) { /* * Provide Skeleton for HTML4 Browsers */ // Prepare var emptyFunction = function(){}; History.pushState = History.pushState||emptyFunction; History.replaceState = History.replaceState||emptyFunction; } // History.emulated.pushState // Native pushState Implementation else { /* * Use native HTML5 History API Implementation */ /** * History.onPopState(event,extra) * Refresh the Current State */ History.onPopState = function(event,extra){ // Prepare var stateId = false, newState = false, currentHash, currentState; // Reset the double check History.doubleCheckComplete(); // Check for a Hash, and handle apporiatly currentHash = History.getHash(); if ( currentHash ) { // Expand Hash currentState = History.extractState(currentHash||History.getLocationHref(),true); if ( currentState ) { // We were able to parse it, it must be a State! // Let's forward to replaceState //History.debug('History.onPopState: state anchor', currentHash, currentState); History.replaceState(currentState.data, currentState.title, currentState.url, false); } else { // Traditional Anchor //History.debug('History.onPopState: traditional anchor', currentHash); History.Adapter.trigger(window,'anchorchange'); History.busy(false); } // We don't care for hashes History.expectedStateId = false; return false; } // Ensure stateId = History.Adapter.extractEventData('state',event,extra) || false; // Fetch State if ( stateId ) { // Vanilla: Back/forward button was used newState = History.getStateById(stateId); } else if ( History.expectedStateId ) { // Vanilla: A new state was pushed, and popstate was called manually newState = History.getStateById(History.expectedStateId); } else { // Initial State newState = History.extractState(History.getLocationHref()); } // The State did not exist in our store if ( !newState ) { // Regenerate the State newState = History.createStateObject(null,null,History.getLocationHref()); } // Clean History.expectedStateId = false; // Check if we are the same state if ( History.isLastSavedState(newState) ) { // There has been no change (just the page's hash has finally propagated) //History.debug('History.onPopState: no change', newState, History.savedStates); History.busy(false); return false; } // Store the State History.storeState(newState); History.saveState(newState); // Force update of the title History.setTitle(newState); // Fire Our Event History.Adapter.trigger(window,'statechange'); History.busy(false); // Return true return true; }; History.Adapter.bind(window,'popstate',History.onPopState); /** * History.pushState(data,title,url) * Add a new State to the history object, become it, and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.pushState = function(data,title,url,queue){ //History.debug('History.pushState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.pushState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.pushState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.pushState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End pushState closure return true; }; /** * History.replaceState(data,title,url) * Replace the State and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.replaceState = function(data,title,url,queue){ //History.debug('History.replaceState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.replaceState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.replaceState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.replaceState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End replaceState closure return true; }; } // !History.emulated.pushState // ==================================================================== // Initialise /** * Load the Store */ if ( sessionStorage ) { // Fetch try { History.store = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { History.store = {}; } // Normalize History.normalizeStore(); } else { // Default Load History.store = {}; History.normalizeStore(); } /** * Clear Intervals on exit to prevent memory leaks */ History.Adapter.bind(window,"unload",History.clearAllIntervals); /** * Create the initial State */ History.saveState(History.storeState(History.extractState(History.getLocationHref(),true))); /** * Bind for Saving Store */ if ( sessionStorage ) { // When the page is closed History.onUnload = function(){ // Prepare var currentStore, item, currentStoreString; // Fetch try { currentStore = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { currentStore = {}; } // Ensure currentStore.idToState = currentStore.idToState || {}; currentStore.urlToId = currentStore.urlToId || {}; currentStore.stateToId = currentStore.stateToId || {}; // Sync for ( item in History.idToState ) { if ( !History.idToState.hasOwnProperty(item) ) { continue; } currentStore.idToState[item] = History.idToState[item]; } for ( item in History.urlToId ) { if ( !History.urlToId.hasOwnProperty(item) ) { continue; } currentStore.urlToId[item] = History.urlToId[item]; } for ( item in History.stateToId ) { if ( !History.stateToId.hasOwnProperty(item) ) { continue; } currentStore.stateToId[item] = History.stateToId[item]; } // Update History.store = currentStore; History.normalizeStore(); // In Safari, going into Private Browsing mode causes the // Session Storage object to still exist but if you try and use // or set any property/function of it it throws the exception // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to // add something to storage that exceeded the quota." infinitely // every second. currentStoreString = JSON.stringify(currentStore); try { // Store sessionStorage.setItem('History.store', currentStoreString); } catch (e) { if (e.code === DOMException.QUOTA_EXCEEDED_ERR) { if (sessionStorage.length) { // Workaround for a bug seen on iPads. Sometimes the quota exceeded error comes up and simply // removing/resetting the storage can work. sessionStorage.removeItem('History.store'); sessionStorage.setItem('History.store', currentStoreString); } else { // Otherwise, we're probably private browsing in Safari, so we'll ignore the exception. } } else { throw e; } } }; // For Internet Explorer History.intervalList.push(setInterval(History.onUnload,History.options.storeInterval)); // For Other Browsers History.Adapter.bind(window,'beforeunload',History.onUnload); History.Adapter.bind(window,'unload',History.onUnload); // Both are enabled for consistency } // Non-Native pushState Implementation if ( !History.emulated.pushState ) { // Be aware, the following is only for native pushState implementations // If you are wanting to include something for all browsers // Then include it above this if block /** * Setup Safari Fix */ if ( History.bugs.safariPoll ) { History.intervalList.push(setInterval(History.safariStatePoll, History.options.safariPollInterval)); } /** * Ensure Cross Browser Compatibility */ if ( navigator.vendor === 'Apple Computer, Inc.' || (navigator.appCodeName||'') === 'Mozilla' ) { /** * Fix Safari HashChange Issue */ // Setup Alias History.Adapter.bind(window,'hashchange',function(){ History.Adapter.trigger(window,'popstate'); }); // Initialise Alias if ( History.getHash() ) { History.Adapter.onDomLoad(function(){ History.Adapter.trigger(window,'hashchange'); }); } } } // !History.emulated.pushState }; // History.initCore // Try to Initialise History if (!History.options || !History.options.delayInit) { History.init(); } })(window); ================================================ FILE: scripts/bundled-uncompressed/html4+html5/right.history.js ================================================ /* json2.js 2012-10-08 Public Domain. NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. See http://www.JSON.org/js.html This code should be minified before deployment. See http://javascript.crockford.com/jsmin.html USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO NOT CONTROL. This file creates a global JSON object containing two methods: stringify and parse. JSON.stringify(value, replacer, space) value any JavaScript value, usually an object or array. replacer an optional parameter that determines how object values are stringified for objects. It can be a function or an array of strings. space an optional parameter that specifies the indentation of nested structures. If it is omitted, the text will be packed without extra whitespace. If it is a number, it will specify the number of spaces to indent at each level. If it is a string (such as '\t' or ' '), it contains the characters used to indent at each level. This method produces a JSON text from a JavaScript value. When an object value is found, if the object contains a toJSON method, its toJSON method will be called and the result will be stringified. A toJSON method does not serialize: it returns the value represented by the name/value pair that should be serialized, or undefined if nothing should be serialized. The toJSON method will be passed the key associated with the value, and this will be bound to the value For example, this would serialize Dates as ISO strings. Date.prototype.toJSON = function (key) { function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } return this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z'; }; You can provide an optional replacer method. It will be passed the key and value of each member, with this bound to the containing object. The value that is returned from your method will be serialized. If your method returns undefined, then the member will be excluded from the serialization. If the replacer parameter is an array of strings, then it will be used to select the members to be serialized. It filters the results such that only members with keys listed in the replacer array are stringified. Values that do not have JSON representations, such as undefined or functions, will not be serialized. Such values in objects will be dropped; in arrays they will be replaced with null. You can use a replacer function to replace those with JSON values. JSON.stringify(undefined) returns undefined. The optional space parameter produces a stringification of the value that is filled with line breaks and indentation to make it easier to read. If the space parameter is a non-empty string, then that string will be used for indentation. If the space parameter is a number, then the indentation will be that many spaces. Example: text = JSON.stringify(['e', {pluribus: 'unum'}]); // text is '["e",{"pluribus":"unum"}]' text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' text = JSON.stringify([new Date()], function (key, value) { return this[key] instanceof Date ? 'Date(' + this[key] + ')' : value; }); // text is '["Date(---current time---)"]' JSON.parse(text, reviver) This method parses a JSON text to produce an object or array. It can throw a SyntaxError exception. The optional reviver parameter is a function that can filter and transform the results. It receives each of the keys and values, and its return value is used instead of the original value. If it returns what it received, then the structure is not modified. If it returns undefined then the member is deleted. Example: // Parse the text. Values that look like ISO date strings will // be converted to Date objects. myData = JSON.parse(text, function (key, value) { var a; if (typeof value === 'string') { a = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); if (a) { return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6])); } } return value; }); myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { var d; if (typeof value === 'string' && value.slice(0, 5) === 'Date(' && value.slice(-1) === ')') { d = new Date(value.slice(5, -1)); if (d) { return d; } } return value; }); This is a reference implementation. You are free to copy, modify, or redistribute. */ /*jslint evil: true, regexp: true */ /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, lastIndex, length, parse, prototype, push, replace, slice, stringify, test, toJSON, toString, valueOf */ // Create a JSON object only if one does not already exist. We create the // methods in a closure to avoid creating global variables. if (typeof JSON !== 'object') { JSON = {}; } (function () { 'use strict'; function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } if (typeof Date.prototype.toJSON !== 'function') { Date.prototype.toJSON = function (key) { return isFinite(this.valueOf()) ? this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z' : null; }; String.prototype.toJSON = Number.prototype.toJSON = Boolean.prototype.toJSON = function (key) { return this.valueOf(); }; } var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, gap, indent, meta = { // table of character substitutions '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\' }, rep; function quote(string) { // If the string contains no control characters, no quote characters, and no // backslash characters, then we can safely slap some quotes around it. // Otherwise we must also replace the offending characters with safe escape // sequences. escapable.lastIndex = 0; return escapable.test(string) ? '"' + string.replace(escapable, function (a) { var c = meta[a]; return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }) + '"' : '"' + string + '"'; } function str(key, holder) { // Produce a string from holder[key]. var i, // The loop counter. k, // The member key. v, // The member value. length, mind = gap, partial, value = holder[key]; // If the value has a toJSON method, call it to obtain a replacement value. if (value && typeof value === 'object' && typeof value.toJSON === 'function') { value = value.toJSON(key); } // If we were called with a replacer function, then call the replacer to // obtain a replacement value. if (typeof rep === 'function') { value = rep.call(holder, key, value); } // What happens next depends on the value's type. switch (typeof value) { case 'string': return quote(value); case 'number': // JSON numbers must be finite. Encode non-finite numbers as null. return isFinite(value) ? String(value) : 'null'; case 'boolean': case 'null': // If the value is a boolean or null, convert it to a string. Note: // typeof null does not produce 'null'. The case is included here in // the remote chance that this gets fixed someday. return String(value); // If the type is 'object', we might be dealing with an object or an array or // null. case 'object': // Due to a specification blunder in ECMAScript, typeof null is 'object', // so watch out for that case. if (!value) { return 'null'; } // Make an array to hold the partial results of stringifying this object value. gap += indent; partial = []; // Is the value an array? if (Object.prototype.toString.apply(value) === '[object Array]') { // The value is an array. Stringify every element. Use null as a placeholder // for non-JSON values. length = value.length; for (i = 0; i < length; i += 1) { partial[i] = str(i, value) || 'null'; } // Join all of the elements together, separated with commas, and wrap them in // brackets. v = partial.length === 0 ? '[]' : gap ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : '[' + partial.join(',') + ']'; gap = mind; return v; } // If the replacer is an array, use it to select the members to be stringified. if (rep && typeof rep === 'object') { length = rep.length; for (i = 0; i < length; i += 1) { if (typeof rep[i] === 'string') { k = rep[i]; v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } else { // Otherwise, iterate through all of the keys in the object. for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } // Join all of the member texts together, separated with commas, // and wrap them in braces. v = partial.length === 0 ? '{}' : gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : '{' + partial.join(',') + '}'; gap = mind; return v; } } // If the JSON object does not yet have a stringify method, give it one. if (typeof JSON.stringify !== 'function') { JSON.stringify = function (value, replacer, space) { // The stringify method takes a value and an optional replacer, and an optional // space parameter, and returns a JSON text. The replacer can be a function // that can replace values, or an array of strings that will select the keys. // A default replacer method can be provided. Use of the space parameter can // produce text that is more easily readable. var i; gap = ''; indent = ''; // If the space parameter is a number, make an indent string containing that // many spaces. if (typeof space === 'number') { for (i = 0; i < space; i += 1) { indent += ' '; } // If the space parameter is a string, it will be used as the indent string. } else if (typeof space === 'string') { indent = space; } // If there is a replacer, it must be a function or an array. // Otherwise, throw an error. rep = replacer; if (replacer && typeof replacer !== 'function' && (typeof replacer !== 'object' || typeof replacer.length !== 'number')) { throw new Error('JSON.stringify'); } // Make a fake root object containing our value under the key of ''. // Return the result of stringifying the value. return str('', {'': value}); }; } // If the JSON object does not yet have a parse method, give it one. if (typeof JSON.parse !== 'function') { JSON.parse = function (text, reviver) { // The parse method takes a text and an optional reviver function, and returns // a JavaScript value if the text is a valid JSON text. var j; function walk(holder, key) { // The walk method is used to recursively walk the resulting structure so // that modifications can be made. var k, v, value = holder[key]; if (value && typeof value === 'object') { for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = walk(value, k); if (v !== undefined) { value[k] = v; } else { delete value[k]; } } } } return reviver.call(holder, key, value); } // Parsing happens in four stages. In the first stage, we replace certain // Unicode characters with escape sequences. JavaScript handles many characters // incorrectly, either silently deleting them, or treating them as line endings. text = String(text); cx.lastIndex = 0; if (cx.test(text)) { text = text.replace(cx, function (a) { return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }); } // In the second stage, we run the text against regular expressions that look // for non-JSON patterns. We are especially concerned with '()' and 'new' // because they can cause invocation, and '=' because it can cause mutation. // But just to be safe, we want to reject all unexpected forms. // We split the second stage into 4 regexp operations in order to work around // crippling inefficiencies in IE's and Safari's regexp engines. First we // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we // replace all simple value tokens with ']' characters. Third, we delete all // open brackets that follow a colon or comma or that begin the text. Finally, // we look to see that the remaining characters are only whitespace or ']' or // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. if (/^[\],:{}\s]*$/ .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { // In the third stage we use the eval function to compile the text into a // JavaScript structure. The '{' operator is subject to a syntactic ambiguity // in JavaScript: it can begin a block or an object literal. We wrap the text // in parens to eliminate the ambiguity. j = eval('(' + text + ')'); // In the optional fourth stage, we recursively walk the new structure, passing // each name/value pair to a reviver function for possible transformation. return typeof reviver === 'function' ? walk({'': j}, '') : j; } // If the text is not JSON parseable, then a SyntaxError is thrown. throw new SyntaxError('JSON.parse'); }; } }());/** * History.js RightJS Adapter * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ // Closure (function(window,undefined){ "use strict"; // Localise Globals var History = window.History = window.History||{}, document = window.document, RightJS = window.RightJS, $ = RightJS.$; // Check Existence if ( typeof History.Adapter !== 'undefined' ) { throw new Error('History.js Adapter has already been loaded...'); } // Add the Adapter History.Adapter = { /** * History.Adapter.bind(el,event,callback) * @param {Element|Selector} el * @param {String} event - custom and standard events * @param {Function} callback * @return */ bind: function(el,event,callback){ $(el).on(event,callback); }, /** * History.Adapter.trigger(el,event) * @param {Element|Selector} el * @param {String} event - custom and standard events * @param {Object} extraEventData - a object of extra event data * @return */ trigger: function(el,event,extraEventData){ $(el).fire(event,extraEventData); }, /** * History.Adapter.extractEventData(key,event,extra) * @param {String} key - key for the event data to extract * @param {String} event - custom and standard events * @return {mixed} */ extractEventData: function(key,event){ // Right.js Native // Right.js Custom var result = (event && event._ && event._[key]) || undefined; // Return return result; }, /** * History.Adapter.onDomLoad(callback) * @param {Function} callback * @return */ onDomLoad: function(callback) { $(document).onReady(callback); } }; // Try and Initialise History if ( typeof History.init !== 'undefined' ) { History.init(); } })(window); /** * History.js HTML4 Support * Depends on the HTML5 Support * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window,undefined){ "use strict"; // ======================================================================== // Initialise // Localise Globals var document = window.document, // Make sure we are using the correct document setTimeout = window.setTimeout||setTimeout, clearTimeout = window.clearTimeout||clearTimeout, setInterval = window.setInterval||setInterval, History = window.History = window.History||{}; // Public History Object // Check Existence if ( typeof History.initHtml4 !== 'undefined' ) { throw new Error('History.js HTML4 Support has already been loaded...'); } // ======================================================================== // Initialise HTML4 Support // Initialise HTML4 Support History.initHtml4 = function(){ // Initialise if ( typeof History.initHtml4.initialized !== 'undefined' ) { // Already Loaded return false; } else { History.initHtml4.initialized = true; } // ==================================================================== // Properties /** * History.enabled * Is History enabled? */ History.enabled = true; // ==================================================================== // Hash Storage /** * History.savedHashes * Store the hashes in an array */ History.savedHashes = []; /** * History.isLastHash(newHash) * Checks if the hash is the last hash * @param {string} newHash * @return {boolean} true */ History.isLastHash = function(newHash){ // Prepare var oldHash = History.getHashByIndex(), isLast; // Check isLast = newHash === oldHash; // Return isLast return isLast; }; /** * History.isHashEqual(newHash, oldHash) * Checks to see if two hashes are functionally equal * @param {string} newHash * @param {string} oldHash * @return {boolean} true */ History.isHashEqual = function(newHash, oldHash){ newHash = encodeURIComponent(newHash).replace(/%25/g, "%"); oldHash = encodeURIComponent(oldHash).replace(/%25/g, "%"); return newHash === oldHash; }; /** * History.saveHash(newHash) * Push a Hash * @param {string} newHash * @return {boolean} true */ History.saveHash = function(newHash){ // Check Hash if ( History.isLastHash(newHash) ) { return false; } // Push the Hash History.savedHashes.push(newHash); // Return true return true; }; /** * History.getHashByIndex() * Gets a hash by the index * @param {integer} index * @return {string} */ History.getHashByIndex = function(index){ // Prepare var hash = null; // Handle if ( typeof index === 'undefined' ) { // Get the last inserted hash = History.savedHashes[History.savedHashes.length-1]; } else if ( index < 0 ) { // Get from the end hash = History.savedHashes[History.savedHashes.length+index]; } else { // Get from the beginning hash = History.savedHashes[index]; } // Return hash return hash; }; // ==================================================================== // Discarded States /** * History.discardedHashes * A hashed array of discarded hashes */ History.discardedHashes = {}; /** * History.discardedStates * A hashed array of discarded states */ History.discardedStates = {}; /** * History.discardState(State) * Discards the state by ignoring it through History * @param {object} State * @return {true} */ History.discardState = function(discardedState,forwardState,backState){ //History.debug('History.discardState', arguments); // Prepare var discardedStateHash = History.getHashByState(discardedState), discardObject; // Create Discard Object discardObject = { 'discardedState': discardedState, 'backState': backState, 'forwardState': forwardState }; // Add to DiscardedStates History.discardedStates[discardedStateHash] = discardObject; // Return true return true; }; /** * History.discardHash(hash) * Discards the hash by ignoring it through History * @param {string} hash * @return {true} */ History.discardHash = function(discardedHash,forwardState,backState){ //History.debug('History.discardState', arguments); // Create Discard Object var discardObject = { 'discardedHash': discardedHash, 'backState': backState, 'forwardState': forwardState }; // Add to discardedHash History.discardedHashes[discardedHash] = discardObject; // Return true return true; }; /** * History.discardedState(State) * Checks to see if the state is discarded * @param {object} State * @return {bool} */ History.discardedState = function(State){ // Prepare var StateHash = History.getHashByState(State), discarded; // Check discarded = History.discardedStates[StateHash]||false; // Return true return discarded; }; /** * History.discardedHash(hash) * Checks to see if the state is discarded * @param {string} State * @return {bool} */ History.discardedHash = function(hash){ // Check var discarded = History.discardedHashes[hash]||false; // Return true return discarded; }; /** * History.recycleState(State) * Allows a discarded state to be used again * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.recycleState = function(State){ //History.debug('History.recycleState', arguments); // Prepare var StateHash = History.getHashByState(State); // Remove from DiscardedStates if ( History.discardedState(State) ) { delete History.discardedStates[StateHash]; } // Return true return true; }; // ==================================================================== // HTML4 HashChange Support if ( History.emulated.hashChange ) { /* * We must emulate the HTML4 HashChange Support by manually checking for hash changes */ /** * History.hashChangeInit() * Init the HashChange Emulation */ History.hashChangeInit = function(){ // Define our Checker Function History.checkerFunction = null; // Define some variables that will help in our checker function var lastDocumentHash = '', iframeId, iframe, lastIframeHash, checkerRunning, startedWithHash = Boolean(History.getHash()); // Handle depending on the browser if ( History.isInternetExplorer() ) { // IE6 and IE7 // We need to use an iframe to emulate the back and forward buttons // Create iFrame iframeId = 'historyjs-iframe'; iframe = document.createElement('iframe'); // Adjust iFarme // IE 6 requires iframe to have a src on HTTPS pages, otherwise it will throw a // "This page contains both secure and nonsecure items" warning. iframe.setAttribute('id', iframeId); iframe.setAttribute('src', '#'); iframe.style.display = 'none'; // Append iFrame document.body.appendChild(iframe); // Create initial history entry iframe.contentWindow.document.open(); iframe.contentWindow.document.close(); // Define some variables that will help in our checker function lastIframeHash = ''; checkerRunning = false; // Define the checker function History.checkerFunction = function(){ // Check Running if ( checkerRunning ) { return false; } // Update Running checkerRunning = true; // Fetch var documentHash = History.getHash(), iframeHash = History.getHash(iframe.contentWindow.document); // The Document Hash has changed (application caused) if ( documentHash !== lastDocumentHash ) { // Equalise lastDocumentHash = documentHash; // Create a history entry in the iframe if ( iframeHash !== documentHash ) { //History.debug('hashchange.checker: iframe hash change', 'documentHash (new):', documentHash, 'iframeHash (old):', iframeHash); // Equalise lastIframeHash = iframeHash = documentHash; // Create History Entry iframe.contentWindow.document.open(); iframe.contentWindow.document.close(); // Update the iframe's hash iframe.contentWindow.document.location.hash = History.escapeHash(documentHash); } // Trigger Hashchange Event History.Adapter.trigger(window,'hashchange'); } // The iFrame Hash has changed (back button caused) else if ( iframeHash !== lastIframeHash ) { //History.debug('hashchange.checker: iframe hash out of sync', 'iframeHash (new):', iframeHash, 'documentHash (old):', documentHash); // Equalise lastIframeHash = iframeHash; // If there is no iframe hash that means we're at the original // iframe state. // And if there was a hash on the original request, the original // iframe state was replaced instantly, so skip this state and take // the user back to where they came from. if (startedWithHash && iframeHash === '') { History.back(); } else { // Update the Hash History.setHash(iframeHash,false); } } // Reset Running checkerRunning = false; // Return true return true; }; } else { // We are not IE // Firefox 1 or 2, Opera // Define the checker function History.checkerFunction = function(){ // Prepare var documentHash = History.getHash()||''; // The Document Hash has changed (application caused) if ( documentHash !== lastDocumentHash ) { // Equalise lastDocumentHash = documentHash; // Trigger Hashchange Event History.Adapter.trigger(window,'hashchange'); } // Return true return true; }; } // Apply the checker function History.intervalList.push(setInterval(History.checkerFunction, History.options.hashChangeInterval)); // Done return true; }; // History.hashChangeInit // Bind hashChangeInit History.Adapter.onDomLoad(History.hashChangeInit); } // History.emulated.hashChange // ==================================================================== // HTML5 State Support // Non-Native pushState Implementation if ( History.emulated.pushState ) { /* * We must emulate the HTML5 State Management by using HTML4 HashChange */ /** * History.onHashChange(event) * Trigger HTML5's window.onpopstate via HTML4 HashChange Support */ History.onHashChange = function(event){ //History.debug('History.onHashChange', arguments); // Prepare var currentUrl = ((event && event.newURL) || History.getLocationHref()), currentHash = History.getHashByUrl(currentUrl), currentState = null, currentStateHash = null, currentStateHashExits = null, discardObject; // Check if we are the same state if ( History.isLastHash(currentHash) ) { // There has been no change (just the page's hash has finally propagated) //History.debug('History.onHashChange: no change'); History.busy(false); return false; } // Reset the double check History.doubleCheckComplete(); // Store our location for use in detecting back/forward direction History.saveHash(currentHash); // Expand Hash if ( currentHash && History.isTraditionalAnchor(currentHash) ) { //History.debug('History.onHashChange: traditional anchor', currentHash); // Traditional Anchor Hash History.Adapter.trigger(window,'anchorchange'); History.busy(false); return false; } // Create State currentState = History.extractState(History.getFullUrl(currentHash||History.getLocationHref()),true); // Check if we are the same state if ( History.isLastSavedState(currentState) ) { //History.debug('History.onHashChange: no change'); // There has been no change (just the page's hash has finally propagated) History.busy(false); return false; } // Create the state Hash currentStateHash = History.getHashByState(currentState); // Check if we are DiscardedState discardObject = History.discardedState(currentState); if ( discardObject ) { // Ignore this state as it has been discarded and go back to the state before it if ( History.getHashByIndex(-2) === History.getHashByState(discardObject.forwardState) ) { // We are going backwards //History.debug('History.onHashChange: go backwards'); History.back(false); } else { // We are going forwards //History.debug('History.onHashChange: go forwards'); History.forward(false); } return false; } // Push the new HTML5 State //History.debug('History.onHashChange: success hashchange'); History.pushState(currentState.data,currentState.title,encodeURI(currentState.url),false); // End onHashChange closure return true; }; History.Adapter.bind(window,'hashchange',History.onHashChange); /** * History.pushState(data,title,url) * Add a new State to the history object, become it, and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.pushState = function(data,title,url,queue){ //History.debug('History.pushState: called', arguments); // We assume that the URL passed in is URI-encoded, but this makes // sure that it's fully URI encoded; any '%'s that are encoded are // converted back into '%'s url = encodeURI(url).replace(/%25/g, "%"); // Check the State if ( History.getHashByUrl(url) ) { throw new Error('History.js does not support states with fragment-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.pushState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.pushState, args: arguments, queue: queue }); return false; } // Make Busy History.busy(true); // Fetch the State Object var newState = History.createStateObject(data,title,url), newStateHash = History.getHashByState(newState), oldState = History.getState(false), oldStateHash = History.getHashByState(oldState), html4Hash = History.getHash(), wasExpected = History.expectedStateId == newState.id; // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Recycle the State History.recycleState(newState); // Force update of the title History.setTitle(newState); // Check if we are the same State if ( newStateHash === oldStateHash ) { //History.debug('History.pushState: no change', newStateHash); History.busy(false); return false; } // Update HTML5 State History.saveState(newState); // Fire HTML5 Event if(!wasExpected) History.Adapter.trigger(window,'statechange'); // Update HTML4 Hash if ( !History.isHashEqual(newStateHash, html4Hash) && !History.isHashEqual(newStateHash, History.getShortUrl(History.getLocationHref())) ) { History.setHash(newStateHash,false); } History.busy(false); // End pushState closure return true; }; /** * History.replaceState(data,title,url) * Replace the State and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.replaceState = function(data,title,url,queue){ //History.debug('History.replaceState: called', arguments); // We assume that the URL passed in is URI-encoded, but this makes // sure that it's fully URI encoded; any '%'s that are encoded are // converted back into '%'s url = encodeURI(url).replace(/%25/g, "%"); // Check the State if ( History.getHashByUrl(url) ) { throw new Error('History.js does not support states with fragment-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.replaceState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.replaceState, args: arguments, queue: queue }); return false; } // Make Busy History.busy(true); // Fetch the State Objects var newState = History.createStateObject(data,title,url), newStateHash = History.getHashByState(newState), oldState = History.getState(false), oldStateHash = History.getHashByState(oldState), previousState = History.getStateByIndex(-2); // Discard Old State History.discardState(oldState,newState,previousState); // If the url hasn't changed, just store and save the state // and fire a statechange event to be consistent with the // html 5 api if ( newStateHash === oldStateHash ) { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Recycle the State History.recycleState(newState); // Force update of the title History.setTitle(newState); // Update HTML5 State History.saveState(newState); // Fire HTML5 Event //History.debug('History.pushState: trigger popstate'); History.Adapter.trigger(window,'statechange'); History.busy(false); } else { // Alias to PushState History.pushState(newState.data,newState.title,newState.url,false); } // End replaceState closure return true; }; } // History.emulated.pushState // ==================================================================== // Initialise // Non-Native pushState Implementation if ( History.emulated.pushState ) { /** * Ensure initial state is handled correctly */ if ( History.getHash() && !History.emulated.hashChange ) { History.Adapter.onDomLoad(function(){ History.Adapter.trigger(window,'hashchange'); }); } } // History.emulated.pushState }; // History.initHtml4 // Try to Initialise History if ( typeof History.init !== 'undefined' ) { History.init(); } })(window); /** * History.js Core * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window,undefined){ "use strict"; // ======================================================================== // Initialise // Localise Globals var console = window.console||undefined, // Prevent a JSLint complain document = window.document, // Make sure we are using the correct document navigator = window.navigator, // Make sure we are using the correct navigator sessionStorage = false, // sessionStorage setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, setInterval = window.setInterval, clearInterval = window.clearInterval, JSON = window.JSON, alert = window.alert, History = window.History = window.History||{}, // Public History Object history = window.history; // Old History Object try { sessionStorage = window.sessionStorage; // This will throw an exception in some browsers when cookies/localStorage are explicitly disabled (i.e. Chrome) sessionStorage.setItem('TEST', '1'); sessionStorage.removeItem('TEST'); } catch(e) { sessionStorage = false; } // MooTools Compatibility JSON.stringify = JSON.stringify||JSON.encode; JSON.parse = JSON.parse||JSON.decode; // Check Existence if ( typeof History.init !== 'undefined' ) { throw new Error('History.js Core has already been loaded...'); } // Initialise History History.init = function(options){ // Check Load Status of Adapter if ( typeof History.Adapter === 'undefined' ) { return false; } // Check Load Status of Core if ( typeof History.initCore !== 'undefined' ) { History.initCore(); } // Check Load Status of HTML4 Support if ( typeof History.initHtml4 !== 'undefined' ) { History.initHtml4(); } // Return true return true; }; // ======================================================================== // Initialise Core // Initialise Core History.initCore = function(options){ // Initialise if ( typeof History.initCore.initialized !== 'undefined' ) { // Already Loaded return false; } else { History.initCore.initialized = true; } // ==================================================================== // Options /** * History.options * Configurable options */ History.options = History.options||{}; /** * History.options.hashChangeInterval * How long should the interval be before hashchange checks */ History.options.hashChangeInterval = History.options.hashChangeInterval || 100; /** * History.options.safariPollInterval * How long should the interval be before safari poll checks */ History.options.safariPollInterval = History.options.safariPollInterval || 500; /** * History.options.doubleCheckInterval * How long should the interval be before we perform a double check */ History.options.doubleCheckInterval = History.options.doubleCheckInterval || 500; /** * History.options.disableSuid * Force History not to append suid */ History.options.disableSuid = History.options.disableSuid || false; /** * History.options.storeInterval * How long should we wait between store calls */ History.options.storeInterval = History.options.storeInterval || 1000; /** * History.options.busyDelay * How long should we wait between busy events */ History.options.busyDelay = History.options.busyDelay || 250; /** * History.options.debug * If true will enable debug messages to be logged */ History.options.debug = History.options.debug || false; /** * History.options.initialTitle * What is the title of the initial state */ History.options.initialTitle = History.options.initialTitle || document.title; /** * History.options.html4Mode * If true, will force HTMl4 mode (hashtags) */ History.options.html4Mode = History.options.html4Mode || false; /** * History.options.delayInit * Want to override default options and call init manually. */ History.options.delayInit = History.options.delayInit || false; // ==================================================================== // Interval record /** * History.intervalList * List of intervals set, to be cleared when document is unloaded. */ History.intervalList = []; /** * History.clearAllIntervals * Clears all setInterval instances. */ History.clearAllIntervals = function(){ var i, il = History.intervalList; if (typeof il !== "undefined" && il !== null) { for (i = 0; i < il.length; i++) { clearInterval(il[i]); } History.intervalList = null; } }; // ==================================================================== // Debug /** * History.debug(message,...) * Logs the passed arguments if debug enabled */ History.debug = function(){ if ( (History.options.debug||false) ) { History.log.apply(History,arguments); } }; /** * History.log(message,...) * Logs the passed arguments */ History.log = function(){ // Prepare var consoleExists = !(typeof console === 'undefined' || typeof console.log === 'undefined' || typeof console.log.apply === 'undefined'), textarea = document.getElementById('log'), message, i,n, args,arg ; // Write to Console if ( consoleExists ) { args = Array.prototype.slice.call(arguments); message = args.shift(); if ( typeof console.debug !== 'undefined' ) { console.debug.apply(console,[message,args]); } else { console.log.apply(console,[message,args]); } } else { message = ("\n"+arguments[0]+"\n"); } // Write to log for ( i=1,n=arguments.length; i * @author James Padolsey */ History.getInternetExplorerMajorVersion = function(){ var result = History.getInternetExplorerMajorVersion.cached = (typeof History.getInternetExplorerMajorVersion.cached !== 'undefined') ? History.getInternetExplorerMajorVersion.cached : (function(){ var v = 3, div = document.createElement('div'), all = div.getElementsByTagName('i'); while ( (div.innerHTML = '') && all[0] ) {} return (v > 4) ? v : false; })() ; return result; }; /** * History.isInternetExplorer() * Are we using Internet Explorer? * @return {boolean} * @license Public Domain * @author Benjamin Arthur Lupton */ History.isInternetExplorer = function(){ var result = History.isInternetExplorer.cached = (typeof History.isInternetExplorer.cached !== 'undefined') ? History.isInternetExplorer.cached : Boolean(History.getInternetExplorerMajorVersion()) ; return result; }; /** * History.emulated * Which features require emulating? */ if (History.options.html4Mode) { History.emulated = { pushState : true, hashChange: true }; } else { History.emulated = { pushState: !Boolean( window.history && window.history.pushState && window.history.replaceState && !( (/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i).test(navigator.userAgent) /* disable for versions of iOS before version 4.3 (8F190) */ || (/AppleWebKit\/5([0-2]|3[0-2])/i).test(navigator.userAgent) /* disable for the mercury iOS browser, or at least older versions of the webkit engine */ ) ), hashChange: Boolean( !(('onhashchange' in window) || ('onhashchange' in document)) || (History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8) ) }; } /** * History.enabled * Is History enabled? */ History.enabled = !History.emulated.pushState; /** * History.bugs * Which bugs are present */ History.bugs = { /** * Safari 5 and Safari iOS 4 fail to return to the correct state once a hash is replaced by a `replaceState` call * https://bugs.webkit.org/show_bug.cgi?id=56249 */ setHash: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * Safari 5 and Safari iOS 4 sometimes fail to apply the state change under busy conditions * https://bugs.webkit.org/show_bug.cgi?id=42940 */ safariPoll: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * MSIE 6 and 7 sometimes do not apply a hash even it was told to (requiring a second call to the apply function) */ ieDoubleCheck: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8), /** * MSIE 6 requires the entire hash to be encoded for the hashes to trigger the onHashChange event */ hashEscape: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 7) }; /** * History.isEmptyObject(obj) * Checks to see if the Object is Empty * @param {Object} obj * @return {boolean} */ History.isEmptyObject = function(obj) { for ( var name in obj ) { if ( obj.hasOwnProperty(name) ) { return false; } } return true; }; /** * History.cloneObject(obj) * Clones a object and eliminate all references to the original contexts * @param {Object} obj * @return {Object} */ History.cloneObject = function(obj) { var hash,newObj; if ( obj ) { hash = JSON.stringify(obj); newObj = JSON.parse(hash); } else { newObj = {}; } return newObj; }; // ==================================================================== // URL Helpers /** * History.getRootUrl() * Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com" * @return {String} rootUrl */ History.getRootUrl = function(){ // Create var rootUrl = document.location.protocol+'//'+(document.location.hostname||document.location.host); if ( document.location.port||false ) { rootUrl += ':'+document.location.port; } rootUrl += '/'; // Return return rootUrl; }; /** * History.getBaseHref() * Fetches the `href` attribute of the `` element if it exists * @return {String} baseHref */ History.getBaseHref = function(){ // Create var baseElements = document.getElementsByTagName('base'), baseElement = null, baseHref = ''; // Test for Base Element if ( baseElements.length === 1 ) { // Prepare for Base Element baseElement = baseElements[0]; baseHref = baseElement.href.replace(/[^\/]+$/,''); } // Adjust trailing slash baseHref = baseHref.replace(/\/+$/,''); if ( baseHref ) baseHref += '/'; // Return return baseHref; }; /** * History.getBaseUrl() * Fetches the baseHref or basePageUrl or rootUrl (whichever one exists first) * @return {String} baseUrl */ History.getBaseUrl = function(){ // Create var baseUrl = History.getBaseHref()||History.getBasePageUrl()||History.getRootUrl(); // Return return baseUrl; }; /** * History.getPageUrl() * Fetches the URL of the current page * @return {String} pageUrl */ History.getPageUrl = function(){ // Fetch var State = History.getState(false,false), stateUrl = (State||{}).url||History.getLocationHref(), pageUrl; // Create pageUrl = stateUrl.replace(/\/+$/,'').replace(/[^\/]+$/,function(part,index,string){ return (/\./).test(part) ? part : part+'/'; }); // Return return pageUrl; }; /** * History.getBasePageUrl() * Fetches the Url of the directory of the current page * @return {String} basePageUrl */ History.getBasePageUrl = function(){ // Create var basePageUrl = (History.getLocationHref()).replace(/[#\?].*/,'').replace(/[^\/]+$/,function(part,index,string){ return (/[^\/]$/).test(part) ? '' : part; }).replace(/\/+$/,'')+'/'; // Return return basePageUrl; }; /** * History.getFullUrl(url) * Ensures that we have an absolute URL and not a relative URL * @param {string} url * @param {Boolean} allowBaseHref * @return {string} fullUrl */ History.getFullUrl = function(url,allowBaseHref){ // Prepare var fullUrl = url, firstChar = url.substring(0,1); allowBaseHref = (typeof allowBaseHref === 'undefined') ? true : allowBaseHref; // Check if ( /[a-z]+\:\/\//.test(url) ) { // Full URL } else if ( firstChar === '/' ) { // Root URL fullUrl = History.getRootUrl()+url.replace(/^\/+/,''); } else if ( firstChar === '#' ) { // Anchor URL fullUrl = History.getPageUrl().replace(/#.*/,'')+url; } else if ( firstChar === '?' ) { // Query URL fullUrl = History.getPageUrl().replace(/[\?#].*/,'')+url; } else { // Relative URL if ( allowBaseHref ) { fullUrl = History.getBaseUrl()+url.replace(/^(\.\/)+/,''); } else { fullUrl = History.getBasePageUrl()+url.replace(/^(\.\/)+/,''); } // We have an if condition above as we do not want hashes // which are relative to the baseHref in our URLs // as if the baseHref changes, then all our bookmarks // would now point to different locations // whereas the basePageUrl will always stay the same } // Return return fullUrl.replace(/\#$/,''); }; /** * History.getShortUrl(url) * Ensures that we have a relative URL and not a absolute URL * @param {string} url * @return {string} url */ History.getShortUrl = function(url){ // Prepare var shortUrl = url, baseUrl = History.getBaseUrl(), rootUrl = History.getRootUrl(); // Trim baseUrl if ( History.emulated.pushState ) { // We are in a if statement as when pushState is not emulated // The actual url these short urls are relative to can change // So within the same session, we the url may end up somewhere different shortUrl = shortUrl.replace(baseUrl,''); } // Trim rootUrl shortUrl = shortUrl.replace(rootUrl,'/'); // Ensure we can still detect it as a state if ( History.isTraditionalAnchor(shortUrl) ) { shortUrl = './'+shortUrl; } // Clean It shortUrl = shortUrl.replace(/^(\.\/)+/g,'./').replace(/\#$/,''); // Return return shortUrl; }; /** * History.getLocationHref(document) * Returns a normalized version of document.location.href * accounting for browser inconsistencies, etc. * * This URL will be URI-encoded and will include the hash * * @param {object} document * @return {string} url */ History.getLocationHref = function(doc) { doc = doc || document; // most of the time, this will be true if (doc.URL === doc.location.href) return doc.location.href; // some versions of webkit URI-decode document.location.href // but they leave document.URL in an encoded state if (doc.location.href === decodeURIComponent(doc.URL)) return doc.URL; // FF 3.6 only updates document.URL when a page is reloaded // document.location.href is updated correctly if (doc.location.hash && decodeURIComponent(doc.location.href.replace(/^[^#]+/, "")) === doc.location.hash) return doc.location.href; if (doc.URL.indexOf('#') == -1 && doc.location.href.indexOf('#') != -1) return doc.location.href; return doc.URL || doc.location.href; }; // ==================================================================== // State Storage /** * History.store * The store for all session specific data */ History.store = {}; /** * History.idToState * 1-1: State ID to State Object */ History.idToState = History.idToState||{}; /** * History.stateToId * 1-1: State String to State ID */ History.stateToId = History.stateToId||{}; /** * History.urlToId * 1-1: State URL to State ID */ History.urlToId = History.urlToId||{}; /** * History.storedStates * Store the states in an array */ History.storedStates = History.storedStates||[]; /** * History.savedStates * Saved the states in an array */ History.savedStates = History.savedStates||[]; /** * History.noramlizeStore() * Noramlize the store by adding necessary values */ History.normalizeStore = function(){ History.store.idToState = History.store.idToState||{}; History.store.urlToId = History.store.urlToId||{}; History.store.stateToId = History.store.stateToId||{}; }; /** * History.getState() * Get an object containing the data, title and url of the current state * @param {Boolean} friendly * @param {Boolean} create * @return {Object} State */ History.getState = function(friendly,create){ // Prepare if ( typeof friendly === 'undefined' ) { friendly = true; } if ( typeof create === 'undefined' ) { create = true; } // Fetch var State = History.getLastSavedState(); // Create if ( !State && create ) { State = History.createStateObject(); } // Adjust if ( friendly ) { State = History.cloneObject(State); State.url = State.cleanUrl||State.url; } // Return return State; }; /** * History.getIdByState(State) * Gets a ID for a State * @param {State} newState * @return {String} id */ History.getIdByState = function(newState){ // Fetch ID var id = History.extractId(newState.url), str; if ( !id ) { // Find ID via State String str = History.getStateString(newState); if ( typeof History.stateToId[str] !== 'undefined' ) { id = History.stateToId[str]; } else if ( typeof History.store.stateToId[str] !== 'undefined' ) { id = History.store.stateToId[str]; } else { // Generate a new ID while ( true ) { id = (new Date()).getTime() + String(Math.random()).replace(/\D/g,''); if ( typeof History.idToState[id] === 'undefined' && typeof History.store.idToState[id] === 'undefined' ) { break; } } // Apply the new State to the ID History.stateToId[str] = id; History.idToState[id] = newState; } } // Return ID return id; }; /** * History.normalizeState(State) * Expands a State Object * @param {object} State * @return {object} */ History.normalizeState = function(oldState){ // Variables var newState, dataNotEmpty; // Prepare if ( !oldState || (typeof oldState !== 'object') ) { oldState = {}; } // Check if ( typeof oldState.normalized !== 'undefined' ) { return oldState; } // Adjust if ( !oldState.data || (typeof oldState.data !== 'object') ) { oldState.data = {}; } // ---------------------------------------------------------------- // Create newState = {}; newState.normalized = true; newState.title = oldState.title||''; newState.url = History.getFullUrl(oldState.url?oldState.url:(History.getLocationHref())); newState.hash = History.getShortUrl(newState.url); newState.data = History.cloneObject(oldState.data); // Fetch ID newState.id = History.getIdByState(newState); // ---------------------------------------------------------------- // Clean the URL newState.cleanUrl = newState.url.replace(/\??\&_suid.*/,''); newState.url = newState.cleanUrl; // Check to see if we have more than just a url dataNotEmpty = !History.isEmptyObject(newState.data); // Apply if ( (newState.title || dataNotEmpty) && History.options.disableSuid !== true ) { // Add ID to Hash newState.hash = History.getShortUrl(newState.url).replace(/\??\&_suid.*/,''); if ( !/\?/.test(newState.hash) ) { newState.hash += '?'; } newState.hash += '&_suid='+newState.id; } // Create the Hashed URL newState.hashedUrl = History.getFullUrl(newState.hash); // ---------------------------------------------------------------- // Update the URL if we have a duplicate if ( (History.emulated.pushState || History.bugs.safariPoll) && History.hasUrlDuplicate(newState) ) { newState.url = newState.hashedUrl; } // ---------------------------------------------------------------- // Return return newState; }; /** * History.createStateObject(data,title,url) * Creates a object based on the data, title and url state params * @param {object} data * @param {string} title * @param {string} url * @return {object} */ History.createStateObject = function(data,title,url){ // Hashify var State = { 'data': data, 'title': title, 'url': url }; // Expand the State State = History.normalizeState(State); // Return object return State; }; /** * History.getStateById(id) * Get a state by it's UID * @param {String} id */ History.getStateById = function(id){ // Prepare id = String(id); // Retrieve var State = History.idToState[id] || History.store.idToState[id] || undefined; // Return State return State; }; /** * Get a State's String * @param {State} passedState */ History.getStateString = function(passedState){ // Prepare var State, cleanedState, str; // Fetch State = History.normalizeState(passedState); // Clean cleanedState = { data: State.data, title: passedState.title, url: passedState.url }; // Fetch str = JSON.stringify(cleanedState); // Return return str; }; /** * Get a State's ID * @param {State} passedState * @return {String} id */ History.getStateId = function(passedState){ // Prepare var State, id; // Fetch State = History.normalizeState(passedState); // Fetch id = State.id; // Return return id; }; /** * History.getHashByState(State) * Creates a Hash for the State Object * @param {State} passedState * @return {String} hash */ History.getHashByState = function(passedState){ // Prepare var State, hash; // Fetch State = History.normalizeState(passedState); // Hash hash = State.hash; // Return return hash; }; /** * History.extractId(url_or_hash) * Get a State ID by it's URL or Hash * @param {string} url_or_hash * @return {string} id */ History.extractId = function ( url_or_hash ) { // Prepare var id,parts,url, tmp; // Extract // If the URL has a #, use the id from before the # if (url_or_hash.indexOf('#') != -1) { tmp = url_or_hash.split("#")[0]; } else { tmp = url_or_hash; } parts = /(.*)\&_suid=([0-9]+)$/.exec(tmp); url = parts ? (parts[1]||url_or_hash) : url_or_hash; id = parts ? String(parts[2]||'') : ''; // Return return id||false; }; /** * History.isTraditionalAnchor * Checks to see if the url is a traditional anchor or not * @param {String} url_or_hash * @return {Boolean} */ History.isTraditionalAnchor = function(url_or_hash){ // Check var isTraditional = !(/[\/\?\.]/.test(url_or_hash)); // Return return isTraditional; }; /** * History.extractState * Get a State by it's URL or Hash * @param {String} url_or_hash * @return {State|null} */ History.extractState = function(url_or_hash,create){ // Prepare var State = null, id, url; create = create||false; // Fetch SUID id = History.extractId(url_or_hash); if ( id ) { State = History.getStateById(id); } // Fetch SUID returned no State if ( !State ) { // Fetch URL url = History.getFullUrl(url_or_hash); // Check URL id = History.getIdByUrl(url)||false; if ( id ) { State = History.getStateById(id); } // Create State if ( !State && create && !History.isTraditionalAnchor(url_or_hash) ) { State = History.createStateObject(null,null,url); } } // Return return State; }; /** * History.getIdByUrl() * Get a State ID by a State URL */ History.getIdByUrl = function(url){ // Fetch var id = History.urlToId[url] || History.store.urlToId[url] || undefined; // Return return id; }; /** * History.getLastSavedState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastSavedState = function(){ return History.savedStates[History.savedStates.length-1]||undefined; }; /** * History.getLastStoredState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastStoredState = function(){ return History.storedStates[History.storedStates.length-1]||undefined; }; /** * History.hasUrlDuplicate * Checks if a Url will have a url conflict * @param {Object} newState * @return {Boolean} hasDuplicate */ History.hasUrlDuplicate = function(newState) { // Prepare var hasDuplicate = false, oldState; // Fetch oldState = History.extractState(newState.url); // Check hasDuplicate = oldState && oldState.id !== newState.id; // Return return hasDuplicate; }; /** * History.storeState * Store a State * @param {Object} newState * @return {Object} newState */ History.storeState = function(newState){ // Store the State History.urlToId[newState.url] = newState.id; // Push the State History.storedStates.push(History.cloneObject(newState)); // Return newState return newState; }; /** * History.isLastSavedState(newState) * Tests to see if the state is the last state * @param {Object} newState * @return {boolean} isLast */ History.isLastSavedState = function(newState){ // Prepare var isLast = false, newId, oldState, oldId; // Check if ( History.savedStates.length ) { newId = newState.id; oldState = History.getLastSavedState(); oldId = oldState.id; // Check isLast = (newId === oldId); } // Return return isLast; }; /** * History.saveState * Push a State * @param {Object} newState * @return {boolean} changed */ History.saveState = function(newState){ // Check Hash if ( History.isLastSavedState(newState) ) { return false; } // Push the State History.savedStates.push(History.cloneObject(newState)); // Return true return true; }; /** * History.getStateByIndex() * Gets a state by the index * @param {integer} index * @return {Object} */ History.getStateByIndex = function(index){ // Prepare var State = null; // Handle if ( typeof index === 'undefined' ) { // Get the last inserted State = History.savedStates[History.savedStates.length-1]; } else if ( index < 0 ) { // Get from the end State = History.savedStates[History.savedStates.length+index]; } else { // Get from the beginning State = History.savedStates[index]; } // Return State return State; }; /** * History.getCurrentIndex() * Gets the current index * @return (integer) */ History.getCurrentIndex = function(){ // Prepare var index = null; // No states saved if(History.savedStates.length < 1) { index = 0; } else { index = History.savedStates.length-1; } return index; }; // ==================================================================== // Hash Helpers /** * History.getHash() * @param {Location=} location * Gets the current document hash * Note: unlike location.hash, this is guaranteed to return the escaped hash in all browsers * @return {string} */ History.getHash = function(doc){ var url = History.getLocationHref(doc), hash; hash = History.getHashByUrl(url); return hash; }; /** * History.unescapeHash() * normalize and Unescape a Hash * @param {String} hash * @return {string} */ History.unescapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Unescape hash result = decodeURIComponent(result); // Return result return result; }; /** * History.normalizeHash() * normalize a hash across browsers * @return {string} */ History.normalizeHash = function(hash){ // Prepare var result = hash.replace(/[^#]*#/,'').replace(/#.*/, ''); // Return result return result; }; /** * History.setHash(hash) * Sets the document hash * @param {string} hash * @return {History} */ History.setHash = function(hash,queue){ // Prepare var State, pageUrl; // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.setHash: we must wait', arguments); History.pushQueue({ scope: History, callback: History.setHash, args: arguments, queue: queue }); return false; } // Log //History.debug('History.setHash: called',hash); // Make Busy + Continue History.busy(true); // Check if hash is a state State = History.extractState(hash,true); if ( State && !History.emulated.pushState ) { // Hash is a state so skip the setHash //History.debug('History.setHash: Hash is a state so skipping the hash set with a direct pushState call',arguments); // PushState History.pushState(State.data,State.title,State.url,false); } else if ( History.getHash() !== hash ) { // Hash is a proper hash, so apply it // Handle browser bugs if ( History.bugs.setHash ) { // Fix Safari Bug https://bugs.webkit.org/show_bug.cgi?id=56249 // Fetch the base page pageUrl = History.getPageUrl(); // Safari hash apply History.pushState(null,null,pageUrl+'#'+hash,false); } else { // Normal hash apply document.location.hash = hash; } } // Chain return History; }; /** * History.escape() * normalize and Escape a Hash * @return {string} */ History.escapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Escape hash result = window.encodeURIComponent(result); // IE6 Escape Bug if ( !History.bugs.hashEscape ) { // Restore common parts result = result .replace(/\%21/g,'!') .replace(/\%26/g,'&') .replace(/\%3D/g,'=') .replace(/\%3F/g,'?'); } // Return result return result; }; /** * History.getHashByUrl(url) * Extracts the Hash from a URL * @param {string} url * @return {string} url */ History.getHashByUrl = function(url){ // Extract the hash var hash = String(url) .replace(/([^#]*)#?([^#]*)#?(.*)/, '$2') ; // Unescape hash hash = History.unescapeHash(hash); // Return hash return hash; }; /** * History.setTitle(title) * Applies the title to the document * @param {State} newState * @return {Boolean} */ History.setTitle = function(newState){ // Prepare var title = newState.title, firstState; // Initial if ( !title ) { firstState = History.getStateByIndex(0); if ( firstState && firstState.url === newState.url ) { title = firstState.title||History.options.initialTitle; } } // Apply try { document.getElementsByTagName('title')[0].innerHTML = title.replace('<','<').replace('>','>').replace(' & ',' & '); } catch ( Exception ) { } document.title = title; // Chain return History; }; // ==================================================================== // Queueing /** * History.queues * The list of queues to use * First In, First Out */ History.queues = []; /** * History.busy(value) * @param {boolean} value [optional] * @return {boolean} busy */ History.busy = function(value){ // Apply if ( typeof value !== 'undefined' ) { //History.debug('History.busy: changing ['+(History.busy.flag||false)+'] to ['+(value||false)+']', History.queues.length); History.busy.flag = value; } // Default else if ( typeof History.busy.flag === 'undefined' ) { History.busy.flag = false; } // Queue if ( !History.busy.flag ) { // Execute the next item in the queue clearTimeout(History.busy.timeout); var fireNext = function(){ var i, queue, item; if ( History.busy.flag ) return; for ( i=History.queues.length-1; i >= 0; --i ) { queue = History.queues[i]; if ( queue.length === 0 ) continue; item = queue.shift(); History.fireQueueItem(item); History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } }; History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } // Return return History.busy.flag; }; /** * History.busy.flag */ History.busy.flag = false; /** * History.fireQueueItem(item) * Fire a Queue Item * @param {Object} item * @return {Mixed} result */ History.fireQueueItem = function(item){ return item.callback.apply(item.scope||History,item.args||[]); }; /** * History.pushQueue(callback,args) * Add an item to the queue * @param {Object} item [scope,callback,args,queue] */ History.pushQueue = function(item){ // Prepare the queue History.queues[item.queue||0] = History.queues[item.queue||0]||[]; // Add to the queue History.queues[item.queue||0].push(item); // Chain return History; }; /** * History.queue (item,queue), (func,queue), (func), (item) * Either firs the item now if not busy, or adds it to the queue */ History.queue = function(item,queue){ // Prepare if ( typeof item === 'function' ) { item = { callback: item }; } if ( typeof queue !== 'undefined' ) { item.queue = queue; } // Handle if ( History.busy() ) { History.pushQueue(item); } else { History.fireQueueItem(item); } // Chain return History; }; /** * History.clearQueue() * Clears the Queue */ History.clearQueue = function(){ History.busy.flag = false; History.queues = []; return History; }; // ==================================================================== // IE Bug Fix /** * History.stateChanged * States whether or not the state has changed since the last double check was initialised */ History.stateChanged = false; /** * History.doubleChecker * Contains the timeout used for the double checks */ History.doubleChecker = false; /** * History.doubleCheckComplete() * Complete a double check * @return {History} */ History.doubleCheckComplete = function(){ // Update History.stateChanged = true; // Clear History.doubleCheckClear(); // Chain return History; }; /** * History.doubleCheckClear() * Clear a double check * @return {History} */ History.doubleCheckClear = function(){ // Clear if ( History.doubleChecker ) { clearTimeout(History.doubleChecker); History.doubleChecker = false; } // Chain return History; }; /** * History.doubleCheck() * Create a double check * @return {History} */ History.doubleCheck = function(tryAgain){ // Reset History.stateChanged = false; History.doubleCheckClear(); // Fix IE6,IE7 bug where calling history.back or history.forward does not actually change the hash (whereas doing it manually does) // Fix Safari 5 bug where sometimes the state does not change: https://bugs.webkit.org/show_bug.cgi?id=42940 if ( History.bugs.ieDoubleCheck ) { // Apply Check History.doubleChecker = setTimeout( function(){ History.doubleCheckClear(); if ( !History.stateChanged ) { //History.debug('History.doubleCheck: State has not yet changed, trying again', arguments); // Re-Attempt tryAgain(); } return true; }, History.options.doubleCheckInterval ); } // Chain return History; }; // ==================================================================== // Safari Bug Fix /** * History.safariStatePoll() * Poll the current state * @return {History} */ History.safariStatePoll = function(){ // Poll the URL // Get the Last State which has the new URL var urlState = History.extractState(History.getLocationHref()), newState; // Check for a difference if ( !History.isLastSavedState(urlState) ) { newState = urlState; } else { return; } // Check if we have a state with that url // If not create it if ( !newState ) { //History.debug('History.safariStatePoll: new'); newState = History.createStateObject(); } // Apply the New State //History.debug('History.safariStatePoll: trigger'); History.Adapter.trigger(window,'popstate'); // Chain return History; }; // ==================================================================== // State Aliases /** * History.back(queue) * Send the browser history back one item * @param {Integer} queue [optional] */ History.back = function(queue){ //History.debug('History.back: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.back: we must wait', arguments); History.pushQueue({ scope: History, callback: History.back, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.back(false); }); // Go back history.go(-1); // End back closure return true; }; /** * History.forward(queue) * Send the browser history forward one item * @param {Integer} queue [optional] */ History.forward = function(queue){ //History.debug('History.forward: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.forward: we must wait', arguments); History.pushQueue({ scope: History, callback: History.forward, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.forward(false); }); // Go forward history.go(1); // End forward closure return true; }; /** * History.go(index,queue) * Send the browser history back or forward index times * @param {Integer} queue [optional] */ History.go = function(index,queue){ //History.debug('History.go: called', arguments); // Prepare var i; // Handle if ( index > 0 ) { // Forward for ( i=1; i<=index; ++i ) { History.forward(queue); } } else if ( index < 0 ) { // Backward for ( i=-1; i>=index; --i ) { History.back(queue); } } else { throw new Error('History.go: History.go requires a positive or negative integer passed.'); } // Chain return History; }; // ==================================================================== // HTML5 State Support // Non-Native pushState Implementation if ( History.emulated.pushState ) { /* * Provide Skeleton for HTML4 Browsers */ // Prepare var emptyFunction = function(){}; History.pushState = History.pushState||emptyFunction; History.replaceState = History.replaceState||emptyFunction; } // History.emulated.pushState // Native pushState Implementation else { /* * Use native HTML5 History API Implementation */ /** * History.onPopState(event,extra) * Refresh the Current State */ History.onPopState = function(event,extra){ // Prepare var stateId = false, newState = false, currentHash, currentState; // Reset the double check History.doubleCheckComplete(); // Check for a Hash, and handle apporiatly currentHash = History.getHash(); if ( currentHash ) { // Expand Hash currentState = History.extractState(currentHash||History.getLocationHref(),true); if ( currentState ) { // We were able to parse it, it must be a State! // Let's forward to replaceState //History.debug('History.onPopState: state anchor', currentHash, currentState); History.replaceState(currentState.data, currentState.title, currentState.url, false); } else { // Traditional Anchor //History.debug('History.onPopState: traditional anchor', currentHash); History.Adapter.trigger(window,'anchorchange'); History.busy(false); } // We don't care for hashes History.expectedStateId = false; return false; } // Ensure stateId = History.Adapter.extractEventData('state',event,extra) || false; // Fetch State if ( stateId ) { // Vanilla: Back/forward button was used newState = History.getStateById(stateId); } else if ( History.expectedStateId ) { // Vanilla: A new state was pushed, and popstate was called manually newState = History.getStateById(History.expectedStateId); } else { // Initial State newState = History.extractState(History.getLocationHref()); } // The State did not exist in our store if ( !newState ) { // Regenerate the State newState = History.createStateObject(null,null,History.getLocationHref()); } // Clean History.expectedStateId = false; // Check if we are the same state if ( History.isLastSavedState(newState) ) { // There has been no change (just the page's hash has finally propagated) //History.debug('History.onPopState: no change', newState, History.savedStates); History.busy(false); return false; } // Store the State History.storeState(newState); History.saveState(newState); // Force update of the title History.setTitle(newState); // Fire Our Event History.Adapter.trigger(window,'statechange'); History.busy(false); // Return true return true; }; History.Adapter.bind(window,'popstate',History.onPopState); /** * History.pushState(data,title,url) * Add a new State to the history object, become it, and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.pushState = function(data,title,url,queue){ //History.debug('History.pushState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.pushState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.pushState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.pushState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End pushState closure return true; }; /** * History.replaceState(data,title,url) * Replace the State and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.replaceState = function(data,title,url,queue){ //History.debug('History.replaceState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.replaceState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.replaceState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.replaceState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End replaceState closure return true; }; } // !History.emulated.pushState // ==================================================================== // Initialise /** * Load the Store */ if ( sessionStorage ) { // Fetch try { History.store = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { History.store = {}; } // Normalize History.normalizeStore(); } else { // Default Load History.store = {}; History.normalizeStore(); } /** * Clear Intervals on exit to prevent memory leaks */ History.Adapter.bind(window,"unload",History.clearAllIntervals); /** * Create the initial State */ History.saveState(History.storeState(History.extractState(History.getLocationHref(),true))); /** * Bind for Saving Store */ if ( sessionStorage ) { // When the page is closed History.onUnload = function(){ // Prepare var currentStore, item, currentStoreString; // Fetch try { currentStore = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { currentStore = {}; } // Ensure currentStore.idToState = currentStore.idToState || {}; currentStore.urlToId = currentStore.urlToId || {}; currentStore.stateToId = currentStore.stateToId || {}; // Sync for ( item in History.idToState ) { if ( !History.idToState.hasOwnProperty(item) ) { continue; } currentStore.idToState[item] = History.idToState[item]; } for ( item in History.urlToId ) { if ( !History.urlToId.hasOwnProperty(item) ) { continue; } currentStore.urlToId[item] = History.urlToId[item]; } for ( item in History.stateToId ) { if ( !History.stateToId.hasOwnProperty(item) ) { continue; } currentStore.stateToId[item] = History.stateToId[item]; } // Update History.store = currentStore; History.normalizeStore(); // In Safari, going into Private Browsing mode causes the // Session Storage object to still exist but if you try and use // or set any property/function of it it throws the exception // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to // add something to storage that exceeded the quota." infinitely // every second. currentStoreString = JSON.stringify(currentStore); try { // Store sessionStorage.setItem('History.store', currentStoreString); } catch (e) { if (e.code === DOMException.QUOTA_EXCEEDED_ERR) { if (sessionStorage.length) { // Workaround for a bug seen on iPads. Sometimes the quota exceeded error comes up and simply // removing/resetting the storage can work. sessionStorage.removeItem('History.store'); sessionStorage.setItem('History.store', currentStoreString); } else { // Otherwise, we're probably private browsing in Safari, so we'll ignore the exception. } } else { throw e; } } }; // For Internet Explorer History.intervalList.push(setInterval(History.onUnload,History.options.storeInterval)); // For Other Browsers History.Adapter.bind(window,'beforeunload',History.onUnload); History.Adapter.bind(window,'unload',History.onUnload); // Both are enabled for consistency } // Non-Native pushState Implementation if ( !History.emulated.pushState ) { // Be aware, the following is only for native pushState implementations // If you are wanting to include something for all browsers // Then include it above this if block /** * Setup Safari Fix */ if ( History.bugs.safariPoll ) { History.intervalList.push(setInterval(History.safariStatePoll, History.options.safariPollInterval)); } /** * Ensure Cross Browser Compatibility */ if ( navigator.vendor === 'Apple Computer, Inc.' || (navigator.appCodeName||'') === 'Mozilla' ) { /** * Fix Safari HashChange Issue */ // Setup Alias History.Adapter.bind(window,'hashchange',function(){ History.Adapter.trigger(window,'popstate'); }); // Initialise Alias if ( History.getHash() ) { History.Adapter.onDomLoad(function(){ History.Adapter.trigger(window,'hashchange'); }); } } } // !History.emulated.pushState }; // History.initCore // Try to Initialise History if (!History.options || !History.options.delayInit) { History.init(); } })(window); ================================================ FILE: scripts/bundled-uncompressed/html4+html5/zepto.history.js ================================================ /* json2.js 2012-10-08 Public Domain. NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. See http://www.JSON.org/js.html This code should be minified before deployment. See http://javascript.crockford.com/jsmin.html USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO NOT CONTROL. This file creates a global JSON object containing two methods: stringify and parse. JSON.stringify(value, replacer, space) value any JavaScript value, usually an object or array. replacer an optional parameter that determines how object values are stringified for objects. It can be a function or an array of strings. space an optional parameter that specifies the indentation of nested structures. If it is omitted, the text will be packed without extra whitespace. If it is a number, it will specify the number of spaces to indent at each level. If it is a string (such as '\t' or ' '), it contains the characters used to indent at each level. This method produces a JSON text from a JavaScript value. When an object value is found, if the object contains a toJSON method, its toJSON method will be called and the result will be stringified. A toJSON method does not serialize: it returns the value represented by the name/value pair that should be serialized, or undefined if nothing should be serialized. The toJSON method will be passed the key associated with the value, and this will be bound to the value For example, this would serialize Dates as ISO strings. Date.prototype.toJSON = function (key) { function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } return this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z'; }; You can provide an optional replacer method. It will be passed the key and value of each member, with this bound to the containing object. The value that is returned from your method will be serialized. If your method returns undefined, then the member will be excluded from the serialization. If the replacer parameter is an array of strings, then it will be used to select the members to be serialized. It filters the results such that only members with keys listed in the replacer array are stringified. Values that do not have JSON representations, such as undefined or functions, will not be serialized. Such values in objects will be dropped; in arrays they will be replaced with null. You can use a replacer function to replace those with JSON values. JSON.stringify(undefined) returns undefined. The optional space parameter produces a stringification of the value that is filled with line breaks and indentation to make it easier to read. If the space parameter is a non-empty string, then that string will be used for indentation. If the space parameter is a number, then the indentation will be that many spaces. Example: text = JSON.stringify(['e', {pluribus: 'unum'}]); // text is '["e",{"pluribus":"unum"}]' text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' text = JSON.stringify([new Date()], function (key, value) { return this[key] instanceof Date ? 'Date(' + this[key] + ')' : value; }); // text is '["Date(---current time---)"]' JSON.parse(text, reviver) This method parses a JSON text to produce an object or array. It can throw a SyntaxError exception. The optional reviver parameter is a function that can filter and transform the results. It receives each of the keys and values, and its return value is used instead of the original value. If it returns what it received, then the structure is not modified. If it returns undefined then the member is deleted. Example: // Parse the text. Values that look like ISO date strings will // be converted to Date objects. myData = JSON.parse(text, function (key, value) { var a; if (typeof value === 'string') { a = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); if (a) { return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6])); } } return value; }); myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { var d; if (typeof value === 'string' && value.slice(0, 5) === 'Date(' && value.slice(-1) === ')') { d = new Date(value.slice(5, -1)); if (d) { return d; } } return value; }); This is a reference implementation. You are free to copy, modify, or redistribute. */ /*jslint evil: true, regexp: true */ /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, lastIndex, length, parse, prototype, push, replace, slice, stringify, test, toJSON, toString, valueOf */ // Create a JSON object only if one does not already exist. We create the // methods in a closure to avoid creating global variables. if (typeof JSON !== 'object') { JSON = {}; } (function () { 'use strict'; function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } if (typeof Date.prototype.toJSON !== 'function') { Date.prototype.toJSON = function (key) { return isFinite(this.valueOf()) ? this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z' : null; }; String.prototype.toJSON = Number.prototype.toJSON = Boolean.prototype.toJSON = function (key) { return this.valueOf(); }; } var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, gap, indent, meta = { // table of character substitutions '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\' }, rep; function quote(string) { // If the string contains no control characters, no quote characters, and no // backslash characters, then we can safely slap some quotes around it. // Otherwise we must also replace the offending characters with safe escape // sequences. escapable.lastIndex = 0; return escapable.test(string) ? '"' + string.replace(escapable, function (a) { var c = meta[a]; return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }) + '"' : '"' + string + '"'; } function str(key, holder) { // Produce a string from holder[key]. var i, // The loop counter. k, // The member key. v, // The member value. length, mind = gap, partial, value = holder[key]; // If the value has a toJSON method, call it to obtain a replacement value. if (value && typeof value === 'object' && typeof value.toJSON === 'function') { value = value.toJSON(key); } // If we were called with a replacer function, then call the replacer to // obtain a replacement value. if (typeof rep === 'function') { value = rep.call(holder, key, value); } // What happens next depends on the value's type. switch (typeof value) { case 'string': return quote(value); case 'number': // JSON numbers must be finite. Encode non-finite numbers as null. return isFinite(value) ? String(value) : 'null'; case 'boolean': case 'null': // If the value is a boolean or null, convert it to a string. Note: // typeof null does not produce 'null'. The case is included here in // the remote chance that this gets fixed someday. return String(value); // If the type is 'object', we might be dealing with an object or an array or // null. case 'object': // Due to a specification blunder in ECMAScript, typeof null is 'object', // so watch out for that case. if (!value) { return 'null'; } // Make an array to hold the partial results of stringifying this object value. gap += indent; partial = []; // Is the value an array? if (Object.prototype.toString.apply(value) === '[object Array]') { // The value is an array. Stringify every element. Use null as a placeholder // for non-JSON values. length = value.length; for (i = 0; i < length; i += 1) { partial[i] = str(i, value) || 'null'; } // Join all of the elements together, separated with commas, and wrap them in // brackets. v = partial.length === 0 ? '[]' : gap ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : '[' + partial.join(',') + ']'; gap = mind; return v; } // If the replacer is an array, use it to select the members to be stringified. if (rep && typeof rep === 'object') { length = rep.length; for (i = 0; i < length; i += 1) { if (typeof rep[i] === 'string') { k = rep[i]; v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } else { // Otherwise, iterate through all of the keys in the object. for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } // Join all of the member texts together, separated with commas, // and wrap them in braces. v = partial.length === 0 ? '{}' : gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : '{' + partial.join(',') + '}'; gap = mind; return v; } } // If the JSON object does not yet have a stringify method, give it one. if (typeof JSON.stringify !== 'function') { JSON.stringify = function (value, replacer, space) { // The stringify method takes a value and an optional replacer, and an optional // space parameter, and returns a JSON text. The replacer can be a function // that can replace values, or an array of strings that will select the keys. // A default replacer method can be provided. Use of the space parameter can // produce text that is more easily readable. var i; gap = ''; indent = ''; // If the space parameter is a number, make an indent string containing that // many spaces. if (typeof space === 'number') { for (i = 0; i < space; i += 1) { indent += ' '; } // If the space parameter is a string, it will be used as the indent string. } else if (typeof space === 'string') { indent = space; } // If there is a replacer, it must be a function or an array. // Otherwise, throw an error. rep = replacer; if (replacer && typeof replacer !== 'function' && (typeof replacer !== 'object' || typeof replacer.length !== 'number')) { throw new Error('JSON.stringify'); } // Make a fake root object containing our value under the key of ''. // Return the result of stringifying the value. return str('', {'': value}); }; } // If the JSON object does not yet have a parse method, give it one. if (typeof JSON.parse !== 'function') { JSON.parse = function (text, reviver) { // The parse method takes a text and an optional reviver function, and returns // a JavaScript value if the text is a valid JSON text. var j; function walk(holder, key) { // The walk method is used to recursively walk the resulting structure so // that modifications can be made. var k, v, value = holder[key]; if (value && typeof value === 'object') { for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = walk(value, k); if (v !== undefined) { value[k] = v; } else { delete value[k]; } } } } return reviver.call(holder, key, value); } // Parsing happens in four stages. In the first stage, we replace certain // Unicode characters with escape sequences. JavaScript handles many characters // incorrectly, either silently deleting them, or treating them as line endings. text = String(text); cx.lastIndex = 0; if (cx.test(text)) { text = text.replace(cx, function (a) { return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }); } // In the second stage, we run the text against regular expressions that look // for non-JSON patterns. We are especially concerned with '()' and 'new' // because they can cause invocation, and '=' because it can cause mutation. // But just to be safe, we want to reject all unexpected forms. // We split the second stage into 4 regexp operations in order to work around // crippling inefficiencies in IE's and Safari's regexp engines. First we // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we // replace all simple value tokens with ']' characters. Third, we delete all // open brackets that follow a colon or comma or that begin the text. Finally, // we look to see that the remaining characters are only whitespace or ']' or // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. if (/^[\],:{}\s]*$/ .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { // In the third stage we use the eval function to compile the text into a // JavaScript structure. The '{' operator is subject to a syntactic ambiguity // in JavaScript: it can begin a block or an object literal. We wrap the text // in parens to eliminate the ambiguity. j = eval('(' + text + ')'); // In the optional fourth stage, we recursively walk the new structure, passing // each name/value pair to a reviver function for possible transformation. return typeof reviver === 'function' ? walk({'': j}, '') : j; } // If the text is not JSON parseable, then a SyntaxError is thrown. throw new SyntaxError('JSON.parse'); }; } }());/** * History.js Zepto Adapter * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ // Closure (function(window,undefined){ "use strict"; // Localise Globals var History = window.History = window.History||{}, Zepto = window.Zepto; // Check Existence if ( typeof History.Adapter !== 'undefined' ) { throw new Error('History.js Adapter has already been loaded...'); } // Add the Adapter History.Adapter = { /** * History.Adapter.bind(el,event,callback) * @param {Element|string} el * @param {string} event - custom and standard events * @param {function} callback * @return {void} */ bind: function(el,event,callback){ new Zepto(el).bind(event,callback); }, /** * History.Adapter.trigger(el,event) * @param {Element|string} el * @param {string} event - custom and standard events * @return {void} */ trigger: function(el,event){ new Zepto(el).trigger(event); }, /** * History.Adapter.extractEventData(key,event,extra) * @param {string} key - key for the event data to extract * @param {string} event - custom and standard events * @return {mixed} */ extractEventData: function(key,event){ // Zepto Native var result = (event && event[key]) || undefined; // Return return result; }, /** * History.Adapter.onDomLoad(callback) * @param {function} callback * @return {void} */ onDomLoad: function(callback) { new Zepto(callback); } }; // Try and Initialise History if ( typeof History.init !== 'undefined' ) { History.init(); } })(window); /** * History.js HTML4 Support * Depends on the HTML5 Support * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window,undefined){ "use strict"; // ======================================================================== // Initialise // Localise Globals var document = window.document, // Make sure we are using the correct document setTimeout = window.setTimeout||setTimeout, clearTimeout = window.clearTimeout||clearTimeout, setInterval = window.setInterval||setInterval, History = window.History = window.History||{}; // Public History Object // Check Existence if ( typeof History.initHtml4 !== 'undefined' ) { throw new Error('History.js HTML4 Support has already been loaded...'); } // ======================================================================== // Initialise HTML4 Support // Initialise HTML4 Support History.initHtml4 = function(){ // Initialise if ( typeof History.initHtml4.initialized !== 'undefined' ) { // Already Loaded return false; } else { History.initHtml4.initialized = true; } // ==================================================================== // Properties /** * History.enabled * Is History enabled? */ History.enabled = true; // ==================================================================== // Hash Storage /** * History.savedHashes * Store the hashes in an array */ History.savedHashes = []; /** * History.isLastHash(newHash) * Checks if the hash is the last hash * @param {string} newHash * @return {boolean} true */ History.isLastHash = function(newHash){ // Prepare var oldHash = History.getHashByIndex(), isLast; // Check isLast = newHash === oldHash; // Return isLast return isLast; }; /** * History.isHashEqual(newHash, oldHash) * Checks to see if two hashes are functionally equal * @param {string} newHash * @param {string} oldHash * @return {boolean} true */ History.isHashEqual = function(newHash, oldHash){ newHash = encodeURIComponent(newHash).replace(/%25/g, "%"); oldHash = encodeURIComponent(oldHash).replace(/%25/g, "%"); return newHash === oldHash; }; /** * History.saveHash(newHash) * Push a Hash * @param {string} newHash * @return {boolean} true */ History.saveHash = function(newHash){ // Check Hash if ( History.isLastHash(newHash) ) { return false; } // Push the Hash History.savedHashes.push(newHash); // Return true return true; }; /** * History.getHashByIndex() * Gets a hash by the index * @param {integer} index * @return {string} */ History.getHashByIndex = function(index){ // Prepare var hash = null; // Handle if ( typeof index === 'undefined' ) { // Get the last inserted hash = History.savedHashes[History.savedHashes.length-1]; } else if ( index < 0 ) { // Get from the end hash = History.savedHashes[History.savedHashes.length+index]; } else { // Get from the beginning hash = History.savedHashes[index]; } // Return hash return hash; }; // ==================================================================== // Discarded States /** * History.discardedHashes * A hashed array of discarded hashes */ History.discardedHashes = {}; /** * History.discardedStates * A hashed array of discarded states */ History.discardedStates = {}; /** * History.discardState(State) * Discards the state by ignoring it through History * @param {object} State * @return {true} */ History.discardState = function(discardedState,forwardState,backState){ //History.debug('History.discardState', arguments); // Prepare var discardedStateHash = History.getHashByState(discardedState), discardObject; // Create Discard Object discardObject = { 'discardedState': discardedState, 'backState': backState, 'forwardState': forwardState }; // Add to DiscardedStates History.discardedStates[discardedStateHash] = discardObject; // Return true return true; }; /** * History.discardHash(hash) * Discards the hash by ignoring it through History * @param {string} hash * @return {true} */ History.discardHash = function(discardedHash,forwardState,backState){ //History.debug('History.discardState', arguments); // Create Discard Object var discardObject = { 'discardedHash': discardedHash, 'backState': backState, 'forwardState': forwardState }; // Add to discardedHash History.discardedHashes[discardedHash] = discardObject; // Return true return true; }; /** * History.discardedState(State) * Checks to see if the state is discarded * @param {object} State * @return {bool} */ History.discardedState = function(State){ // Prepare var StateHash = History.getHashByState(State), discarded; // Check discarded = History.discardedStates[StateHash]||false; // Return true return discarded; }; /** * History.discardedHash(hash) * Checks to see if the state is discarded * @param {string} State * @return {bool} */ History.discardedHash = function(hash){ // Check var discarded = History.discardedHashes[hash]||false; // Return true return discarded; }; /** * History.recycleState(State) * Allows a discarded state to be used again * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.recycleState = function(State){ //History.debug('History.recycleState', arguments); // Prepare var StateHash = History.getHashByState(State); // Remove from DiscardedStates if ( History.discardedState(State) ) { delete History.discardedStates[StateHash]; } // Return true return true; }; // ==================================================================== // HTML4 HashChange Support if ( History.emulated.hashChange ) { /* * We must emulate the HTML4 HashChange Support by manually checking for hash changes */ /** * History.hashChangeInit() * Init the HashChange Emulation */ History.hashChangeInit = function(){ // Define our Checker Function History.checkerFunction = null; // Define some variables that will help in our checker function var lastDocumentHash = '', iframeId, iframe, lastIframeHash, checkerRunning, startedWithHash = Boolean(History.getHash()); // Handle depending on the browser if ( History.isInternetExplorer() ) { // IE6 and IE7 // We need to use an iframe to emulate the back and forward buttons // Create iFrame iframeId = 'historyjs-iframe'; iframe = document.createElement('iframe'); // Adjust iFarme // IE 6 requires iframe to have a src on HTTPS pages, otherwise it will throw a // "This page contains both secure and nonsecure items" warning. iframe.setAttribute('id', iframeId); iframe.setAttribute('src', '#'); iframe.style.display = 'none'; // Append iFrame document.body.appendChild(iframe); // Create initial history entry iframe.contentWindow.document.open(); iframe.contentWindow.document.close(); // Define some variables that will help in our checker function lastIframeHash = ''; checkerRunning = false; // Define the checker function History.checkerFunction = function(){ // Check Running if ( checkerRunning ) { return false; } // Update Running checkerRunning = true; // Fetch var documentHash = History.getHash(), iframeHash = History.getHash(iframe.contentWindow.document); // The Document Hash has changed (application caused) if ( documentHash !== lastDocumentHash ) { // Equalise lastDocumentHash = documentHash; // Create a history entry in the iframe if ( iframeHash !== documentHash ) { //History.debug('hashchange.checker: iframe hash change', 'documentHash (new):', documentHash, 'iframeHash (old):', iframeHash); // Equalise lastIframeHash = iframeHash = documentHash; // Create History Entry iframe.contentWindow.document.open(); iframe.contentWindow.document.close(); // Update the iframe's hash iframe.contentWindow.document.location.hash = History.escapeHash(documentHash); } // Trigger Hashchange Event History.Adapter.trigger(window,'hashchange'); } // The iFrame Hash has changed (back button caused) else if ( iframeHash !== lastIframeHash ) { //History.debug('hashchange.checker: iframe hash out of sync', 'iframeHash (new):', iframeHash, 'documentHash (old):', documentHash); // Equalise lastIframeHash = iframeHash; // If there is no iframe hash that means we're at the original // iframe state. // And if there was a hash on the original request, the original // iframe state was replaced instantly, so skip this state and take // the user back to where they came from. if (startedWithHash && iframeHash === '') { History.back(); } else { // Update the Hash History.setHash(iframeHash,false); } } // Reset Running checkerRunning = false; // Return true return true; }; } else { // We are not IE // Firefox 1 or 2, Opera // Define the checker function History.checkerFunction = function(){ // Prepare var documentHash = History.getHash()||''; // The Document Hash has changed (application caused) if ( documentHash !== lastDocumentHash ) { // Equalise lastDocumentHash = documentHash; // Trigger Hashchange Event History.Adapter.trigger(window,'hashchange'); } // Return true return true; }; } // Apply the checker function History.intervalList.push(setInterval(History.checkerFunction, History.options.hashChangeInterval)); // Done return true; }; // History.hashChangeInit // Bind hashChangeInit History.Adapter.onDomLoad(History.hashChangeInit); } // History.emulated.hashChange // ==================================================================== // HTML5 State Support // Non-Native pushState Implementation if ( History.emulated.pushState ) { /* * We must emulate the HTML5 State Management by using HTML4 HashChange */ /** * History.onHashChange(event) * Trigger HTML5's window.onpopstate via HTML4 HashChange Support */ History.onHashChange = function(event){ //History.debug('History.onHashChange', arguments); // Prepare var currentUrl = ((event && event.newURL) || History.getLocationHref()), currentHash = History.getHashByUrl(currentUrl), currentState = null, currentStateHash = null, currentStateHashExits = null, discardObject; // Check if we are the same state if ( History.isLastHash(currentHash) ) { // There has been no change (just the page's hash has finally propagated) //History.debug('History.onHashChange: no change'); History.busy(false); return false; } // Reset the double check History.doubleCheckComplete(); // Store our location for use in detecting back/forward direction History.saveHash(currentHash); // Expand Hash if ( currentHash && History.isTraditionalAnchor(currentHash) ) { //History.debug('History.onHashChange: traditional anchor', currentHash); // Traditional Anchor Hash History.Adapter.trigger(window,'anchorchange'); History.busy(false); return false; } // Create State currentState = History.extractState(History.getFullUrl(currentHash||History.getLocationHref()),true); // Check if we are the same state if ( History.isLastSavedState(currentState) ) { //History.debug('History.onHashChange: no change'); // There has been no change (just the page's hash has finally propagated) History.busy(false); return false; } // Create the state Hash currentStateHash = History.getHashByState(currentState); // Check if we are DiscardedState discardObject = History.discardedState(currentState); if ( discardObject ) { // Ignore this state as it has been discarded and go back to the state before it if ( History.getHashByIndex(-2) === History.getHashByState(discardObject.forwardState) ) { // We are going backwards //History.debug('History.onHashChange: go backwards'); History.back(false); } else { // We are going forwards //History.debug('History.onHashChange: go forwards'); History.forward(false); } return false; } // Push the new HTML5 State //History.debug('History.onHashChange: success hashchange'); History.pushState(currentState.data,currentState.title,encodeURI(currentState.url),false); // End onHashChange closure return true; }; History.Adapter.bind(window,'hashchange',History.onHashChange); /** * History.pushState(data,title,url) * Add a new State to the history object, become it, and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.pushState = function(data,title,url,queue){ //History.debug('History.pushState: called', arguments); // We assume that the URL passed in is URI-encoded, but this makes // sure that it's fully URI encoded; any '%'s that are encoded are // converted back into '%'s url = encodeURI(url).replace(/%25/g, "%"); // Check the State if ( History.getHashByUrl(url) ) { throw new Error('History.js does not support states with fragment-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.pushState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.pushState, args: arguments, queue: queue }); return false; } // Make Busy History.busy(true); // Fetch the State Object var newState = History.createStateObject(data,title,url), newStateHash = History.getHashByState(newState), oldState = History.getState(false), oldStateHash = History.getHashByState(oldState), html4Hash = History.getHash(), wasExpected = History.expectedStateId == newState.id; // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Recycle the State History.recycleState(newState); // Force update of the title History.setTitle(newState); // Check if we are the same State if ( newStateHash === oldStateHash ) { //History.debug('History.pushState: no change', newStateHash); History.busy(false); return false; } // Update HTML5 State History.saveState(newState); // Fire HTML5 Event if(!wasExpected) History.Adapter.trigger(window,'statechange'); // Update HTML4 Hash if ( !History.isHashEqual(newStateHash, html4Hash) && !History.isHashEqual(newStateHash, History.getShortUrl(History.getLocationHref())) ) { History.setHash(newStateHash,false); } History.busy(false); // End pushState closure return true; }; /** * History.replaceState(data,title,url) * Replace the State and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.replaceState = function(data,title,url,queue){ //History.debug('History.replaceState: called', arguments); // We assume that the URL passed in is URI-encoded, but this makes // sure that it's fully URI encoded; any '%'s that are encoded are // converted back into '%'s url = encodeURI(url).replace(/%25/g, "%"); // Check the State if ( History.getHashByUrl(url) ) { throw new Error('History.js does not support states with fragment-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.replaceState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.replaceState, args: arguments, queue: queue }); return false; } // Make Busy History.busy(true); // Fetch the State Objects var newState = History.createStateObject(data,title,url), newStateHash = History.getHashByState(newState), oldState = History.getState(false), oldStateHash = History.getHashByState(oldState), previousState = History.getStateByIndex(-2); // Discard Old State History.discardState(oldState,newState,previousState); // If the url hasn't changed, just store and save the state // and fire a statechange event to be consistent with the // html 5 api if ( newStateHash === oldStateHash ) { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Recycle the State History.recycleState(newState); // Force update of the title History.setTitle(newState); // Update HTML5 State History.saveState(newState); // Fire HTML5 Event //History.debug('History.pushState: trigger popstate'); History.Adapter.trigger(window,'statechange'); History.busy(false); } else { // Alias to PushState History.pushState(newState.data,newState.title,newState.url,false); } // End replaceState closure return true; }; } // History.emulated.pushState // ==================================================================== // Initialise // Non-Native pushState Implementation if ( History.emulated.pushState ) { /** * Ensure initial state is handled correctly */ if ( History.getHash() && !History.emulated.hashChange ) { History.Adapter.onDomLoad(function(){ History.Adapter.trigger(window,'hashchange'); }); } } // History.emulated.pushState }; // History.initHtml4 // Try to Initialise History if ( typeof History.init !== 'undefined' ) { History.init(); } })(window); /** * History.js Core * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window,undefined){ "use strict"; // ======================================================================== // Initialise // Localise Globals var console = window.console||undefined, // Prevent a JSLint complain document = window.document, // Make sure we are using the correct document navigator = window.navigator, // Make sure we are using the correct navigator sessionStorage = false, // sessionStorage setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, setInterval = window.setInterval, clearInterval = window.clearInterval, JSON = window.JSON, alert = window.alert, History = window.History = window.History||{}, // Public History Object history = window.history; // Old History Object try { sessionStorage = window.sessionStorage; // This will throw an exception in some browsers when cookies/localStorage are explicitly disabled (i.e. Chrome) sessionStorage.setItem('TEST', '1'); sessionStorage.removeItem('TEST'); } catch(e) { sessionStorage = false; } // MooTools Compatibility JSON.stringify = JSON.stringify||JSON.encode; JSON.parse = JSON.parse||JSON.decode; // Check Existence if ( typeof History.init !== 'undefined' ) { throw new Error('History.js Core has already been loaded...'); } // Initialise History History.init = function(options){ // Check Load Status of Adapter if ( typeof History.Adapter === 'undefined' ) { return false; } // Check Load Status of Core if ( typeof History.initCore !== 'undefined' ) { History.initCore(); } // Check Load Status of HTML4 Support if ( typeof History.initHtml4 !== 'undefined' ) { History.initHtml4(); } // Return true return true; }; // ======================================================================== // Initialise Core // Initialise Core History.initCore = function(options){ // Initialise if ( typeof History.initCore.initialized !== 'undefined' ) { // Already Loaded return false; } else { History.initCore.initialized = true; } // ==================================================================== // Options /** * History.options * Configurable options */ History.options = History.options||{}; /** * History.options.hashChangeInterval * How long should the interval be before hashchange checks */ History.options.hashChangeInterval = History.options.hashChangeInterval || 100; /** * History.options.safariPollInterval * How long should the interval be before safari poll checks */ History.options.safariPollInterval = History.options.safariPollInterval || 500; /** * History.options.doubleCheckInterval * How long should the interval be before we perform a double check */ History.options.doubleCheckInterval = History.options.doubleCheckInterval || 500; /** * History.options.disableSuid * Force History not to append suid */ History.options.disableSuid = History.options.disableSuid || false; /** * History.options.storeInterval * How long should we wait between store calls */ History.options.storeInterval = History.options.storeInterval || 1000; /** * History.options.busyDelay * How long should we wait between busy events */ History.options.busyDelay = History.options.busyDelay || 250; /** * History.options.debug * If true will enable debug messages to be logged */ History.options.debug = History.options.debug || false; /** * History.options.initialTitle * What is the title of the initial state */ History.options.initialTitle = History.options.initialTitle || document.title; /** * History.options.html4Mode * If true, will force HTMl4 mode (hashtags) */ History.options.html4Mode = History.options.html4Mode || false; /** * History.options.delayInit * Want to override default options and call init manually. */ History.options.delayInit = History.options.delayInit || false; // ==================================================================== // Interval record /** * History.intervalList * List of intervals set, to be cleared when document is unloaded. */ History.intervalList = []; /** * History.clearAllIntervals * Clears all setInterval instances. */ History.clearAllIntervals = function(){ var i, il = History.intervalList; if (typeof il !== "undefined" && il !== null) { for (i = 0; i < il.length; i++) { clearInterval(il[i]); } History.intervalList = null; } }; // ==================================================================== // Debug /** * History.debug(message,...) * Logs the passed arguments if debug enabled */ History.debug = function(){ if ( (History.options.debug||false) ) { History.log.apply(History,arguments); } }; /** * History.log(message,...) * Logs the passed arguments */ History.log = function(){ // Prepare var consoleExists = !(typeof console === 'undefined' || typeof console.log === 'undefined' || typeof console.log.apply === 'undefined'), textarea = document.getElementById('log'), message, i,n, args,arg ; // Write to Console if ( consoleExists ) { args = Array.prototype.slice.call(arguments); message = args.shift(); if ( typeof console.debug !== 'undefined' ) { console.debug.apply(console,[message,args]); } else { console.log.apply(console,[message,args]); } } else { message = ("\n"+arguments[0]+"\n"); } // Write to log for ( i=1,n=arguments.length; i * @author James Padolsey */ History.getInternetExplorerMajorVersion = function(){ var result = History.getInternetExplorerMajorVersion.cached = (typeof History.getInternetExplorerMajorVersion.cached !== 'undefined') ? History.getInternetExplorerMajorVersion.cached : (function(){ var v = 3, div = document.createElement('div'), all = div.getElementsByTagName('i'); while ( (div.innerHTML = '') && all[0] ) {} return (v > 4) ? v : false; })() ; return result; }; /** * History.isInternetExplorer() * Are we using Internet Explorer? * @return {boolean} * @license Public Domain * @author Benjamin Arthur Lupton */ History.isInternetExplorer = function(){ var result = History.isInternetExplorer.cached = (typeof History.isInternetExplorer.cached !== 'undefined') ? History.isInternetExplorer.cached : Boolean(History.getInternetExplorerMajorVersion()) ; return result; }; /** * History.emulated * Which features require emulating? */ if (History.options.html4Mode) { History.emulated = { pushState : true, hashChange: true }; } else { History.emulated = { pushState: !Boolean( window.history && window.history.pushState && window.history.replaceState && !( (/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i).test(navigator.userAgent) /* disable for versions of iOS before version 4.3 (8F190) */ || (/AppleWebKit\/5([0-2]|3[0-2])/i).test(navigator.userAgent) /* disable for the mercury iOS browser, or at least older versions of the webkit engine */ ) ), hashChange: Boolean( !(('onhashchange' in window) || ('onhashchange' in document)) || (History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8) ) }; } /** * History.enabled * Is History enabled? */ History.enabled = !History.emulated.pushState; /** * History.bugs * Which bugs are present */ History.bugs = { /** * Safari 5 and Safari iOS 4 fail to return to the correct state once a hash is replaced by a `replaceState` call * https://bugs.webkit.org/show_bug.cgi?id=56249 */ setHash: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * Safari 5 and Safari iOS 4 sometimes fail to apply the state change under busy conditions * https://bugs.webkit.org/show_bug.cgi?id=42940 */ safariPoll: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * MSIE 6 and 7 sometimes do not apply a hash even it was told to (requiring a second call to the apply function) */ ieDoubleCheck: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8), /** * MSIE 6 requires the entire hash to be encoded for the hashes to trigger the onHashChange event */ hashEscape: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 7) }; /** * History.isEmptyObject(obj) * Checks to see if the Object is Empty * @param {Object} obj * @return {boolean} */ History.isEmptyObject = function(obj) { for ( var name in obj ) { if ( obj.hasOwnProperty(name) ) { return false; } } return true; }; /** * History.cloneObject(obj) * Clones a object and eliminate all references to the original contexts * @param {Object} obj * @return {Object} */ History.cloneObject = function(obj) { var hash,newObj; if ( obj ) { hash = JSON.stringify(obj); newObj = JSON.parse(hash); } else { newObj = {}; } return newObj; }; // ==================================================================== // URL Helpers /** * History.getRootUrl() * Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com" * @return {String} rootUrl */ History.getRootUrl = function(){ // Create var rootUrl = document.location.protocol+'//'+(document.location.hostname||document.location.host); if ( document.location.port||false ) { rootUrl += ':'+document.location.port; } rootUrl += '/'; // Return return rootUrl; }; /** * History.getBaseHref() * Fetches the `href` attribute of the `` element if it exists * @return {String} baseHref */ History.getBaseHref = function(){ // Create var baseElements = document.getElementsByTagName('base'), baseElement = null, baseHref = ''; // Test for Base Element if ( baseElements.length === 1 ) { // Prepare for Base Element baseElement = baseElements[0]; baseHref = baseElement.href.replace(/[^\/]+$/,''); } // Adjust trailing slash baseHref = baseHref.replace(/\/+$/,''); if ( baseHref ) baseHref += '/'; // Return return baseHref; }; /** * History.getBaseUrl() * Fetches the baseHref or basePageUrl or rootUrl (whichever one exists first) * @return {String} baseUrl */ History.getBaseUrl = function(){ // Create var baseUrl = History.getBaseHref()||History.getBasePageUrl()||History.getRootUrl(); // Return return baseUrl; }; /** * History.getPageUrl() * Fetches the URL of the current page * @return {String} pageUrl */ History.getPageUrl = function(){ // Fetch var State = History.getState(false,false), stateUrl = (State||{}).url||History.getLocationHref(), pageUrl; // Create pageUrl = stateUrl.replace(/\/+$/,'').replace(/[^\/]+$/,function(part,index,string){ return (/\./).test(part) ? part : part+'/'; }); // Return return pageUrl; }; /** * History.getBasePageUrl() * Fetches the Url of the directory of the current page * @return {String} basePageUrl */ History.getBasePageUrl = function(){ // Create var basePageUrl = (History.getLocationHref()).replace(/[#\?].*/,'').replace(/[^\/]+$/,function(part,index,string){ return (/[^\/]$/).test(part) ? '' : part; }).replace(/\/+$/,'')+'/'; // Return return basePageUrl; }; /** * History.getFullUrl(url) * Ensures that we have an absolute URL and not a relative URL * @param {string} url * @param {Boolean} allowBaseHref * @return {string} fullUrl */ History.getFullUrl = function(url,allowBaseHref){ // Prepare var fullUrl = url, firstChar = url.substring(0,1); allowBaseHref = (typeof allowBaseHref === 'undefined') ? true : allowBaseHref; // Check if ( /[a-z]+\:\/\//.test(url) ) { // Full URL } else if ( firstChar === '/' ) { // Root URL fullUrl = History.getRootUrl()+url.replace(/^\/+/,''); } else if ( firstChar === '#' ) { // Anchor URL fullUrl = History.getPageUrl().replace(/#.*/,'')+url; } else if ( firstChar === '?' ) { // Query URL fullUrl = History.getPageUrl().replace(/[\?#].*/,'')+url; } else { // Relative URL if ( allowBaseHref ) { fullUrl = History.getBaseUrl()+url.replace(/^(\.\/)+/,''); } else { fullUrl = History.getBasePageUrl()+url.replace(/^(\.\/)+/,''); } // We have an if condition above as we do not want hashes // which are relative to the baseHref in our URLs // as if the baseHref changes, then all our bookmarks // would now point to different locations // whereas the basePageUrl will always stay the same } // Return return fullUrl.replace(/\#$/,''); }; /** * History.getShortUrl(url) * Ensures that we have a relative URL and not a absolute URL * @param {string} url * @return {string} url */ History.getShortUrl = function(url){ // Prepare var shortUrl = url, baseUrl = History.getBaseUrl(), rootUrl = History.getRootUrl(); // Trim baseUrl if ( History.emulated.pushState ) { // We are in a if statement as when pushState is not emulated // The actual url these short urls are relative to can change // So within the same session, we the url may end up somewhere different shortUrl = shortUrl.replace(baseUrl,''); } // Trim rootUrl shortUrl = shortUrl.replace(rootUrl,'/'); // Ensure we can still detect it as a state if ( History.isTraditionalAnchor(shortUrl) ) { shortUrl = './'+shortUrl; } // Clean It shortUrl = shortUrl.replace(/^(\.\/)+/g,'./').replace(/\#$/,''); // Return return shortUrl; }; /** * History.getLocationHref(document) * Returns a normalized version of document.location.href * accounting for browser inconsistencies, etc. * * This URL will be URI-encoded and will include the hash * * @param {object} document * @return {string} url */ History.getLocationHref = function(doc) { doc = doc || document; // most of the time, this will be true if (doc.URL === doc.location.href) return doc.location.href; // some versions of webkit URI-decode document.location.href // but they leave document.URL in an encoded state if (doc.location.href === decodeURIComponent(doc.URL)) return doc.URL; // FF 3.6 only updates document.URL when a page is reloaded // document.location.href is updated correctly if (doc.location.hash && decodeURIComponent(doc.location.href.replace(/^[^#]+/, "")) === doc.location.hash) return doc.location.href; if (doc.URL.indexOf('#') == -1 && doc.location.href.indexOf('#') != -1) return doc.location.href; return doc.URL || doc.location.href; }; // ==================================================================== // State Storage /** * History.store * The store for all session specific data */ History.store = {}; /** * History.idToState * 1-1: State ID to State Object */ History.idToState = History.idToState||{}; /** * History.stateToId * 1-1: State String to State ID */ History.stateToId = History.stateToId||{}; /** * History.urlToId * 1-1: State URL to State ID */ History.urlToId = History.urlToId||{}; /** * History.storedStates * Store the states in an array */ History.storedStates = History.storedStates||[]; /** * History.savedStates * Saved the states in an array */ History.savedStates = History.savedStates||[]; /** * History.noramlizeStore() * Noramlize the store by adding necessary values */ History.normalizeStore = function(){ History.store.idToState = History.store.idToState||{}; History.store.urlToId = History.store.urlToId||{}; History.store.stateToId = History.store.stateToId||{}; }; /** * History.getState() * Get an object containing the data, title and url of the current state * @param {Boolean} friendly * @param {Boolean} create * @return {Object} State */ History.getState = function(friendly,create){ // Prepare if ( typeof friendly === 'undefined' ) { friendly = true; } if ( typeof create === 'undefined' ) { create = true; } // Fetch var State = History.getLastSavedState(); // Create if ( !State && create ) { State = History.createStateObject(); } // Adjust if ( friendly ) { State = History.cloneObject(State); State.url = State.cleanUrl||State.url; } // Return return State; }; /** * History.getIdByState(State) * Gets a ID for a State * @param {State} newState * @return {String} id */ History.getIdByState = function(newState){ // Fetch ID var id = History.extractId(newState.url), str; if ( !id ) { // Find ID via State String str = History.getStateString(newState); if ( typeof History.stateToId[str] !== 'undefined' ) { id = History.stateToId[str]; } else if ( typeof History.store.stateToId[str] !== 'undefined' ) { id = History.store.stateToId[str]; } else { // Generate a new ID while ( true ) { id = (new Date()).getTime() + String(Math.random()).replace(/\D/g,''); if ( typeof History.idToState[id] === 'undefined' && typeof History.store.idToState[id] === 'undefined' ) { break; } } // Apply the new State to the ID History.stateToId[str] = id; History.idToState[id] = newState; } } // Return ID return id; }; /** * History.normalizeState(State) * Expands a State Object * @param {object} State * @return {object} */ History.normalizeState = function(oldState){ // Variables var newState, dataNotEmpty; // Prepare if ( !oldState || (typeof oldState !== 'object') ) { oldState = {}; } // Check if ( typeof oldState.normalized !== 'undefined' ) { return oldState; } // Adjust if ( !oldState.data || (typeof oldState.data !== 'object') ) { oldState.data = {}; } // ---------------------------------------------------------------- // Create newState = {}; newState.normalized = true; newState.title = oldState.title||''; newState.url = History.getFullUrl(oldState.url?oldState.url:(History.getLocationHref())); newState.hash = History.getShortUrl(newState.url); newState.data = History.cloneObject(oldState.data); // Fetch ID newState.id = History.getIdByState(newState); // ---------------------------------------------------------------- // Clean the URL newState.cleanUrl = newState.url.replace(/\??\&_suid.*/,''); newState.url = newState.cleanUrl; // Check to see if we have more than just a url dataNotEmpty = !History.isEmptyObject(newState.data); // Apply if ( (newState.title || dataNotEmpty) && History.options.disableSuid !== true ) { // Add ID to Hash newState.hash = History.getShortUrl(newState.url).replace(/\??\&_suid.*/,''); if ( !/\?/.test(newState.hash) ) { newState.hash += '?'; } newState.hash += '&_suid='+newState.id; } // Create the Hashed URL newState.hashedUrl = History.getFullUrl(newState.hash); // ---------------------------------------------------------------- // Update the URL if we have a duplicate if ( (History.emulated.pushState || History.bugs.safariPoll) && History.hasUrlDuplicate(newState) ) { newState.url = newState.hashedUrl; } // ---------------------------------------------------------------- // Return return newState; }; /** * History.createStateObject(data,title,url) * Creates a object based on the data, title and url state params * @param {object} data * @param {string} title * @param {string} url * @return {object} */ History.createStateObject = function(data,title,url){ // Hashify var State = { 'data': data, 'title': title, 'url': url }; // Expand the State State = History.normalizeState(State); // Return object return State; }; /** * History.getStateById(id) * Get a state by it's UID * @param {String} id */ History.getStateById = function(id){ // Prepare id = String(id); // Retrieve var State = History.idToState[id] || History.store.idToState[id] || undefined; // Return State return State; }; /** * Get a State's String * @param {State} passedState */ History.getStateString = function(passedState){ // Prepare var State, cleanedState, str; // Fetch State = History.normalizeState(passedState); // Clean cleanedState = { data: State.data, title: passedState.title, url: passedState.url }; // Fetch str = JSON.stringify(cleanedState); // Return return str; }; /** * Get a State's ID * @param {State} passedState * @return {String} id */ History.getStateId = function(passedState){ // Prepare var State, id; // Fetch State = History.normalizeState(passedState); // Fetch id = State.id; // Return return id; }; /** * History.getHashByState(State) * Creates a Hash for the State Object * @param {State} passedState * @return {String} hash */ History.getHashByState = function(passedState){ // Prepare var State, hash; // Fetch State = History.normalizeState(passedState); // Hash hash = State.hash; // Return return hash; }; /** * History.extractId(url_or_hash) * Get a State ID by it's URL or Hash * @param {string} url_or_hash * @return {string} id */ History.extractId = function ( url_or_hash ) { // Prepare var id,parts,url, tmp; // Extract // If the URL has a #, use the id from before the # if (url_or_hash.indexOf('#') != -1) { tmp = url_or_hash.split("#")[0]; } else { tmp = url_or_hash; } parts = /(.*)\&_suid=([0-9]+)$/.exec(tmp); url = parts ? (parts[1]||url_or_hash) : url_or_hash; id = parts ? String(parts[2]||'') : ''; // Return return id||false; }; /** * History.isTraditionalAnchor * Checks to see if the url is a traditional anchor or not * @param {String} url_or_hash * @return {Boolean} */ History.isTraditionalAnchor = function(url_or_hash){ // Check var isTraditional = !(/[\/\?\.]/.test(url_or_hash)); // Return return isTraditional; }; /** * History.extractState * Get a State by it's URL or Hash * @param {String} url_or_hash * @return {State|null} */ History.extractState = function(url_or_hash,create){ // Prepare var State = null, id, url; create = create||false; // Fetch SUID id = History.extractId(url_or_hash); if ( id ) { State = History.getStateById(id); } // Fetch SUID returned no State if ( !State ) { // Fetch URL url = History.getFullUrl(url_or_hash); // Check URL id = History.getIdByUrl(url)||false; if ( id ) { State = History.getStateById(id); } // Create State if ( !State && create && !History.isTraditionalAnchor(url_or_hash) ) { State = History.createStateObject(null,null,url); } } // Return return State; }; /** * History.getIdByUrl() * Get a State ID by a State URL */ History.getIdByUrl = function(url){ // Fetch var id = History.urlToId[url] || History.store.urlToId[url] || undefined; // Return return id; }; /** * History.getLastSavedState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastSavedState = function(){ return History.savedStates[History.savedStates.length-1]||undefined; }; /** * History.getLastStoredState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastStoredState = function(){ return History.storedStates[History.storedStates.length-1]||undefined; }; /** * History.hasUrlDuplicate * Checks if a Url will have a url conflict * @param {Object} newState * @return {Boolean} hasDuplicate */ History.hasUrlDuplicate = function(newState) { // Prepare var hasDuplicate = false, oldState; // Fetch oldState = History.extractState(newState.url); // Check hasDuplicate = oldState && oldState.id !== newState.id; // Return return hasDuplicate; }; /** * History.storeState * Store a State * @param {Object} newState * @return {Object} newState */ History.storeState = function(newState){ // Store the State History.urlToId[newState.url] = newState.id; // Push the State History.storedStates.push(History.cloneObject(newState)); // Return newState return newState; }; /** * History.isLastSavedState(newState) * Tests to see if the state is the last state * @param {Object} newState * @return {boolean} isLast */ History.isLastSavedState = function(newState){ // Prepare var isLast = false, newId, oldState, oldId; // Check if ( History.savedStates.length ) { newId = newState.id; oldState = History.getLastSavedState(); oldId = oldState.id; // Check isLast = (newId === oldId); } // Return return isLast; }; /** * History.saveState * Push a State * @param {Object} newState * @return {boolean} changed */ History.saveState = function(newState){ // Check Hash if ( History.isLastSavedState(newState) ) { return false; } // Push the State History.savedStates.push(History.cloneObject(newState)); // Return true return true; }; /** * History.getStateByIndex() * Gets a state by the index * @param {integer} index * @return {Object} */ History.getStateByIndex = function(index){ // Prepare var State = null; // Handle if ( typeof index === 'undefined' ) { // Get the last inserted State = History.savedStates[History.savedStates.length-1]; } else if ( index < 0 ) { // Get from the end State = History.savedStates[History.savedStates.length+index]; } else { // Get from the beginning State = History.savedStates[index]; } // Return State return State; }; /** * History.getCurrentIndex() * Gets the current index * @return (integer) */ History.getCurrentIndex = function(){ // Prepare var index = null; // No states saved if(History.savedStates.length < 1) { index = 0; } else { index = History.savedStates.length-1; } return index; }; // ==================================================================== // Hash Helpers /** * History.getHash() * @param {Location=} location * Gets the current document hash * Note: unlike location.hash, this is guaranteed to return the escaped hash in all browsers * @return {string} */ History.getHash = function(doc){ var url = History.getLocationHref(doc), hash; hash = History.getHashByUrl(url); return hash; }; /** * History.unescapeHash() * normalize and Unescape a Hash * @param {String} hash * @return {string} */ History.unescapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Unescape hash result = decodeURIComponent(result); // Return result return result; }; /** * History.normalizeHash() * normalize a hash across browsers * @return {string} */ History.normalizeHash = function(hash){ // Prepare var result = hash.replace(/[^#]*#/,'').replace(/#.*/, ''); // Return result return result; }; /** * History.setHash(hash) * Sets the document hash * @param {string} hash * @return {History} */ History.setHash = function(hash,queue){ // Prepare var State, pageUrl; // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.setHash: we must wait', arguments); History.pushQueue({ scope: History, callback: History.setHash, args: arguments, queue: queue }); return false; } // Log //History.debug('History.setHash: called',hash); // Make Busy + Continue History.busy(true); // Check if hash is a state State = History.extractState(hash,true); if ( State && !History.emulated.pushState ) { // Hash is a state so skip the setHash //History.debug('History.setHash: Hash is a state so skipping the hash set with a direct pushState call',arguments); // PushState History.pushState(State.data,State.title,State.url,false); } else if ( History.getHash() !== hash ) { // Hash is a proper hash, so apply it // Handle browser bugs if ( History.bugs.setHash ) { // Fix Safari Bug https://bugs.webkit.org/show_bug.cgi?id=56249 // Fetch the base page pageUrl = History.getPageUrl(); // Safari hash apply History.pushState(null,null,pageUrl+'#'+hash,false); } else { // Normal hash apply document.location.hash = hash; } } // Chain return History; }; /** * History.escape() * normalize and Escape a Hash * @return {string} */ History.escapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Escape hash result = window.encodeURIComponent(result); // IE6 Escape Bug if ( !History.bugs.hashEscape ) { // Restore common parts result = result .replace(/\%21/g,'!') .replace(/\%26/g,'&') .replace(/\%3D/g,'=') .replace(/\%3F/g,'?'); } // Return result return result; }; /** * History.getHashByUrl(url) * Extracts the Hash from a URL * @param {string} url * @return {string} url */ History.getHashByUrl = function(url){ // Extract the hash var hash = String(url) .replace(/([^#]*)#?([^#]*)#?(.*)/, '$2') ; // Unescape hash hash = History.unescapeHash(hash); // Return hash return hash; }; /** * History.setTitle(title) * Applies the title to the document * @param {State} newState * @return {Boolean} */ History.setTitle = function(newState){ // Prepare var title = newState.title, firstState; // Initial if ( !title ) { firstState = History.getStateByIndex(0); if ( firstState && firstState.url === newState.url ) { title = firstState.title||History.options.initialTitle; } } // Apply try { document.getElementsByTagName('title')[0].innerHTML = title.replace('<','<').replace('>','>').replace(' & ',' & '); } catch ( Exception ) { } document.title = title; // Chain return History; }; // ==================================================================== // Queueing /** * History.queues * The list of queues to use * First In, First Out */ History.queues = []; /** * History.busy(value) * @param {boolean} value [optional] * @return {boolean} busy */ History.busy = function(value){ // Apply if ( typeof value !== 'undefined' ) { //History.debug('History.busy: changing ['+(History.busy.flag||false)+'] to ['+(value||false)+']', History.queues.length); History.busy.flag = value; } // Default else if ( typeof History.busy.flag === 'undefined' ) { History.busy.flag = false; } // Queue if ( !History.busy.flag ) { // Execute the next item in the queue clearTimeout(History.busy.timeout); var fireNext = function(){ var i, queue, item; if ( History.busy.flag ) return; for ( i=History.queues.length-1; i >= 0; --i ) { queue = History.queues[i]; if ( queue.length === 0 ) continue; item = queue.shift(); History.fireQueueItem(item); History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } }; History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } // Return return History.busy.flag; }; /** * History.busy.flag */ History.busy.flag = false; /** * History.fireQueueItem(item) * Fire a Queue Item * @param {Object} item * @return {Mixed} result */ History.fireQueueItem = function(item){ return item.callback.apply(item.scope||History,item.args||[]); }; /** * History.pushQueue(callback,args) * Add an item to the queue * @param {Object} item [scope,callback,args,queue] */ History.pushQueue = function(item){ // Prepare the queue History.queues[item.queue||0] = History.queues[item.queue||0]||[]; // Add to the queue History.queues[item.queue||0].push(item); // Chain return History; }; /** * History.queue (item,queue), (func,queue), (func), (item) * Either firs the item now if not busy, or adds it to the queue */ History.queue = function(item,queue){ // Prepare if ( typeof item === 'function' ) { item = { callback: item }; } if ( typeof queue !== 'undefined' ) { item.queue = queue; } // Handle if ( History.busy() ) { History.pushQueue(item); } else { History.fireQueueItem(item); } // Chain return History; }; /** * History.clearQueue() * Clears the Queue */ History.clearQueue = function(){ History.busy.flag = false; History.queues = []; return History; }; // ==================================================================== // IE Bug Fix /** * History.stateChanged * States whether or not the state has changed since the last double check was initialised */ History.stateChanged = false; /** * History.doubleChecker * Contains the timeout used for the double checks */ History.doubleChecker = false; /** * History.doubleCheckComplete() * Complete a double check * @return {History} */ History.doubleCheckComplete = function(){ // Update History.stateChanged = true; // Clear History.doubleCheckClear(); // Chain return History; }; /** * History.doubleCheckClear() * Clear a double check * @return {History} */ History.doubleCheckClear = function(){ // Clear if ( History.doubleChecker ) { clearTimeout(History.doubleChecker); History.doubleChecker = false; } // Chain return History; }; /** * History.doubleCheck() * Create a double check * @return {History} */ History.doubleCheck = function(tryAgain){ // Reset History.stateChanged = false; History.doubleCheckClear(); // Fix IE6,IE7 bug where calling history.back or history.forward does not actually change the hash (whereas doing it manually does) // Fix Safari 5 bug where sometimes the state does not change: https://bugs.webkit.org/show_bug.cgi?id=42940 if ( History.bugs.ieDoubleCheck ) { // Apply Check History.doubleChecker = setTimeout( function(){ History.doubleCheckClear(); if ( !History.stateChanged ) { //History.debug('History.doubleCheck: State has not yet changed, trying again', arguments); // Re-Attempt tryAgain(); } return true; }, History.options.doubleCheckInterval ); } // Chain return History; }; // ==================================================================== // Safari Bug Fix /** * History.safariStatePoll() * Poll the current state * @return {History} */ History.safariStatePoll = function(){ // Poll the URL // Get the Last State which has the new URL var urlState = History.extractState(History.getLocationHref()), newState; // Check for a difference if ( !History.isLastSavedState(urlState) ) { newState = urlState; } else { return; } // Check if we have a state with that url // If not create it if ( !newState ) { //History.debug('History.safariStatePoll: new'); newState = History.createStateObject(); } // Apply the New State //History.debug('History.safariStatePoll: trigger'); History.Adapter.trigger(window,'popstate'); // Chain return History; }; // ==================================================================== // State Aliases /** * History.back(queue) * Send the browser history back one item * @param {Integer} queue [optional] */ History.back = function(queue){ //History.debug('History.back: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.back: we must wait', arguments); History.pushQueue({ scope: History, callback: History.back, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.back(false); }); // Go back history.go(-1); // End back closure return true; }; /** * History.forward(queue) * Send the browser history forward one item * @param {Integer} queue [optional] */ History.forward = function(queue){ //History.debug('History.forward: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.forward: we must wait', arguments); History.pushQueue({ scope: History, callback: History.forward, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.forward(false); }); // Go forward history.go(1); // End forward closure return true; }; /** * History.go(index,queue) * Send the browser history back or forward index times * @param {Integer} queue [optional] */ History.go = function(index,queue){ //History.debug('History.go: called', arguments); // Prepare var i; // Handle if ( index > 0 ) { // Forward for ( i=1; i<=index; ++i ) { History.forward(queue); } } else if ( index < 0 ) { // Backward for ( i=-1; i>=index; --i ) { History.back(queue); } } else { throw new Error('History.go: History.go requires a positive or negative integer passed.'); } // Chain return History; }; // ==================================================================== // HTML5 State Support // Non-Native pushState Implementation if ( History.emulated.pushState ) { /* * Provide Skeleton for HTML4 Browsers */ // Prepare var emptyFunction = function(){}; History.pushState = History.pushState||emptyFunction; History.replaceState = History.replaceState||emptyFunction; } // History.emulated.pushState // Native pushState Implementation else { /* * Use native HTML5 History API Implementation */ /** * History.onPopState(event,extra) * Refresh the Current State */ History.onPopState = function(event,extra){ // Prepare var stateId = false, newState = false, currentHash, currentState; // Reset the double check History.doubleCheckComplete(); // Check for a Hash, and handle apporiatly currentHash = History.getHash(); if ( currentHash ) { // Expand Hash currentState = History.extractState(currentHash||History.getLocationHref(),true); if ( currentState ) { // We were able to parse it, it must be a State! // Let's forward to replaceState //History.debug('History.onPopState: state anchor', currentHash, currentState); History.replaceState(currentState.data, currentState.title, currentState.url, false); } else { // Traditional Anchor //History.debug('History.onPopState: traditional anchor', currentHash); History.Adapter.trigger(window,'anchorchange'); History.busy(false); } // We don't care for hashes History.expectedStateId = false; return false; } // Ensure stateId = History.Adapter.extractEventData('state',event,extra) || false; // Fetch State if ( stateId ) { // Vanilla: Back/forward button was used newState = History.getStateById(stateId); } else if ( History.expectedStateId ) { // Vanilla: A new state was pushed, and popstate was called manually newState = History.getStateById(History.expectedStateId); } else { // Initial State newState = History.extractState(History.getLocationHref()); } // The State did not exist in our store if ( !newState ) { // Regenerate the State newState = History.createStateObject(null,null,History.getLocationHref()); } // Clean History.expectedStateId = false; // Check if we are the same state if ( History.isLastSavedState(newState) ) { // There has been no change (just the page's hash has finally propagated) //History.debug('History.onPopState: no change', newState, History.savedStates); History.busy(false); return false; } // Store the State History.storeState(newState); History.saveState(newState); // Force update of the title History.setTitle(newState); // Fire Our Event History.Adapter.trigger(window,'statechange'); History.busy(false); // Return true return true; }; History.Adapter.bind(window,'popstate',History.onPopState); /** * History.pushState(data,title,url) * Add a new State to the history object, become it, and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.pushState = function(data,title,url,queue){ //History.debug('History.pushState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.pushState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.pushState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.pushState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End pushState closure return true; }; /** * History.replaceState(data,title,url) * Replace the State and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.replaceState = function(data,title,url,queue){ //History.debug('History.replaceState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.replaceState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.replaceState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.replaceState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End replaceState closure return true; }; } // !History.emulated.pushState // ==================================================================== // Initialise /** * Load the Store */ if ( sessionStorage ) { // Fetch try { History.store = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { History.store = {}; } // Normalize History.normalizeStore(); } else { // Default Load History.store = {}; History.normalizeStore(); } /** * Clear Intervals on exit to prevent memory leaks */ History.Adapter.bind(window,"unload",History.clearAllIntervals); /** * Create the initial State */ History.saveState(History.storeState(History.extractState(History.getLocationHref(),true))); /** * Bind for Saving Store */ if ( sessionStorage ) { // When the page is closed History.onUnload = function(){ // Prepare var currentStore, item, currentStoreString; // Fetch try { currentStore = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { currentStore = {}; } // Ensure currentStore.idToState = currentStore.idToState || {}; currentStore.urlToId = currentStore.urlToId || {}; currentStore.stateToId = currentStore.stateToId || {}; // Sync for ( item in History.idToState ) { if ( !History.idToState.hasOwnProperty(item) ) { continue; } currentStore.idToState[item] = History.idToState[item]; } for ( item in History.urlToId ) { if ( !History.urlToId.hasOwnProperty(item) ) { continue; } currentStore.urlToId[item] = History.urlToId[item]; } for ( item in History.stateToId ) { if ( !History.stateToId.hasOwnProperty(item) ) { continue; } currentStore.stateToId[item] = History.stateToId[item]; } // Update History.store = currentStore; History.normalizeStore(); // In Safari, going into Private Browsing mode causes the // Session Storage object to still exist but if you try and use // or set any property/function of it it throws the exception // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to // add something to storage that exceeded the quota." infinitely // every second. currentStoreString = JSON.stringify(currentStore); try { // Store sessionStorage.setItem('History.store', currentStoreString); } catch (e) { if (e.code === DOMException.QUOTA_EXCEEDED_ERR) { if (sessionStorage.length) { // Workaround for a bug seen on iPads. Sometimes the quota exceeded error comes up and simply // removing/resetting the storage can work. sessionStorage.removeItem('History.store'); sessionStorage.setItem('History.store', currentStoreString); } else { // Otherwise, we're probably private browsing in Safari, so we'll ignore the exception. } } else { throw e; } } }; // For Internet Explorer History.intervalList.push(setInterval(History.onUnload,History.options.storeInterval)); // For Other Browsers History.Adapter.bind(window,'beforeunload',History.onUnload); History.Adapter.bind(window,'unload',History.onUnload); // Both are enabled for consistency } // Non-Native pushState Implementation if ( !History.emulated.pushState ) { // Be aware, the following is only for native pushState implementations // If you are wanting to include something for all browsers // Then include it above this if block /** * Setup Safari Fix */ if ( History.bugs.safariPoll ) { History.intervalList.push(setInterval(History.safariStatePoll, History.options.safariPollInterval)); } /** * Ensure Cross Browser Compatibility */ if ( navigator.vendor === 'Apple Computer, Inc.' || (navigator.appCodeName||'') === 'Mozilla' ) { /** * Fix Safari HashChange Issue */ // Setup Alias History.Adapter.bind(window,'hashchange',function(){ History.Adapter.trigger(window,'popstate'); }); // Initialise Alias if ( History.getHash() ) { History.Adapter.onDomLoad(function(){ History.Adapter.trigger(window,'hashchange'); }); } } } // !History.emulated.pushState }; // History.initCore // Try to Initialise History if (!History.options || !History.options.delayInit) { History.init(); } })(window); ================================================ FILE: scripts/bundled-uncompressed/html5/dojo.history.js ================================================ /** * History.js Dojo Adapter * * Essentially the same as the native adapter but uses dojo/ready for the dom load callback. * * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ // Closure (function(window,undefined){ "use strict"; // Localise Globals var History = window.History = window.History||{}, require = window.require; // Check Existence if ( typeof History.Adapter !== 'undefined' ) { throw new Error('History.js Adapter has already been loaded...'); } // Add the Adapter History.Adapter = { /** * History.Adapter.handlers[uid][eventName] = Array */ handlers: {}, /** * History.Adapter._uid * The current element unique identifier */ _uid: 1, /** * History.Adapter.uid(element) * @param {Element} element * @return {String} uid */ uid: function(element){ return element._uid || (element._uid = History.Adapter._uid++); }, /** * History.Adapter.bind(el,event,callback) * @param {Element} element * @param {String} eventName - custom and standard events * @param {Function} callback * @return */ bind: function(element,eventName,callback){ // Prepare var uid = History.Adapter.uid(element); // Apply Listener History.Adapter.handlers[uid] = History.Adapter.handlers[uid] || {}; History.Adapter.handlers[uid][eventName] = History.Adapter.handlers[uid][eventName] || []; History.Adapter.handlers[uid][eventName].push(callback); // Bind Global Listener element['on'+eventName] = (function(element,eventName){ return function(event){ History.Adapter.trigger(element,eventName,event); }; })(element,eventName); }, /** * History.Adapter.trigger(el,event) * @param {Element} element * @param {String} eventName - custom and standard events * @param {Object} event - a object of event data * @return */ trigger: function(element,eventName,event){ // Prepare event = event || {}; var uid = History.Adapter.uid(element), i,n; // Apply Listener History.Adapter.handlers[uid] = History.Adapter.handlers[uid] || {}; History.Adapter.handlers[uid][eventName] = History.Adapter.handlers[uid][eventName] || []; // Fire Listeners for ( i=0,n=History.Adapter.handlers[uid][eventName].length; i * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window,undefined){ "use strict"; // ======================================================================== // Initialise // Localise Globals var console = window.console||undefined, // Prevent a JSLint complain document = window.document, // Make sure we are using the correct document navigator = window.navigator, // Make sure we are using the correct navigator sessionStorage = false, // sessionStorage setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, setInterval = window.setInterval, clearInterval = window.clearInterval, JSON = window.JSON, alert = window.alert, History = window.History = window.History||{}, // Public History Object history = window.history; // Old History Object try { sessionStorage = window.sessionStorage; // This will throw an exception in some browsers when cookies/localStorage are explicitly disabled (i.e. Chrome) sessionStorage.setItem('TEST', '1'); sessionStorage.removeItem('TEST'); } catch(e) { sessionStorage = false; } // MooTools Compatibility JSON.stringify = JSON.stringify||JSON.encode; JSON.parse = JSON.parse||JSON.decode; // Check Existence if ( typeof History.init !== 'undefined' ) { throw new Error('History.js Core has already been loaded...'); } // Initialise History History.init = function(options){ // Check Load Status of Adapter if ( typeof History.Adapter === 'undefined' ) { return false; } // Check Load Status of Core if ( typeof History.initCore !== 'undefined' ) { History.initCore(); } // Check Load Status of HTML4 Support if ( typeof History.initHtml4 !== 'undefined' ) { History.initHtml4(); } // Return true return true; }; // ======================================================================== // Initialise Core // Initialise Core History.initCore = function(options){ // Initialise if ( typeof History.initCore.initialized !== 'undefined' ) { // Already Loaded return false; } else { History.initCore.initialized = true; } // ==================================================================== // Options /** * History.options * Configurable options */ History.options = History.options||{}; /** * History.options.hashChangeInterval * How long should the interval be before hashchange checks */ History.options.hashChangeInterval = History.options.hashChangeInterval || 100; /** * History.options.safariPollInterval * How long should the interval be before safari poll checks */ History.options.safariPollInterval = History.options.safariPollInterval || 500; /** * History.options.doubleCheckInterval * How long should the interval be before we perform a double check */ History.options.doubleCheckInterval = History.options.doubleCheckInterval || 500; /** * History.options.disableSuid * Force History not to append suid */ History.options.disableSuid = History.options.disableSuid || false; /** * History.options.storeInterval * How long should we wait between store calls */ History.options.storeInterval = History.options.storeInterval || 1000; /** * History.options.busyDelay * How long should we wait between busy events */ History.options.busyDelay = History.options.busyDelay || 250; /** * History.options.debug * If true will enable debug messages to be logged */ History.options.debug = History.options.debug || false; /** * History.options.initialTitle * What is the title of the initial state */ History.options.initialTitle = History.options.initialTitle || document.title; /** * History.options.html4Mode * If true, will force HTMl4 mode (hashtags) */ History.options.html4Mode = History.options.html4Mode || false; /** * History.options.delayInit * Want to override default options and call init manually. */ History.options.delayInit = History.options.delayInit || false; // ==================================================================== // Interval record /** * History.intervalList * List of intervals set, to be cleared when document is unloaded. */ History.intervalList = []; /** * History.clearAllIntervals * Clears all setInterval instances. */ History.clearAllIntervals = function(){ var i, il = History.intervalList; if (typeof il !== "undefined" && il !== null) { for (i = 0; i < il.length; i++) { clearInterval(il[i]); } History.intervalList = null; } }; // ==================================================================== // Debug /** * History.debug(message,...) * Logs the passed arguments if debug enabled */ History.debug = function(){ if ( (History.options.debug||false) ) { History.log.apply(History,arguments); } }; /** * History.log(message,...) * Logs the passed arguments */ History.log = function(){ // Prepare var consoleExists = !(typeof console === 'undefined' || typeof console.log === 'undefined' || typeof console.log.apply === 'undefined'), textarea = document.getElementById('log'), message, i,n, args,arg ; // Write to Console if ( consoleExists ) { args = Array.prototype.slice.call(arguments); message = args.shift(); if ( typeof console.debug !== 'undefined' ) { console.debug.apply(console,[message,args]); } else { console.log.apply(console,[message,args]); } } else { message = ("\n"+arguments[0]+"\n"); } // Write to log for ( i=1,n=arguments.length; i * @author James Padolsey */ History.getInternetExplorerMajorVersion = function(){ var result = History.getInternetExplorerMajorVersion.cached = (typeof History.getInternetExplorerMajorVersion.cached !== 'undefined') ? History.getInternetExplorerMajorVersion.cached : (function(){ var v = 3, div = document.createElement('div'), all = div.getElementsByTagName('i'); while ( (div.innerHTML = '') && all[0] ) {} return (v > 4) ? v : false; })() ; return result; }; /** * History.isInternetExplorer() * Are we using Internet Explorer? * @return {boolean} * @license Public Domain * @author Benjamin Arthur Lupton */ History.isInternetExplorer = function(){ var result = History.isInternetExplorer.cached = (typeof History.isInternetExplorer.cached !== 'undefined') ? History.isInternetExplorer.cached : Boolean(History.getInternetExplorerMajorVersion()) ; return result; }; /** * History.emulated * Which features require emulating? */ if (History.options.html4Mode) { History.emulated = { pushState : true, hashChange: true }; } else { History.emulated = { pushState: !Boolean( window.history && window.history.pushState && window.history.replaceState && !( (/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i).test(navigator.userAgent) /* disable for versions of iOS before version 4.3 (8F190) */ || (/AppleWebKit\/5([0-2]|3[0-2])/i).test(navigator.userAgent) /* disable for the mercury iOS browser, or at least older versions of the webkit engine */ ) ), hashChange: Boolean( !(('onhashchange' in window) || ('onhashchange' in document)) || (History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8) ) }; } /** * History.enabled * Is History enabled? */ History.enabled = !History.emulated.pushState; /** * History.bugs * Which bugs are present */ History.bugs = { /** * Safari 5 and Safari iOS 4 fail to return to the correct state once a hash is replaced by a `replaceState` call * https://bugs.webkit.org/show_bug.cgi?id=56249 */ setHash: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * Safari 5 and Safari iOS 4 sometimes fail to apply the state change under busy conditions * https://bugs.webkit.org/show_bug.cgi?id=42940 */ safariPoll: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * MSIE 6 and 7 sometimes do not apply a hash even it was told to (requiring a second call to the apply function) */ ieDoubleCheck: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8), /** * MSIE 6 requires the entire hash to be encoded for the hashes to trigger the onHashChange event */ hashEscape: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 7) }; /** * History.isEmptyObject(obj) * Checks to see if the Object is Empty * @param {Object} obj * @return {boolean} */ History.isEmptyObject = function(obj) { for ( var name in obj ) { if ( obj.hasOwnProperty(name) ) { return false; } } return true; }; /** * History.cloneObject(obj) * Clones a object and eliminate all references to the original contexts * @param {Object} obj * @return {Object} */ History.cloneObject = function(obj) { var hash,newObj; if ( obj ) { hash = JSON.stringify(obj); newObj = JSON.parse(hash); } else { newObj = {}; } return newObj; }; // ==================================================================== // URL Helpers /** * History.getRootUrl() * Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com" * @return {String} rootUrl */ History.getRootUrl = function(){ // Create var rootUrl = document.location.protocol+'//'+(document.location.hostname||document.location.host); if ( document.location.port||false ) { rootUrl += ':'+document.location.port; } rootUrl += '/'; // Return return rootUrl; }; /** * History.getBaseHref() * Fetches the `href` attribute of the `` element if it exists * @return {String} baseHref */ History.getBaseHref = function(){ // Create var baseElements = document.getElementsByTagName('base'), baseElement = null, baseHref = ''; // Test for Base Element if ( baseElements.length === 1 ) { // Prepare for Base Element baseElement = baseElements[0]; baseHref = baseElement.href.replace(/[^\/]+$/,''); } // Adjust trailing slash baseHref = baseHref.replace(/\/+$/,''); if ( baseHref ) baseHref += '/'; // Return return baseHref; }; /** * History.getBaseUrl() * Fetches the baseHref or basePageUrl or rootUrl (whichever one exists first) * @return {String} baseUrl */ History.getBaseUrl = function(){ // Create var baseUrl = History.getBaseHref()||History.getBasePageUrl()||History.getRootUrl(); // Return return baseUrl; }; /** * History.getPageUrl() * Fetches the URL of the current page * @return {String} pageUrl */ History.getPageUrl = function(){ // Fetch var State = History.getState(false,false), stateUrl = (State||{}).url||History.getLocationHref(), pageUrl; // Create pageUrl = stateUrl.replace(/\/+$/,'').replace(/[^\/]+$/,function(part,index,string){ return (/\./).test(part) ? part : part+'/'; }); // Return return pageUrl; }; /** * History.getBasePageUrl() * Fetches the Url of the directory of the current page * @return {String} basePageUrl */ History.getBasePageUrl = function(){ // Create var basePageUrl = (History.getLocationHref()).replace(/[#\?].*/,'').replace(/[^\/]+$/,function(part,index,string){ return (/[^\/]$/).test(part) ? '' : part; }).replace(/\/+$/,'')+'/'; // Return return basePageUrl; }; /** * History.getFullUrl(url) * Ensures that we have an absolute URL and not a relative URL * @param {string} url * @param {Boolean} allowBaseHref * @return {string} fullUrl */ History.getFullUrl = function(url,allowBaseHref){ // Prepare var fullUrl = url, firstChar = url.substring(0,1); allowBaseHref = (typeof allowBaseHref === 'undefined') ? true : allowBaseHref; // Check if ( /[a-z]+\:\/\//.test(url) ) { // Full URL } else if ( firstChar === '/' ) { // Root URL fullUrl = History.getRootUrl()+url.replace(/^\/+/,''); } else if ( firstChar === '#' ) { // Anchor URL fullUrl = History.getPageUrl().replace(/#.*/,'')+url; } else if ( firstChar === '?' ) { // Query URL fullUrl = History.getPageUrl().replace(/[\?#].*/,'')+url; } else { // Relative URL if ( allowBaseHref ) { fullUrl = History.getBaseUrl()+url.replace(/^(\.\/)+/,''); } else { fullUrl = History.getBasePageUrl()+url.replace(/^(\.\/)+/,''); } // We have an if condition above as we do not want hashes // which are relative to the baseHref in our URLs // as if the baseHref changes, then all our bookmarks // would now point to different locations // whereas the basePageUrl will always stay the same } // Return return fullUrl.replace(/\#$/,''); }; /** * History.getShortUrl(url) * Ensures that we have a relative URL and not a absolute URL * @param {string} url * @return {string} url */ History.getShortUrl = function(url){ // Prepare var shortUrl = url, baseUrl = History.getBaseUrl(), rootUrl = History.getRootUrl(); // Trim baseUrl if ( History.emulated.pushState ) { // We are in a if statement as when pushState is not emulated // The actual url these short urls are relative to can change // So within the same session, we the url may end up somewhere different shortUrl = shortUrl.replace(baseUrl,''); } // Trim rootUrl shortUrl = shortUrl.replace(rootUrl,'/'); // Ensure we can still detect it as a state if ( History.isTraditionalAnchor(shortUrl) ) { shortUrl = './'+shortUrl; } // Clean It shortUrl = shortUrl.replace(/^(\.\/)+/g,'./').replace(/\#$/,''); // Return return shortUrl; }; /** * History.getLocationHref(document) * Returns a normalized version of document.location.href * accounting for browser inconsistencies, etc. * * This URL will be URI-encoded and will include the hash * * @param {object} document * @return {string} url */ History.getLocationHref = function(doc) { doc = doc || document; // most of the time, this will be true if (doc.URL === doc.location.href) return doc.location.href; // some versions of webkit URI-decode document.location.href // but they leave document.URL in an encoded state if (doc.location.href === decodeURIComponent(doc.URL)) return doc.URL; // FF 3.6 only updates document.URL when a page is reloaded // document.location.href is updated correctly if (doc.location.hash && decodeURIComponent(doc.location.href.replace(/^[^#]+/, "")) === doc.location.hash) return doc.location.href; if (doc.URL.indexOf('#') == -1 && doc.location.href.indexOf('#') != -1) return doc.location.href; return doc.URL || doc.location.href; }; // ==================================================================== // State Storage /** * History.store * The store for all session specific data */ History.store = {}; /** * History.idToState * 1-1: State ID to State Object */ History.idToState = History.idToState||{}; /** * History.stateToId * 1-1: State String to State ID */ History.stateToId = History.stateToId||{}; /** * History.urlToId * 1-1: State URL to State ID */ History.urlToId = History.urlToId||{}; /** * History.storedStates * Store the states in an array */ History.storedStates = History.storedStates||[]; /** * History.savedStates * Saved the states in an array */ History.savedStates = History.savedStates||[]; /** * History.noramlizeStore() * Noramlize the store by adding necessary values */ History.normalizeStore = function(){ History.store.idToState = History.store.idToState||{}; History.store.urlToId = History.store.urlToId||{}; History.store.stateToId = History.store.stateToId||{}; }; /** * History.getState() * Get an object containing the data, title and url of the current state * @param {Boolean} friendly * @param {Boolean} create * @return {Object} State */ History.getState = function(friendly,create){ // Prepare if ( typeof friendly === 'undefined' ) { friendly = true; } if ( typeof create === 'undefined' ) { create = true; } // Fetch var State = History.getLastSavedState(); // Create if ( !State && create ) { State = History.createStateObject(); } // Adjust if ( friendly ) { State = History.cloneObject(State); State.url = State.cleanUrl||State.url; } // Return return State; }; /** * History.getIdByState(State) * Gets a ID for a State * @param {State} newState * @return {String} id */ History.getIdByState = function(newState){ // Fetch ID var id = History.extractId(newState.url), str; if ( !id ) { // Find ID via State String str = History.getStateString(newState); if ( typeof History.stateToId[str] !== 'undefined' ) { id = History.stateToId[str]; } else if ( typeof History.store.stateToId[str] !== 'undefined' ) { id = History.store.stateToId[str]; } else { // Generate a new ID while ( true ) { id = (new Date()).getTime() + String(Math.random()).replace(/\D/g,''); if ( typeof History.idToState[id] === 'undefined' && typeof History.store.idToState[id] === 'undefined' ) { break; } } // Apply the new State to the ID History.stateToId[str] = id; History.idToState[id] = newState; } } // Return ID return id; }; /** * History.normalizeState(State) * Expands a State Object * @param {object} State * @return {object} */ History.normalizeState = function(oldState){ // Variables var newState, dataNotEmpty; // Prepare if ( !oldState || (typeof oldState !== 'object') ) { oldState = {}; } // Check if ( typeof oldState.normalized !== 'undefined' ) { return oldState; } // Adjust if ( !oldState.data || (typeof oldState.data !== 'object') ) { oldState.data = {}; } // ---------------------------------------------------------------- // Create newState = {}; newState.normalized = true; newState.title = oldState.title||''; newState.url = History.getFullUrl(oldState.url?oldState.url:(History.getLocationHref())); newState.hash = History.getShortUrl(newState.url); newState.data = History.cloneObject(oldState.data); // Fetch ID newState.id = History.getIdByState(newState); // ---------------------------------------------------------------- // Clean the URL newState.cleanUrl = newState.url.replace(/\??\&_suid.*/,''); newState.url = newState.cleanUrl; // Check to see if we have more than just a url dataNotEmpty = !History.isEmptyObject(newState.data); // Apply if ( (newState.title || dataNotEmpty) && History.options.disableSuid !== true ) { // Add ID to Hash newState.hash = History.getShortUrl(newState.url).replace(/\??\&_suid.*/,''); if ( !/\?/.test(newState.hash) ) { newState.hash += '?'; } newState.hash += '&_suid='+newState.id; } // Create the Hashed URL newState.hashedUrl = History.getFullUrl(newState.hash); // ---------------------------------------------------------------- // Update the URL if we have a duplicate if ( (History.emulated.pushState || History.bugs.safariPoll) && History.hasUrlDuplicate(newState) ) { newState.url = newState.hashedUrl; } // ---------------------------------------------------------------- // Return return newState; }; /** * History.createStateObject(data,title,url) * Creates a object based on the data, title and url state params * @param {object} data * @param {string} title * @param {string} url * @return {object} */ History.createStateObject = function(data,title,url){ // Hashify var State = { 'data': data, 'title': title, 'url': url }; // Expand the State State = History.normalizeState(State); // Return object return State; }; /** * History.getStateById(id) * Get a state by it's UID * @param {String} id */ History.getStateById = function(id){ // Prepare id = String(id); // Retrieve var State = History.idToState[id] || History.store.idToState[id] || undefined; // Return State return State; }; /** * Get a State's String * @param {State} passedState */ History.getStateString = function(passedState){ // Prepare var State, cleanedState, str; // Fetch State = History.normalizeState(passedState); // Clean cleanedState = { data: State.data, title: passedState.title, url: passedState.url }; // Fetch str = JSON.stringify(cleanedState); // Return return str; }; /** * Get a State's ID * @param {State} passedState * @return {String} id */ History.getStateId = function(passedState){ // Prepare var State, id; // Fetch State = History.normalizeState(passedState); // Fetch id = State.id; // Return return id; }; /** * History.getHashByState(State) * Creates a Hash for the State Object * @param {State} passedState * @return {String} hash */ History.getHashByState = function(passedState){ // Prepare var State, hash; // Fetch State = History.normalizeState(passedState); // Hash hash = State.hash; // Return return hash; }; /** * History.extractId(url_or_hash) * Get a State ID by it's URL or Hash * @param {string} url_or_hash * @return {string} id */ History.extractId = function ( url_or_hash ) { // Prepare var id,parts,url, tmp; // Extract // If the URL has a #, use the id from before the # if (url_or_hash.indexOf('#') != -1) { tmp = url_or_hash.split("#")[0]; } else { tmp = url_or_hash; } parts = /(.*)\&_suid=([0-9]+)$/.exec(tmp); url = parts ? (parts[1]||url_or_hash) : url_or_hash; id = parts ? String(parts[2]||'') : ''; // Return return id||false; }; /** * History.isTraditionalAnchor * Checks to see if the url is a traditional anchor or not * @param {String} url_or_hash * @return {Boolean} */ History.isTraditionalAnchor = function(url_or_hash){ // Check var isTraditional = !(/[\/\?\.]/.test(url_or_hash)); // Return return isTraditional; }; /** * History.extractState * Get a State by it's URL or Hash * @param {String} url_or_hash * @return {State|null} */ History.extractState = function(url_or_hash,create){ // Prepare var State = null, id, url; create = create||false; // Fetch SUID id = History.extractId(url_or_hash); if ( id ) { State = History.getStateById(id); } // Fetch SUID returned no State if ( !State ) { // Fetch URL url = History.getFullUrl(url_or_hash); // Check URL id = History.getIdByUrl(url)||false; if ( id ) { State = History.getStateById(id); } // Create State if ( !State && create && !History.isTraditionalAnchor(url_or_hash) ) { State = History.createStateObject(null,null,url); } } // Return return State; }; /** * History.getIdByUrl() * Get a State ID by a State URL */ History.getIdByUrl = function(url){ // Fetch var id = History.urlToId[url] || History.store.urlToId[url] || undefined; // Return return id; }; /** * History.getLastSavedState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastSavedState = function(){ return History.savedStates[History.savedStates.length-1]||undefined; }; /** * History.getLastStoredState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastStoredState = function(){ return History.storedStates[History.storedStates.length-1]||undefined; }; /** * History.hasUrlDuplicate * Checks if a Url will have a url conflict * @param {Object} newState * @return {Boolean} hasDuplicate */ History.hasUrlDuplicate = function(newState) { // Prepare var hasDuplicate = false, oldState; // Fetch oldState = History.extractState(newState.url); // Check hasDuplicate = oldState && oldState.id !== newState.id; // Return return hasDuplicate; }; /** * History.storeState * Store a State * @param {Object} newState * @return {Object} newState */ History.storeState = function(newState){ // Store the State History.urlToId[newState.url] = newState.id; // Push the State History.storedStates.push(History.cloneObject(newState)); // Return newState return newState; }; /** * History.isLastSavedState(newState) * Tests to see if the state is the last state * @param {Object} newState * @return {boolean} isLast */ History.isLastSavedState = function(newState){ // Prepare var isLast = false, newId, oldState, oldId; // Check if ( History.savedStates.length ) { newId = newState.id; oldState = History.getLastSavedState(); oldId = oldState.id; // Check isLast = (newId === oldId); } // Return return isLast; }; /** * History.saveState * Push a State * @param {Object} newState * @return {boolean} changed */ History.saveState = function(newState){ // Check Hash if ( History.isLastSavedState(newState) ) { return false; } // Push the State History.savedStates.push(History.cloneObject(newState)); // Return true return true; }; /** * History.getStateByIndex() * Gets a state by the index * @param {integer} index * @return {Object} */ History.getStateByIndex = function(index){ // Prepare var State = null; // Handle if ( typeof index === 'undefined' ) { // Get the last inserted State = History.savedStates[History.savedStates.length-1]; } else if ( index < 0 ) { // Get from the end State = History.savedStates[History.savedStates.length+index]; } else { // Get from the beginning State = History.savedStates[index]; } // Return State return State; }; /** * History.getCurrentIndex() * Gets the current index * @return (integer) */ History.getCurrentIndex = function(){ // Prepare var index = null; // No states saved if(History.savedStates.length < 1) { index = 0; } else { index = History.savedStates.length-1; } return index; }; // ==================================================================== // Hash Helpers /** * History.getHash() * @param {Location=} location * Gets the current document hash * Note: unlike location.hash, this is guaranteed to return the escaped hash in all browsers * @return {string} */ History.getHash = function(doc){ var url = History.getLocationHref(doc), hash; hash = History.getHashByUrl(url); return hash; }; /** * History.unescapeHash() * normalize and Unescape a Hash * @param {String} hash * @return {string} */ History.unescapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Unescape hash result = decodeURIComponent(result); // Return result return result; }; /** * History.normalizeHash() * normalize a hash across browsers * @return {string} */ History.normalizeHash = function(hash){ // Prepare var result = hash.replace(/[^#]*#/,'').replace(/#.*/, ''); // Return result return result; }; /** * History.setHash(hash) * Sets the document hash * @param {string} hash * @return {History} */ History.setHash = function(hash,queue){ // Prepare var State, pageUrl; // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.setHash: we must wait', arguments); History.pushQueue({ scope: History, callback: History.setHash, args: arguments, queue: queue }); return false; } // Log //History.debug('History.setHash: called',hash); // Make Busy + Continue History.busy(true); // Check if hash is a state State = History.extractState(hash,true); if ( State && !History.emulated.pushState ) { // Hash is a state so skip the setHash //History.debug('History.setHash: Hash is a state so skipping the hash set with a direct pushState call',arguments); // PushState History.pushState(State.data,State.title,State.url,false); } else if ( History.getHash() !== hash ) { // Hash is a proper hash, so apply it // Handle browser bugs if ( History.bugs.setHash ) { // Fix Safari Bug https://bugs.webkit.org/show_bug.cgi?id=56249 // Fetch the base page pageUrl = History.getPageUrl(); // Safari hash apply History.pushState(null,null,pageUrl+'#'+hash,false); } else { // Normal hash apply document.location.hash = hash; } } // Chain return History; }; /** * History.escape() * normalize and Escape a Hash * @return {string} */ History.escapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Escape hash result = window.encodeURIComponent(result); // IE6 Escape Bug if ( !History.bugs.hashEscape ) { // Restore common parts result = result .replace(/\%21/g,'!') .replace(/\%26/g,'&') .replace(/\%3D/g,'=') .replace(/\%3F/g,'?'); } // Return result return result; }; /** * History.getHashByUrl(url) * Extracts the Hash from a URL * @param {string} url * @return {string} url */ History.getHashByUrl = function(url){ // Extract the hash var hash = String(url) .replace(/([^#]*)#?([^#]*)#?(.*)/, '$2') ; // Unescape hash hash = History.unescapeHash(hash); // Return hash return hash; }; /** * History.setTitle(title) * Applies the title to the document * @param {State} newState * @return {Boolean} */ History.setTitle = function(newState){ // Prepare var title = newState.title, firstState; // Initial if ( !title ) { firstState = History.getStateByIndex(0); if ( firstState && firstState.url === newState.url ) { title = firstState.title||History.options.initialTitle; } } // Apply try { document.getElementsByTagName('title')[0].innerHTML = title.replace('<','<').replace('>','>').replace(' & ',' & '); } catch ( Exception ) { } document.title = title; // Chain return History; }; // ==================================================================== // Queueing /** * History.queues * The list of queues to use * First In, First Out */ History.queues = []; /** * History.busy(value) * @param {boolean} value [optional] * @return {boolean} busy */ History.busy = function(value){ // Apply if ( typeof value !== 'undefined' ) { //History.debug('History.busy: changing ['+(History.busy.flag||false)+'] to ['+(value||false)+']', History.queues.length); History.busy.flag = value; } // Default else if ( typeof History.busy.flag === 'undefined' ) { History.busy.flag = false; } // Queue if ( !History.busy.flag ) { // Execute the next item in the queue clearTimeout(History.busy.timeout); var fireNext = function(){ var i, queue, item; if ( History.busy.flag ) return; for ( i=History.queues.length-1; i >= 0; --i ) { queue = History.queues[i]; if ( queue.length === 0 ) continue; item = queue.shift(); History.fireQueueItem(item); History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } }; History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } // Return return History.busy.flag; }; /** * History.busy.flag */ History.busy.flag = false; /** * History.fireQueueItem(item) * Fire a Queue Item * @param {Object} item * @return {Mixed} result */ History.fireQueueItem = function(item){ return item.callback.apply(item.scope||History,item.args||[]); }; /** * History.pushQueue(callback,args) * Add an item to the queue * @param {Object} item [scope,callback,args,queue] */ History.pushQueue = function(item){ // Prepare the queue History.queues[item.queue||0] = History.queues[item.queue||0]||[]; // Add to the queue History.queues[item.queue||0].push(item); // Chain return History; }; /** * History.queue (item,queue), (func,queue), (func), (item) * Either firs the item now if not busy, or adds it to the queue */ History.queue = function(item,queue){ // Prepare if ( typeof item === 'function' ) { item = { callback: item }; } if ( typeof queue !== 'undefined' ) { item.queue = queue; } // Handle if ( History.busy() ) { History.pushQueue(item); } else { History.fireQueueItem(item); } // Chain return History; }; /** * History.clearQueue() * Clears the Queue */ History.clearQueue = function(){ History.busy.flag = false; History.queues = []; return History; }; // ==================================================================== // IE Bug Fix /** * History.stateChanged * States whether or not the state has changed since the last double check was initialised */ History.stateChanged = false; /** * History.doubleChecker * Contains the timeout used for the double checks */ History.doubleChecker = false; /** * History.doubleCheckComplete() * Complete a double check * @return {History} */ History.doubleCheckComplete = function(){ // Update History.stateChanged = true; // Clear History.doubleCheckClear(); // Chain return History; }; /** * History.doubleCheckClear() * Clear a double check * @return {History} */ History.doubleCheckClear = function(){ // Clear if ( History.doubleChecker ) { clearTimeout(History.doubleChecker); History.doubleChecker = false; } // Chain return History; }; /** * History.doubleCheck() * Create a double check * @return {History} */ History.doubleCheck = function(tryAgain){ // Reset History.stateChanged = false; History.doubleCheckClear(); // Fix IE6,IE7 bug where calling history.back or history.forward does not actually change the hash (whereas doing it manually does) // Fix Safari 5 bug where sometimes the state does not change: https://bugs.webkit.org/show_bug.cgi?id=42940 if ( History.bugs.ieDoubleCheck ) { // Apply Check History.doubleChecker = setTimeout( function(){ History.doubleCheckClear(); if ( !History.stateChanged ) { //History.debug('History.doubleCheck: State has not yet changed, trying again', arguments); // Re-Attempt tryAgain(); } return true; }, History.options.doubleCheckInterval ); } // Chain return History; }; // ==================================================================== // Safari Bug Fix /** * History.safariStatePoll() * Poll the current state * @return {History} */ History.safariStatePoll = function(){ // Poll the URL // Get the Last State which has the new URL var urlState = History.extractState(History.getLocationHref()), newState; // Check for a difference if ( !History.isLastSavedState(urlState) ) { newState = urlState; } else { return; } // Check if we have a state with that url // If not create it if ( !newState ) { //History.debug('History.safariStatePoll: new'); newState = History.createStateObject(); } // Apply the New State //History.debug('History.safariStatePoll: trigger'); History.Adapter.trigger(window,'popstate'); // Chain return History; }; // ==================================================================== // State Aliases /** * History.back(queue) * Send the browser history back one item * @param {Integer} queue [optional] */ History.back = function(queue){ //History.debug('History.back: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.back: we must wait', arguments); History.pushQueue({ scope: History, callback: History.back, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.back(false); }); // Go back history.go(-1); // End back closure return true; }; /** * History.forward(queue) * Send the browser history forward one item * @param {Integer} queue [optional] */ History.forward = function(queue){ //History.debug('History.forward: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.forward: we must wait', arguments); History.pushQueue({ scope: History, callback: History.forward, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.forward(false); }); // Go forward history.go(1); // End forward closure return true; }; /** * History.go(index,queue) * Send the browser history back or forward index times * @param {Integer} queue [optional] */ History.go = function(index,queue){ //History.debug('History.go: called', arguments); // Prepare var i; // Handle if ( index > 0 ) { // Forward for ( i=1; i<=index; ++i ) { History.forward(queue); } } else if ( index < 0 ) { // Backward for ( i=-1; i>=index; --i ) { History.back(queue); } } else { throw new Error('History.go: History.go requires a positive or negative integer passed.'); } // Chain return History; }; // ==================================================================== // HTML5 State Support // Non-Native pushState Implementation if ( History.emulated.pushState ) { /* * Provide Skeleton for HTML4 Browsers */ // Prepare var emptyFunction = function(){}; History.pushState = History.pushState||emptyFunction; History.replaceState = History.replaceState||emptyFunction; } // History.emulated.pushState // Native pushState Implementation else { /* * Use native HTML5 History API Implementation */ /** * History.onPopState(event,extra) * Refresh the Current State */ History.onPopState = function(event,extra){ // Prepare var stateId = false, newState = false, currentHash, currentState; // Reset the double check History.doubleCheckComplete(); // Check for a Hash, and handle apporiatly currentHash = History.getHash(); if ( currentHash ) { // Expand Hash currentState = History.extractState(currentHash||History.getLocationHref(),true); if ( currentState ) { // We were able to parse it, it must be a State! // Let's forward to replaceState //History.debug('History.onPopState: state anchor', currentHash, currentState); History.replaceState(currentState.data, currentState.title, currentState.url, false); } else { // Traditional Anchor //History.debug('History.onPopState: traditional anchor', currentHash); History.Adapter.trigger(window,'anchorchange'); History.busy(false); } // We don't care for hashes History.expectedStateId = false; return false; } // Ensure stateId = History.Adapter.extractEventData('state',event,extra) || false; // Fetch State if ( stateId ) { // Vanilla: Back/forward button was used newState = History.getStateById(stateId); } else if ( History.expectedStateId ) { // Vanilla: A new state was pushed, and popstate was called manually newState = History.getStateById(History.expectedStateId); } else { // Initial State newState = History.extractState(History.getLocationHref()); } // The State did not exist in our store if ( !newState ) { // Regenerate the State newState = History.createStateObject(null,null,History.getLocationHref()); } // Clean History.expectedStateId = false; // Check if we are the same state if ( History.isLastSavedState(newState) ) { // There has been no change (just the page's hash has finally propagated) //History.debug('History.onPopState: no change', newState, History.savedStates); History.busy(false); return false; } // Store the State History.storeState(newState); History.saveState(newState); // Force update of the title History.setTitle(newState); // Fire Our Event History.Adapter.trigger(window,'statechange'); History.busy(false); // Return true return true; }; History.Adapter.bind(window,'popstate',History.onPopState); /** * History.pushState(data,title,url) * Add a new State to the history object, become it, and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.pushState = function(data,title,url,queue){ //History.debug('History.pushState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.pushState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.pushState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.pushState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End pushState closure return true; }; /** * History.replaceState(data,title,url) * Replace the State and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.replaceState = function(data,title,url,queue){ //History.debug('History.replaceState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.replaceState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.replaceState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.replaceState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End replaceState closure return true; }; } // !History.emulated.pushState // ==================================================================== // Initialise /** * Load the Store */ if ( sessionStorage ) { // Fetch try { History.store = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { History.store = {}; } // Normalize History.normalizeStore(); } else { // Default Load History.store = {}; History.normalizeStore(); } /** * Clear Intervals on exit to prevent memory leaks */ History.Adapter.bind(window,"unload",History.clearAllIntervals); /** * Create the initial State */ History.saveState(History.storeState(History.extractState(History.getLocationHref(),true))); /** * Bind for Saving Store */ if ( sessionStorage ) { // When the page is closed History.onUnload = function(){ // Prepare var currentStore, item, currentStoreString; // Fetch try { currentStore = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { currentStore = {}; } // Ensure currentStore.idToState = currentStore.idToState || {}; currentStore.urlToId = currentStore.urlToId || {}; currentStore.stateToId = currentStore.stateToId || {}; // Sync for ( item in History.idToState ) { if ( !History.idToState.hasOwnProperty(item) ) { continue; } currentStore.idToState[item] = History.idToState[item]; } for ( item in History.urlToId ) { if ( !History.urlToId.hasOwnProperty(item) ) { continue; } currentStore.urlToId[item] = History.urlToId[item]; } for ( item in History.stateToId ) { if ( !History.stateToId.hasOwnProperty(item) ) { continue; } currentStore.stateToId[item] = History.stateToId[item]; } // Update History.store = currentStore; History.normalizeStore(); // In Safari, going into Private Browsing mode causes the // Session Storage object to still exist but if you try and use // or set any property/function of it it throws the exception // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to // add something to storage that exceeded the quota." infinitely // every second. currentStoreString = JSON.stringify(currentStore); try { // Store sessionStorage.setItem('History.store', currentStoreString); } catch (e) { if (e.code === DOMException.QUOTA_EXCEEDED_ERR) { if (sessionStorage.length) { // Workaround for a bug seen on iPads. Sometimes the quota exceeded error comes up and simply // removing/resetting the storage can work. sessionStorage.removeItem('History.store'); sessionStorage.setItem('History.store', currentStoreString); } else { // Otherwise, we're probably private browsing in Safari, so we'll ignore the exception. } } else { throw e; } } }; // For Internet Explorer History.intervalList.push(setInterval(History.onUnload,History.options.storeInterval)); // For Other Browsers History.Adapter.bind(window,'beforeunload',History.onUnload); History.Adapter.bind(window,'unload',History.onUnload); // Both are enabled for consistency } // Non-Native pushState Implementation if ( !History.emulated.pushState ) { // Be aware, the following is only for native pushState implementations // If you are wanting to include something for all browsers // Then include it above this if block /** * Setup Safari Fix */ if ( History.bugs.safariPoll ) { History.intervalList.push(setInterval(History.safariStatePoll, History.options.safariPollInterval)); } /** * Ensure Cross Browser Compatibility */ if ( navigator.vendor === 'Apple Computer, Inc.' || (navigator.appCodeName||'') === 'Mozilla' ) { /** * Fix Safari HashChange Issue */ // Setup Alias History.Adapter.bind(window,'hashchange',function(){ History.Adapter.trigger(window,'popstate'); }); // Initialise Alias if ( History.getHash() ) { History.Adapter.onDomLoad(function(){ History.Adapter.trigger(window,'hashchange'); }); } } } // !History.emulated.pushState }; // History.initCore // Try to Initialise History if (!History.options || !History.options.delayInit) { History.init(); } })(window); ================================================ FILE: scripts/bundled-uncompressed/html5/extjs.history.js ================================================ /** * History.js ExtJS Adapter * @author Sean Adkinson * @copyright 2012 Sean Adkinson * @license New BSD License */ // Closure (function(window,undefined){ "use strict"; // Localise Globals var History = window.History = window.History||{}, Ext = window.Ext; window.JSON = { stringify: Ext.JSON.encode, parse: Ext.JSON.decode }; // Check Existence if ( typeof History.Adapter !== 'undefined' ) { throw new Error('History.js Adapter has already been loaded...'); } // Add the Adapter History.Adapter = { observables: {}, /** * History.Adapter.bind(el,event,callback) * @param {Element|string} el * @param {string} event - custom and standard events * @param {function} callback * @param {Object} scope * @return {void} */ bind: function(element,eventName,callback,scope){ Ext.EventManager.addListener(element, eventName, callback, scope); //bind an observable to the element that will let us "trigger" events on it var id = Ext.id(element, 'history-'), observable = this.observables[id]; if (!observable) { observable = Ext.create('Ext.util.Observable'); this.observables[id] = observable; } observable.on(eventName, callback, scope); }, /** * History.Adapter.trigger(el,event) * @param {Element|string} el * @param {string} event - custom and standard events * @param {Object=} extra - a object of extra event data (optional) * @return {void} */ trigger: function(element,eventName,extra){ var id = Ext.id(element, 'history-'), observable = this.observables[id]; if (observable) { observable.fireEvent(eventName, extra); } }, /** * History.Adapter.extractEventData(key,event,extra) * @param {string} key - key for the event data to extract * @param {string} event - custom and standard events * @param {Object=} extra - a object of extra event data (optional) * @return {mixed} */ extractEventData: function(key,event,extra){ var result = (event && event.browserEvent && event.browserEvent[key]) || (extra && extra[key]) || undefined; return result; }, /** * History.Adapter.onDomLoad(callback) * @param {function} callback * @return {void} */ onDomLoad: function(callback) { Ext.onReady(callback); } }; // Try and Initialise History if ( typeof History.init !== 'undefined' ) { History.init(); } })(window);/** * History.js Core * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window,undefined){ "use strict"; // ======================================================================== // Initialise // Localise Globals var console = window.console||undefined, // Prevent a JSLint complain document = window.document, // Make sure we are using the correct document navigator = window.navigator, // Make sure we are using the correct navigator sessionStorage = false, // sessionStorage setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, setInterval = window.setInterval, clearInterval = window.clearInterval, JSON = window.JSON, alert = window.alert, History = window.History = window.History||{}, // Public History Object history = window.history; // Old History Object try { sessionStorage = window.sessionStorage; // This will throw an exception in some browsers when cookies/localStorage are explicitly disabled (i.e. Chrome) sessionStorage.setItem('TEST', '1'); sessionStorage.removeItem('TEST'); } catch(e) { sessionStorage = false; } // MooTools Compatibility JSON.stringify = JSON.stringify||JSON.encode; JSON.parse = JSON.parse||JSON.decode; // Check Existence if ( typeof History.init !== 'undefined' ) { throw new Error('History.js Core has already been loaded...'); } // Initialise History History.init = function(options){ // Check Load Status of Adapter if ( typeof History.Adapter === 'undefined' ) { return false; } // Check Load Status of Core if ( typeof History.initCore !== 'undefined' ) { History.initCore(); } // Check Load Status of HTML4 Support if ( typeof History.initHtml4 !== 'undefined' ) { History.initHtml4(); } // Return true return true; }; // ======================================================================== // Initialise Core // Initialise Core History.initCore = function(options){ // Initialise if ( typeof History.initCore.initialized !== 'undefined' ) { // Already Loaded return false; } else { History.initCore.initialized = true; } // ==================================================================== // Options /** * History.options * Configurable options */ History.options = History.options||{}; /** * History.options.hashChangeInterval * How long should the interval be before hashchange checks */ History.options.hashChangeInterval = History.options.hashChangeInterval || 100; /** * History.options.safariPollInterval * How long should the interval be before safari poll checks */ History.options.safariPollInterval = History.options.safariPollInterval || 500; /** * History.options.doubleCheckInterval * How long should the interval be before we perform a double check */ History.options.doubleCheckInterval = History.options.doubleCheckInterval || 500; /** * History.options.disableSuid * Force History not to append suid */ History.options.disableSuid = History.options.disableSuid || false; /** * History.options.storeInterval * How long should we wait between store calls */ History.options.storeInterval = History.options.storeInterval || 1000; /** * History.options.busyDelay * How long should we wait between busy events */ History.options.busyDelay = History.options.busyDelay || 250; /** * History.options.debug * If true will enable debug messages to be logged */ History.options.debug = History.options.debug || false; /** * History.options.initialTitle * What is the title of the initial state */ History.options.initialTitle = History.options.initialTitle || document.title; /** * History.options.html4Mode * If true, will force HTMl4 mode (hashtags) */ History.options.html4Mode = History.options.html4Mode || false; /** * History.options.delayInit * Want to override default options and call init manually. */ History.options.delayInit = History.options.delayInit || false; // ==================================================================== // Interval record /** * History.intervalList * List of intervals set, to be cleared when document is unloaded. */ History.intervalList = []; /** * History.clearAllIntervals * Clears all setInterval instances. */ History.clearAllIntervals = function(){ var i, il = History.intervalList; if (typeof il !== "undefined" && il !== null) { for (i = 0; i < il.length; i++) { clearInterval(il[i]); } History.intervalList = null; } }; // ==================================================================== // Debug /** * History.debug(message,...) * Logs the passed arguments if debug enabled */ History.debug = function(){ if ( (History.options.debug||false) ) { History.log.apply(History,arguments); } }; /** * History.log(message,...) * Logs the passed arguments */ History.log = function(){ // Prepare var consoleExists = !(typeof console === 'undefined' || typeof console.log === 'undefined' || typeof console.log.apply === 'undefined'), textarea = document.getElementById('log'), message, i,n, args,arg ; // Write to Console if ( consoleExists ) { args = Array.prototype.slice.call(arguments); message = args.shift(); if ( typeof console.debug !== 'undefined' ) { console.debug.apply(console,[message,args]); } else { console.log.apply(console,[message,args]); } } else { message = ("\n"+arguments[0]+"\n"); } // Write to log for ( i=1,n=arguments.length; i * @author James Padolsey */ History.getInternetExplorerMajorVersion = function(){ var result = History.getInternetExplorerMajorVersion.cached = (typeof History.getInternetExplorerMajorVersion.cached !== 'undefined') ? History.getInternetExplorerMajorVersion.cached : (function(){ var v = 3, div = document.createElement('div'), all = div.getElementsByTagName('i'); while ( (div.innerHTML = '') && all[0] ) {} return (v > 4) ? v : false; })() ; return result; }; /** * History.isInternetExplorer() * Are we using Internet Explorer? * @return {boolean} * @license Public Domain * @author Benjamin Arthur Lupton */ History.isInternetExplorer = function(){ var result = History.isInternetExplorer.cached = (typeof History.isInternetExplorer.cached !== 'undefined') ? History.isInternetExplorer.cached : Boolean(History.getInternetExplorerMajorVersion()) ; return result; }; /** * History.emulated * Which features require emulating? */ if (History.options.html4Mode) { History.emulated = { pushState : true, hashChange: true }; } else { History.emulated = { pushState: !Boolean( window.history && window.history.pushState && window.history.replaceState && !( (/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i).test(navigator.userAgent) /* disable for versions of iOS before version 4.3 (8F190) */ || (/AppleWebKit\/5([0-2]|3[0-2])/i).test(navigator.userAgent) /* disable for the mercury iOS browser, or at least older versions of the webkit engine */ ) ), hashChange: Boolean( !(('onhashchange' in window) || ('onhashchange' in document)) || (History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8) ) }; } /** * History.enabled * Is History enabled? */ History.enabled = !History.emulated.pushState; /** * History.bugs * Which bugs are present */ History.bugs = { /** * Safari 5 and Safari iOS 4 fail to return to the correct state once a hash is replaced by a `replaceState` call * https://bugs.webkit.org/show_bug.cgi?id=56249 */ setHash: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * Safari 5 and Safari iOS 4 sometimes fail to apply the state change under busy conditions * https://bugs.webkit.org/show_bug.cgi?id=42940 */ safariPoll: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * MSIE 6 and 7 sometimes do not apply a hash even it was told to (requiring a second call to the apply function) */ ieDoubleCheck: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8), /** * MSIE 6 requires the entire hash to be encoded for the hashes to trigger the onHashChange event */ hashEscape: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 7) }; /** * History.isEmptyObject(obj) * Checks to see if the Object is Empty * @param {Object} obj * @return {boolean} */ History.isEmptyObject = function(obj) { for ( var name in obj ) { if ( obj.hasOwnProperty(name) ) { return false; } } return true; }; /** * History.cloneObject(obj) * Clones a object and eliminate all references to the original contexts * @param {Object} obj * @return {Object} */ History.cloneObject = function(obj) { var hash,newObj; if ( obj ) { hash = JSON.stringify(obj); newObj = JSON.parse(hash); } else { newObj = {}; } return newObj; }; // ==================================================================== // URL Helpers /** * History.getRootUrl() * Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com" * @return {String} rootUrl */ History.getRootUrl = function(){ // Create var rootUrl = document.location.protocol+'//'+(document.location.hostname||document.location.host); if ( document.location.port||false ) { rootUrl += ':'+document.location.port; } rootUrl += '/'; // Return return rootUrl; }; /** * History.getBaseHref() * Fetches the `href` attribute of the `` element if it exists * @return {String} baseHref */ History.getBaseHref = function(){ // Create var baseElements = document.getElementsByTagName('base'), baseElement = null, baseHref = ''; // Test for Base Element if ( baseElements.length === 1 ) { // Prepare for Base Element baseElement = baseElements[0]; baseHref = baseElement.href.replace(/[^\/]+$/,''); } // Adjust trailing slash baseHref = baseHref.replace(/\/+$/,''); if ( baseHref ) baseHref += '/'; // Return return baseHref; }; /** * History.getBaseUrl() * Fetches the baseHref or basePageUrl or rootUrl (whichever one exists first) * @return {String} baseUrl */ History.getBaseUrl = function(){ // Create var baseUrl = History.getBaseHref()||History.getBasePageUrl()||History.getRootUrl(); // Return return baseUrl; }; /** * History.getPageUrl() * Fetches the URL of the current page * @return {String} pageUrl */ History.getPageUrl = function(){ // Fetch var State = History.getState(false,false), stateUrl = (State||{}).url||History.getLocationHref(), pageUrl; // Create pageUrl = stateUrl.replace(/\/+$/,'').replace(/[^\/]+$/,function(part,index,string){ return (/\./).test(part) ? part : part+'/'; }); // Return return pageUrl; }; /** * History.getBasePageUrl() * Fetches the Url of the directory of the current page * @return {String} basePageUrl */ History.getBasePageUrl = function(){ // Create var basePageUrl = (History.getLocationHref()).replace(/[#\?].*/,'').replace(/[^\/]+$/,function(part,index,string){ return (/[^\/]$/).test(part) ? '' : part; }).replace(/\/+$/,'')+'/'; // Return return basePageUrl; }; /** * History.getFullUrl(url) * Ensures that we have an absolute URL and not a relative URL * @param {string} url * @param {Boolean} allowBaseHref * @return {string} fullUrl */ History.getFullUrl = function(url,allowBaseHref){ // Prepare var fullUrl = url, firstChar = url.substring(0,1); allowBaseHref = (typeof allowBaseHref === 'undefined') ? true : allowBaseHref; // Check if ( /[a-z]+\:\/\//.test(url) ) { // Full URL } else if ( firstChar === '/' ) { // Root URL fullUrl = History.getRootUrl()+url.replace(/^\/+/,''); } else if ( firstChar === '#' ) { // Anchor URL fullUrl = History.getPageUrl().replace(/#.*/,'')+url; } else if ( firstChar === '?' ) { // Query URL fullUrl = History.getPageUrl().replace(/[\?#].*/,'')+url; } else { // Relative URL if ( allowBaseHref ) { fullUrl = History.getBaseUrl()+url.replace(/^(\.\/)+/,''); } else { fullUrl = History.getBasePageUrl()+url.replace(/^(\.\/)+/,''); } // We have an if condition above as we do not want hashes // which are relative to the baseHref in our URLs // as if the baseHref changes, then all our bookmarks // would now point to different locations // whereas the basePageUrl will always stay the same } // Return return fullUrl.replace(/\#$/,''); }; /** * History.getShortUrl(url) * Ensures that we have a relative URL and not a absolute URL * @param {string} url * @return {string} url */ History.getShortUrl = function(url){ // Prepare var shortUrl = url, baseUrl = History.getBaseUrl(), rootUrl = History.getRootUrl(); // Trim baseUrl if ( History.emulated.pushState ) { // We are in a if statement as when pushState is not emulated // The actual url these short urls are relative to can change // So within the same session, we the url may end up somewhere different shortUrl = shortUrl.replace(baseUrl,''); } // Trim rootUrl shortUrl = shortUrl.replace(rootUrl,'/'); // Ensure we can still detect it as a state if ( History.isTraditionalAnchor(shortUrl) ) { shortUrl = './'+shortUrl; } // Clean It shortUrl = shortUrl.replace(/^(\.\/)+/g,'./').replace(/\#$/,''); // Return return shortUrl; }; /** * History.getLocationHref(document) * Returns a normalized version of document.location.href * accounting for browser inconsistencies, etc. * * This URL will be URI-encoded and will include the hash * * @param {object} document * @return {string} url */ History.getLocationHref = function(doc) { doc = doc || document; // most of the time, this will be true if (doc.URL === doc.location.href) return doc.location.href; // some versions of webkit URI-decode document.location.href // but they leave document.URL in an encoded state if (doc.location.href === decodeURIComponent(doc.URL)) return doc.URL; // FF 3.6 only updates document.URL when a page is reloaded // document.location.href is updated correctly if (doc.location.hash && decodeURIComponent(doc.location.href.replace(/^[^#]+/, "")) === doc.location.hash) return doc.location.href; if (doc.URL.indexOf('#') == -1 && doc.location.href.indexOf('#') != -1) return doc.location.href; return doc.URL || doc.location.href; }; // ==================================================================== // State Storage /** * History.store * The store for all session specific data */ History.store = {}; /** * History.idToState * 1-1: State ID to State Object */ History.idToState = History.idToState||{}; /** * History.stateToId * 1-1: State String to State ID */ History.stateToId = History.stateToId||{}; /** * History.urlToId * 1-1: State URL to State ID */ History.urlToId = History.urlToId||{}; /** * History.storedStates * Store the states in an array */ History.storedStates = History.storedStates||[]; /** * History.savedStates * Saved the states in an array */ History.savedStates = History.savedStates||[]; /** * History.noramlizeStore() * Noramlize the store by adding necessary values */ History.normalizeStore = function(){ History.store.idToState = History.store.idToState||{}; History.store.urlToId = History.store.urlToId||{}; History.store.stateToId = History.store.stateToId||{}; }; /** * History.getState() * Get an object containing the data, title and url of the current state * @param {Boolean} friendly * @param {Boolean} create * @return {Object} State */ History.getState = function(friendly,create){ // Prepare if ( typeof friendly === 'undefined' ) { friendly = true; } if ( typeof create === 'undefined' ) { create = true; } // Fetch var State = History.getLastSavedState(); // Create if ( !State && create ) { State = History.createStateObject(); } // Adjust if ( friendly ) { State = History.cloneObject(State); State.url = State.cleanUrl||State.url; } // Return return State; }; /** * History.getIdByState(State) * Gets a ID for a State * @param {State} newState * @return {String} id */ History.getIdByState = function(newState){ // Fetch ID var id = History.extractId(newState.url), str; if ( !id ) { // Find ID via State String str = History.getStateString(newState); if ( typeof History.stateToId[str] !== 'undefined' ) { id = History.stateToId[str]; } else if ( typeof History.store.stateToId[str] !== 'undefined' ) { id = History.store.stateToId[str]; } else { // Generate a new ID while ( true ) { id = (new Date()).getTime() + String(Math.random()).replace(/\D/g,''); if ( typeof History.idToState[id] === 'undefined' && typeof History.store.idToState[id] === 'undefined' ) { break; } } // Apply the new State to the ID History.stateToId[str] = id; History.idToState[id] = newState; } } // Return ID return id; }; /** * History.normalizeState(State) * Expands a State Object * @param {object} State * @return {object} */ History.normalizeState = function(oldState){ // Variables var newState, dataNotEmpty; // Prepare if ( !oldState || (typeof oldState !== 'object') ) { oldState = {}; } // Check if ( typeof oldState.normalized !== 'undefined' ) { return oldState; } // Adjust if ( !oldState.data || (typeof oldState.data !== 'object') ) { oldState.data = {}; } // ---------------------------------------------------------------- // Create newState = {}; newState.normalized = true; newState.title = oldState.title||''; newState.url = History.getFullUrl(oldState.url?oldState.url:(History.getLocationHref())); newState.hash = History.getShortUrl(newState.url); newState.data = History.cloneObject(oldState.data); // Fetch ID newState.id = History.getIdByState(newState); // ---------------------------------------------------------------- // Clean the URL newState.cleanUrl = newState.url.replace(/\??\&_suid.*/,''); newState.url = newState.cleanUrl; // Check to see if we have more than just a url dataNotEmpty = !History.isEmptyObject(newState.data); // Apply if ( (newState.title || dataNotEmpty) && History.options.disableSuid !== true ) { // Add ID to Hash newState.hash = History.getShortUrl(newState.url).replace(/\??\&_suid.*/,''); if ( !/\?/.test(newState.hash) ) { newState.hash += '?'; } newState.hash += '&_suid='+newState.id; } // Create the Hashed URL newState.hashedUrl = History.getFullUrl(newState.hash); // ---------------------------------------------------------------- // Update the URL if we have a duplicate if ( (History.emulated.pushState || History.bugs.safariPoll) && History.hasUrlDuplicate(newState) ) { newState.url = newState.hashedUrl; } // ---------------------------------------------------------------- // Return return newState; }; /** * History.createStateObject(data,title,url) * Creates a object based on the data, title and url state params * @param {object} data * @param {string} title * @param {string} url * @return {object} */ History.createStateObject = function(data,title,url){ // Hashify var State = { 'data': data, 'title': title, 'url': url }; // Expand the State State = History.normalizeState(State); // Return object return State; }; /** * History.getStateById(id) * Get a state by it's UID * @param {String} id */ History.getStateById = function(id){ // Prepare id = String(id); // Retrieve var State = History.idToState[id] || History.store.idToState[id] || undefined; // Return State return State; }; /** * Get a State's String * @param {State} passedState */ History.getStateString = function(passedState){ // Prepare var State, cleanedState, str; // Fetch State = History.normalizeState(passedState); // Clean cleanedState = { data: State.data, title: passedState.title, url: passedState.url }; // Fetch str = JSON.stringify(cleanedState); // Return return str; }; /** * Get a State's ID * @param {State} passedState * @return {String} id */ History.getStateId = function(passedState){ // Prepare var State, id; // Fetch State = History.normalizeState(passedState); // Fetch id = State.id; // Return return id; }; /** * History.getHashByState(State) * Creates a Hash for the State Object * @param {State} passedState * @return {String} hash */ History.getHashByState = function(passedState){ // Prepare var State, hash; // Fetch State = History.normalizeState(passedState); // Hash hash = State.hash; // Return return hash; }; /** * History.extractId(url_or_hash) * Get a State ID by it's URL or Hash * @param {string} url_or_hash * @return {string} id */ History.extractId = function ( url_or_hash ) { // Prepare var id,parts,url, tmp; // Extract // If the URL has a #, use the id from before the # if (url_or_hash.indexOf('#') != -1) { tmp = url_or_hash.split("#")[0]; } else { tmp = url_or_hash; } parts = /(.*)\&_suid=([0-9]+)$/.exec(tmp); url = parts ? (parts[1]||url_or_hash) : url_or_hash; id = parts ? String(parts[2]||'') : ''; // Return return id||false; }; /** * History.isTraditionalAnchor * Checks to see if the url is a traditional anchor or not * @param {String} url_or_hash * @return {Boolean} */ History.isTraditionalAnchor = function(url_or_hash){ // Check var isTraditional = !(/[\/\?\.]/.test(url_or_hash)); // Return return isTraditional; }; /** * History.extractState * Get a State by it's URL or Hash * @param {String} url_or_hash * @return {State|null} */ History.extractState = function(url_or_hash,create){ // Prepare var State = null, id, url; create = create||false; // Fetch SUID id = History.extractId(url_or_hash); if ( id ) { State = History.getStateById(id); } // Fetch SUID returned no State if ( !State ) { // Fetch URL url = History.getFullUrl(url_or_hash); // Check URL id = History.getIdByUrl(url)||false; if ( id ) { State = History.getStateById(id); } // Create State if ( !State && create && !History.isTraditionalAnchor(url_or_hash) ) { State = History.createStateObject(null,null,url); } } // Return return State; }; /** * History.getIdByUrl() * Get a State ID by a State URL */ History.getIdByUrl = function(url){ // Fetch var id = History.urlToId[url] || History.store.urlToId[url] || undefined; // Return return id; }; /** * History.getLastSavedState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastSavedState = function(){ return History.savedStates[History.savedStates.length-1]||undefined; }; /** * History.getLastStoredState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastStoredState = function(){ return History.storedStates[History.storedStates.length-1]||undefined; }; /** * History.hasUrlDuplicate * Checks if a Url will have a url conflict * @param {Object} newState * @return {Boolean} hasDuplicate */ History.hasUrlDuplicate = function(newState) { // Prepare var hasDuplicate = false, oldState; // Fetch oldState = History.extractState(newState.url); // Check hasDuplicate = oldState && oldState.id !== newState.id; // Return return hasDuplicate; }; /** * History.storeState * Store a State * @param {Object} newState * @return {Object} newState */ History.storeState = function(newState){ // Store the State History.urlToId[newState.url] = newState.id; // Push the State History.storedStates.push(History.cloneObject(newState)); // Return newState return newState; }; /** * History.isLastSavedState(newState) * Tests to see if the state is the last state * @param {Object} newState * @return {boolean} isLast */ History.isLastSavedState = function(newState){ // Prepare var isLast = false, newId, oldState, oldId; // Check if ( History.savedStates.length ) { newId = newState.id; oldState = History.getLastSavedState(); oldId = oldState.id; // Check isLast = (newId === oldId); } // Return return isLast; }; /** * History.saveState * Push a State * @param {Object} newState * @return {boolean} changed */ History.saveState = function(newState){ // Check Hash if ( History.isLastSavedState(newState) ) { return false; } // Push the State History.savedStates.push(History.cloneObject(newState)); // Return true return true; }; /** * History.getStateByIndex() * Gets a state by the index * @param {integer} index * @return {Object} */ History.getStateByIndex = function(index){ // Prepare var State = null; // Handle if ( typeof index === 'undefined' ) { // Get the last inserted State = History.savedStates[History.savedStates.length-1]; } else if ( index < 0 ) { // Get from the end State = History.savedStates[History.savedStates.length+index]; } else { // Get from the beginning State = History.savedStates[index]; } // Return State return State; }; /** * History.getCurrentIndex() * Gets the current index * @return (integer) */ History.getCurrentIndex = function(){ // Prepare var index = null; // No states saved if(History.savedStates.length < 1) { index = 0; } else { index = History.savedStates.length-1; } return index; }; // ==================================================================== // Hash Helpers /** * History.getHash() * @param {Location=} location * Gets the current document hash * Note: unlike location.hash, this is guaranteed to return the escaped hash in all browsers * @return {string} */ History.getHash = function(doc){ var url = History.getLocationHref(doc), hash; hash = History.getHashByUrl(url); return hash; }; /** * History.unescapeHash() * normalize and Unescape a Hash * @param {String} hash * @return {string} */ History.unescapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Unescape hash result = decodeURIComponent(result); // Return result return result; }; /** * History.normalizeHash() * normalize a hash across browsers * @return {string} */ History.normalizeHash = function(hash){ // Prepare var result = hash.replace(/[^#]*#/,'').replace(/#.*/, ''); // Return result return result; }; /** * History.setHash(hash) * Sets the document hash * @param {string} hash * @return {History} */ History.setHash = function(hash,queue){ // Prepare var State, pageUrl; // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.setHash: we must wait', arguments); History.pushQueue({ scope: History, callback: History.setHash, args: arguments, queue: queue }); return false; } // Log //History.debug('History.setHash: called',hash); // Make Busy + Continue History.busy(true); // Check if hash is a state State = History.extractState(hash,true); if ( State && !History.emulated.pushState ) { // Hash is a state so skip the setHash //History.debug('History.setHash: Hash is a state so skipping the hash set with a direct pushState call',arguments); // PushState History.pushState(State.data,State.title,State.url,false); } else if ( History.getHash() !== hash ) { // Hash is a proper hash, so apply it // Handle browser bugs if ( History.bugs.setHash ) { // Fix Safari Bug https://bugs.webkit.org/show_bug.cgi?id=56249 // Fetch the base page pageUrl = History.getPageUrl(); // Safari hash apply History.pushState(null,null,pageUrl+'#'+hash,false); } else { // Normal hash apply document.location.hash = hash; } } // Chain return History; }; /** * History.escape() * normalize and Escape a Hash * @return {string} */ History.escapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Escape hash result = window.encodeURIComponent(result); // IE6 Escape Bug if ( !History.bugs.hashEscape ) { // Restore common parts result = result .replace(/\%21/g,'!') .replace(/\%26/g,'&') .replace(/\%3D/g,'=') .replace(/\%3F/g,'?'); } // Return result return result; }; /** * History.getHashByUrl(url) * Extracts the Hash from a URL * @param {string} url * @return {string} url */ History.getHashByUrl = function(url){ // Extract the hash var hash = String(url) .replace(/([^#]*)#?([^#]*)#?(.*)/, '$2') ; // Unescape hash hash = History.unescapeHash(hash); // Return hash return hash; }; /** * History.setTitle(title) * Applies the title to the document * @param {State} newState * @return {Boolean} */ History.setTitle = function(newState){ // Prepare var title = newState.title, firstState; // Initial if ( !title ) { firstState = History.getStateByIndex(0); if ( firstState && firstState.url === newState.url ) { title = firstState.title||History.options.initialTitle; } } // Apply try { document.getElementsByTagName('title')[0].innerHTML = title.replace('<','<').replace('>','>').replace(' & ',' & '); } catch ( Exception ) { } document.title = title; // Chain return History; }; // ==================================================================== // Queueing /** * History.queues * The list of queues to use * First In, First Out */ History.queues = []; /** * History.busy(value) * @param {boolean} value [optional] * @return {boolean} busy */ History.busy = function(value){ // Apply if ( typeof value !== 'undefined' ) { //History.debug('History.busy: changing ['+(History.busy.flag||false)+'] to ['+(value||false)+']', History.queues.length); History.busy.flag = value; } // Default else if ( typeof History.busy.flag === 'undefined' ) { History.busy.flag = false; } // Queue if ( !History.busy.flag ) { // Execute the next item in the queue clearTimeout(History.busy.timeout); var fireNext = function(){ var i, queue, item; if ( History.busy.flag ) return; for ( i=History.queues.length-1; i >= 0; --i ) { queue = History.queues[i]; if ( queue.length === 0 ) continue; item = queue.shift(); History.fireQueueItem(item); History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } }; History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } // Return return History.busy.flag; }; /** * History.busy.flag */ History.busy.flag = false; /** * History.fireQueueItem(item) * Fire a Queue Item * @param {Object} item * @return {Mixed} result */ History.fireQueueItem = function(item){ return item.callback.apply(item.scope||History,item.args||[]); }; /** * History.pushQueue(callback,args) * Add an item to the queue * @param {Object} item [scope,callback,args,queue] */ History.pushQueue = function(item){ // Prepare the queue History.queues[item.queue||0] = History.queues[item.queue||0]||[]; // Add to the queue History.queues[item.queue||0].push(item); // Chain return History; }; /** * History.queue (item,queue), (func,queue), (func), (item) * Either firs the item now if not busy, or adds it to the queue */ History.queue = function(item,queue){ // Prepare if ( typeof item === 'function' ) { item = { callback: item }; } if ( typeof queue !== 'undefined' ) { item.queue = queue; } // Handle if ( History.busy() ) { History.pushQueue(item); } else { History.fireQueueItem(item); } // Chain return History; }; /** * History.clearQueue() * Clears the Queue */ History.clearQueue = function(){ History.busy.flag = false; History.queues = []; return History; }; // ==================================================================== // IE Bug Fix /** * History.stateChanged * States whether or not the state has changed since the last double check was initialised */ History.stateChanged = false; /** * History.doubleChecker * Contains the timeout used for the double checks */ History.doubleChecker = false; /** * History.doubleCheckComplete() * Complete a double check * @return {History} */ History.doubleCheckComplete = function(){ // Update History.stateChanged = true; // Clear History.doubleCheckClear(); // Chain return History; }; /** * History.doubleCheckClear() * Clear a double check * @return {History} */ History.doubleCheckClear = function(){ // Clear if ( History.doubleChecker ) { clearTimeout(History.doubleChecker); History.doubleChecker = false; } // Chain return History; }; /** * History.doubleCheck() * Create a double check * @return {History} */ History.doubleCheck = function(tryAgain){ // Reset History.stateChanged = false; History.doubleCheckClear(); // Fix IE6,IE7 bug where calling history.back or history.forward does not actually change the hash (whereas doing it manually does) // Fix Safari 5 bug where sometimes the state does not change: https://bugs.webkit.org/show_bug.cgi?id=42940 if ( History.bugs.ieDoubleCheck ) { // Apply Check History.doubleChecker = setTimeout( function(){ History.doubleCheckClear(); if ( !History.stateChanged ) { //History.debug('History.doubleCheck: State has not yet changed, trying again', arguments); // Re-Attempt tryAgain(); } return true; }, History.options.doubleCheckInterval ); } // Chain return History; }; // ==================================================================== // Safari Bug Fix /** * History.safariStatePoll() * Poll the current state * @return {History} */ History.safariStatePoll = function(){ // Poll the URL // Get the Last State which has the new URL var urlState = History.extractState(History.getLocationHref()), newState; // Check for a difference if ( !History.isLastSavedState(urlState) ) { newState = urlState; } else { return; } // Check if we have a state with that url // If not create it if ( !newState ) { //History.debug('History.safariStatePoll: new'); newState = History.createStateObject(); } // Apply the New State //History.debug('History.safariStatePoll: trigger'); History.Adapter.trigger(window,'popstate'); // Chain return History; }; // ==================================================================== // State Aliases /** * History.back(queue) * Send the browser history back one item * @param {Integer} queue [optional] */ History.back = function(queue){ //History.debug('History.back: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.back: we must wait', arguments); History.pushQueue({ scope: History, callback: History.back, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.back(false); }); // Go back history.go(-1); // End back closure return true; }; /** * History.forward(queue) * Send the browser history forward one item * @param {Integer} queue [optional] */ History.forward = function(queue){ //History.debug('History.forward: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.forward: we must wait', arguments); History.pushQueue({ scope: History, callback: History.forward, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.forward(false); }); // Go forward history.go(1); // End forward closure return true; }; /** * History.go(index,queue) * Send the browser history back or forward index times * @param {Integer} queue [optional] */ History.go = function(index,queue){ //History.debug('History.go: called', arguments); // Prepare var i; // Handle if ( index > 0 ) { // Forward for ( i=1; i<=index; ++i ) { History.forward(queue); } } else if ( index < 0 ) { // Backward for ( i=-1; i>=index; --i ) { History.back(queue); } } else { throw new Error('History.go: History.go requires a positive or negative integer passed.'); } // Chain return History; }; // ==================================================================== // HTML5 State Support // Non-Native pushState Implementation if ( History.emulated.pushState ) { /* * Provide Skeleton for HTML4 Browsers */ // Prepare var emptyFunction = function(){}; History.pushState = History.pushState||emptyFunction; History.replaceState = History.replaceState||emptyFunction; } // History.emulated.pushState // Native pushState Implementation else { /* * Use native HTML5 History API Implementation */ /** * History.onPopState(event,extra) * Refresh the Current State */ History.onPopState = function(event,extra){ // Prepare var stateId = false, newState = false, currentHash, currentState; // Reset the double check History.doubleCheckComplete(); // Check for a Hash, and handle apporiatly currentHash = History.getHash(); if ( currentHash ) { // Expand Hash currentState = History.extractState(currentHash||History.getLocationHref(),true); if ( currentState ) { // We were able to parse it, it must be a State! // Let's forward to replaceState //History.debug('History.onPopState: state anchor', currentHash, currentState); History.replaceState(currentState.data, currentState.title, currentState.url, false); } else { // Traditional Anchor //History.debug('History.onPopState: traditional anchor', currentHash); History.Adapter.trigger(window,'anchorchange'); History.busy(false); } // We don't care for hashes History.expectedStateId = false; return false; } // Ensure stateId = History.Adapter.extractEventData('state',event,extra) || false; // Fetch State if ( stateId ) { // Vanilla: Back/forward button was used newState = History.getStateById(stateId); } else if ( History.expectedStateId ) { // Vanilla: A new state was pushed, and popstate was called manually newState = History.getStateById(History.expectedStateId); } else { // Initial State newState = History.extractState(History.getLocationHref()); } // The State did not exist in our store if ( !newState ) { // Regenerate the State newState = History.createStateObject(null,null,History.getLocationHref()); } // Clean History.expectedStateId = false; // Check if we are the same state if ( History.isLastSavedState(newState) ) { // There has been no change (just the page's hash has finally propagated) //History.debug('History.onPopState: no change', newState, History.savedStates); History.busy(false); return false; } // Store the State History.storeState(newState); History.saveState(newState); // Force update of the title History.setTitle(newState); // Fire Our Event History.Adapter.trigger(window,'statechange'); History.busy(false); // Return true return true; }; History.Adapter.bind(window,'popstate',History.onPopState); /** * History.pushState(data,title,url) * Add a new State to the history object, become it, and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.pushState = function(data,title,url,queue){ //History.debug('History.pushState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.pushState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.pushState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.pushState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End pushState closure return true; }; /** * History.replaceState(data,title,url) * Replace the State and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.replaceState = function(data,title,url,queue){ //History.debug('History.replaceState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.replaceState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.replaceState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.replaceState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End replaceState closure return true; }; } // !History.emulated.pushState // ==================================================================== // Initialise /** * Load the Store */ if ( sessionStorage ) { // Fetch try { History.store = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { History.store = {}; } // Normalize History.normalizeStore(); } else { // Default Load History.store = {}; History.normalizeStore(); } /** * Clear Intervals on exit to prevent memory leaks */ History.Adapter.bind(window,"unload",History.clearAllIntervals); /** * Create the initial State */ History.saveState(History.storeState(History.extractState(History.getLocationHref(),true))); /** * Bind for Saving Store */ if ( sessionStorage ) { // When the page is closed History.onUnload = function(){ // Prepare var currentStore, item, currentStoreString; // Fetch try { currentStore = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { currentStore = {}; } // Ensure currentStore.idToState = currentStore.idToState || {}; currentStore.urlToId = currentStore.urlToId || {}; currentStore.stateToId = currentStore.stateToId || {}; // Sync for ( item in History.idToState ) { if ( !History.idToState.hasOwnProperty(item) ) { continue; } currentStore.idToState[item] = History.idToState[item]; } for ( item in History.urlToId ) { if ( !History.urlToId.hasOwnProperty(item) ) { continue; } currentStore.urlToId[item] = History.urlToId[item]; } for ( item in History.stateToId ) { if ( !History.stateToId.hasOwnProperty(item) ) { continue; } currentStore.stateToId[item] = History.stateToId[item]; } // Update History.store = currentStore; History.normalizeStore(); // In Safari, going into Private Browsing mode causes the // Session Storage object to still exist but if you try and use // or set any property/function of it it throws the exception // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to // add something to storage that exceeded the quota." infinitely // every second. currentStoreString = JSON.stringify(currentStore); try { // Store sessionStorage.setItem('History.store', currentStoreString); } catch (e) { if (e.code === DOMException.QUOTA_EXCEEDED_ERR) { if (sessionStorage.length) { // Workaround for a bug seen on iPads. Sometimes the quota exceeded error comes up and simply // removing/resetting the storage can work. sessionStorage.removeItem('History.store'); sessionStorage.setItem('History.store', currentStoreString); } else { // Otherwise, we're probably private browsing in Safari, so we'll ignore the exception. } } else { throw e; } } }; // For Internet Explorer History.intervalList.push(setInterval(History.onUnload,History.options.storeInterval)); // For Other Browsers History.Adapter.bind(window,'beforeunload',History.onUnload); History.Adapter.bind(window,'unload',History.onUnload); // Both are enabled for consistency } // Non-Native pushState Implementation if ( !History.emulated.pushState ) { // Be aware, the following is only for native pushState implementations // If you are wanting to include something for all browsers // Then include it above this if block /** * Setup Safari Fix */ if ( History.bugs.safariPoll ) { History.intervalList.push(setInterval(History.safariStatePoll, History.options.safariPollInterval)); } /** * Ensure Cross Browser Compatibility */ if ( navigator.vendor === 'Apple Computer, Inc.' || (navigator.appCodeName||'') === 'Mozilla' ) { /** * Fix Safari HashChange Issue */ // Setup Alias History.Adapter.bind(window,'hashchange',function(){ History.Adapter.trigger(window,'popstate'); }); // Initialise Alias if ( History.getHash() ) { History.Adapter.onDomLoad(function(){ History.Adapter.trigger(window,'hashchange'); }); } } } // !History.emulated.pushState }; // History.initCore // Try to Initialise History if (!History.options || !History.options.delayInit) { History.init(); } })(window); ================================================ FILE: scripts/bundled-uncompressed/html5/jquery.history.js ================================================ /** * History.js jQuery Adapter * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ // Closure (function(window,undefined){ "use strict"; // Localise Globals var History = window.History = window.History||{}, jQuery = window.jQuery; // Check Existence if ( typeof History.Adapter !== 'undefined' ) { throw new Error('History.js Adapter has already been loaded...'); } // Add the Adapter History.Adapter = { /** * History.Adapter.bind(el,event,callback) * @param {Element|string} el * @param {string} event - custom and standard events * @param {function} callback * @return {void} */ bind: function(el,event,callback){ jQuery(el).bind(event,callback); }, /** * History.Adapter.trigger(el,event) * @param {Element|string} el * @param {string} event - custom and standard events * @param {Object=} extra - a object of extra event data (optional) * @return {void} */ trigger: function(el,event,extra){ jQuery(el).trigger(event,extra); }, /** * History.Adapter.extractEventData(key,event,extra) * @param {string} key - key for the event data to extract * @param {string} event - custom and standard events * @param {Object=} extra - a object of extra event data (optional) * @return {mixed} */ extractEventData: function(key,event,extra){ // jQuery Native then jQuery Custom var result = (event && event.originalEvent && event.originalEvent[key]) || (extra && extra[key]) || undefined; // Return return result; }, /** * History.Adapter.onDomLoad(callback) * @param {function} callback * @return {void} */ onDomLoad: function(callback) { jQuery(callback); } }; // Try and Initialise History if ( typeof History.init !== 'undefined' ) { History.init(); } })(window); /** * History.js Core * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window,undefined){ "use strict"; // ======================================================================== // Initialise // Localise Globals var console = window.console||undefined, // Prevent a JSLint complain document = window.document, // Make sure we are using the correct document navigator = window.navigator, // Make sure we are using the correct navigator sessionStorage = false, // sessionStorage setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, setInterval = window.setInterval, clearInterval = window.clearInterval, JSON = window.JSON, alert = window.alert, History = window.History = window.History||{}, // Public History Object history = window.history; // Old History Object try { sessionStorage = window.sessionStorage; // This will throw an exception in some browsers when cookies/localStorage are explicitly disabled (i.e. Chrome) sessionStorage.setItem('TEST', '1'); sessionStorage.removeItem('TEST'); } catch(e) { sessionStorage = false; } // MooTools Compatibility JSON.stringify = JSON.stringify||JSON.encode; JSON.parse = JSON.parse||JSON.decode; // Check Existence if ( typeof History.init !== 'undefined' ) { throw new Error('History.js Core has already been loaded...'); } // Initialise History History.init = function(options){ // Check Load Status of Adapter if ( typeof History.Adapter === 'undefined' ) { return false; } // Check Load Status of Core if ( typeof History.initCore !== 'undefined' ) { History.initCore(); } // Check Load Status of HTML4 Support if ( typeof History.initHtml4 !== 'undefined' ) { History.initHtml4(); } // Return true return true; }; // ======================================================================== // Initialise Core // Initialise Core History.initCore = function(options){ // Initialise if ( typeof History.initCore.initialized !== 'undefined' ) { // Already Loaded return false; } else { History.initCore.initialized = true; } // ==================================================================== // Options /** * History.options * Configurable options */ History.options = History.options||{}; /** * History.options.hashChangeInterval * How long should the interval be before hashchange checks */ History.options.hashChangeInterval = History.options.hashChangeInterval || 100; /** * History.options.safariPollInterval * How long should the interval be before safari poll checks */ History.options.safariPollInterval = History.options.safariPollInterval || 500; /** * History.options.doubleCheckInterval * How long should the interval be before we perform a double check */ History.options.doubleCheckInterval = History.options.doubleCheckInterval || 500; /** * History.options.disableSuid * Force History not to append suid */ History.options.disableSuid = History.options.disableSuid || false; /** * History.options.storeInterval * How long should we wait between store calls */ History.options.storeInterval = History.options.storeInterval || 1000; /** * History.options.busyDelay * How long should we wait between busy events */ History.options.busyDelay = History.options.busyDelay || 250; /** * History.options.debug * If true will enable debug messages to be logged */ History.options.debug = History.options.debug || false; /** * History.options.initialTitle * What is the title of the initial state */ History.options.initialTitle = History.options.initialTitle || document.title; /** * History.options.html4Mode * If true, will force HTMl4 mode (hashtags) */ History.options.html4Mode = History.options.html4Mode || false; /** * History.options.delayInit * Want to override default options and call init manually. */ History.options.delayInit = History.options.delayInit || false; // ==================================================================== // Interval record /** * History.intervalList * List of intervals set, to be cleared when document is unloaded. */ History.intervalList = []; /** * History.clearAllIntervals * Clears all setInterval instances. */ History.clearAllIntervals = function(){ var i, il = History.intervalList; if (typeof il !== "undefined" && il !== null) { for (i = 0; i < il.length; i++) { clearInterval(il[i]); } History.intervalList = null; } }; // ==================================================================== // Debug /** * History.debug(message,...) * Logs the passed arguments if debug enabled */ History.debug = function(){ if ( (History.options.debug||false) ) { History.log.apply(History,arguments); } }; /** * History.log(message,...) * Logs the passed arguments */ History.log = function(){ // Prepare var consoleExists = !(typeof console === 'undefined' || typeof console.log === 'undefined' || typeof console.log.apply === 'undefined'), textarea = document.getElementById('log'), message, i,n, args,arg ; // Write to Console if ( consoleExists ) { args = Array.prototype.slice.call(arguments); message = args.shift(); if ( typeof console.debug !== 'undefined' ) { console.debug.apply(console,[message,args]); } else { console.log.apply(console,[message,args]); } } else { message = ("\n"+arguments[0]+"\n"); } // Write to log for ( i=1,n=arguments.length; i * @author James Padolsey */ History.getInternetExplorerMajorVersion = function(){ var result = History.getInternetExplorerMajorVersion.cached = (typeof History.getInternetExplorerMajorVersion.cached !== 'undefined') ? History.getInternetExplorerMajorVersion.cached : (function(){ var v = 3, div = document.createElement('div'), all = div.getElementsByTagName('i'); while ( (div.innerHTML = '') && all[0] ) {} return (v > 4) ? v : false; })() ; return result; }; /** * History.isInternetExplorer() * Are we using Internet Explorer? * @return {boolean} * @license Public Domain * @author Benjamin Arthur Lupton */ History.isInternetExplorer = function(){ var result = History.isInternetExplorer.cached = (typeof History.isInternetExplorer.cached !== 'undefined') ? History.isInternetExplorer.cached : Boolean(History.getInternetExplorerMajorVersion()) ; return result; }; /** * History.emulated * Which features require emulating? */ if (History.options.html4Mode) { History.emulated = { pushState : true, hashChange: true }; } else { History.emulated = { pushState: !Boolean( window.history && window.history.pushState && window.history.replaceState && !( (/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i).test(navigator.userAgent) /* disable for versions of iOS before version 4.3 (8F190) */ || (/AppleWebKit\/5([0-2]|3[0-2])/i).test(navigator.userAgent) /* disable for the mercury iOS browser, or at least older versions of the webkit engine */ ) ), hashChange: Boolean( !(('onhashchange' in window) || ('onhashchange' in document)) || (History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8) ) }; } /** * History.enabled * Is History enabled? */ History.enabled = !History.emulated.pushState; /** * History.bugs * Which bugs are present */ History.bugs = { /** * Safari 5 and Safari iOS 4 fail to return to the correct state once a hash is replaced by a `replaceState` call * https://bugs.webkit.org/show_bug.cgi?id=56249 */ setHash: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * Safari 5 and Safari iOS 4 sometimes fail to apply the state change under busy conditions * https://bugs.webkit.org/show_bug.cgi?id=42940 */ safariPoll: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * MSIE 6 and 7 sometimes do not apply a hash even it was told to (requiring a second call to the apply function) */ ieDoubleCheck: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8), /** * MSIE 6 requires the entire hash to be encoded for the hashes to trigger the onHashChange event */ hashEscape: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 7) }; /** * History.isEmptyObject(obj) * Checks to see if the Object is Empty * @param {Object} obj * @return {boolean} */ History.isEmptyObject = function(obj) { for ( var name in obj ) { if ( obj.hasOwnProperty(name) ) { return false; } } return true; }; /** * History.cloneObject(obj) * Clones a object and eliminate all references to the original contexts * @param {Object} obj * @return {Object} */ History.cloneObject = function(obj) { var hash,newObj; if ( obj ) { hash = JSON.stringify(obj); newObj = JSON.parse(hash); } else { newObj = {}; } return newObj; }; // ==================================================================== // URL Helpers /** * History.getRootUrl() * Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com" * @return {String} rootUrl */ History.getRootUrl = function(){ // Create var rootUrl = document.location.protocol+'//'+(document.location.hostname||document.location.host); if ( document.location.port||false ) { rootUrl += ':'+document.location.port; } rootUrl += '/'; // Return return rootUrl; }; /** * History.getBaseHref() * Fetches the `href` attribute of the `` element if it exists * @return {String} baseHref */ History.getBaseHref = function(){ // Create var baseElements = document.getElementsByTagName('base'), baseElement = null, baseHref = ''; // Test for Base Element if ( baseElements.length === 1 ) { // Prepare for Base Element baseElement = baseElements[0]; baseHref = baseElement.href.replace(/[^\/]+$/,''); } // Adjust trailing slash baseHref = baseHref.replace(/\/+$/,''); if ( baseHref ) baseHref += '/'; // Return return baseHref; }; /** * History.getBaseUrl() * Fetches the baseHref or basePageUrl or rootUrl (whichever one exists first) * @return {String} baseUrl */ History.getBaseUrl = function(){ // Create var baseUrl = History.getBaseHref()||History.getBasePageUrl()||History.getRootUrl(); // Return return baseUrl; }; /** * History.getPageUrl() * Fetches the URL of the current page * @return {String} pageUrl */ History.getPageUrl = function(){ // Fetch var State = History.getState(false,false), stateUrl = (State||{}).url||History.getLocationHref(), pageUrl; // Create pageUrl = stateUrl.replace(/\/+$/,'').replace(/[^\/]+$/,function(part,index,string){ return (/\./).test(part) ? part : part+'/'; }); // Return return pageUrl; }; /** * History.getBasePageUrl() * Fetches the Url of the directory of the current page * @return {String} basePageUrl */ History.getBasePageUrl = function(){ // Create var basePageUrl = (History.getLocationHref()).replace(/[#\?].*/,'').replace(/[^\/]+$/,function(part,index,string){ return (/[^\/]$/).test(part) ? '' : part; }).replace(/\/+$/,'')+'/'; // Return return basePageUrl; }; /** * History.getFullUrl(url) * Ensures that we have an absolute URL and not a relative URL * @param {string} url * @param {Boolean} allowBaseHref * @return {string} fullUrl */ History.getFullUrl = function(url,allowBaseHref){ // Prepare var fullUrl = url, firstChar = url.substring(0,1); allowBaseHref = (typeof allowBaseHref === 'undefined') ? true : allowBaseHref; // Check if ( /[a-z]+\:\/\//.test(url) ) { // Full URL } else if ( firstChar === '/' ) { // Root URL fullUrl = History.getRootUrl()+url.replace(/^\/+/,''); } else if ( firstChar === '#' ) { // Anchor URL fullUrl = History.getPageUrl().replace(/#.*/,'')+url; } else if ( firstChar === '?' ) { // Query URL fullUrl = History.getPageUrl().replace(/[\?#].*/,'')+url; } else { // Relative URL if ( allowBaseHref ) { fullUrl = History.getBaseUrl()+url.replace(/^(\.\/)+/,''); } else { fullUrl = History.getBasePageUrl()+url.replace(/^(\.\/)+/,''); } // We have an if condition above as we do not want hashes // which are relative to the baseHref in our URLs // as if the baseHref changes, then all our bookmarks // would now point to different locations // whereas the basePageUrl will always stay the same } // Return return fullUrl.replace(/\#$/,''); }; /** * History.getShortUrl(url) * Ensures that we have a relative URL and not a absolute URL * @param {string} url * @return {string} url */ History.getShortUrl = function(url){ // Prepare var shortUrl = url, baseUrl = History.getBaseUrl(), rootUrl = History.getRootUrl(); // Trim baseUrl if ( History.emulated.pushState ) { // We are in a if statement as when pushState is not emulated // The actual url these short urls are relative to can change // So within the same session, we the url may end up somewhere different shortUrl = shortUrl.replace(baseUrl,''); } // Trim rootUrl shortUrl = shortUrl.replace(rootUrl,'/'); // Ensure we can still detect it as a state if ( History.isTraditionalAnchor(shortUrl) ) { shortUrl = './'+shortUrl; } // Clean It shortUrl = shortUrl.replace(/^(\.\/)+/g,'./').replace(/\#$/,''); // Return return shortUrl; }; /** * History.getLocationHref(document) * Returns a normalized version of document.location.href * accounting for browser inconsistencies, etc. * * This URL will be URI-encoded and will include the hash * * @param {object} document * @return {string} url */ History.getLocationHref = function(doc) { doc = doc || document; // most of the time, this will be true if (doc.URL === doc.location.href) return doc.location.href; // some versions of webkit URI-decode document.location.href // but they leave document.URL in an encoded state if (doc.location.href === decodeURIComponent(doc.URL)) return doc.URL; // FF 3.6 only updates document.URL when a page is reloaded // document.location.href is updated correctly if (doc.location.hash && decodeURIComponent(doc.location.href.replace(/^[^#]+/, "")) === doc.location.hash) return doc.location.href; if (doc.URL.indexOf('#') == -1 && doc.location.href.indexOf('#') != -1) return doc.location.href; return doc.URL || doc.location.href; }; // ==================================================================== // State Storage /** * History.store * The store for all session specific data */ History.store = {}; /** * History.idToState * 1-1: State ID to State Object */ History.idToState = History.idToState||{}; /** * History.stateToId * 1-1: State String to State ID */ History.stateToId = History.stateToId||{}; /** * History.urlToId * 1-1: State URL to State ID */ History.urlToId = History.urlToId||{}; /** * History.storedStates * Store the states in an array */ History.storedStates = History.storedStates||[]; /** * History.savedStates * Saved the states in an array */ History.savedStates = History.savedStates||[]; /** * History.noramlizeStore() * Noramlize the store by adding necessary values */ History.normalizeStore = function(){ History.store.idToState = History.store.idToState||{}; History.store.urlToId = History.store.urlToId||{}; History.store.stateToId = History.store.stateToId||{}; }; /** * History.getState() * Get an object containing the data, title and url of the current state * @param {Boolean} friendly * @param {Boolean} create * @return {Object} State */ History.getState = function(friendly,create){ // Prepare if ( typeof friendly === 'undefined' ) { friendly = true; } if ( typeof create === 'undefined' ) { create = true; } // Fetch var State = History.getLastSavedState(); // Create if ( !State && create ) { State = History.createStateObject(); } // Adjust if ( friendly ) { State = History.cloneObject(State); State.url = State.cleanUrl||State.url; } // Return return State; }; /** * History.getIdByState(State) * Gets a ID for a State * @param {State} newState * @return {String} id */ History.getIdByState = function(newState){ // Fetch ID var id = History.extractId(newState.url), str; if ( !id ) { // Find ID via State String str = History.getStateString(newState); if ( typeof History.stateToId[str] !== 'undefined' ) { id = History.stateToId[str]; } else if ( typeof History.store.stateToId[str] !== 'undefined' ) { id = History.store.stateToId[str]; } else { // Generate a new ID while ( true ) { id = (new Date()).getTime() + String(Math.random()).replace(/\D/g,''); if ( typeof History.idToState[id] === 'undefined' && typeof History.store.idToState[id] === 'undefined' ) { break; } } // Apply the new State to the ID History.stateToId[str] = id; History.idToState[id] = newState; } } // Return ID return id; }; /** * History.normalizeState(State) * Expands a State Object * @param {object} State * @return {object} */ History.normalizeState = function(oldState){ // Variables var newState, dataNotEmpty; // Prepare if ( !oldState || (typeof oldState !== 'object') ) { oldState = {}; } // Check if ( typeof oldState.normalized !== 'undefined' ) { return oldState; } // Adjust if ( !oldState.data || (typeof oldState.data !== 'object') ) { oldState.data = {}; } // ---------------------------------------------------------------- // Create newState = {}; newState.normalized = true; newState.title = oldState.title||''; newState.url = History.getFullUrl(oldState.url?oldState.url:(History.getLocationHref())); newState.hash = History.getShortUrl(newState.url); newState.data = History.cloneObject(oldState.data); // Fetch ID newState.id = History.getIdByState(newState); // ---------------------------------------------------------------- // Clean the URL newState.cleanUrl = newState.url.replace(/\??\&_suid.*/,''); newState.url = newState.cleanUrl; // Check to see if we have more than just a url dataNotEmpty = !History.isEmptyObject(newState.data); // Apply if ( (newState.title || dataNotEmpty) && History.options.disableSuid !== true ) { // Add ID to Hash newState.hash = History.getShortUrl(newState.url).replace(/\??\&_suid.*/,''); if ( !/\?/.test(newState.hash) ) { newState.hash += '?'; } newState.hash += '&_suid='+newState.id; } // Create the Hashed URL newState.hashedUrl = History.getFullUrl(newState.hash); // ---------------------------------------------------------------- // Update the URL if we have a duplicate if ( (History.emulated.pushState || History.bugs.safariPoll) && History.hasUrlDuplicate(newState) ) { newState.url = newState.hashedUrl; } // ---------------------------------------------------------------- // Return return newState; }; /** * History.createStateObject(data,title,url) * Creates a object based on the data, title and url state params * @param {object} data * @param {string} title * @param {string} url * @return {object} */ History.createStateObject = function(data,title,url){ // Hashify var State = { 'data': data, 'title': title, 'url': url }; // Expand the State State = History.normalizeState(State); // Return object return State; }; /** * History.getStateById(id) * Get a state by it's UID * @param {String} id */ History.getStateById = function(id){ // Prepare id = String(id); // Retrieve var State = History.idToState[id] || History.store.idToState[id] || undefined; // Return State return State; }; /** * Get a State's String * @param {State} passedState */ History.getStateString = function(passedState){ // Prepare var State, cleanedState, str; // Fetch State = History.normalizeState(passedState); // Clean cleanedState = { data: State.data, title: passedState.title, url: passedState.url }; // Fetch str = JSON.stringify(cleanedState); // Return return str; }; /** * Get a State's ID * @param {State} passedState * @return {String} id */ History.getStateId = function(passedState){ // Prepare var State, id; // Fetch State = History.normalizeState(passedState); // Fetch id = State.id; // Return return id; }; /** * History.getHashByState(State) * Creates a Hash for the State Object * @param {State} passedState * @return {String} hash */ History.getHashByState = function(passedState){ // Prepare var State, hash; // Fetch State = History.normalizeState(passedState); // Hash hash = State.hash; // Return return hash; }; /** * History.extractId(url_or_hash) * Get a State ID by it's URL or Hash * @param {string} url_or_hash * @return {string} id */ History.extractId = function ( url_or_hash ) { // Prepare var id,parts,url, tmp; // Extract // If the URL has a #, use the id from before the # if (url_or_hash.indexOf('#') != -1) { tmp = url_or_hash.split("#")[0]; } else { tmp = url_or_hash; } parts = /(.*)\&_suid=([0-9]+)$/.exec(tmp); url = parts ? (parts[1]||url_or_hash) : url_or_hash; id = parts ? String(parts[2]||'') : ''; // Return return id||false; }; /** * History.isTraditionalAnchor * Checks to see if the url is a traditional anchor or not * @param {String} url_or_hash * @return {Boolean} */ History.isTraditionalAnchor = function(url_or_hash){ // Check var isTraditional = !(/[\/\?\.]/.test(url_or_hash)); // Return return isTraditional; }; /** * History.extractState * Get a State by it's URL or Hash * @param {String} url_or_hash * @return {State|null} */ History.extractState = function(url_or_hash,create){ // Prepare var State = null, id, url; create = create||false; // Fetch SUID id = History.extractId(url_or_hash); if ( id ) { State = History.getStateById(id); } // Fetch SUID returned no State if ( !State ) { // Fetch URL url = History.getFullUrl(url_or_hash); // Check URL id = History.getIdByUrl(url)||false; if ( id ) { State = History.getStateById(id); } // Create State if ( !State && create && !History.isTraditionalAnchor(url_or_hash) ) { State = History.createStateObject(null,null,url); } } // Return return State; }; /** * History.getIdByUrl() * Get a State ID by a State URL */ History.getIdByUrl = function(url){ // Fetch var id = History.urlToId[url] || History.store.urlToId[url] || undefined; // Return return id; }; /** * History.getLastSavedState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastSavedState = function(){ return History.savedStates[History.savedStates.length-1]||undefined; }; /** * History.getLastStoredState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastStoredState = function(){ return History.storedStates[History.storedStates.length-1]||undefined; }; /** * History.hasUrlDuplicate * Checks if a Url will have a url conflict * @param {Object} newState * @return {Boolean} hasDuplicate */ History.hasUrlDuplicate = function(newState) { // Prepare var hasDuplicate = false, oldState; // Fetch oldState = History.extractState(newState.url); // Check hasDuplicate = oldState && oldState.id !== newState.id; // Return return hasDuplicate; }; /** * History.storeState * Store a State * @param {Object} newState * @return {Object} newState */ History.storeState = function(newState){ // Store the State History.urlToId[newState.url] = newState.id; // Push the State History.storedStates.push(History.cloneObject(newState)); // Return newState return newState; }; /** * History.isLastSavedState(newState) * Tests to see if the state is the last state * @param {Object} newState * @return {boolean} isLast */ History.isLastSavedState = function(newState){ // Prepare var isLast = false, newId, oldState, oldId; // Check if ( History.savedStates.length ) { newId = newState.id; oldState = History.getLastSavedState(); oldId = oldState.id; // Check isLast = (newId === oldId); } // Return return isLast; }; /** * History.saveState * Push a State * @param {Object} newState * @return {boolean} changed */ History.saveState = function(newState){ // Check Hash if ( History.isLastSavedState(newState) ) { return false; } // Push the State History.savedStates.push(History.cloneObject(newState)); // Return true return true; }; /** * History.getStateByIndex() * Gets a state by the index * @param {integer} index * @return {Object} */ History.getStateByIndex = function(index){ // Prepare var State = null; // Handle if ( typeof index === 'undefined' ) { // Get the last inserted State = History.savedStates[History.savedStates.length-1]; } else if ( index < 0 ) { // Get from the end State = History.savedStates[History.savedStates.length+index]; } else { // Get from the beginning State = History.savedStates[index]; } // Return State return State; }; /** * History.getCurrentIndex() * Gets the current index * @return (integer) */ History.getCurrentIndex = function(){ // Prepare var index = null; // No states saved if(History.savedStates.length < 1) { index = 0; } else { index = History.savedStates.length-1; } return index; }; // ==================================================================== // Hash Helpers /** * History.getHash() * @param {Location=} location * Gets the current document hash * Note: unlike location.hash, this is guaranteed to return the escaped hash in all browsers * @return {string} */ History.getHash = function(doc){ var url = History.getLocationHref(doc), hash; hash = History.getHashByUrl(url); return hash; }; /** * History.unescapeHash() * normalize and Unescape a Hash * @param {String} hash * @return {string} */ History.unescapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Unescape hash result = decodeURIComponent(result); // Return result return result; }; /** * History.normalizeHash() * normalize a hash across browsers * @return {string} */ History.normalizeHash = function(hash){ // Prepare var result = hash.replace(/[^#]*#/,'').replace(/#.*/, ''); // Return result return result; }; /** * History.setHash(hash) * Sets the document hash * @param {string} hash * @return {History} */ History.setHash = function(hash,queue){ // Prepare var State, pageUrl; // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.setHash: we must wait', arguments); History.pushQueue({ scope: History, callback: History.setHash, args: arguments, queue: queue }); return false; } // Log //History.debug('History.setHash: called',hash); // Make Busy + Continue History.busy(true); // Check if hash is a state State = History.extractState(hash,true); if ( State && !History.emulated.pushState ) { // Hash is a state so skip the setHash //History.debug('History.setHash: Hash is a state so skipping the hash set with a direct pushState call',arguments); // PushState History.pushState(State.data,State.title,State.url,false); } else if ( History.getHash() !== hash ) { // Hash is a proper hash, so apply it // Handle browser bugs if ( History.bugs.setHash ) { // Fix Safari Bug https://bugs.webkit.org/show_bug.cgi?id=56249 // Fetch the base page pageUrl = History.getPageUrl(); // Safari hash apply History.pushState(null,null,pageUrl+'#'+hash,false); } else { // Normal hash apply document.location.hash = hash; } } // Chain return History; }; /** * History.escape() * normalize and Escape a Hash * @return {string} */ History.escapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Escape hash result = window.encodeURIComponent(result); // IE6 Escape Bug if ( !History.bugs.hashEscape ) { // Restore common parts result = result .replace(/\%21/g,'!') .replace(/\%26/g,'&') .replace(/\%3D/g,'=') .replace(/\%3F/g,'?'); } // Return result return result; }; /** * History.getHashByUrl(url) * Extracts the Hash from a URL * @param {string} url * @return {string} url */ History.getHashByUrl = function(url){ // Extract the hash var hash = String(url) .replace(/([^#]*)#?([^#]*)#?(.*)/, '$2') ; // Unescape hash hash = History.unescapeHash(hash); // Return hash return hash; }; /** * History.setTitle(title) * Applies the title to the document * @param {State} newState * @return {Boolean} */ History.setTitle = function(newState){ // Prepare var title = newState.title, firstState; // Initial if ( !title ) { firstState = History.getStateByIndex(0); if ( firstState && firstState.url === newState.url ) { title = firstState.title||History.options.initialTitle; } } // Apply try { document.getElementsByTagName('title')[0].innerHTML = title.replace('<','<').replace('>','>').replace(' & ',' & '); } catch ( Exception ) { } document.title = title; // Chain return History; }; // ==================================================================== // Queueing /** * History.queues * The list of queues to use * First In, First Out */ History.queues = []; /** * History.busy(value) * @param {boolean} value [optional] * @return {boolean} busy */ History.busy = function(value){ // Apply if ( typeof value !== 'undefined' ) { //History.debug('History.busy: changing ['+(History.busy.flag||false)+'] to ['+(value||false)+']', History.queues.length); History.busy.flag = value; } // Default else if ( typeof History.busy.flag === 'undefined' ) { History.busy.flag = false; } // Queue if ( !History.busy.flag ) { // Execute the next item in the queue clearTimeout(History.busy.timeout); var fireNext = function(){ var i, queue, item; if ( History.busy.flag ) return; for ( i=History.queues.length-1; i >= 0; --i ) { queue = History.queues[i]; if ( queue.length === 0 ) continue; item = queue.shift(); History.fireQueueItem(item); History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } }; History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } // Return return History.busy.flag; }; /** * History.busy.flag */ History.busy.flag = false; /** * History.fireQueueItem(item) * Fire a Queue Item * @param {Object} item * @return {Mixed} result */ History.fireQueueItem = function(item){ return item.callback.apply(item.scope||History,item.args||[]); }; /** * History.pushQueue(callback,args) * Add an item to the queue * @param {Object} item [scope,callback,args,queue] */ History.pushQueue = function(item){ // Prepare the queue History.queues[item.queue||0] = History.queues[item.queue||0]||[]; // Add to the queue History.queues[item.queue||0].push(item); // Chain return History; }; /** * History.queue (item,queue), (func,queue), (func), (item) * Either firs the item now if not busy, or adds it to the queue */ History.queue = function(item,queue){ // Prepare if ( typeof item === 'function' ) { item = { callback: item }; } if ( typeof queue !== 'undefined' ) { item.queue = queue; } // Handle if ( History.busy() ) { History.pushQueue(item); } else { History.fireQueueItem(item); } // Chain return History; }; /** * History.clearQueue() * Clears the Queue */ History.clearQueue = function(){ History.busy.flag = false; History.queues = []; return History; }; // ==================================================================== // IE Bug Fix /** * History.stateChanged * States whether or not the state has changed since the last double check was initialised */ History.stateChanged = false; /** * History.doubleChecker * Contains the timeout used for the double checks */ History.doubleChecker = false; /** * History.doubleCheckComplete() * Complete a double check * @return {History} */ History.doubleCheckComplete = function(){ // Update History.stateChanged = true; // Clear History.doubleCheckClear(); // Chain return History; }; /** * History.doubleCheckClear() * Clear a double check * @return {History} */ History.doubleCheckClear = function(){ // Clear if ( History.doubleChecker ) { clearTimeout(History.doubleChecker); History.doubleChecker = false; } // Chain return History; }; /** * History.doubleCheck() * Create a double check * @return {History} */ History.doubleCheck = function(tryAgain){ // Reset History.stateChanged = false; History.doubleCheckClear(); // Fix IE6,IE7 bug where calling history.back or history.forward does not actually change the hash (whereas doing it manually does) // Fix Safari 5 bug where sometimes the state does not change: https://bugs.webkit.org/show_bug.cgi?id=42940 if ( History.bugs.ieDoubleCheck ) { // Apply Check History.doubleChecker = setTimeout( function(){ History.doubleCheckClear(); if ( !History.stateChanged ) { //History.debug('History.doubleCheck: State has not yet changed, trying again', arguments); // Re-Attempt tryAgain(); } return true; }, History.options.doubleCheckInterval ); } // Chain return History; }; // ==================================================================== // Safari Bug Fix /** * History.safariStatePoll() * Poll the current state * @return {History} */ History.safariStatePoll = function(){ // Poll the URL // Get the Last State which has the new URL var urlState = History.extractState(History.getLocationHref()), newState; // Check for a difference if ( !History.isLastSavedState(urlState) ) { newState = urlState; } else { return; } // Check if we have a state with that url // If not create it if ( !newState ) { //History.debug('History.safariStatePoll: new'); newState = History.createStateObject(); } // Apply the New State //History.debug('History.safariStatePoll: trigger'); History.Adapter.trigger(window,'popstate'); // Chain return History; }; // ==================================================================== // State Aliases /** * History.back(queue) * Send the browser history back one item * @param {Integer} queue [optional] */ History.back = function(queue){ //History.debug('History.back: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.back: we must wait', arguments); History.pushQueue({ scope: History, callback: History.back, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.back(false); }); // Go back history.go(-1); // End back closure return true; }; /** * History.forward(queue) * Send the browser history forward one item * @param {Integer} queue [optional] */ History.forward = function(queue){ //History.debug('History.forward: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.forward: we must wait', arguments); History.pushQueue({ scope: History, callback: History.forward, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.forward(false); }); // Go forward history.go(1); // End forward closure return true; }; /** * History.go(index,queue) * Send the browser history back or forward index times * @param {Integer} queue [optional] */ History.go = function(index,queue){ //History.debug('History.go: called', arguments); // Prepare var i; // Handle if ( index > 0 ) { // Forward for ( i=1; i<=index; ++i ) { History.forward(queue); } } else if ( index < 0 ) { // Backward for ( i=-1; i>=index; --i ) { History.back(queue); } } else { throw new Error('History.go: History.go requires a positive or negative integer passed.'); } // Chain return History; }; // ==================================================================== // HTML5 State Support // Non-Native pushState Implementation if ( History.emulated.pushState ) { /* * Provide Skeleton for HTML4 Browsers */ // Prepare var emptyFunction = function(){}; History.pushState = History.pushState||emptyFunction; History.replaceState = History.replaceState||emptyFunction; } // History.emulated.pushState // Native pushState Implementation else { /* * Use native HTML5 History API Implementation */ /** * History.onPopState(event,extra) * Refresh the Current State */ History.onPopState = function(event,extra){ // Prepare var stateId = false, newState = false, currentHash, currentState; // Reset the double check History.doubleCheckComplete(); // Check for a Hash, and handle apporiatly currentHash = History.getHash(); if ( currentHash ) { // Expand Hash currentState = History.extractState(currentHash||History.getLocationHref(),true); if ( currentState ) { // We were able to parse it, it must be a State! // Let's forward to replaceState //History.debug('History.onPopState: state anchor', currentHash, currentState); History.replaceState(currentState.data, currentState.title, currentState.url, false); } else { // Traditional Anchor //History.debug('History.onPopState: traditional anchor', currentHash); History.Adapter.trigger(window,'anchorchange'); History.busy(false); } // We don't care for hashes History.expectedStateId = false; return false; } // Ensure stateId = History.Adapter.extractEventData('state',event,extra) || false; // Fetch State if ( stateId ) { // Vanilla: Back/forward button was used newState = History.getStateById(stateId); } else if ( History.expectedStateId ) { // Vanilla: A new state was pushed, and popstate was called manually newState = History.getStateById(History.expectedStateId); } else { // Initial State newState = History.extractState(History.getLocationHref()); } // The State did not exist in our store if ( !newState ) { // Regenerate the State newState = History.createStateObject(null,null,History.getLocationHref()); } // Clean History.expectedStateId = false; // Check if we are the same state if ( History.isLastSavedState(newState) ) { // There has been no change (just the page's hash has finally propagated) //History.debug('History.onPopState: no change', newState, History.savedStates); History.busy(false); return false; } // Store the State History.storeState(newState); History.saveState(newState); // Force update of the title History.setTitle(newState); // Fire Our Event History.Adapter.trigger(window,'statechange'); History.busy(false); // Return true return true; }; History.Adapter.bind(window,'popstate',History.onPopState); /** * History.pushState(data,title,url) * Add a new State to the history object, become it, and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.pushState = function(data,title,url,queue){ //History.debug('History.pushState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.pushState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.pushState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.pushState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End pushState closure return true; }; /** * History.replaceState(data,title,url) * Replace the State and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.replaceState = function(data,title,url,queue){ //History.debug('History.replaceState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.replaceState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.replaceState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.replaceState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End replaceState closure return true; }; } // !History.emulated.pushState // ==================================================================== // Initialise /** * Load the Store */ if ( sessionStorage ) { // Fetch try { History.store = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { History.store = {}; } // Normalize History.normalizeStore(); } else { // Default Load History.store = {}; History.normalizeStore(); } /** * Clear Intervals on exit to prevent memory leaks */ History.Adapter.bind(window,"unload",History.clearAllIntervals); /** * Create the initial State */ History.saveState(History.storeState(History.extractState(History.getLocationHref(),true))); /** * Bind for Saving Store */ if ( sessionStorage ) { // When the page is closed History.onUnload = function(){ // Prepare var currentStore, item, currentStoreString; // Fetch try { currentStore = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { currentStore = {}; } // Ensure currentStore.idToState = currentStore.idToState || {}; currentStore.urlToId = currentStore.urlToId || {}; currentStore.stateToId = currentStore.stateToId || {}; // Sync for ( item in History.idToState ) { if ( !History.idToState.hasOwnProperty(item) ) { continue; } currentStore.idToState[item] = History.idToState[item]; } for ( item in History.urlToId ) { if ( !History.urlToId.hasOwnProperty(item) ) { continue; } currentStore.urlToId[item] = History.urlToId[item]; } for ( item in History.stateToId ) { if ( !History.stateToId.hasOwnProperty(item) ) { continue; } currentStore.stateToId[item] = History.stateToId[item]; } // Update History.store = currentStore; History.normalizeStore(); // In Safari, going into Private Browsing mode causes the // Session Storage object to still exist but if you try and use // or set any property/function of it it throws the exception // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to // add something to storage that exceeded the quota." infinitely // every second. currentStoreString = JSON.stringify(currentStore); try { // Store sessionStorage.setItem('History.store', currentStoreString); } catch (e) { if (e.code === DOMException.QUOTA_EXCEEDED_ERR) { if (sessionStorage.length) { // Workaround for a bug seen on iPads. Sometimes the quota exceeded error comes up and simply // removing/resetting the storage can work. sessionStorage.removeItem('History.store'); sessionStorage.setItem('History.store', currentStoreString); } else { // Otherwise, we're probably private browsing in Safari, so we'll ignore the exception. } } else { throw e; } } }; // For Internet Explorer History.intervalList.push(setInterval(History.onUnload,History.options.storeInterval)); // For Other Browsers History.Adapter.bind(window,'beforeunload',History.onUnload); History.Adapter.bind(window,'unload',History.onUnload); // Both are enabled for consistency } // Non-Native pushState Implementation if ( !History.emulated.pushState ) { // Be aware, the following is only for native pushState implementations // If you are wanting to include something for all browsers // Then include it above this if block /** * Setup Safari Fix */ if ( History.bugs.safariPoll ) { History.intervalList.push(setInterval(History.safariStatePoll, History.options.safariPollInterval)); } /** * Ensure Cross Browser Compatibility */ if ( navigator.vendor === 'Apple Computer, Inc.' || (navigator.appCodeName||'') === 'Mozilla' ) { /** * Fix Safari HashChange Issue */ // Setup Alias History.Adapter.bind(window,'hashchange',function(){ History.Adapter.trigger(window,'popstate'); }); // Initialise Alias if ( History.getHash() ) { History.Adapter.onDomLoad(function(){ History.Adapter.trigger(window,'hashchange'); }); } } } // !History.emulated.pushState }; // History.initCore // Try to Initialise History if (!History.options || !History.options.delayInit) { History.init(); } })(window); ================================================ FILE: scripts/bundled-uncompressed/html5/mootools.history.js ================================================ /** * History.js MooTools Adapter * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ // Closure (function(window,undefined){ "use strict"; // Localise Globals var History = window.History = window.History||{}, MooTools = window.MooTools, Element = window.Element; // Check Existence if ( typeof History.Adapter !== 'undefined' ) { throw new Error('History.js Adapter has already been loaded...'); } // Make MooTools aware of History.js Events Object.append(Element.NativeEvents,{ 'popstate':2, 'hashchange':2 }); // Add the Adapter History.Adapter = { /** * History.Adapter.bind(el,event,callback) * @param {Element|string} el * @param {string} event - custom and standard events * @param {function} callback * @return {void} */ bind: function(el,event,callback){ var El = typeof el === 'string' ? document.id(el) : el; El.addEvent(event,callback); }, /** * History.Adapter.trigger(el,event) * @param {Element|string} el * @param {string} event - custom and standard events * @param {Object=} extra - a object of extra event data (optional) * @return void */ trigger: function(el,event,extra){ var El = typeof el === 'string' ? document.id(el) : el; El.fireEvent(event,extra); }, /** * History.Adapter.extractEventData(key,event,extra) * @param {string} key - key for the event data to extract * @param {string} event - custom and standard events * @return {mixed} */ extractEventData: function(key,event){ // MooTools Native then MooTools Custom var result = (event && event.event && event.event[key]) || (event && event[key]) || undefined; // Return return result; }, /** * History.Adapter.onDomLoad(callback) * @param {function} callback * @return {void} */ onDomLoad: function(callback) { window.addEvent('domready',callback); } }; // Try and Initialise History if ( typeof History.init !== 'undefined' ) { History.init(); } })(window); /** * History.js Core * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window,undefined){ "use strict"; // ======================================================================== // Initialise // Localise Globals var console = window.console||undefined, // Prevent a JSLint complain document = window.document, // Make sure we are using the correct document navigator = window.navigator, // Make sure we are using the correct navigator sessionStorage = false, // sessionStorage setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, setInterval = window.setInterval, clearInterval = window.clearInterval, JSON = window.JSON, alert = window.alert, History = window.History = window.History||{}, // Public History Object history = window.history; // Old History Object try { sessionStorage = window.sessionStorage; // This will throw an exception in some browsers when cookies/localStorage are explicitly disabled (i.e. Chrome) sessionStorage.setItem('TEST', '1'); sessionStorage.removeItem('TEST'); } catch(e) { sessionStorage = false; } // MooTools Compatibility JSON.stringify = JSON.stringify||JSON.encode; JSON.parse = JSON.parse||JSON.decode; // Check Existence if ( typeof History.init !== 'undefined' ) { throw new Error('History.js Core has already been loaded...'); } // Initialise History History.init = function(options){ // Check Load Status of Adapter if ( typeof History.Adapter === 'undefined' ) { return false; } // Check Load Status of Core if ( typeof History.initCore !== 'undefined' ) { History.initCore(); } // Check Load Status of HTML4 Support if ( typeof History.initHtml4 !== 'undefined' ) { History.initHtml4(); } // Return true return true; }; // ======================================================================== // Initialise Core // Initialise Core History.initCore = function(options){ // Initialise if ( typeof History.initCore.initialized !== 'undefined' ) { // Already Loaded return false; } else { History.initCore.initialized = true; } // ==================================================================== // Options /** * History.options * Configurable options */ History.options = History.options||{}; /** * History.options.hashChangeInterval * How long should the interval be before hashchange checks */ History.options.hashChangeInterval = History.options.hashChangeInterval || 100; /** * History.options.safariPollInterval * How long should the interval be before safari poll checks */ History.options.safariPollInterval = History.options.safariPollInterval || 500; /** * History.options.doubleCheckInterval * How long should the interval be before we perform a double check */ History.options.doubleCheckInterval = History.options.doubleCheckInterval || 500; /** * History.options.disableSuid * Force History not to append suid */ History.options.disableSuid = History.options.disableSuid || false; /** * History.options.storeInterval * How long should we wait between store calls */ History.options.storeInterval = History.options.storeInterval || 1000; /** * History.options.busyDelay * How long should we wait between busy events */ History.options.busyDelay = History.options.busyDelay || 250; /** * History.options.debug * If true will enable debug messages to be logged */ History.options.debug = History.options.debug || false; /** * History.options.initialTitle * What is the title of the initial state */ History.options.initialTitle = History.options.initialTitle || document.title; /** * History.options.html4Mode * If true, will force HTMl4 mode (hashtags) */ History.options.html4Mode = History.options.html4Mode || false; /** * History.options.delayInit * Want to override default options and call init manually. */ History.options.delayInit = History.options.delayInit || false; // ==================================================================== // Interval record /** * History.intervalList * List of intervals set, to be cleared when document is unloaded. */ History.intervalList = []; /** * History.clearAllIntervals * Clears all setInterval instances. */ History.clearAllIntervals = function(){ var i, il = History.intervalList; if (typeof il !== "undefined" && il !== null) { for (i = 0; i < il.length; i++) { clearInterval(il[i]); } History.intervalList = null; } }; // ==================================================================== // Debug /** * History.debug(message,...) * Logs the passed arguments if debug enabled */ History.debug = function(){ if ( (History.options.debug||false) ) { History.log.apply(History,arguments); } }; /** * History.log(message,...) * Logs the passed arguments */ History.log = function(){ // Prepare var consoleExists = !(typeof console === 'undefined' || typeof console.log === 'undefined' || typeof console.log.apply === 'undefined'), textarea = document.getElementById('log'), message, i,n, args,arg ; // Write to Console if ( consoleExists ) { args = Array.prototype.slice.call(arguments); message = args.shift(); if ( typeof console.debug !== 'undefined' ) { console.debug.apply(console,[message,args]); } else { console.log.apply(console,[message,args]); } } else { message = ("\n"+arguments[0]+"\n"); } // Write to log for ( i=1,n=arguments.length; i * @author James Padolsey */ History.getInternetExplorerMajorVersion = function(){ var result = History.getInternetExplorerMajorVersion.cached = (typeof History.getInternetExplorerMajorVersion.cached !== 'undefined') ? History.getInternetExplorerMajorVersion.cached : (function(){ var v = 3, div = document.createElement('div'), all = div.getElementsByTagName('i'); while ( (div.innerHTML = '') && all[0] ) {} return (v > 4) ? v : false; })() ; return result; }; /** * History.isInternetExplorer() * Are we using Internet Explorer? * @return {boolean} * @license Public Domain * @author Benjamin Arthur Lupton */ History.isInternetExplorer = function(){ var result = History.isInternetExplorer.cached = (typeof History.isInternetExplorer.cached !== 'undefined') ? History.isInternetExplorer.cached : Boolean(History.getInternetExplorerMajorVersion()) ; return result; }; /** * History.emulated * Which features require emulating? */ if (History.options.html4Mode) { History.emulated = { pushState : true, hashChange: true }; } else { History.emulated = { pushState: !Boolean( window.history && window.history.pushState && window.history.replaceState && !( (/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i).test(navigator.userAgent) /* disable for versions of iOS before version 4.3 (8F190) */ || (/AppleWebKit\/5([0-2]|3[0-2])/i).test(navigator.userAgent) /* disable for the mercury iOS browser, or at least older versions of the webkit engine */ ) ), hashChange: Boolean( !(('onhashchange' in window) || ('onhashchange' in document)) || (History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8) ) }; } /** * History.enabled * Is History enabled? */ History.enabled = !History.emulated.pushState; /** * History.bugs * Which bugs are present */ History.bugs = { /** * Safari 5 and Safari iOS 4 fail to return to the correct state once a hash is replaced by a `replaceState` call * https://bugs.webkit.org/show_bug.cgi?id=56249 */ setHash: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * Safari 5 and Safari iOS 4 sometimes fail to apply the state change under busy conditions * https://bugs.webkit.org/show_bug.cgi?id=42940 */ safariPoll: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * MSIE 6 and 7 sometimes do not apply a hash even it was told to (requiring a second call to the apply function) */ ieDoubleCheck: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8), /** * MSIE 6 requires the entire hash to be encoded for the hashes to trigger the onHashChange event */ hashEscape: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 7) }; /** * History.isEmptyObject(obj) * Checks to see if the Object is Empty * @param {Object} obj * @return {boolean} */ History.isEmptyObject = function(obj) { for ( var name in obj ) { if ( obj.hasOwnProperty(name) ) { return false; } } return true; }; /** * History.cloneObject(obj) * Clones a object and eliminate all references to the original contexts * @param {Object} obj * @return {Object} */ History.cloneObject = function(obj) { var hash,newObj; if ( obj ) { hash = JSON.stringify(obj); newObj = JSON.parse(hash); } else { newObj = {}; } return newObj; }; // ==================================================================== // URL Helpers /** * History.getRootUrl() * Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com" * @return {String} rootUrl */ History.getRootUrl = function(){ // Create var rootUrl = document.location.protocol+'//'+(document.location.hostname||document.location.host); if ( document.location.port||false ) { rootUrl += ':'+document.location.port; } rootUrl += '/'; // Return return rootUrl; }; /** * History.getBaseHref() * Fetches the `href` attribute of the `` element if it exists * @return {String} baseHref */ History.getBaseHref = function(){ // Create var baseElements = document.getElementsByTagName('base'), baseElement = null, baseHref = ''; // Test for Base Element if ( baseElements.length === 1 ) { // Prepare for Base Element baseElement = baseElements[0]; baseHref = baseElement.href.replace(/[^\/]+$/,''); } // Adjust trailing slash baseHref = baseHref.replace(/\/+$/,''); if ( baseHref ) baseHref += '/'; // Return return baseHref; }; /** * History.getBaseUrl() * Fetches the baseHref or basePageUrl or rootUrl (whichever one exists first) * @return {String} baseUrl */ History.getBaseUrl = function(){ // Create var baseUrl = History.getBaseHref()||History.getBasePageUrl()||History.getRootUrl(); // Return return baseUrl; }; /** * History.getPageUrl() * Fetches the URL of the current page * @return {String} pageUrl */ History.getPageUrl = function(){ // Fetch var State = History.getState(false,false), stateUrl = (State||{}).url||History.getLocationHref(), pageUrl; // Create pageUrl = stateUrl.replace(/\/+$/,'').replace(/[^\/]+$/,function(part,index,string){ return (/\./).test(part) ? part : part+'/'; }); // Return return pageUrl; }; /** * History.getBasePageUrl() * Fetches the Url of the directory of the current page * @return {String} basePageUrl */ History.getBasePageUrl = function(){ // Create var basePageUrl = (History.getLocationHref()).replace(/[#\?].*/,'').replace(/[^\/]+$/,function(part,index,string){ return (/[^\/]$/).test(part) ? '' : part; }).replace(/\/+$/,'')+'/'; // Return return basePageUrl; }; /** * History.getFullUrl(url) * Ensures that we have an absolute URL and not a relative URL * @param {string} url * @param {Boolean} allowBaseHref * @return {string} fullUrl */ History.getFullUrl = function(url,allowBaseHref){ // Prepare var fullUrl = url, firstChar = url.substring(0,1); allowBaseHref = (typeof allowBaseHref === 'undefined') ? true : allowBaseHref; // Check if ( /[a-z]+\:\/\//.test(url) ) { // Full URL } else if ( firstChar === '/' ) { // Root URL fullUrl = History.getRootUrl()+url.replace(/^\/+/,''); } else if ( firstChar === '#' ) { // Anchor URL fullUrl = History.getPageUrl().replace(/#.*/,'')+url; } else if ( firstChar === '?' ) { // Query URL fullUrl = History.getPageUrl().replace(/[\?#].*/,'')+url; } else { // Relative URL if ( allowBaseHref ) { fullUrl = History.getBaseUrl()+url.replace(/^(\.\/)+/,''); } else { fullUrl = History.getBasePageUrl()+url.replace(/^(\.\/)+/,''); } // We have an if condition above as we do not want hashes // which are relative to the baseHref in our URLs // as if the baseHref changes, then all our bookmarks // would now point to different locations // whereas the basePageUrl will always stay the same } // Return return fullUrl.replace(/\#$/,''); }; /** * History.getShortUrl(url) * Ensures that we have a relative URL and not a absolute URL * @param {string} url * @return {string} url */ History.getShortUrl = function(url){ // Prepare var shortUrl = url, baseUrl = History.getBaseUrl(), rootUrl = History.getRootUrl(); // Trim baseUrl if ( History.emulated.pushState ) { // We are in a if statement as when pushState is not emulated // The actual url these short urls are relative to can change // So within the same session, we the url may end up somewhere different shortUrl = shortUrl.replace(baseUrl,''); } // Trim rootUrl shortUrl = shortUrl.replace(rootUrl,'/'); // Ensure we can still detect it as a state if ( History.isTraditionalAnchor(shortUrl) ) { shortUrl = './'+shortUrl; } // Clean It shortUrl = shortUrl.replace(/^(\.\/)+/g,'./').replace(/\#$/,''); // Return return shortUrl; }; /** * History.getLocationHref(document) * Returns a normalized version of document.location.href * accounting for browser inconsistencies, etc. * * This URL will be URI-encoded and will include the hash * * @param {object} document * @return {string} url */ History.getLocationHref = function(doc) { doc = doc || document; // most of the time, this will be true if (doc.URL === doc.location.href) return doc.location.href; // some versions of webkit URI-decode document.location.href // but they leave document.URL in an encoded state if (doc.location.href === decodeURIComponent(doc.URL)) return doc.URL; // FF 3.6 only updates document.URL when a page is reloaded // document.location.href is updated correctly if (doc.location.hash && decodeURIComponent(doc.location.href.replace(/^[^#]+/, "")) === doc.location.hash) return doc.location.href; if (doc.URL.indexOf('#') == -1 && doc.location.href.indexOf('#') != -1) return doc.location.href; return doc.URL || doc.location.href; }; // ==================================================================== // State Storage /** * History.store * The store for all session specific data */ History.store = {}; /** * History.idToState * 1-1: State ID to State Object */ History.idToState = History.idToState||{}; /** * History.stateToId * 1-1: State String to State ID */ History.stateToId = History.stateToId||{}; /** * History.urlToId * 1-1: State URL to State ID */ History.urlToId = History.urlToId||{}; /** * History.storedStates * Store the states in an array */ History.storedStates = History.storedStates||[]; /** * History.savedStates * Saved the states in an array */ History.savedStates = History.savedStates||[]; /** * History.noramlizeStore() * Noramlize the store by adding necessary values */ History.normalizeStore = function(){ History.store.idToState = History.store.idToState||{}; History.store.urlToId = History.store.urlToId||{}; History.store.stateToId = History.store.stateToId||{}; }; /** * History.getState() * Get an object containing the data, title and url of the current state * @param {Boolean} friendly * @param {Boolean} create * @return {Object} State */ History.getState = function(friendly,create){ // Prepare if ( typeof friendly === 'undefined' ) { friendly = true; } if ( typeof create === 'undefined' ) { create = true; } // Fetch var State = History.getLastSavedState(); // Create if ( !State && create ) { State = History.createStateObject(); } // Adjust if ( friendly ) { State = History.cloneObject(State); State.url = State.cleanUrl||State.url; } // Return return State; }; /** * History.getIdByState(State) * Gets a ID for a State * @param {State} newState * @return {String} id */ History.getIdByState = function(newState){ // Fetch ID var id = History.extractId(newState.url), str; if ( !id ) { // Find ID via State String str = History.getStateString(newState); if ( typeof History.stateToId[str] !== 'undefined' ) { id = History.stateToId[str]; } else if ( typeof History.store.stateToId[str] !== 'undefined' ) { id = History.store.stateToId[str]; } else { // Generate a new ID while ( true ) { id = (new Date()).getTime() + String(Math.random()).replace(/\D/g,''); if ( typeof History.idToState[id] === 'undefined' && typeof History.store.idToState[id] === 'undefined' ) { break; } } // Apply the new State to the ID History.stateToId[str] = id; History.idToState[id] = newState; } } // Return ID return id; }; /** * History.normalizeState(State) * Expands a State Object * @param {object} State * @return {object} */ History.normalizeState = function(oldState){ // Variables var newState, dataNotEmpty; // Prepare if ( !oldState || (typeof oldState !== 'object') ) { oldState = {}; } // Check if ( typeof oldState.normalized !== 'undefined' ) { return oldState; } // Adjust if ( !oldState.data || (typeof oldState.data !== 'object') ) { oldState.data = {}; } // ---------------------------------------------------------------- // Create newState = {}; newState.normalized = true; newState.title = oldState.title||''; newState.url = History.getFullUrl(oldState.url?oldState.url:(History.getLocationHref())); newState.hash = History.getShortUrl(newState.url); newState.data = History.cloneObject(oldState.data); // Fetch ID newState.id = History.getIdByState(newState); // ---------------------------------------------------------------- // Clean the URL newState.cleanUrl = newState.url.replace(/\??\&_suid.*/,''); newState.url = newState.cleanUrl; // Check to see if we have more than just a url dataNotEmpty = !History.isEmptyObject(newState.data); // Apply if ( (newState.title || dataNotEmpty) && History.options.disableSuid !== true ) { // Add ID to Hash newState.hash = History.getShortUrl(newState.url).replace(/\??\&_suid.*/,''); if ( !/\?/.test(newState.hash) ) { newState.hash += '?'; } newState.hash += '&_suid='+newState.id; } // Create the Hashed URL newState.hashedUrl = History.getFullUrl(newState.hash); // ---------------------------------------------------------------- // Update the URL if we have a duplicate if ( (History.emulated.pushState || History.bugs.safariPoll) && History.hasUrlDuplicate(newState) ) { newState.url = newState.hashedUrl; } // ---------------------------------------------------------------- // Return return newState; }; /** * History.createStateObject(data,title,url) * Creates a object based on the data, title and url state params * @param {object} data * @param {string} title * @param {string} url * @return {object} */ History.createStateObject = function(data,title,url){ // Hashify var State = { 'data': data, 'title': title, 'url': url }; // Expand the State State = History.normalizeState(State); // Return object return State; }; /** * History.getStateById(id) * Get a state by it's UID * @param {String} id */ History.getStateById = function(id){ // Prepare id = String(id); // Retrieve var State = History.idToState[id] || History.store.idToState[id] || undefined; // Return State return State; }; /** * Get a State's String * @param {State} passedState */ History.getStateString = function(passedState){ // Prepare var State, cleanedState, str; // Fetch State = History.normalizeState(passedState); // Clean cleanedState = { data: State.data, title: passedState.title, url: passedState.url }; // Fetch str = JSON.stringify(cleanedState); // Return return str; }; /** * Get a State's ID * @param {State} passedState * @return {String} id */ History.getStateId = function(passedState){ // Prepare var State, id; // Fetch State = History.normalizeState(passedState); // Fetch id = State.id; // Return return id; }; /** * History.getHashByState(State) * Creates a Hash for the State Object * @param {State} passedState * @return {String} hash */ History.getHashByState = function(passedState){ // Prepare var State, hash; // Fetch State = History.normalizeState(passedState); // Hash hash = State.hash; // Return return hash; }; /** * History.extractId(url_or_hash) * Get a State ID by it's URL or Hash * @param {string} url_or_hash * @return {string} id */ History.extractId = function ( url_or_hash ) { // Prepare var id,parts,url, tmp; // Extract // If the URL has a #, use the id from before the # if (url_or_hash.indexOf('#') != -1) { tmp = url_or_hash.split("#")[0]; } else { tmp = url_or_hash; } parts = /(.*)\&_suid=([0-9]+)$/.exec(tmp); url = parts ? (parts[1]||url_or_hash) : url_or_hash; id = parts ? String(parts[2]||'') : ''; // Return return id||false; }; /** * History.isTraditionalAnchor * Checks to see if the url is a traditional anchor or not * @param {String} url_or_hash * @return {Boolean} */ History.isTraditionalAnchor = function(url_or_hash){ // Check var isTraditional = !(/[\/\?\.]/.test(url_or_hash)); // Return return isTraditional; }; /** * History.extractState * Get a State by it's URL or Hash * @param {String} url_or_hash * @return {State|null} */ History.extractState = function(url_or_hash,create){ // Prepare var State = null, id, url; create = create||false; // Fetch SUID id = History.extractId(url_or_hash); if ( id ) { State = History.getStateById(id); } // Fetch SUID returned no State if ( !State ) { // Fetch URL url = History.getFullUrl(url_or_hash); // Check URL id = History.getIdByUrl(url)||false; if ( id ) { State = History.getStateById(id); } // Create State if ( !State && create && !History.isTraditionalAnchor(url_or_hash) ) { State = History.createStateObject(null,null,url); } } // Return return State; }; /** * History.getIdByUrl() * Get a State ID by a State URL */ History.getIdByUrl = function(url){ // Fetch var id = History.urlToId[url] || History.store.urlToId[url] || undefined; // Return return id; }; /** * History.getLastSavedState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastSavedState = function(){ return History.savedStates[History.savedStates.length-1]||undefined; }; /** * History.getLastStoredState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastStoredState = function(){ return History.storedStates[History.storedStates.length-1]||undefined; }; /** * History.hasUrlDuplicate * Checks if a Url will have a url conflict * @param {Object} newState * @return {Boolean} hasDuplicate */ History.hasUrlDuplicate = function(newState) { // Prepare var hasDuplicate = false, oldState; // Fetch oldState = History.extractState(newState.url); // Check hasDuplicate = oldState && oldState.id !== newState.id; // Return return hasDuplicate; }; /** * History.storeState * Store a State * @param {Object} newState * @return {Object} newState */ History.storeState = function(newState){ // Store the State History.urlToId[newState.url] = newState.id; // Push the State History.storedStates.push(History.cloneObject(newState)); // Return newState return newState; }; /** * History.isLastSavedState(newState) * Tests to see if the state is the last state * @param {Object} newState * @return {boolean} isLast */ History.isLastSavedState = function(newState){ // Prepare var isLast = false, newId, oldState, oldId; // Check if ( History.savedStates.length ) { newId = newState.id; oldState = History.getLastSavedState(); oldId = oldState.id; // Check isLast = (newId === oldId); } // Return return isLast; }; /** * History.saveState * Push a State * @param {Object} newState * @return {boolean} changed */ History.saveState = function(newState){ // Check Hash if ( History.isLastSavedState(newState) ) { return false; } // Push the State History.savedStates.push(History.cloneObject(newState)); // Return true return true; }; /** * History.getStateByIndex() * Gets a state by the index * @param {integer} index * @return {Object} */ History.getStateByIndex = function(index){ // Prepare var State = null; // Handle if ( typeof index === 'undefined' ) { // Get the last inserted State = History.savedStates[History.savedStates.length-1]; } else if ( index < 0 ) { // Get from the end State = History.savedStates[History.savedStates.length+index]; } else { // Get from the beginning State = History.savedStates[index]; } // Return State return State; }; /** * History.getCurrentIndex() * Gets the current index * @return (integer) */ History.getCurrentIndex = function(){ // Prepare var index = null; // No states saved if(History.savedStates.length < 1) { index = 0; } else { index = History.savedStates.length-1; } return index; }; // ==================================================================== // Hash Helpers /** * History.getHash() * @param {Location=} location * Gets the current document hash * Note: unlike location.hash, this is guaranteed to return the escaped hash in all browsers * @return {string} */ History.getHash = function(doc){ var url = History.getLocationHref(doc), hash; hash = History.getHashByUrl(url); return hash; }; /** * History.unescapeHash() * normalize and Unescape a Hash * @param {String} hash * @return {string} */ History.unescapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Unescape hash result = decodeURIComponent(result); // Return result return result; }; /** * History.normalizeHash() * normalize a hash across browsers * @return {string} */ History.normalizeHash = function(hash){ // Prepare var result = hash.replace(/[^#]*#/,'').replace(/#.*/, ''); // Return result return result; }; /** * History.setHash(hash) * Sets the document hash * @param {string} hash * @return {History} */ History.setHash = function(hash,queue){ // Prepare var State, pageUrl; // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.setHash: we must wait', arguments); History.pushQueue({ scope: History, callback: History.setHash, args: arguments, queue: queue }); return false; } // Log //History.debug('History.setHash: called',hash); // Make Busy + Continue History.busy(true); // Check if hash is a state State = History.extractState(hash,true); if ( State && !History.emulated.pushState ) { // Hash is a state so skip the setHash //History.debug('History.setHash: Hash is a state so skipping the hash set with a direct pushState call',arguments); // PushState History.pushState(State.data,State.title,State.url,false); } else if ( History.getHash() !== hash ) { // Hash is a proper hash, so apply it // Handle browser bugs if ( History.bugs.setHash ) { // Fix Safari Bug https://bugs.webkit.org/show_bug.cgi?id=56249 // Fetch the base page pageUrl = History.getPageUrl(); // Safari hash apply History.pushState(null,null,pageUrl+'#'+hash,false); } else { // Normal hash apply document.location.hash = hash; } } // Chain return History; }; /** * History.escape() * normalize and Escape a Hash * @return {string} */ History.escapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Escape hash result = window.encodeURIComponent(result); // IE6 Escape Bug if ( !History.bugs.hashEscape ) { // Restore common parts result = result .replace(/\%21/g,'!') .replace(/\%26/g,'&') .replace(/\%3D/g,'=') .replace(/\%3F/g,'?'); } // Return result return result; }; /** * History.getHashByUrl(url) * Extracts the Hash from a URL * @param {string} url * @return {string} url */ History.getHashByUrl = function(url){ // Extract the hash var hash = String(url) .replace(/([^#]*)#?([^#]*)#?(.*)/, '$2') ; // Unescape hash hash = History.unescapeHash(hash); // Return hash return hash; }; /** * History.setTitle(title) * Applies the title to the document * @param {State} newState * @return {Boolean} */ History.setTitle = function(newState){ // Prepare var title = newState.title, firstState; // Initial if ( !title ) { firstState = History.getStateByIndex(0); if ( firstState && firstState.url === newState.url ) { title = firstState.title||History.options.initialTitle; } } // Apply try { document.getElementsByTagName('title')[0].innerHTML = title.replace('<','<').replace('>','>').replace(' & ',' & '); } catch ( Exception ) { } document.title = title; // Chain return History; }; // ==================================================================== // Queueing /** * History.queues * The list of queues to use * First In, First Out */ History.queues = []; /** * History.busy(value) * @param {boolean} value [optional] * @return {boolean} busy */ History.busy = function(value){ // Apply if ( typeof value !== 'undefined' ) { //History.debug('History.busy: changing ['+(History.busy.flag||false)+'] to ['+(value||false)+']', History.queues.length); History.busy.flag = value; } // Default else if ( typeof History.busy.flag === 'undefined' ) { History.busy.flag = false; } // Queue if ( !History.busy.flag ) { // Execute the next item in the queue clearTimeout(History.busy.timeout); var fireNext = function(){ var i, queue, item; if ( History.busy.flag ) return; for ( i=History.queues.length-1; i >= 0; --i ) { queue = History.queues[i]; if ( queue.length === 0 ) continue; item = queue.shift(); History.fireQueueItem(item); History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } }; History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } // Return return History.busy.flag; }; /** * History.busy.flag */ History.busy.flag = false; /** * History.fireQueueItem(item) * Fire a Queue Item * @param {Object} item * @return {Mixed} result */ History.fireQueueItem = function(item){ return item.callback.apply(item.scope||History,item.args||[]); }; /** * History.pushQueue(callback,args) * Add an item to the queue * @param {Object} item [scope,callback,args,queue] */ History.pushQueue = function(item){ // Prepare the queue History.queues[item.queue||0] = History.queues[item.queue||0]||[]; // Add to the queue History.queues[item.queue||0].push(item); // Chain return History; }; /** * History.queue (item,queue), (func,queue), (func), (item) * Either firs the item now if not busy, or adds it to the queue */ History.queue = function(item,queue){ // Prepare if ( typeof item === 'function' ) { item = { callback: item }; } if ( typeof queue !== 'undefined' ) { item.queue = queue; } // Handle if ( History.busy() ) { History.pushQueue(item); } else { History.fireQueueItem(item); } // Chain return History; }; /** * History.clearQueue() * Clears the Queue */ History.clearQueue = function(){ History.busy.flag = false; History.queues = []; return History; }; // ==================================================================== // IE Bug Fix /** * History.stateChanged * States whether or not the state has changed since the last double check was initialised */ History.stateChanged = false; /** * History.doubleChecker * Contains the timeout used for the double checks */ History.doubleChecker = false; /** * History.doubleCheckComplete() * Complete a double check * @return {History} */ History.doubleCheckComplete = function(){ // Update History.stateChanged = true; // Clear History.doubleCheckClear(); // Chain return History; }; /** * History.doubleCheckClear() * Clear a double check * @return {History} */ History.doubleCheckClear = function(){ // Clear if ( History.doubleChecker ) { clearTimeout(History.doubleChecker); History.doubleChecker = false; } // Chain return History; }; /** * History.doubleCheck() * Create a double check * @return {History} */ History.doubleCheck = function(tryAgain){ // Reset History.stateChanged = false; History.doubleCheckClear(); // Fix IE6,IE7 bug where calling history.back or history.forward does not actually change the hash (whereas doing it manually does) // Fix Safari 5 bug where sometimes the state does not change: https://bugs.webkit.org/show_bug.cgi?id=42940 if ( History.bugs.ieDoubleCheck ) { // Apply Check History.doubleChecker = setTimeout( function(){ History.doubleCheckClear(); if ( !History.stateChanged ) { //History.debug('History.doubleCheck: State has not yet changed, trying again', arguments); // Re-Attempt tryAgain(); } return true; }, History.options.doubleCheckInterval ); } // Chain return History; }; // ==================================================================== // Safari Bug Fix /** * History.safariStatePoll() * Poll the current state * @return {History} */ History.safariStatePoll = function(){ // Poll the URL // Get the Last State which has the new URL var urlState = History.extractState(History.getLocationHref()), newState; // Check for a difference if ( !History.isLastSavedState(urlState) ) { newState = urlState; } else { return; } // Check if we have a state with that url // If not create it if ( !newState ) { //History.debug('History.safariStatePoll: new'); newState = History.createStateObject(); } // Apply the New State //History.debug('History.safariStatePoll: trigger'); History.Adapter.trigger(window,'popstate'); // Chain return History; }; // ==================================================================== // State Aliases /** * History.back(queue) * Send the browser history back one item * @param {Integer} queue [optional] */ History.back = function(queue){ //History.debug('History.back: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.back: we must wait', arguments); History.pushQueue({ scope: History, callback: History.back, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.back(false); }); // Go back history.go(-1); // End back closure return true; }; /** * History.forward(queue) * Send the browser history forward one item * @param {Integer} queue [optional] */ History.forward = function(queue){ //History.debug('History.forward: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.forward: we must wait', arguments); History.pushQueue({ scope: History, callback: History.forward, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.forward(false); }); // Go forward history.go(1); // End forward closure return true; }; /** * History.go(index,queue) * Send the browser history back or forward index times * @param {Integer} queue [optional] */ History.go = function(index,queue){ //History.debug('History.go: called', arguments); // Prepare var i; // Handle if ( index > 0 ) { // Forward for ( i=1; i<=index; ++i ) { History.forward(queue); } } else if ( index < 0 ) { // Backward for ( i=-1; i>=index; --i ) { History.back(queue); } } else { throw new Error('History.go: History.go requires a positive or negative integer passed.'); } // Chain return History; }; // ==================================================================== // HTML5 State Support // Non-Native pushState Implementation if ( History.emulated.pushState ) { /* * Provide Skeleton for HTML4 Browsers */ // Prepare var emptyFunction = function(){}; History.pushState = History.pushState||emptyFunction; History.replaceState = History.replaceState||emptyFunction; } // History.emulated.pushState // Native pushState Implementation else { /* * Use native HTML5 History API Implementation */ /** * History.onPopState(event,extra) * Refresh the Current State */ History.onPopState = function(event,extra){ // Prepare var stateId = false, newState = false, currentHash, currentState; // Reset the double check History.doubleCheckComplete(); // Check for a Hash, and handle apporiatly currentHash = History.getHash(); if ( currentHash ) { // Expand Hash currentState = History.extractState(currentHash||History.getLocationHref(),true); if ( currentState ) { // We were able to parse it, it must be a State! // Let's forward to replaceState //History.debug('History.onPopState: state anchor', currentHash, currentState); History.replaceState(currentState.data, currentState.title, currentState.url, false); } else { // Traditional Anchor //History.debug('History.onPopState: traditional anchor', currentHash); History.Adapter.trigger(window,'anchorchange'); History.busy(false); } // We don't care for hashes History.expectedStateId = false; return false; } // Ensure stateId = History.Adapter.extractEventData('state',event,extra) || false; // Fetch State if ( stateId ) { // Vanilla: Back/forward button was used newState = History.getStateById(stateId); } else if ( History.expectedStateId ) { // Vanilla: A new state was pushed, and popstate was called manually newState = History.getStateById(History.expectedStateId); } else { // Initial State newState = History.extractState(History.getLocationHref()); } // The State did not exist in our store if ( !newState ) { // Regenerate the State newState = History.createStateObject(null,null,History.getLocationHref()); } // Clean History.expectedStateId = false; // Check if we are the same state if ( History.isLastSavedState(newState) ) { // There has been no change (just the page's hash has finally propagated) //History.debug('History.onPopState: no change', newState, History.savedStates); History.busy(false); return false; } // Store the State History.storeState(newState); History.saveState(newState); // Force update of the title History.setTitle(newState); // Fire Our Event History.Adapter.trigger(window,'statechange'); History.busy(false); // Return true return true; }; History.Adapter.bind(window,'popstate',History.onPopState); /** * History.pushState(data,title,url) * Add a new State to the history object, become it, and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.pushState = function(data,title,url,queue){ //History.debug('History.pushState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.pushState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.pushState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.pushState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End pushState closure return true; }; /** * History.replaceState(data,title,url) * Replace the State and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.replaceState = function(data,title,url,queue){ //History.debug('History.replaceState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.replaceState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.replaceState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.replaceState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End replaceState closure return true; }; } // !History.emulated.pushState // ==================================================================== // Initialise /** * Load the Store */ if ( sessionStorage ) { // Fetch try { History.store = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { History.store = {}; } // Normalize History.normalizeStore(); } else { // Default Load History.store = {}; History.normalizeStore(); } /** * Clear Intervals on exit to prevent memory leaks */ History.Adapter.bind(window,"unload",History.clearAllIntervals); /** * Create the initial State */ History.saveState(History.storeState(History.extractState(History.getLocationHref(),true))); /** * Bind for Saving Store */ if ( sessionStorage ) { // When the page is closed History.onUnload = function(){ // Prepare var currentStore, item, currentStoreString; // Fetch try { currentStore = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { currentStore = {}; } // Ensure currentStore.idToState = currentStore.idToState || {}; currentStore.urlToId = currentStore.urlToId || {}; currentStore.stateToId = currentStore.stateToId || {}; // Sync for ( item in History.idToState ) { if ( !History.idToState.hasOwnProperty(item) ) { continue; } currentStore.idToState[item] = History.idToState[item]; } for ( item in History.urlToId ) { if ( !History.urlToId.hasOwnProperty(item) ) { continue; } currentStore.urlToId[item] = History.urlToId[item]; } for ( item in History.stateToId ) { if ( !History.stateToId.hasOwnProperty(item) ) { continue; } currentStore.stateToId[item] = History.stateToId[item]; } // Update History.store = currentStore; History.normalizeStore(); // In Safari, going into Private Browsing mode causes the // Session Storage object to still exist but if you try and use // or set any property/function of it it throws the exception // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to // add something to storage that exceeded the quota." infinitely // every second. currentStoreString = JSON.stringify(currentStore); try { // Store sessionStorage.setItem('History.store', currentStoreString); } catch (e) { if (e.code === DOMException.QUOTA_EXCEEDED_ERR) { if (sessionStorage.length) { // Workaround for a bug seen on iPads. Sometimes the quota exceeded error comes up and simply // removing/resetting the storage can work. sessionStorage.removeItem('History.store'); sessionStorage.setItem('History.store', currentStoreString); } else { // Otherwise, we're probably private browsing in Safari, so we'll ignore the exception. } } else { throw e; } } }; // For Internet Explorer History.intervalList.push(setInterval(History.onUnload,History.options.storeInterval)); // For Other Browsers History.Adapter.bind(window,'beforeunload',History.onUnload); History.Adapter.bind(window,'unload',History.onUnload); // Both are enabled for consistency } // Non-Native pushState Implementation if ( !History.emulated.pushState ) { // Be aware, the following is only for native pushState implementations // If you are wanting to include something for all browsers // Then include it above this if block /** * Setup Safari Fix */ if ( History.bugs.safariPoll ) { History.intervalList.push(setInterval(History.safariStatePoll, History.options.safariPollInterval)); } /** * Ensure Cross Browser Compatibility */ if ( navigator.vendor === 'Apple Computer, Inc.' || (navigator.appCodeName||'') === 'Mozilla' ) { /** * Fix Safari HashChange Issue */ // Setup Alias History.Adapter.bind(window,'hashchange',function(){ History.Adapter.trigger(window,'popstate'); }); // Initialise Alias if ( History.getHash() ) { History.Adapter.onDomLoad(function(){ History.Adapter.trigger(window,'hashchange'); }); } } } // !History.emulated.pushState }; // History.initCore // Try to Initialise History if (!History.options || !History.options.delayInit) { History.init(); } })(window); ================================================ FILE: scripts/bundled-uncompressed/html5/native.history.js ================================================ /** * History.js Native Adapter * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ // Closure (function(window,undefined){ "use strict"; // Localise Globals var History = window.History = window.History||{}; // Check Existence if ( typeof History.Adapter !== 'undefined' ) { throw new Error('History.js Adapter has already been loaded...'); } // Add the Adapter History.Adapter = { /** * History.Adapter.handlers[uid][eventName] = Array */ handlers: {}, /** * History.Adapter._uid * The current element unique identifier */ _uid: 1, /** * History.Adapter.uid(element) * @param {Element} element * @return {String} uid */ uid: function(element){ return element._uid || (element._uid = History.Adapter._uid++); }, /** * History.Adapter.bind(el,event,callback) * @param {Element} element * @param {String} eventName - custom and standard events * @param {Function} callback * @return */ bind: function(element,eventName,callback){ // Prepare var uid = History.Adapter.uid(element); // Apply Listener History.Adapter.handlers[uid] = History.Adapter.handlers[uid] || {}; History.Adapter.handlers[uid][eventName] = History.Adapter.handlers[uid][eventName] || []; History.Adapter.handlers[uid][eventName].push(callback); // Bind Global Listener element['on'+eventName] = (function(element,eventName){ return function(event){ History.Adapter.trigger(element,eventName,event); }; })(element,eventName); }, /** * History.Adapter.trigger(el,event) * @param {Element} element * @param {String} eventName - custom and standard events * @param {Object} event - a object of event data * @return */ trigger: function(element,eventName,event){ // Prepare event = event || {}; var uid = History.Adapter.uid(element), i,n; // Apply Listener History.Adapter.handlers[uid] = History.Adapter.handlers[uid] || {}; History.Adapter.handlers[uid][eventName] = History.Adapter.handlers[uid][eventName] || []; // Fire Listeners for ( i=0,n=History.Adapter.handlers[uid][eventName].length; i * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window,undefined){ "use strict"; // ======================================================================== // Initialise // Localise Globals var console = window.console||undefined, // Prevent a JSLint complain document = window.document, // Make sure we are using the correct document navigator = window.navigator, // Make sure we are using the correct navigator sessionStorage = false, // sessionStorage setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, setInterval = window.setInterval, clearInterval = window.clearInterval, JSON = window.JSON, alert = window.alert, History = window.History = window.History||{}, // Public History Object history = window.history; // Old History Object try { sessionStorage = window.sessionStorage; // This will throw an exception in some browsers when cookies/localStorage are explicitly disabled (i.e. Chrome) sessionStorage.setItem('TEST', '1'); sessionStorage.removeItem('TEST'); } catch(e) { sessionStorage = false; } // MooTools Compatibility JSON.stringify = JSON.stringify||JSON.encode; JSON.parse = JSON.parse||JSON.decode; // Check Existence if ( typeof History.init !== 'undefined' ) { throw new Error('History.js Core has already been loaded...'); } // Initialise History History.init = function(options){ // Check Load Status of Adapter if ( typeof History.Adapter === 'undefined' ) { return false; } // Check Load Status of Core if ( typeof History.initCore !== 'undefined' ) { History.initCore(); } // Check Load Status of HTML4 Support if ( typeof History.initHtml4 !== 'undefined' ) { History.initHtml4(); } // Return true return true; }; // ======================================================================== // Initialise Core // Initialise Core History.initCore = function(options){ // Initialise if ( typeof History.initCore.initialized !== 'undefined' ) { // Already Loaded return false; } else { History.initCore.initialized = true; } // ==================================================================== // Options /** * History.options * Configurable options */ History.options = History.options||{}; /** * History.options.hashChangeInterval * How long should the interval be before hashchange checks */ History.options.hashChangeInterval = History.options.hashChangeInterval || 100; /** * History.options.safariPollInterval * How long should the interval be before safari poll checks */ History.options.safariPollInterval = History.options.safariPollInterval || 500; /** * History.options.doubleCheckInterval * How long should the interval be before we perform a double check */ History.options.doubleCheckInterval = History.options.doubleCheckInterval || 500; /** * History.options.disableSuid * Force History not to append suid */ History.options.disableSuid = History.options.disableSuid || false; /** * History.options.storeInterval * How long should we wait between store calls */ History.options.storeInterval = History.options.storeInterval || 1000; /** * History.options.busyDelay * How long should we wait between busy events */ History.options.busyDelay = History.options.busyDelay || 250; /** * History.options.debug * If true will enable debug messages to be logged */ History.options.debug = History.options.debug || false; /** * History.options.initialTitle * What is the title of the initial state */ History.options.initialTitle = History.options.initialTitle || document.title; /** * History.options.html4Mode * If true, will force HTMl4 mode (hashtags) */ History.options.html4Mode = History.options.html4Mode || false; /** * History.options.delayInit * Want to override default options and call init manually. */ History.options.delayInit = History.options.delayInit || false; // ==================================================================== // Interval record /** * History.intervalList * List of intervals set, to be cleared when document is unloaded. */ History.intervalList = []; /** * History.clearAllIntervals * Clears all setInterval instances. */ History.clearAllIntervals = function(){ var i, il = History.intervalList; if (typeof il !== "undefined" && il !== null) { for (i = 0; i < il.length; i++) { clearInterval(il[i]); } History.intervalList = null; } }; // ==================================================================== // Debug /** * History.debug(message,...) * Logs the passed arguments if debug enabled */ History.debug = function(){ if ( (History.options.debug||false) ) { History.log.apply(History,arguments); } }; /** * History.log(message,...) * Logs the passed arguments */ History.log = function(){ // Prepare var consoleExists = !(typeof console === 'undefined' || typeof console.log === 'undefined' || typeof console.log.apply === 'undefined'), textarea = document.getElementById('log'), message, i,n, args,arg ; // Write to Console if ( consoleExists ) { args = Array.prototype.slice.call(arguments); message = args.shift(); if ( typeof console.debug !== 'undefined' ) { console.debug.apply(console,[message,args]); } else { console.log.apply(console,[message,args]); } } else { message = ("\n"+arguments[0]+"\n"); } // Write to log for ( i=1,n=arguments.length; i * @author James Padolsey */ History.getInternetExplorerMajorVersion = function(){ var result = History.getInternetExplorerMajorVersion.cached = (typeof History.getInternetExplorerMajorVersion.cached !== 'undefined') ? History.getInternetExplorerMajorVersion.cached : (function(){ var v = 3, div = document.createElement('div'), all = div.getElementsByTagName('i'); while ( (div.innerHTML = '') && all[0] ) {} return (v > 4) ? v : false; })() ; return result; }; /** * History.isInternetExplorer() * Are we using Internet Explorer? * @return {boolean} * @license Public Domain * @author Benjamin Arthur Lupton */ History.isInternetExplorer = function(){ var result = History.isInternetExplorer.cached = (typeof History.isInternetExplorer.cached !== 'undefined') ? History.isInternetExplorer.cached : Boolean(History.getInternetExplorerMajorVersion()) ; return result; }; /** * History.emulated * Which features require emulating? */ if (History.options.html4Mode) { History.emulated = { pushState : true, hashChange: true }; } else { History.emulated = { pushState: !Boolean( window.history && window.history.pushState && window.history.replaceState && !( (/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i).test(navigator.userAgent) /* disable for versions of iOS before version 4.3 (8F190) */ || (/AppleWebKit\/5([0-2]|3[0-2])/i).test(navigator.userAgent) /* disable for the mercury iOS browser, or at least older versions of the webkit engine */ ) ), hashChange: Boolean( !(('onhashchange' in window) || ('onhashchange' in document)) || (History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8) ) }; } /** * History.enabled * Is History enabled? */ History.enabled = !History.emulated.pushState; /** * History.bugs * Which bugs are present */ History.bugs = { /** * Safari 5 and Safari iOS 4 fail to return to the correct state once a hash is replaced by a `replaceState` call * https://bugs.webkit.org/show_bug.cgi?id=56249 */ setHash: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * Safari 5 and Safari iOS 4 sometimes fail to apply the state change under busy conditions * https://bugs.webkit.org/show_bug.cgi?id=42940 */ safariPoll: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * MSIE 6 and 7 sometimes do not apply a hash even it was told to (requiring a second call to the apply function) */ ieDoubleCheck: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8), /** * MSIE 6 requires the entire hash to be encoded for the hashes to trigger the onHashChange event */ hashEscape: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 7) }; /** * History.isEmptyObject(obj) * Checks to see if the Object is Empty * @param {Object} obj * @return {boolean} */ History.isEmptyObject = function(obj) { for ( var name in obj ) { if ( obj.hasOwnProperty(name) ) { return false; } } return true; }; /** * History.cloneObject(obj) * Clones a object and eliminate all references to the original contexts * @param {Object} obj * @return {Object} */ History.cloneObject = function(obj) { var hash,newObj; if ( obj ) { hash = JSON.stringify(obj); newObj = JSON.parse(hash); } else { newObj = {}; } return newObj; }; // ==================================================================== // URL Helpers /** * History.getRootUrl() * Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com" * @return {String} rootUrl */ History.getRootUrl = function(){ // Create var rootUrl = document.location.protocol+'//'+(document.location.hostname||document.location.host); if ( document.location.port||false ) { rootUrl += ':'+document.location.port; } rootUrl += '/'; // Return return rootUrl; }; /** * History.getBaseHref() * Fetches the `href` attribute of the `` element if it exists * @return {String} baseHref */ History.getBaseHref = function(){ // Create var baseElements = document.getElementsByTagName('base'), baseElement = null, baseHref = ''; // Test for Base Element if ( baseElements.length === 1 ) { // Prepare for Base Element baseElement = baseElements[0]; baseHref = baseElement.href.replace(/[^\/]+$/,''); } // Adjust trailing slash baseHref = baseHref.replace(/\/+$/,''); if ( baseHref ) baseHref += '/'; // Return return baseHref; }; /** * History.getBaseUrl() * Fetches the baseHref or basePageUrl or rootUrl (whichever one exists first) * @return {String} baseUrl */ History.getBaseUrl = function(){ // Create var baseUrl = History.getBaseHref()||History.getBasePageUrl()||History.getRootUrl(); // Return return baseUrl; }; /** * History.getPageUrl() * Fetches the URL of the current page * @return {String} pageUrl */ History.getPageUrl = function(){ // Fetch var State = History.getState(false,false), stateUrl = (State||{}).url||History.getLocationHref(), pageUrl; // Create pageUrl = stateUrl.replace(/\/+$/,'').replace(/[^\/]+$/,function(part,index,string){ return (/\./).test(part) ? part : part+'/'; }); // Return return pageUrl; }; /** * History.getBasePageUrl() * Fetches the Url of the directory of the current page * @return {String} basePageUrl */ History.getBasePageUrl = function(){ // Create var basePageUrl = (History.getLocationHref()).replace(/[#\?].*/,'').replace(/[^\/]+$/,function(part,index,string){ return (/[^\/]$/).test(part) ? '' : part; }).replace(/\/+$/,'')+'/'; // Return return basePageUrl; }; /** * History.getFullUrl(url) * Ensures that we have an absolute URL and not a relative URL * @param {string} url * @param {Boolean} allowBaseHref * @return {string} fullUrl */ History.getFullUrl = function(url,allowBaseHref){ // Prepare var fullUrl = url, firstChar = url.substring(0,1); allowBaseHref = (typeof allowBaseHref === 'undefined') ? true : allowBaseHref; // Check if ( /[a-z]+\:\/\//.test(url) ) { // Full URL } else if ( firstChar === '/' ) { // Root URL fullUrl = History.getRootUrl()+url.replace(/^\/+/,''); } else if ( firstChar === '#' ) { // Anchor URL fullUrl = History.getPageUrl().replace(/#.*/,'')+url; } else if ( firstChar === '?' ) { // Query URL fullUrl = History.getPageUrl().replace(/[\?#].*/,'')+url; } else { // Relative URL if ( allowBaseHref ) { fullUrl = History.getBaseUrl()+url.replace(/^(\.\/)+/,''); } else { fullUrl = History.getBasePageUrl()+url.replace(/^(\.\/)+/,''); } // We have an if condition above as we do not want hashes // which are relative to the baseHref in our URLs // as if the baseHref changes, then all our bookmarks // would now point to different locations // whereas the basePageUrl will always stay the same } // Return return fullUrl.replace(/\#$/,''); }; /** * History.getShortUrl(url) * Ensures that we have a relative URL and not a absolute URL * @param {string} url * @return {string} url */ History.getShortUrl = function(url){ // Prepare var shortUrl = url, baseUrl = History.getBaseUrl(), rootUrl = History.getRootUrl(); // Trim baseUrl if ( History.emulated.pushState ) { // We are in a if statement as when pushState is not emulated // The actual url these short urls are relative to can change // So within the same session, we the url may end up somewhere different shortUrl = shortUrl.replace(baseUrl,''); } // Trim rootUrl shortUrl = shortUrl.replace(rootUrl,'/'); // Ensure we can still detect it as a state if ( History.isTraditionalAnchor(shortUrl) ) { shortUrl = './'+shortUrl; } // Clean It shortUrl = shortUrl.replace(/^(\.\/)+/g,'./').replace(/\#$/,''); // Return return shortUrl; }; /** * History.getLocationHref(document) * Returns a normalized version of document.location.href * accounting for browser inconsistencies, etc. * * This URL will be URI-encoded and will include the hash * * @param {object} document * @return {string} url */ History.getLocationHref = function(doc) { doc = doc || document; // most of the time, this will be true if (doc.URL === doc.location.href) return doc.location.href; // some versions of webkit URI-decode document.location.href // but they leave document.URL in an encoded state if (doc.location.href === decodeURIComponent(doc.URL)) return doc.URL; // FF 3.6 only updates document.URL when a page is reloaded // document.location.href is updated correctly if (doc.location.hash && decodeURIComponent(doc.location.href.replace(/^[^#]+/, "")) === doc.location.hash) return doc.location.href; if (doc.URL.indexOf('#') == -1 && doc.location.href.indexOf('#') != -1) return doc.location.href; return doc.URL || doc.location.href; }; // ==================================================================== // State Storage /** * History.store * The store for all session specific data */ History.store = {}; /** * History.idToState * 1-1: State ID to State Object */ History.idToState = History.idToState||{}; /** * History.stateToId * 1-1: State String to State ID */ History.stateToId = History.stateToId||{}; /** * History.urlToId * 1-1: State URL to State ID */ History.urlToId = History.urlToId||{}; /** * History.storedStates * Store the states in an array */ History.storedStates = History.storedStates||[]; /** * History.savedStates * Saved the states in an array */ History.savedStates = History.savedStates||[]; /** * History.noramlizeStore() * Noramlize the store by adding necessary values */ History.normalizeStore = function(){ History.store.idToState = History.store.idToState||{}; History.store.urlToId = History.store.urlToId||{}; History.store.stateToId = History.store.stateToId||{}; }; /** * History.getState() * Get an object containing the data, title and url of the current state * @param {Boolean} friendly * @param {Boolean} create * @return {Object} State */ History.getState = function(friendly,create){ // Prepare if ( typeof friendly === 'undefined' ) { friendly = true; } if ( typeof create === 'undefined' ) { create = true; } // Fetch var State = History.getLastSavedState(); // Create if ( !State && create ) { State = History.createStateObject(); } // Adjust if ( friendly ) { State = History.cloneObject(State); State.url = State.cleanUrl||State.url; } // Return return State; }; /** * History.getIdByState(State) * Gets a ID for a State * @param {State} newState * @return {String} id */ History.getIdByState = function(newState){ // Fetch ID var id = History.extractId(newState.url), str; if ( !id ) { // Find ID via State String str = History.getStateString(newState); if ( typeof History.stateToId[str] !== 'undefined' ) { id = History.stateToId[str]; } else if ( typeof History.store.stateToId[str] !== 'undefined' ) { id = History.store.stateToId[str]; } else { // Generate a new ID while ( true ) { id = (new Date()).getTime() + String(Math.random()).replace(/\D/g,''); if ( typeof History.idToState[id] === 'undefined' && typeof History.store.idToState[id] === 'undefined' ) { break; } } // Apply the new State to the ID History.stateToId[str] = id; History.idToState[id] = newState; } } // Return ID return id; }; /** * History.normalizeState(State) * Expands a State Object * @param {object} State * @return {object} */ History.normalizeState = function(oldState){ // Variables var newState, dataNotEmpty; // Prepare if ( !oldState || (typeof oldState !== 'object') ) { oldState = {}; } // Check if ( typeof oldState.normalized !== 'undefined' ) { return oldState; } // Adjust if ( !oldState.data || (typeof oldState.data !== 'object') ) { oldState.data = {}; } // ---------------------------------------------------------------- // Create newState = {}; newState.normalized = true; newState.title = oldState.title||''; newState.url = History.getFullUrl(oldState.url?oldState.url:(History.getLocationHref())); newState.hash = History.getShortUrl(newState.url); newState.data = History.cloneObject(oldState.data); // Fetch ID newState.id = History.getIdByState(newState); // ---------------------------------------------------------------- // Clean the URL newState.cleanUrl = newState.url.replace(/\??\&_suid.*/,''); newState.url = newState.cleanUrl; // Check to see if we have more than just a url dataNotEmpty = !History.isEmptyObject(newState.data); // Apply if ( (newState.title || dataNotEmpty) && History.options.disableSuid !== true ) { // Add ID to Hash newState.hash = History.getShortUrl(newState.url).replace(/\??\&_suid.*/,''); if ( !/\?/.test(newState.hash) ) { newState.hash += '?'; } newState.hash += '&_suid='+newState.id; } // Create the Hashed URL newState.hashedUrl = History.getFullUrl(newState.hash); // ---------------------------------------------------------------- // Update the URL if we have a duplicate if ( (History.emulated.pushState || History.bugs.safariPoll) && History.hasUrlDuplicate(newState) ) { newState.url = newState.hashedUrl; } // ---------------------------------------------------------------- // Return return newState; }; /** * History.createStateObject(data,title,url) * Creates a object based on the data, title and url state params * @param {object} data * @param {string} title * @param {string} url * @return {object} */ History.createStateObject = function(data,title,url){ // Hashify var State = { 'data': data, 'title': title, 'url': url }; // Expand the State State = History.normalizeState(State); // Return object return State; }; /** * History.getStateById(id) * Get a state by it's UID * @param {String} id */ History.getStateById = function(id){ // Prepare id = String(id); // Retrieve var State = History.idToState[id] || History.store.idToState[id] || undefined; // Return State return State; }; /** * Get a State's String * @param {State} passedState */ History.getStateString = function(passedState){ // Prepare var State, cleanedState, str; // Fetch State = History.normalizeState(passedState); // Clean cleanedState = { data: State.data, title: passedState.title, url: passedState.url }; // Fetch str = JSON.stringify(cleanedState); // Return return str; }; /** * Get a State's ID * @param {State} passedState * @return {String} id */ History.getStateId = function(passedState){ // Prepare var State, id; // Fetch State = History.normalizeState(passedState); // Fetch id = State.id; // Return return id; }; /** * History.getHashByState(State) * Creates a Hash for the State Object * @param {State} passedState * @return {String} hash */ History.getHashByState = function(passedState){ // Prepare var State, hash; // Fetch State = History.normalizeState(passedState); // Hash hash = State.hash; // Return return hash; }; /** * History.extractId(url_or_hash) * Get a State ID by it's URL or Hash * @param {string} url_or_hash * @return {string} id */ History.extractId = function ( url_or_hash ) { // Prepare var id,parts,url, tmp; // Extract // If the URL has a #, use the id from before the # if (url_or_hash.indexOf('#') != -1) { tmp = url_or_hash.split("#")[0]; } else { tmp = url_or_hash; } parts = /(.*)\&_suid=([0-9]+)$/.exec(tmp); url = parts ? (parts[1]||url_or_hash) : url_or_hash; id = parts ? String(parts[2]||'') : ''; // Return return id||false; }; /** * History.isTraditionalAnchor * Checks to see if the url is a traditional anchor or not * @param {String} url_or_hash * @return {Boolean} */ History.isTraditionalAnchor = function(url_or_hash){ // Check var isTraditional = !(/[\/\?\.]/.test(url_or_hash)); // Return return isTraditional; }; /** * History.extractState * Get a State by it's URL or Hash * @param {String} url_or_hash * @return {State|null} */ History.extractState = function(url_or_hash,create){ // Prepare var State = null, id, url; create = create||false; // Fetch SUID id = History.extractId(url_or_hash); if ( id ) { State = History.getStateById(id); } // Fetch SUID returned no State if ( !State ) { // Fetch URL url = History.getFullUrl(url_or_hash); // Check URL id = History.getIdByUrl(url)||false; if ( id ) { State = History.getStateById(id); } // Create State if ( !State && create && !History.isTraditionalAnchor(url_or_hash) ) { State = History.createStateObject(null,null,url); } } // Return return State; }; /** * History.getIdByUrl() * Get a State ID by a State URL */ History.getIdByUrl = function(url){ // Fetch var id = History.urlToId[url] || History.store.urlToId[url] || undefined; // Return return id; }; /** * History.getLastSavedState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastSavedState = function(){ return History.savedStates[History.savedStates.length-1]||undefined; }; /** * History.getLastStoredState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastStoredState = function(){ return History.storedStates[History.storedStates.length-1]||undefined; }; /** * History.hasUrlDuplicate * Checks if a Url will have a url conflict * @param {Object} newState * @return {Boolean} hasDuplicate */ History.hasUrlDuplicate = function(newState) { // Prepare var hasDuplicate = false, oldState; // Fetch oldState = History.extractState(newState.url); // Check hasDuplicate = oldState && oldState.id !== newState.id; // Return return hasDuplicate; }; /** * History.storeState * Store a State * @param {Object} newState * @return {Object} newState */ History.storeState = function(newState){ // Store the State History.urlToId[newState.url] = newState.id; // Push the State History.storedStates.push(History.cloneObject(newState)); // Return newState return newState; }; /** * History.isLastSavedState(newState) * Tests to see if the state is the last state * @param {Object} newState * @return {boolean} isLast */ History.isLastSavedState = function(newState){ // Prepare var isLast = false, newId, oldState, oldId; // Check if ( History.savedStates.length ) { newId = newState.id; oldState = History.getLastSavedState(); oldId = oldState.id; // Check isLast = (newId === oldId); } // Return return isLast; }; /** * History.saveState * Push a State * @param {Object} newState * @return {boolean} changed */ History.saveState = function(newState){ // Check Hash if ( History.isLastSavedState(newState) ) { return false; } // Push the State History.savedStates.push(History.cloneObject(newState)); // Return true return true; }; /** * History.getStateByIndex() * Gets a state by the index * @param {integer} index * @return {Object} */ History.getStateByIndex = function(index){ // Prepare var State = null; // Handle if ( typeof index === 'undefined' ) { // Get the last inserted State = History.savedStates[History.savedStates.length-1]; } else if ( index < 0 ) { // Get from the end State = History.savedStates[History.savedStates.length+index]; } else { // Get from the beginning State = History.savedStates[index]; } // Return State return State; }; /** * History.getCurrentIndex() * Gets the current index * @return (integer) */ History.getCurrentIndex = function(){ // Prepare var index = null; // No states saved if(History.savedStates.length < 1) { index = 0; } else { index = History.savedStates.length-1; } return index; }; // ==================================================================== // Hash Helpers /** * History.getHash() * @param {Location=} location * Gets the current document hash * Note: unlike location.hash, this is guaranteed to return the escaped hash in all browsers * @return {string} */ History.getHash = function(doc){ var url = History.getLocationHref(doc), hash; hash = History.getHashByUrl(url); return hash; }; /** * History.unescapeHash() * normalize and Unescape a Hash * @param {String} hash * @return {string} */ History.unescapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Unescape hash result = decodeURIComponent(result); // Return result return result; }; /** * History.normalizeHash() * normalize a hash across browsers * @return {string} */ History.normalizeHash = function(hash){ // Prepare var result = hash.replace(/[^#]*#/,'').replace(/#.*/, ''); // Return result return result; }; /** * History.setHash(hash) * Sets the document hash * @param {string} hash * @return {History} */ History.setHash = function(hash,queue){ // Prepare var State, pageUrl; // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.setHash: we must wait', arguments); History.pushQueue({ scope: History, callback: History.setHash, args: arguments, queue: queue }); return false; } // Log //History.debug('History.setHash: called',hash); // Make Busy + Continue History.busy(true); // Check if hash is a state State = History.extractState(hash,true); if ( State && !History.emulated.pushState ) { // Hash is a state so skip the setHash //History.debug('History.setHash: Hash is a state so skipping the hash set with a direct pushState call',arguments); // PushState History.pushState(State.data,State.title,State.url,false); } else if ( History.getHash() !== hash ) { // Hash is a proper hash, so apply it // Handle browser bugs if ( History.bugs.setHash ) { // Fix Safari Bug https://bugs.webkit.org/show_bug.cgi?id=56249 // Fetch the base page pageUrl = History.getPageUrl(); // Safari hash apply History.pushState(null,null,pageUrl+'#'+hash,false); } else { // Normal hash apply document.location.hash = hash; } } // Chain return History; }; /** * History.escape() * normalize and Escape a Hash * @return {string} */ History.escapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Escape hash result = window.encodeURIComponent(result); // IE6 Escape Bug if ( !History.bugs.hashEscape ) { // Restore common parts result = result .replace(/\%21/g,'!') .replace(/\%26/g,'&') .replace(/\%3D/g,'=') .replace(/\%3F/g,'?'); } // Return result return result; }; /** * History.getHashByUrl(url) * Extracts the Hash from a URL * @param {string} url * @return {string} url */ History.getHashByUrl = function(url){ // Extract the hash var hash = String(url) .replace(/([^#]*)#?([^#]*)#?(.*)/, '$2') ; // Unescape hash hash = History.unescapeHash(hash); // Return hash return hash; }; /** * History.setTitle(title) * Applies the title to the document * @param {State} newState * @return {Boolean} */ History.setTitle = function(newState){ // Prepare var title = newState.title, firstState; // Initial if ( !title ) { firstState = History.getStateByIndex(0); if ( firstState && firstState.url === newState.url ) { title = firstState.title||History.options.initialTitle; } } // Apply try { document.getElementsByTagName('title')[0].innerHTML = title.replace('<','<').replace('>','>').replace(' & ',' & '); } catch ( Exception ) { } document.title = title; // Chain return History; }; // ==================================================================== // Queueing /** * History.queues * The list of queues to use * First In, First Out */ History.queues = []; /** * History.busy(value) * @param {boolean} value [optional] * @return {boolean} busy */ History.busy = function(value){ // Apply if ( typeof value !== 'undefined' ) { //History.debug('History.busy: changing ['+(History.busy.flag||false)+'] to ['+(value||false)+']', History.queues.length); History.busy.flag = value; } // Default else if ( typeof History.busy.flag === 'undefined' ) { History.busy.flag = false; } // Queue if ( !History.busy.flag ) { // Execute the next item in the queue clearTimeout(History.busy.timeout); var fireNext = function(){ var i, queue, item; if ( History.busy.flag ) return; for ( i=History.queues.length-1; i >= 0; --i ) { queue = History.queues[i]; if ( queue.length === 0 ) continue; item = queue.shift(); History.fireQueueItem(item); History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } }; History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } // Return return History.busy.flag; }; /** * History.busy.flag */ History.busy.flag = false; /** * History.fireQueueItem(item) * Fire a Queue Item * @param {Object} item * @return {Mixed} result */ History.fireQueueItem = function(item){ return item.callback.apply(item.scope||History,item.args||[]); }; /** * History.pushQueue(callback,args) * Add an item to the queue * @param {Object} item [scope,callback,args,queue] */ History.pushQueue = function(item){ // Prepare the queue History.queues[item.queue||0] = History.queues[item.queue||0]||[]; // Add to the queue History.queues[item.queue||0].push(item); // Chain return History; }; /** * History.queue (item,queue), (func,queue), (func), (item) * Either firs the item now if not busy, or adds it to the queue */ History.queue = function(item,queue){ // Prepare if ( typeof item === 'function' ) { item = { callback: item }; } if ( typeof queue !== 'undefined' ) { item.queue = queue; } // Handle if ( History.busy() ) { History.pushQueue(item); } else { History.fireQueueItem(item); } // Chain return History; }; /** * History.clearQueue() * Clears the Queue */ History.clearQueue = function(){ History.busy.flag = false; History.queues = []; return History; }; // ==================================================================== // IE Bug Fix /** * History.stateChanged * States whether or not the state has changed since the last double check was initialised */ History.stateChanged = false; /** * History.doubleChecker * Contains the timeout used for the double checks */ History.doubleChecker = false; /** * History.doubleCheckComplete() * Complete a double check * @return {History} */ History.doubleCheckComplete = function(){ // Update History.stateChanged = true; // Clear History.doubleCheckClear(); // Chain return History; }; /** * History.doubleCheckClear() * Clear a double check * @return {History} */ History.doubleCheckClear = function(){ // Clear if ( History.doubleChecker ) { clearTimeout(History.doubleChecker); History.doubleChecker = false; } // Chain return History; }; /** * History.doubleCheck() * Create a double check * @return {History} */ History.doubleCheck = function(tryAgain){ // Reset History.stateChanged = false; History.doubleCheckClear(); // Fix IE6,IE7 bug where calling history.back or history.forward does not actually change the hash (whereas doing it manually does) // Fix Safari 5 bug where sometimes the state does not change: https://bugs.webkit.org/show_bug.cgi?id=42940 if ( History.bugs.ieDoubleCheck ) { // Apply Check History.doubleChecker = setTimeout( function(){ History.doubleCheckClear(); if ( !History.stateChanged ) { //History.debug('History.doubleCheck: State has not yet changed, trying again', arguments); // Re-Attempt tryAgain(); } return true; }, History.options.doubleCheckInterval ); } // Chain return History; }; // ==================================================================== // Safari Bug Fix /** * History.safariStatePoll() * Poll the current state * @return {History} */ History.safariStatePoll = function(){ // Poll the URL // Get the Last State which has the new URL var urlState = History.extractState(History.getLocationHref()), newState; // Check for a difference if ( !History.isLastSavedState(urlState) ) { newState = urlState; } else { return; } // Check if we have a state with that url // If not create it if ( !newState ) { //History.debug('History.safariStatePoll: new'); newState = History.createStateObject(); } // Apply the New State //History.debug('History.safariStatePoll: trigger'); History.Adapter.trigger(window,'popstate'); // Chain return History; }; // ==================================================================== // State Aliases /** * History.back(queue) * Send the browser history back one item * @param {Integer} queue [optional] */ History.back = function(queue){ //History.debug('History.back: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.back: we must wait', arguments); History.pushQueue({ scope: History, callback: History.back, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.back(false); }); // Go back history.go(-1); // End back closure return true; }; /** * History.forward(queue) * Send the browser history forward one item * @param {Integer} queue [optional] */ History.forward = function(queue){ //History.debug('History.forward: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.forward: we must wait', arguments); History.pushQueue({ scope: History, callback: History.forward, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.forward(false); }); // Go forward history.go(1); // End forward closure return true; }; /** * History.go(index,queue) * Send the browser history back or forward index times * @param {Integer} queue [optional] */ History.go = function(index,queue){ //History.debug('History.go: called', arguments); // Prepare var i; // Handle if ( index > 0 ) { // Forward for ( i=1; i<=index; ++i ) { History.forward(queue); } } else if ( index < 0 ) { // Backward for ( i=-1; i>=index; --i ) { History.back(queue); } } else { throw new Error('History.go: History.go requires a positive or negative integer passed.'); } // Chain return History; }; // ==================================================================== // HTML5 State Support // Non-Native pushState Implementation if ( History.emulated.pushState ) { /* * Provide Skeleton for HTML4 Browsers */ // Prepare var emptyFunction = function(){}; History.pushState = History.pushState||emptyFunction; History.replaceState = History.replaceState||emptyFunction; } // History.emulated.pushState // Native pushState Implementation else { /* * Use native HTML5 History API Implementation */ /** * History.onPopState(event,extra) * Refresh the Current State */ History.onPopState = function(event,extra){ // Prepare var stateId = false, newState = false, currentHash, currentState; // Reset the double check History.doubleCheckComplete(); // Check for a Hash, and handle apporiatly currentHash = History.getHash(); if ( currentHash ) { // Expand Hash currentState = History.extractState(currentHash||History.getLocationHref(),true); if ( currentState ) { // We were able to parse it, it must be a State! // Let's forward to replaceState //History.debug('History.onPopState: state anchor', currentHash, currentState); History.replaceState(currentState.data, currentState.title, currentState.url, false); } else { // Traditional Anchor //History.debug('History.onPopState: traditional anchor', currentHash); History.Adapter.trigger(window,'anchorchange'); History.busy(false); } // We don't care for hashes History.expectedStateId = false; return false; } // Ensure stateId = History.Adapter.extractEventData('state',event,extra) || false; // Fetch State if ( stateId ) { // Vanilla: Back/forward button was used newState = History.getStateById(stateId); } else if ( History.expectedStateId ) { // Vanilla: A new state was pushed, and popstate was called manually newState = History.getStateById(History.expectedStateId); } else { // Initial State newState = History.extractState(History.getLocationHref()); } // The State did not exist in our store if ( !newState ) { // Regenerate the State newState = History.createStateObject(null,null,History.getLocationHref()); } // Clean History.expectedStateId = false; // Check if we are the same state if ( History.isLastSavedState(newState) ) { // There has been no change (just the page's hash has finally propagated) //History.debug('History.onPopState: no change', newState, History.savedStates); History.busy(false); return false; } // Store the State History.storeState(newState); History.saveState(newState); // Force update of the title History.setTitle(newState); // Fire Our Event History.Adapter.trigger(window,'statechange'); History.busy(false); // Return true return true; }; History.Adapter.bind(window,'popstate',History.onPopState); /** * History.pushState(data,title,url) * Add a new State to the history object, become it, and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.pushState = function(data,title,url,queue){ //History.debug('History.pushState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.pushState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.pushState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.pushState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End pushState closure return true; }; /** * History.replaceState(data,title,url) * Replace the State and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.replaceState = function(data,title,url,queue){ //History.debug('History.replaceState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.replaceState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.replaceState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.replaceState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End replaceState closure return true; }; } // !History.emulated.pushState // ==================================================================== // Initialise /** * Load the Store */ if ( sessionStorage ) { // Fetch try { History.store = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { History.store = {}; } // Normalize History.normalizeStore(); } else { // Default Load History.store = {}; History.normalizeStore(); } /** * Clear Intervals on exit to prevent memory leaks */ History.Adapter.bind(window,"unload",History.clearAllIntervals); /** * Create the initial State */ History.saveState(History.storeState(History.extractState(History.getLocationHref(),true))); /** * Bind for Saving Store */ if ( sessionStorage ) { // When the page is closed History.onUnload = function(){ // Prepare var currentStore, item, currentStoreString; // Fetch try { currentStore = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { currentStore = {}; } // Ensure currentStore.idToState = currentStore.idToState || {}; currentStore.urlToId = currentStore.urlToId || {}; currentStore.stateToId = currentStore.stateToId || {}; // Sync for ( item in History.idToState ) { if ( !History.idToState.hasOwnProperty(item) ) { continue; } currentStore.idToState[item] = History.idToState[item]; } for ( item in History.urlToId ) { if ( !History.urlToId.hasOwnProperty(item) ) { continue; } currentStore.urlToId[item] = History.urlToId[item]; } for ( item in History.stateToId ) { if ( !History.stateToId.hasOwnProperty(item) ) { continue; } currentStore.stateToId[item] = History.stateToId[item]; } // Update History.store = currentStore; History.normalizeStore(); // In Safari, going into Private Browsing mode causes the // Session Storage object to still exist but if you try and use // or set any property/function of it it throws the exception // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to // add something to storage that exceeded the quota." infinitely // every second. currentStoreString = JSON.stringify(currentStore); try { // Store sessionStorage.setItem('History.store', currentStoreString); } catch (e) { if (e.code === DOMException.QUOTA_EXCEEDED_ERR) { if (sessionStorage.length) { // Workaround for a bug seen on iPads. Sometimes the quota exceeded error comes up and simply // removing/resetting the storage can work. sessionStorage.removeItem('History.store'); sessionStorage.setItem('History.store', currentStoreString); } else { // Otherwise, we're probably private browsing in Safari, so we'll ignore the exception. } } else { throw e; } } }; // For Internet Explorer History.intervalList.push(setInterval(History.onUnload,History.options.storeInterval)); // For Other Browsers History.Adapter.bind(window,'beforeunload',History.onUnload); History.Adapter.bind(window,'unload',History.onUnload); // Both are enabled for consistency } // Non-Native pushState Implementation if ( !History.emulated.pushState ) { // Be aware, the following is only for native pushState implementations // If you are wanting to include something for all browsers // Then include it above this if block /** * Setup Safari Fix */ if ( History.bugs.safariPoll ) { History.intervalList.push(setInterval(History.safariStatePoll, History.options.safariPollInterval)); } /** * Ensure Cross Browser Compatibility */ if ( navigator.vendor === 'Apple Computer, Inc.' || (navigator.appCodeName||'') === 'Mozilla' ) { /** * Fix Safari HashChange Issue */ // Setup Alias History.Adapter.bind(window,'hashchange',function(){ History.Adapter.trigger(window,'popstate'); }); // Initialise Alias if ( History.getHash() ) { History.Adapter.onDomLoad(function(){ History.Adapter.trigger(window,'hashchange'); }); } } } // !History.emulated.pushState }; // History.initCore // Try to Initialise History if (!History.options || !History.options.delayInit) { History.init(); } })(window); ================================================ FILE: scripts/bundled-uncompressed/html5/right.history.js ================================================ /** * History.js RightJS Adapter * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ // Closure (function(window,undefined){ "use strict"; // Localise Globals var History = window.History = window.History||{}, document = window.document, RightJS = window.RightJS, $ = RightJS.$; // Check Existence if ( typeof History.Adapter !== 'undefined' ) { throw new Error('History.js Adapter has already been loaded...'); } // Add the Adapter History.Adapter = { /** * History.Adapter.bind(el,event,callback) * @param {Element|Selector} el * @param {String} event - custom and standard events * @param {Function} callback * @return */ bind: function(el,event,callback){ $(el).on(event,callback); }, /** * History.Adapter.trigger(el,event) * @param {Element|Selector} el * @param {String} event - custom and standard events * @param {Object} extraEventData - a object of extra event data * @return */ trigger: function(el,event,extraEventData){ $(el).fire(event,extraEventData); }, /** * History.Adapter.extractEventData(key,event,extra) * @param {String} key - key for the event data to extract * @param {String} event - custom and standard events * @return {mixed} */ extractEventData: function(key,event){ // Right.js Native // Right.js Custom var result = (event && event._ && event._[key]) || undefined; // Return return result; }, /** * History.Adapter.onDomLoad(callback) * @param {Function} callback * @return */ onDomLoad: function(callback) { $(document).onReady(callback); } }; // Try and Initialise History if ( typeof History.init !== 'undefined' ) { History.init(); } })(window); /** * History.js Core * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window,undefined){ "use strict"; // ======================================================================== // Initialise // Localise Globals var console = window.console||undefined, // Prevent a JSLint complain document = window.document, // Make sure we are using the correct document navigator = window.navigator, // Make sure we are using the correct navigator sessionStorage = false, // sessionStorage setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, setInterval = window.setInterval, clearInterval = window.clearInterval, JSON = window.JSON, alert = window.alert, History = window.History = window.History||{}, // Public History Object history = window.history; // Old History Object try { sessionStorage = window.sessionStorage; // This will throw an exception in some browsers when cookies/localStorage are explicitly disabled (i.e. Chrome) sessionStorage.setItem('TEST', '1'); sessionStorage.removeItem('TEST'); } catch(e) { sessionStorage = false; } // MooTools Compatibility JSON.stringify = JSON.stringify||JSON.encode; JSON.parse = JSON.parse||JSON.decode; // Check Existence if ( typeof History.init !== 'undefined' ) { throw new Error('History.js Core has already been loaded...'); } // Initialise History History.init = function(options){ // Check Load Status of Adapter if ( typeof History.Adapter === 'undefined' ) { return false; } // Check Load Status of Core if ( typeof History.initCore !== 'undefined' ) { History.initCore(); } // Check Load Status of HTML4 Support if ( typeof History.initHtml4 !== 'undefined' ) { History.initHtml4(); } // Return true return true; }; // ======================================================================== // Initialise Core // Initialise Core History.initCore = function(options){ // Initialise if ( typeof History.initCore.initialized !== 'undefined' ) { // Already Loaded return false; } else { History.initCore.initialized = true; } // ==================================================================== // Options /** * History.options * Configurable options */ History.options = History.options||{}; /** * History.options.hashChangeInterval * How long should the interval be before hashchange checks */ History.options.hashChangeInterval = History.options.hashChangeInterval || 100; /** * History.options.safariPollInterval * How long should the interval be before safari poll checks */ History.options.safariPollInterval = History.options.safariPollInterval || 500; /** * History.options.doubleCheckInterval * How long should the interval be before we perform a double check */ History.options.doubleCheckInterval = History.options.doubleCheckInterval || 500; /** * History.options.disableSuid * Force History not to append suid */ History.options.disableSuid = History.options.disableSuid || false; /** * History.options.storeInterval * How long should we wait between store calls */ History.options.storeInterval = History.options.storeInterval || 1000; /** * History.options.busyDelay * How long should we wait between busy events */ History.options.busyDelay = History.options.busyDelay || 250; /** * History.options.debug * If true will enable debug messages to be logged */ History.options.debug = History.options.debug || false; /** * History.options.initialTitle * What is the title of the initial state */ History.options.initialTitle = History.options.initialTitle || document.title; /** * History.options.html4Mode * If true, will force HTMl4 mode (hashtags) */ History.options.html4Mode = History.options.html4Mode || false; /** * History.options.delayInit * Want to override default options and call init manually. */ History.options.delayInit = History.options.delayInit || false; // ==================================================================== // Interval record /** * History.intervalList * List of intervals set, to be cleared when document is unloaded. */ History.intervalList = []; /** * History.clearAllIntervals * Clears all setInterval instances. */ History.clearAllIntervals = function(){ var i, il = History.intervalList; if (typeof il !== "undefined" && il !== null) { for (i = 0; i < il.length; i++) { clearInterval(il[i]); } History.intervalList = null; } }; // ==================================================================== // Debug /** * History.debug(message,...) * Logs the passed arguments if debug enabled */ History.debug = function(){ if ( (History.options.debug||false) ) { History.log.apply(History,arguments); } }; /** * History.log(message,...) * Logs the passed arguments */ History.log = function(){ // Prepare var consoleExists = !(typeof console === 'undefined' || typeof console.log === 'undefined' || typeof console.log.apply === 'undefined'), textarea = document.getElementById('log'), message, i,n, args,arg ; // Write to Console if ( consoleExists ) { args = Array.prototype.slice.call(arguments); message = args.shift(); if ( typeof console.debug !== 'undefined' ) { console.debug.apply(console,[message,args]); } else { console.log.apply(console,[message,args]); } } else { message = ("\n"+arguments[0]+"\n"); } // Write to log for ( i=1,n=arguments.length; i * @author James Padolsey */ History.getInternetExplorerMajorVersion = function(){ var result = History.getInternetExplorerMajorVersion.cached = (typeof History.getInternetExplorerMajorVersion.cached !== 'undefined') ? History.getInternetExplorerMajorVersion.cached : (function(){ var v = 3, div = document.createElement('div'), all = div.getElementsByTagName('i'); while ( (div.innerHTML = '') && all[0] ) {} return (v > 4) ? v : false; })() ; return result; }; /** * History.isInternetExplorer() * Are we using Internet Explorer? * @return {boolean} * @license Public Domain * @author Benjamin Arthur Lupton */ History.isInternetExplorer = function(){ var result = History.isInternetExplorer.cached = (typeof History.isInternetExplorer.cached !== 'undefined') ? History.isInternetExplorer.cached : Boolean(History.getInternetExplorerMajorVersion()) ; return result; }; /** * History.emulated * Which features require emulating? */ if (History.options.html4Mode) { History.emulated = { pushState : true, hashChange: true }; } else { History.emulated = { pushState: !Boolean( window.history && window.history.pushState && window.history.replaceState && !( (/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i).test(navigator.userAgent) /* disable for versions of iOS before version 4.3 (8F190) */ || (/AppleWebKit\/5([0-2]|3[0-2])/i).test(navigator.userAgent) /* disable for the mercury iOS browser, or at least older versions of the webkit engine */ ) ), hashChange: Boolean( !(('onhashchange' in window) || ('onhashchange' in document)) || (History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8) ) }; } /** * History.enabled * Is History enabled? */ History.enabled = !History.emulated.pushState; /** * History.bugs * Which bugs are present */ History.bugs = { /** * Safari 5 and Safari iOS 4 fail to return to the correct state once a hash is replaced by a `replaceState` call * https://bugs.webkit.org/show_bug.cgi?id=56249 */ setHash: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * Safari 5 and Safari iOS 4 sometimes fail to apply the state change under busy conditions * https://bugs.webkit.org/show_bug.cgi?id=42940 */ safariPoll: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * MSIE 6 and 7 sometimes do not apply a hash even it was told to (requiring a second call to the apply function) */ ieDoubleCheck: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8), /** * MSIE 6 requires the entire hash to be encoded for the hashes to trigger the onHashChange event */ hashEscape: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 7) }; /** * History.isEmptyObject(obj) * Checks to see if the Object is Empty * @param {Object} obj * @return {boolean} */ History.isEmptyObject = function(obj) { for ( var name in obj ) { if ( obj.hasOwnProperty(name) ) { return false; } } return true; }; /** * History.cloneObject(obj) * Clones a object and eliminate all references to the original contexts * @param {Object} obj * @return {Object} */ History.cloneObject = function(obj) { var hash,newObj; if ( obj ) { hash = JSON.stringify(obj); newObj = JSON.parse(hash); } else { newObj = {}; } return newObj; }; // ==================================================================== // URL Helpers /** * History.getRootUrl() * Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com" * @return {String} rootUrl */ History.getRootUrl = function(){ // Create var rootUrl = document.location.protocol+'//'+(document.location.hostname||document.location.host); if ( document.location.port||false ) { rootUrl += ':'+document.location.port; } rootUrl += '/'; // Return return rootUrl; }; /** * History.getBaseHref() * Fetches the `href` attribute of the `` element if it exists * @return {String} baseHref */ History.getBaseHref = function(){ // Create var baseElements = document.getElementsByTagName('base'), baseElement = null, baseHref = ''; // Test for Base Element if ( baseElements.length === 1 ) { // Prepare for Base Element baseElement = baseElements[0]; baseHref = baseElement.href.replace(/[^\/]+$/,''); } // Adjust trailing slash baseHref = baseHref.replace(/\/+$/,''); if ( baseHref ) baseHref += '/'; // Return return baseHref; }; /** * History.getBaseUrl() * Fetches the baseHref or basePageUrl or rootUrl (whichever one exists first) * @return {String} baseUrl */ History.getBaseUrl = function(){ // Create var baseUrl = History.getBaseHref()||History.getBasePageUrl()||History.getRootUrl(); // Return return baseUrl; }; /** * History.getPageUrl() * Fetches the URL of the current page * @return {String} pageUrl */ History.getPageUrl = function(){ // Fetch var State = History.getState(false,false), stateUrl = (State||{}).url||History.getLocationHref(), pageUrl; // Create pageUrl = stateUrl.replace(/\/+$/,'').replace(/[^\/]+$/,function(part,index,string){ return (/\./).test(part) ? part : part+'/'; }); // Return return pageUrl; }; /** * History.getBasePageUrl() * Fetches the Url of the directory of the current page * @return {String} basePageUrl */ History.getBasePageUrl = function(){ // Create var basePageUrl = (History.getLocationHref()).replace(/[#\?].*/,'').replace(/[^\/]+$/,function(part,index,string){ return (/[^\/]$/).test(part) ? '' : part; }).replace(/\/+$/,'')+'/'; // Return return basePageUrl; }; /** * History.getFullUrl(url) * Ensures that we have an absolute URL and not a relative URL * @param {string} url * @param {Boolean} allowBaseHref * @return {string} fullUrl */ History.getFullUrl = function(url,allowBaseHref){ // Prepare var fullUrl = url, firstChar = url.substring(0,1); allowBaseHref = (typeof allowBaseHref === 'undefined') ? true : allowBaseHref; // Check if ( /[a-z]+\:\/\//.test(url) ) { // Full URL } else if ( firstChar === '/' ) { // Root URL fullUrl = History.getRootUrl()+url.replace(/^\/+/,''); } else if ( firstChar === '#' ) { // Anchor URL fullUrl = History.getPageUrl().replace(/#.*/,'')+url; } else if ( firstChar === '?' ) { // Query URL fullUrl = History.getPageUrl().replace(/[\?#].*/,'')+url; } else { // Relative URL if ( allowBaseHref ) { fullUrl = History.getBaseUrl()+url.replace(/^(\.\/)+/,''); } else { fullUrl = History.getBasePageUrl()+url.replace(/^(\.\/)+/,''); } // We have an if condition above as we do not want hashes // which are relative to the baseHref in our URLs // as if the baseHref changes, then all our bookmarks // would now point to different locations // whereas the basePageUrl will always stay the same } // Return return fullUrl.replace(/\#$/,''); }; /** * History.getShortUrl(url) * Ensures that we have a relative URL and not a absolute URL * @param {string} url * @return {string} url */ History.getShortUrl = function(url){ // Prepare var shortUrl = url, baseUrl = History.getBaseUrl(), rootUrl = History.getRootUrl(); // Trim baseUrl if ( History.emulated.pushState ) { // We are in a if statement as when pushState is not emulated // The actual url these short urls are relative to can change // So within the same session, we the url may end up somewhere different shortUrl = shortUrl.replace(baseUrl,''); } // Trim rootUrl shortUrl = shortUrl.replace(rootUrl,'/'); // Ensure we can still detect it as a state if ( History.isTraditionalAnchor(shortUrl) ) { shortUrl = './'+shortUrl; } // Clean It shortUrl = shortUrl.replace(/^(\.\/)+/g,'./').replace(/\#$/,''); // Return return shortUrl; }; /** * History.getLocationHref(document) * Returns a normalized version of document.location.href * accounting for browser inconsistencies, etc. * * This URL will be URI-encoded and will include the hash * * @param {object} document * @return {string} url */ History.getLocationHref = function(doc) { doc = doc || document; // most of the time, this will be true if (doc.URL === doc.location.href) return doc.location.href; // some versions of webkit URI-decode document.location.href // but they leave document.URL in an encoded state if (doc.location.href === decodeURIComponent(doc.URL)) return doc.URL; // FF 3.6 only updates document.URL when a page is reloaded // document.location.href is updated correctly if (doc.location.hash && decodeURIComponent(doc.location.href.replace(/^[^#]+/, "")) === doc.location.hash) return doc.location.href; if (doc.URL.indexOf('#') == -1 && doc.location.href.indexOf('#') != -1) return doc.location.href; return doc.URL || doc.location.href; }; // ==================================================================== // State Storage /** * History.store * The store for all session specific data */ History.store = {}; /** * History.idToState * 1-1: State ID to State Object */ History.idToState = History.idToState||{}; /** * History.stateToId * 1-1: State String to State ID */ History.stateToId = History.stateToId||{}; /** * History.urlToId * 1-1: State URL to State ID */ History.urlToId = History.urlToId||{}; /** * History.storedStates * Store the states in an array */ History.storedStates = History.storedStates||[]; /** * History.savedStates * Saved the states in an array */ History.savedStates = History.savedStates||[]; /** * History.noramlizeStore() * Noramlize the store by adding necessary values */ History.normalizeStore = function(){ History.store.idToState = History.store.idToState||{}; History.store.urlToId = History.store.urlToId||{}; History.store.stateToId = History.store.stateToId||{}; }; /** * History.getState() * Get an object containing the data, title and url of the current state * @param {Boolean} friendly * @param {Boolean} create * @return {Object} State */ History.getState = function(friendly,create){ // Prepare if ( typeof friendly === 'undefined' ) { friendly = true; } if ( typeof create === 'undefined' ) { create = true; } // Fetch var State = History.getLastSavedState(); // Create if ( !State && create ) { State = History.createStateObject(); } // Adjust if ( friendly ) { State = History.cloneObject(State); State.url = State.cleanUrl||State.url; } // Return return State; }; /** * History.getIdByState(State) * Gets a ID for a State * @param {State} newState * @return {String} id */ History.getIdByState = function(newState){ // Fetch ID var id = History.extractId(newState.url), str; if ( !id ) { // Find ID via State String str = History.getStateString(newState); if ( typeof History.stateToId[str] !== 'undefined' ) { id = History.stateToId[str]; } else if ( typeof History.store.stateToId[str] !== 'undefined' ) { id = History.store.stateToId[str]; } else { // Generate a new ID while ( true ) { id = (new Date()).getTime() + String(Math.random()).replace(/\D/g,''); if ( typeof History.idToState[id] === 'undefined' && typeof History.store.idToState[id] === 'undefined' ) { break; } } // Apply the new State to the ID History.stateToId[str] = id; History.idToState[id] = newState; } } // Return ID return id; }; /** * History.normalizeState(State) * Expands a State Object * @param {object} State * @return {object} */ History.normalizeState = function(oldState){ // Variables var newState, dataNotEmpty; // Prepare if ( !oldState || (typeof oldState !== 'object') ) { oldState = {}; } // Check if ( typeof oldState.normalized !== 'undefined' ) { return oldState; } // Adjust if ( !oldState.data || (typeof oldState.data !== 'object') ) { oldState.data = {}; } // ---------------------------------------------------------------- // Create newState = {}; newState.normalized = true; newState.title = oldState.title||''; newState.url = History.getFullUrl(oldState.url?oldState.url:(History.getLocationHref())); newState.hash = History.getShortUrl(newState.url); newState.data = History.cloneObject(oldState.data); // Fetch ID newState.id = History.getIdByState(newState); // ---------------------------------------------------------------- // Clean the URL newState.cleanUrl = newState.url.replace(/\??\&_suid.*/,''); newState.url = newState.cleanUrl; // Check to see if we have more than just a url dataNotEmpty = !History.isEmptyObject(newState.data); // Apply if ( (newState.title || dataNotEmpty) && History.options.disableSuid !== true ) { // Add ID to Hash newState.hash = History.getShortUrl(newState.url).replace(/\??\&_suid.*/,''); if ( !/\?/.test(newState.hash) ) { newState.hash += '?'; } newState.hash += '&_suid='+newState.id; } // Create the Hashed URL newState.hashedUrl = History.getFullUrl(newState.hash); // ---------------------------------------------------------------- // Update the URL if we have a duplicate if ( (History.emulated.pushState || History.bugs.safariPoll) && History.hasUrlDuplicate(newState) ) { newState.url = newState.hashedUrl; } // ---------------------------------------------------------------- // Return return newState; }; /** * History.createStateObject(data,title,url) * Creates a object based on the data, title and url state params * @param {object} data * @param {string} title * @param {string} url * @return {object} */ History.createStateObject = function(data,title,url){ // Hashify var State = { 'data': data, 'title': title, 'url': url }; // Expand the State State = History.normalizeState(State); // Return object return State; }; /** * History.getStateById(id) * Get a state by it's UID * @param {String} id */ History.getStateById = function(id){ // Prepare id = String(id); // Retrieve var State = History.idToState[id] || History.store.idToState[id] || undefined; // Return State return State; }; /** * Get a State's String * @param {State} passedState */ History.getStateString = function(passedState){ // Prepare var State, cleanedState, str; // Fetch State = History.normalizeState(passedState); // Clean cleanedState = { data: State.data, title: passedState.title, url: passedState.url }; // Fetch str = JSON.stringify(cleanedState); // Return return str; }; /** * Get a State's ID * @param {State} passedState * @return {String} id */ History.getStateId = function(passedState){ // Prepare var State, id; // Fetch State = History.normalizeState(passedState); // Fetch id = State.id; // Return return id; }; /** * History.getHashByState(State) * Creates a Hash for the State Object * @param {State} passedState * @return {String} hash */ History.getHashByState = function(passedState){ // Prepare var State, hash; // Fetch State = History.normalizeState(passedState); // Hash hash = State.hash; // Return return hash; }; /** * History.extractId(url_or_hash) * Get a State ID by it's URL or Hash * @param {string} url_or_hash * @return {string} id */ History.extractId = function ( url_or_hash ) { // Prepare var id,parts,url, tmp; // Extract // If the URL has a #, use the id from before the # if (url_or_hash.indexOf('#') != -1) { tmp = url_or_hash.split("#")[0]; } else { tmp = url_or_hash; } parts = /(.*)\&_suid=([0-9]+)$/.exec(tmp); url = parts ? (parts[1]||url_or_hash) : url_or_hash; id = parts ? String(parts[2]||'') : ''; // Return return id||false; }; /** * History.isTraditionalAnchor * Checks to see if the url is a traditional anchor or not * @param {String} url_or_hash * @return {Boolean} */ History.isTraditionalAnchor = function(url_or_hash){ // Check var isTraditional = !(/[\/\?\.]/.test(url_or_hash)); // Return return isTraditional; }; /** * History.extractState * Get a State by it's URL or Hash * @param {String} url_or_hash * @return {State|null} */ History.extractState = function(url_or_hash,create){ // Prepare var State = null, id, url; create = create||false; // Fetch SUID id = History.extractId(url_or_hash); if ( id ) { State = History.getStateById(id); } // Fetch SUID returned no State if ( !State ) { // Fetch URL url = History.getFullUrl(url_or_hash); // Check URL id = History.getIdByUrl(url)||false; if ( id ) { State = History.getStateById(id); } // Create State if ( !State && create && !History.isTraditionalAnchor(url_or_hash) ) { State = History.createStateObject(null,null,url); } } // Return return State; }; /** * History.getIdByUrl() * Get a State ID by a State URL */ History.getIdByUrl = function(url){ // Fetch var id = History.urlToId[url] || History.store.urlToId[url] || undefined; // Return return id; }; /** * History.getLastSavedState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastSavedState = function(){ return History.savedStates[History.savedStates.length-1]||undefined; }; /** * History.getLastStoredState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastStoredState = function(){ return History.storedStates[History.storedStates.length-1]||undefined; }; /** * History.hasUrlDuplicate * Checks if a Url will have a url conflict * @param {Object} newState * @return {Boolean} hasDuplicate */ History.hasUrlDuplicate = function(newState) { // Prepare var hasDuplicate = false, oldState; // Fetch oldState = History.extractState(newState.url); // Check hasDuplicate = oldState && oldState.id !== newState.id; // Return return hasDuplicate; }; /** * History.storeState * Store a State * @param {Object} newState * @return {Object} newState */ History.storeState = function(newState){ // Store the State History.urlToId[newState.url] = newState.id; // Push the State History.storedStates.push(History.cloneObject(newState)); // Return newState return newState; }; /** * History.isLastSavedState(newState) * Tests to see if the state is the last state * @param {Object} newState * @return {boolean} isLast */ History.isLastSavedState = function(newState){ // Prepare var isLast = false, newId, oldState, oldId; // Check if ( History.savedStates.length ) { newId = newState.id; oldState = History.getLastSavedState(); oldId = oldState.id; // Check isLast = (newId === oldId); } // Return return isLast; }; /** * History.saveState * Push a State * @param {Object} newState * @return {boolean} changed */ History.saveState = function(newState){ // Check Hash if ( History.isLastSavedState(newState) ) { return false; } // Push the State History.savedStates.push(History.cloneObject(newState)); // Return true return true; }; /** * History.getStateByIndex() * Gets a state by the index * @param {integer} index * @return {Object} */ History.getStateByIndex = function(index){ // Prepare var State = null; // Handle if ( typeof index === 'undefined' ) { // Get the last inserted State = History.savedStates[History.savedStates.length-1]; } else if ( index < 0 ) { // Get from the end State = History.savedStates[History.savedStates.length+index]; } else { // Get from the beginning State = History.savedStates[index]; } // Return State return State; }; /** * History.getCurrentIndex() * Gets the current index * @return (integer) */ History.getCurrentIndex = function(){ // Prepare var index = null; // No states saved if(History.savedStates.length < 1) { index = 0; } else { index = History.savedStates.length-1; } return index; }; // ==================================================================== // Hash Helpers /** * History.getHash() * @param {Location=} location * Gets the current document hash * Note: unlike location.hash, this is guaranteed to return the escaped hash in all browsers * @return {string} */ History.getHash = function(doc){ var url = History.getLocationHref(doc), hash; hash = History.getHashByUrl(url); return hash; }; /** * History.unescapeHash() * normalize and Unescape a Hash * @param {String} hash * @return {string} */ History.unescapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Unescape hash result = decodeURIComponent(result); // Return result return result; }; /** * History.normalizeHash() * normalize a hash across browsers * @return {string} */ History.normalizeHash = function(hash){ // Prepare var result = hash.replace(/[^#]*#/,'').replace(/#.*/, ''); // Return result return result; }; /** * History.setHash(hash) * Sets the document hash * @param {string} hash * @return {History} */ History.setHash = function(hash,queue){ // Prepare var State, pageUrl; // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.setHash: we must wait', arguments); History.pushQueue({ scope: History, callback: History.setHash, args: arguments, queue: queue }); return false; } // Log //History.debug('History.setHash: called',hash); // Make Busy + Continue History.busy(true); // Check if hash is a state State = History.extractState(hash,true); if ( State && !History.emulated.pushState ) { // Hash is a state so skip the setHash //History.debug('History.setHash: Hash is a state so skipping the hash set with a direct pushState call',arguments); // PushState History.pushState(State.data,State.title,State.url,false); } else if ( History.getHash() !== hash ) { // Hash is a proper hash, so apply it // Handle browser bugs if ( History.bugs.setHash ) { // Fix Safari Bug https://bugs.webkit.org/show_bug.cgi?id=56249 // Fetch the base page pageUrl = History.getPageUrl(); // Safari hash apply History.pushState(null,null,pageUrl+'#'+hash,false); } else { // Normal hash apply document.location.hash = hash; } } // Chain return History; }; /** * History.escape() * normalize and Escape a Hash * @return {string} */ History.escapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Escape hash result = window.encodeURIComponent(result); // IE6 Escape Bug if ( !History.bugs.hashEscape ) { // Restore common parts result = result .replace(/\%21/g,'!') .replace(/\%26/g,'&') .replace(/\%3D/g,'=') .replace(/\%3F/g,'?'); } // Return result return result; }; /** * History.getHashByUrl(url) * Extracts the Hash from a URL * @param {string} url * @return {string} url */ History.getHashByUrl = function(url){ // Extract the hash var hash = String(url) .replace(/([^#]*)#?([^#]*)#?(.*)/, '$2') ; // Unescape hash hash = History.unescapeHash(hash); // Return hash return hash; }; /** * History.setTitle(title) * Applies the title to the document * @param {State} newState * @return {Boolean} */ History.setTitle = function(newState){ // Prepare var title = newState.title, firstState; // Initial if ( !title ) { firstState = History.getStateByIndex(0); if ( firstState && firstState.url === newState.url ) { title = firstState.title||History.options.initialTitle; } } // Apply try { document.getElementsByTagName('title')[0].innerHTML = title.replace('<','<').replace('>','>').replace(' & ',' & '); } catch ( Exception ) { } document.title = title; // Chain return History; }; // ==================================================================== // Queueing /** * History.queues * The list of queues to use * First In, First Out */ History.queues = []; /** * History.busy(value) * @param {boolean} value [optional] * @return {boolean} busy */ History.busy = function(value){ // Apply if ( typeof value !== 'undefined' ) { //History.debug('History.busy: changing ['+(History.busy.flag||false)+'] to ['+(value||false)+']', History.queues.length); History.busy.flag = value; } // Default else if ( typeof History.busy.flag === 'undefined' ) { History.busy.flag = false; } // Queue if ( !History.busy.flag ) { // Execute the next item in the queue clearTimeout(History.busy.timeout); var fireNext = function(){ var i, queue, item; if ( History.busy.flag ) return; for ( i=History.queues.length-1; i >= 0; --i ) { queue = History.queues[i]; if ( queue.length === 0 ) continue; item = queue.shift(); History.fireQueueItem(item); History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } }; History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } // Return return History.busy.flag; }; /** * History.busy.flag */ History.busy.flag = false; /** * History.fireQueueItem(item) * Fire a Queue Item * @param {Object} item * @return {Mixed} result */ History.fireQueueItem = function(item){ return item.callback.apply(item.scope||History,item.args||[]); }; /** * History.pushQueue(callback,args) * Add an item to the queue * @param {Object} item [scope,callback,args,queue] */ History.pushQueue = function(item){ // Prepare the queue History.queues[item.queue||0] = History.queues[item.queue||0]||[]; // Add to the queue History.queues[item.queue||0].push(item); // Chain return History; }; /** * History.queue (item,queue), (func,queue), (func), (item) * Either firs the item now if not busy, or adds it to the queue */ History.queue = function(item,queue){ // Prepare if ( typeof item === 'function' ) { item = { callback: item }; } if ( typeof queue !== 'undefined' ) { item.queue = queue; } // Handle if ( History.busy() ) { History.pushQueue(item); } else { History.fireQueueItem(item); } // Chain return History; }; /** * History.clearQueue() * Clears the Queue */ History.clearQueue = function(){ History.busy.flag = false; History.queues = []; return History; }; // ==================================================================== // IE Bug Fix /** * History.stateChanged * States whether or not the state has changed since the last double check was initialised */ History.stateChanged = false; /** * History.doubleChecker * Contains the timeout used for the double checks */ History.doubleChecker = false; /** * History.doubleCheckComplete() * Complete a double check * @return {History} */ History.doubleCheckComplete = function(){ // Update History.stateChanged = true; // Clear History.doubleCheckClear(); // Chain return History; }; /** * History.doubleCheckClear() * Clear a double check * @return {History} */ History.doubleCheckClear = function(){ // Clear if ( History.doubleChecker ) { clearTimeout(History.doubleChecker); History.doubleChecker = false; } // Chain return History; }; /** * History.doubleCheck() * Create a double check * @return {History} */ History.doubleCheck = function(tryAgain){ // Reset History.stateChanged = false; History.doubleCheckClear(); // Fix IE6,IE7 bug where calling history.back or history.forward does not actually change the hash (whereas doing it manually does) // Fix Safari 5 bug where sometimes the state does not change: https://bugs.webkit.org/show_bug.cgi?id=42940 if ( History.bugs.ieDoubleCheck ) { // Apply Check History.doubleChecker = setTimeout( function(){ History.doubleCheckClear(); if ( !History.stateChanged ) { //History.debug('History.doubleCheck: State has not yet changed, trying again', arguments); // Re-Attempt tryAgain(); } return true; }, History.options.doubleCheckInterval ); } // Chain return History; }; // ==================================================================== // Safari Bug Fix /** * History.safariStatePoll() * Poll the current state * @return {History} */ History.safariStatePoll = function(){ // Poll the URL // Get the Last State which has the new URL var urlState = History.extractState(History.getLocationHref()), newState; // Check for a difference if ( !History.isLastSavedState(urlState) ) { newState = urlState; } else { return; } // Check if we have a state with that url // If not create it if ( !newState ) { //History.debug('History.safariStatePoll: new'); newState = History.createStateObject(); } // Apply the New State //History.debug('History.safariStatePoll: trigger'); History.Adapter.trigger(window,'popstate'); // Chain return History; }; // ==================================================================== // State Aliases /** * History.back(queue) * Send the browser history back one item * @param {Integer} queue [optional] */ History.back = function(queue){ //History.debug('History.back: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.back: we must wait', arguments); History.pushQueue({ scope: History, callback: History.back, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.back(false); }); // Go back history.go(-1); // End back closure return true; }; /** * History.forward(queue) * Send the browser history forward one item * @param {Integer} queue [optional] */ History.forward = function(queue){ //History.debug('History.forward: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.forward: we must wait', arguments); History.pushQueue({ scope: History, callback: History.forward, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.forward(false); }); // Go forward history.go(1); // End forward closure return true; }; /** * History.go(index,queue) * Send the browser history back or forward index times * @param {Integer} queue [optional] */ History.go = function(index,queue){ //History.debug('History.go: called', arguments); // Prepare var i; // Handle if ( index > 0 ) { // Forward for ( i=1; i<=index; ++i ) { History.forward(queue); } } else if ( index < 0 ) { // Backward for ( i=-1; i>=index; --i ) { History.back(queue); } } else { throw new Error('History.go: History.go requires a positive or negative integer passed.'); } // Chain return History; }; // ==================================================================== // HTML5 State Support // Non-Native pushState Implementation if ( History.emulated.pushState ) { /* * Provide Skeleton for HTML4 Browsers */ // Prepare var emptyFunction = function(){}; History.pushState = History.pushState||emptyFunction; History.replaceState = History.replaceState||emptyFunction; } // History.emulated.pushState // Native pushState Implementation else { /* * Use native HTML5 History API Implementation */ /** * History.onPopState(event,extra) * Refresh the Current State */ History.onPopState = function(event,extra){ // Prepare var stateId = false, newState = false, currentHash, currentState; // Reset the double check History.doubleCheckComplete(); // Check for a Hash, and handle apporiatly currentHash = History.getHash(); if ( currentHash ) { // Expand Hash currentState = History.extractState(currentHash||History.getLocationHref(),true); if ( currentState ) { // We were able to parse it, it must be a State! // Let's forward to replaceState //History.debug('History.onPopState: state anchor', currentHash, currentState); History.replaceState(currentState.data, currentState.title, currentState.url, false); } else { // Traditional Anchor //History.debug('History.onPopState: traditional anchor', currentHash); History.Adapter.trigger(window,'anchorchange'); History.busy(false); } // We don't care for hashes History.expectedStateId = false; return false; } // Ensure stateId = History.Adapter.extractEventData('state',event,extra) || false; // Fetch State if ( stateId ) { // Vanilla: Back/forward button was used newState = History.getStateById(stateId); } else if ( History.expectedStateId ) { // Vanilla: A new state was pushed, and popstate was called manually newState = History.getStateById(History.expectedStateId); } else { // Initial State newState = History.extractState(History.getLocationHref()); } // The State did not exist in our store if ( !newState ) { // Regenerate the State newState = History.createStateObject(null,null,History.getLocationHref()); } // Clean History.expectedStateId = false; // Check if we are the same state if ( History.isLastSavedState(newState) ) { // There has been no change (just the page's hash has finally propagated) //History.debug('History.onPopState: no change', newState, History.savedStates); History.busy(false); return false; } // Store the State History.storeState(newState); History.saveState(newState); // Force update of the title History.setTitle(newState); // Fire Our Event History.Adapter.trigger(window,'statechange'); History.busy(false); // Return true return true; }; History.Adapter.bind(window,'popstate',History.onPopState); /** * History.pushState(data,title,url) * Add a new State to the history object, become it, and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.pushState = function(data,title,url,queue){ //History.debug('History.pushState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.pushState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.pushState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.pushState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End pushState closure return true; }; /** * History.replaceState(data,title,url) * Replace the State and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.replaceState = function(data,title,url,queue){ //History.debug('History.replaceState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.replaceState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.replaceState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.replaceState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End replaceState closure return true; }; } // !History.emulated.pushState // ==================================================================== // Initialise /** * Load the Store */ if ( sessionStorage ) { // Fetch try { History.store = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { History.store = {}; } // Normalize History.normalizeStore(); } else { // Default Load History.store = {}; History.normalizeStore(); } /** * Clear Intervals on exit to prevent memory leaks */ History.Adapter.bind(window,"unload",History.clearAllIntervals); /** * Create the initial State */ History.saveState(History.storeState(History.extractState(History.getLocationHref(),true))); /** * Bind for Saving Store */ if ( sessionStorage ) { // When the page is closed History.onUnload = function(){ // Prepare var currentStore, item, currentStoreString; // Fetch try { currentStore = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { currentStore = {}; } // Ensure currentStore.idToState = currentStore.idToState || {}; currentStore.urlToId = currentStore.urlToId || {}; currentStore.stateToId = currentStore.stateToId || {}; // Sync for ( item in History.idToState ) { if ( !History.idToState.hasOwnProperty(item) ) { continue; } currentStore.idToState[item] = History.idToState[item]; } for ( item in History.urlToId ) { if ( !History.urlToId.hasOwnProperty(item) ) { continue; } currentStore.urlToId[item] = History.urlToId[item]; } for ( item in History.stateToId ) { if ( !History.stateToId.hasOwnProperty(item) ) { continue; } currentStore.stateToId[item] = History.stateToId[item]; } // Update History.store = currentStore; History.normalizeStore(); // In Safari, going into Private Browsing mode causes the // Session Storage object to still exist but if you try and use // or set any property/function of it it throws the exception // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to // add something to storage that exceeded the quota." infinitely // every second. currentStoreString = JSON.stringify(currentStore); try { // Store sessionStorage.setItem('History.store', currentStoreString); } catch (e) { if (e.code === DOMException.QUOTA_EXCEEDED_ERR) { if (sessionStorage.length) { // Workaround for a bug seen on iPads. Sometimes the quota exceeded error comes up and simply // removing/resetting the storage can work. sessionStorage.removeItem('History.store'); sessionStorage.setItem('History.store', currentStoreString); } else { // Otherwise, we're probably private browsing in Safari, so we'll ignore the exception. } } else { throw e; } } }; // For Internet Explorer History.intervalList.push(setInterval(History.onUnload,History.options.storeInterval)); // For Other Browsers History.Adapter.bind(window,'beforeunload',History.onUnload); History.Adapter.bind(window,'unload',History.onUnload); // Both are enabled for consistency } // Non-Native pushState Implementation if ( !History.emulated.pushState ) { // Be aware, the following is only for native pushState implementations // If you are wanting to include something for all browsers // Then include it above this if block /** * Setup Safari Fix */ if ( History.bugs.safariPoll ) { History.intervalList.push(setInterval(History.safariStatePoll, History.options.safariPollInterval)); } /** * Ensure Cross Browser Compatibility */ if ( navigator.vendor === 'Apple Computer, Inc.' || (navigator.appCodeName||'') === 'Mozilla' ) { /** * Fix Safari HashChange Issue */ // Setup Alias History.Adapter.bind(window,'hashchange',function(){ History.Adapter.trigger(window,'popstate'); }); // Initialise Alias if ( History.getHash() ) { History.Adapter.onDomLoad(function(){ History.Adapter.trigger(window,'hashchange'); }); } } } // !History.emulated.pushState }; // History.initCore // Try to Initialise History if (!History.options || !History.options.delayInit) { History.init(); } })(window); ================================================ FILE: scripts/bundled-uncompressed/html5/zepto.history.js ================================================ /** * History.js Zepto Adapter * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ // Closure (function(window,undefined){ "use strict"; // Localise Globals var History = window.History = window.History||{}, Zepto = window.Zepto; // Check Existence if ( typeof History.Adapter !== 'undefined' ) { throw new Error('History.js Adapter has already been loaded...'); } // Add the Adapter History.Adapter = { /** * History.Adapter.bind(el,event,callback) * @param {Element|string} el * @param {string} event - custom and standard events * @param {function} callback * @return {void} */ bind: function(el,event,callback){ new Zepto(el).bind(event,callback); }, /** * History.Adapter.trigger(el,event) * @param {Element|string} el * @param {string} event - custom and standard events * @return {void} */ trigger: function(el,event){ new Zepto(el).trigger(event); }, /** * History.Adapter.extractEventData(key,event,extra) * @param {string} key - key for the event data to extract * @param {string} event - custom and standard events * @return {mixed} */ extractEventData: function(key,event){ // Zepto Native var result = (event && event[key]) || undefined; // Return return result; }, /** * History.Adapter.onDomLoad(callback) * @param {function} callback * @return {void} */ onDomLoad: function(callback) { new Zepto(callback); } }; // Try and Initialise History if ( typeof History.init !== 'undefined' ) { History.init(); } })(window); /** * History.js Core * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window,undefined){ "use strict"; // ======================================================================== // Initialise // Localise Globals var console = window.console||undefined, // Prevent a JSLint complain document = window.document, // Make sure we are using the correct document navigator = window.navigator, // Make sure we are using the correct navigator sessionStorage = false, // sessionStorage setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, setInterval = window.setInterval, clearInterval = window.clearInterval, JSON = window.JSON, alert = window.alert, History = window.History = window.History||{}, // Public History Object history = window.history; // Old History Object try { sessionStorage = window.sessionStorage; // This will throw an exception in some browsers when cookies/localStorage are explicitly disabled (i.e. Chrome) sessionStorage.setItem('TEST', '1'); sessionStorage.removeItem('TEST'); } catch(e) { sessionStorage = false; } // MooTools Compatibility JSON.stringify = JSON.stringify||JSON.encode; JSON.parse = JSON.parse||JSON.decode; // Check Existence if ( typeof History.init !== 'undefined' ) { throw new Error('History.js Core has already been loaded...'); } // Initialise History History.init = function(options){ // Check Load Status of Adapter if ( typeof History.Adapter === 'undefined' ) { return false; } // Check Load Status of Core if ( typeof History.initCore !== 'undefined' ) { History.initCore(); } // Check Load Status of HTML4 Support if ( typeof History.initHtml4 !== 'undefined' ) { History.initHtml4(); } // Return true return true; }; // ======================================================================== // Initialise Core // Initialise Core History.initCore = function(options){ // Initialise if ( typeof History.initCore.initialized !== 'undefined' ) { // Already Loaded return false; } else { History.initCore.initialized = true; } // ==================================================================== // Options /** * History.options * Configurable options */ History.options = History.options||{}; /** * History.options.hashChangeInterval * How long should the interval be before hashchange checks */ History.options.hashChangeInterval = History.options.hashChangeInterval || 100; /** * History.options.safariPollInterval * How long should the interval be before safari poll checks */ History.options.safariPollInterval = History.options.safariPollInterval || 500; /** * History.options.doubleCheckInterval * How long should the interval be before we perform a double check */ History.options.doubleCheckInterval = History.options.doubleCheckInterval || 500; /** * History.options.disableSuid * Force History not to append suid */ History.options.disableSuid = History.options.disableSuid || false; /** * History.options.storeInterval * How long should we wait between store calls */ History.options.storeInterval = History.options.storeInterval || 1000; /** * History.options.busyDelay * How long should we wait between busy events */ History.options.busyDelay = History.options.busyDelay || 250; /** * History.options.debug * If true will enable debug messages to be logged */ History.options.debug = History.options.debug || false; /** * History.options.initialTitle * What is the title of the initial state */ History.options.initialTitle = History.options.initialTitle || document.title; /** * History.options.html4Mode * If true, will force HTMl4 mode (hashtags) */ History.options.html4Mode = History.options.html4Mode || false; /** * History.options.delayInit * Want to override default options and call init manually. */ History.options.delayInit = History.options.delayInit || false; // ==================================================================== // Interval record /** * History.intervalList * List of intervals set, to be cleared when document is unloaded. */ History.intervalList = []; /** * History.clearAllIntervals * Clears all setInterval instances. */ History.clearAllIntervals = function(){ var i, il = History.intervalList; if (typeof il !== "undefined" && il !== null) { for (i = 0; i < il.length; i++) { clearInterval(il[i]); } History.intervalList = null; } }; // ==================================================================== // Debug /** * History.debug(message,...) * Logs the passed arguments if debug enabled */ History.debug = function(){ if ( (History.options.debug||false) ) { History.log.apply(History,arguments); } }; /** * History.log(message,...) * Logs the passed arguments */ History.log = function(){ // Prepare var consoleExists = !(typeof console === 'undefined' || typeof console.log === 'undefined' || typeof console.log.apply === 'undefined'), textarea = document.getElementById('log'), message, i,n, args,arg ; // Write to Console if ( consoleExists ) { args = Array.prototype.slice.call(arguments); message = args.shift(); if ( typeof console.debug !== 'undefined' ) { console.debug.apply(console,[message,args]); } else { console.log.apply(console,[message,args]); } } else { message = ("\n"+arguments[0]+"\n"); } // Write to log for ( i=1,n=arguments.length; i * @author James Padolsey */ History.getInternetExplorerMajorVersion = function(){ var result = History.getInternetExplorerMajorVersion.cached = (typeof History.getInternetExplorerMajorVersion.cached !== 'undefined') ? History.getInternetExplorerMajorVersion.cached : (function(){ var v = 3, div = document.createElement('div'), all = div.getElementsByTagName('i'); while ( (div.innerHTML = '') && all[0] ) {} return (v > 4) ? v : false; })() ; return result; }; /** * History.isInternetExplorer() * Are we using Internet Explorer? * @return {boolean} * @license Public Domain * @author Benjamin Arthur Lupton */ History.isInternetExplorer = function(){ var result = History.isInternetExplorer.cached = (typeof History.isInternetExplorer.cached !== 'undefined') ? History.isInternetExplorer.cached : Boolean(History.getInternetExplorerMajorVersion()) ; return result; }; /** * History.emulated * Which features require emulating? */ if (History.options.html4Mode) { History.emulated = { pushState : true, hashChange: true }; } else { History.emulated = { pushState: !Boolean( window.history && window.history.pushState && window.history.replaceState && !( (/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i).test(navigator.userAgent) /* disable for versions of iOS before version 4.3 (8F190) */ || (/AppleWebKit\/5([0-2]|3[0-2])/i).test(navigator.userAgent) /* disable for the mercury iOS browser, or at least older versions of the webkit engine */ ) ), hashChange: Boolean( !(('onhashchange' in window) || ('onhashchange' in document)) || (History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8) ) }; } /** * History.enabled * Is History enabled? */ History.enabled = !History.emulated.pushState; /** * History.bugs * Which bugs are present */ History.bugs = { /** * Safari 5 and Safari iOS 4 fail to return to the correct state once a hash is replaced by a `replaceState` call * https://bugs.webkit.org/show_bug.cgi?id=56249 */ setHash: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * Safari 5 and Safari iOS 4 sometimes fail to apply the state change under busy conditions * https://bugs.webkit.org/show_bug.cgi?id=42940 */ safariPoll: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * MSIE 6 and 7 sometimes do not apply a hash even it was told to (requiring a second call to the apply function) */ ieDoubleCheck: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8), /** * MSIE 6 requires the entire hash to be encoded for the hashes to trigger the onHashChange event */ hashEscape: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 7) }; /** * History.isEmptyObject(obj) * Checks to see if the Object is Empty * @param {Object} obj * @return {boolean} */ History.isEmptyObject = function(obj) { for ( var name in obj ) { if ( obj.hasOwnProperty(name) ) { return false; } } return true; }; /** * History.cloneObject(obj) * Clones a object and eliminate all references to the original contexts * @param {Object} obj * @return {Object} */ History.cloneObject = function(obj) { var hash,newObj; if ( obj ) { hash = JSON.stringify(obj); newObj = JSON.parse(hash); } else { newObj = {}; } return newObj; }; // ==================================================================== // URL Helpers /** * History.getRootUrl() * Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com" * @return {String} rootUrl */ History.getRootUrl = function(){ // Create var rootUrl = document.location.protocol+'//'+(document.location.hostname||document.location.host); if ( document.location.port||false ) { rootUrl += ':'+document.location.port; } rootUrl += '/'; // Return return rootUrl; }; /** * History.getBaseHref() * Fetches the `href` attribute of the `` element if it exists * @return {String} baseHref */ History.getBaseHref = function(){ // Create var baseElements = document.getElementsByTagName('base'), baseElement = null, baseHref = ''; // Test for Base Element if ( baseElements.length === 1 ) { // Prepare for Base Element baseElement = baseElements[0]; baseHref = baseElement.href.replace(/[^\/]+$/,''); } // Adjust trailing slash baseHref = baseHref.replace(/\/+$/,''); if ( baseHref ) baseHref += '/'; // Return return baseHref; }; /** * History.getBaseUrl() * Fetches the baseHref or basePageUrl or rootUrl (whichever one exists first) * @return {String} baseUrl */ History.getBaseUrl = function(){ // Create var baseUrl = History.getBaseHref()||History.getBasePageUrl()||History.getRootUrl(); // Return return baseUrl; }; /** * History.getPageUrl() * Fetches the URL of the current page * @return {String} pageUrl */ History.getPageUrl = function(){ // Fetch var State = History.getState(false,false), stateUrl = (State||{}).url||History.getLocationHref(), pageUrl; // Create pageUrl = stateUrl.replace(/\/+$/,'').replace(/[^\/]+$/,function(part,index,string){ return (/\./).test(part) ? part : part+'/'; }); // Return return pageUrl; }; /** * History.getBasePageUrl() * Fetches the Url of the directory of the current page * @return {String} basePageUrl */ History.getBasePageUrl = function(){ // Create var basePageUrl = (History.getLocationHref()).replace(/[#\?].*/,'').replace(/[^\/]+$/,function(part,index,string){ return (/[^\/]$/).test(part) ? '' : part; }).replace(/\/+$/,'')+'/'; // Return return basePageUrl; }; /** * History.getFullUrl(url) * Ensures that we have an absolute URL and not a relative URL * @param {string} url * @param {Boolean} allowBaseHref * @return {string} fullUrl */ History.getFullUrl = function(url,allowBaseHref){ // Prepare var fullUrl = url, firstChar = url.substring(0,1); allowBaseHref = (typeof allowBaseHref === 'undefined') ? true : allowBaseHref; // Check if ( /[a-z]+\:\/\//.test(url) ) { // Full URL } else if ( firstChar === '/' ) { // Root URL fullUrl = History.getRootUrl()+url.replace(/^\/+/,''); } else if ( firstChar === '#' ) { // Anchor URL fullUrl = History.getPageUrl().replace(/#.*/,'')+url; } else if ( firstChar === '?' ) { // Query URL fullUrl = History.getPageUrl().replace(/[\?#].*/,'')+url; } else { // Relative URL if ( allowBaseHref ) { fullUrl = History.getBaseUrl()+url.replace(/^(\.\/)+/,''); } else { fullUrl = History.getBasePageUrl()+url.replace(/^(\.\/)+/,''); } // We have an if condition above as we do not want hashes // which are relative to the baseHref in our URLs // as if the baseHref changes, then all our bookmarks // would now point to different locations // whereas the basePageUrl will always stay the same } // Return return fullUrl.replace(/\#$/,''); }; /** * History.getShortUrl(url) * Ensures that we have a relative URL and not a absolute URL * @param {string} url * @return {string} url */ History.getShortUrl = function(url){ // Prepare var shortUrl = url, baseUrl = History.getBaseUrl(), rootUrl = History.getRootUrl(); // Trim baseUrl if ( History.emulated.pushState ) { // We are in a if statement as when pushState is not emulated // The actual url these short urls are relative to can change // So within the same session, we the url may end up somewhere different shortUrl = shortUrl.replace(baseUrl,''); } // Trim rootUrl shortUrl = shortUrl.replace(rootUrl,'/'); // Ensure we can still detect it as a state if ( History.isTraditionalAnchor(shortUrl) ) { shortUrl = './'+shortUrl; } // Clean It shortUrl = shortUrl.replace(/^(\.\/)+/g,'./').replace(/\#$/,''); // Return return shortUrl; }; /** * History.getLocationHref(document) * Returns a normalized version of document.location.href * accounting for browser inconsistencies, etc. * * This URL will be URI-encoded and will include the hash * * @param {object} document * @return {string} url */ History.getLocationHref = function(doc) { doc = doc || document; // most of the time, this will be true if (doc.URL === doc.location.href) return doc.location.href; // some versions of webkit URI-decode document.location.href // but they leave document.URL in an encoded state if (doc.location.href === decodeURIComponent(doc.URL)) return doc.URL; // FF 3.6 only updates document.URL when a page is reloaded // document.location.href is updated correctly if (doc.location.hash && decodeURIComponent(doc.location.href.replace(/^[^#]+/, "")) === doc.location.hash) return doc.location.href; if (doc.URL.indexOf('#') == -1 && doc.location.href.indexOf('#') != -1) return doc.location.href; return doc.URL || doc.location.href; }; // ==================================================================== // State Storage /** * History.store * The store for all session specific data */ History.store = {}; /** * History.idToState * 1-1: State ID to State Object */ History.idToState = History.idToState||{}; /** * History.stateToId * 1-1: State String to State ID */ History.stateToId = History.stateToId||{}; /** * History.urlToId * 1-1: State URL to State ID */ History.urlToId = History.urlToId||{}; /** * History.storedStates * Store the states in an array */ History.storedStates = History.storedStates||[]; /** * History.savedStates * Saved the states in an array */ History.savedStates = History.savedStates||[]; /** * History.noramlizeStore() * Noramlize the store by adding necessary values */ History.normalizeStore = function(){ History.store.idToState = History.store.idToState||{}; History.store.urlToId = History.store.urlToId||{}; History.store.stateToId = History.store.stateToId||{}; }; /** * History.getState() * Get an object containing the data, title and url of the current state * @param {Boolean} friendly * @param {Boolean} create * @return {Object} State */ History.getState = function(friendly,create){ // Prepare if ( typeof friendly === 'undefined' ) { friendly = true; } if ( typeof create === 'undefined' ) { create = true; } // Fetch var State = History.getLastSavedState(); // Create if ( !State && create ) { State = History.createStateObject(); } // Adjust if ( friendly ) { State = History.cloneObject(State); State.url = State.cleanUrl||State.url; } // Return return State; }; /** * History.getIdByState(State) * Gets a ID for a State * @param {State} newState * @return {String} id */ History.getIdByState = function(newState){ // Fetch ID var id = History.extractId(newState.url), str; if ( !id ) { // Find ID via State String str = History.getStateString(newState); if ( typeof History.stateToId[str] !== 'undefined' ) { id = History.stateToId[str]; } else if ( typeof History.store.stateToId[str] !== 'undefined' ) { id = History.store.stateToId[str]; } else { // Generate a new ID while ( true ) { id = (new Date()).getTime() + String(Math.random()).replace(/\D/g,''); if ( typeof History.idToState[id] === 'undefined' && typeof History.store.idToState[id] === 'undefined' ) { break; } } // Apply the new State to the ID History.stateToId[str] = id; History.idToState[id] = newState; } } // Return ID return id; }; /** * History.normalizeState(State) * Expands a State Object * @param {object} State * @return {object} */ History.normalizeState = function(oldState){ // Variables var newState, dataNotEmpty; // Prepare if ( !oldState || (typeof oldState !== 'object') ) { oldState = {}; } // Check if ( typeof oldState.normalized !== 'undefined' ) { return oldState; } // Adjust if ( !oldState.data || (typeof oldState.data !== 'object') ) { oldState.data = {}; } // ---------------------------------------------------------------- // Create newState = {}; newState.normalized = true; newState.title = oldState.title||''; newState.url = History.getFullUrl(oldState.url?oldState.url:(History.getLocationHref())); newState.hash = History.getShortUrl(newState.url); newState.data = History.cloneObject(oldState.data); // Fetch ID newState.id = History.getIdByState(newState); // ---------------------------------------------------------------- // Clean the URL newState.cleanUrl = newState.url.replace(/\??\&_suid.*/,''); newState.url = newState.cleanUrl; // Check to see if we have more than just a url dataNotEmpty = !History.isEmptyObject(newState.data); // Apply if ( (newState.title || dataNotEmpty) && History.options.disableSuid !== true ) { // Add ID to Hash newState.hash = History.getShortUrl(newState.url).replace(/\??\&_suid.*/,''); if ( !/\?/.test(newState.hash) ) { newState.hash += '?'; } newState.hash += '&_suid='+newState.id; } // Create the Hashed URL newState.hashedUrl = History.getFullUrl(newState.hash); // ---------------------------------------------------------------- // Update the URL if we have a duplicate if ( (History.emulated.pushState || History.bugs.safariPoll) && History.hasUrlDuplicate(newState) ) { newState.url = newState.hashedUrl; } // ---------------------------------------------------------------- // Return return newState; }; /** * History.createStateObject(data,title,url) * Creates a object based on the data, title and url state params * @param {object} data * @param {string} title * @param {string} url * @return {object} */ History.createStateObject = function(data,title,url){ // Hashify var State = { 'data': data, 'title': title, 'url': url }; // Expand the State State = History.normalizeState(State); // Return object return State; }; /** * History.getStateById(id) * Get a state by it's UID * @param {String} id */ History.getStateById = function(id){ // Prepare id = String(id); // Retrieve var State = History.idToState[id] || History.store.idToState[id] || undefined; // Return State return State; }; /** * Get a State's String * @param {State} passedState */ History.getStateString = function(passedState){ // Prepare var State, cleanedState, str; // Fetch State = History.normalizeState(passedState); // Clean cleanedState = { data: State.data, title: passedState.title, url: passedState.url }; // Fetch str = JSON.stringify(cleanedState); // Return return str; }; /** * Get a State's ID * @param {State} passedState * @return {String} id */ History.getStateId = function(passedState){ // Prepare var State, id; // Fetch State = History.normalizeState(passedState); // Fetch id = State.id; // Return return id; }; /** * History.getHashByState(State) * Creates a Hash for the State Object * @param {State} passedState * @return {String} hash */ History.getHashByState = function(passedState){ // Prepare var State, hash; // Fetch State = History.normalizeState(passedState); // Hash hash = State.hash; // Return return hash; }; /** * History.extractId(url_or_hash) * Get a State ID by it's URL or Hash * @param {string} url_or_hash * @return {string} id */ History.extractId = function ( url_or_hash ) { // Prepare var id,parts,url, tmp; // Extract // If the URL has a #, use the id from before the # if (url_or_hash.indexOf('#') != -1) { tmp = url_or_hash.split("#")[0]; } else { tmp = url_or_hash; } parts = /(.*)\&_suid=([0-9]+)$/.exec(tmp); url = parts ? (parts[1]||url_or_hash) : url_or_hash; id = parts ? String(parts[2]||'') : ''; // Return return id||false; }; /** * History.isTraditionalAnchor * Checks to see if the url is a traditional anchor or not * @param {String} url_or_hash * @return {Boolean} */ History.isTraditionalAnchor = function(url_or_hash){ // Check var isTraditional = !(/[\/\?\.]/.test(url_or_hash)); // Return return isTraditional; }; /** * History.extractState * Get a State by it's URL or Hash * @param {String} url_or_hash * @return {State|null} */ History.extractState = function(url_or_hash,create){ // Prepare var State = null, id, url; create = create||false; // Fetch SUID id = History.extractId(url_or_hash); if ( id ) { State = History.getStateById(id); } // Fetch SUID returned no State if ( !State ) { // Fetch URL url = History.getFullUrl(url_or_hash); // Check URL id = History.getIdByUrl(url)||false; if ( id ) { State = History.getStateById(id); } // Create State if ( !State && create && !History.isTraditionalAnchor(url_or_hash) ) { State = History.createStateObject(null,null,url); } } // Return return State; }; /** * History.getIdByUrl() * Get a State ID by a State URL */ History.getIdByUrl = function(url){ // Fetch var id = History.urlToId[url] || History.store.urlToId[url] || undefined; // Return return id; }; /** * History.getLastSavedState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastSavedState = function(){ return History.savedStates[History.savedStates.length-1]||undefined; }; /** * History.getLastStoredState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastStoredState = function(){ return History.storedStates[History.storedStates.length-1]||undefined; }; /** * History.hasUrlDuplicate * Checks if a Url will have a url conflict * @param {Object} newState * @return {Boolean} hasDuplicate */ History.hasUrlDuplicate = function(newState) { // Prepare var hasDuplicate = false, oldState; // Fetch oldState = History.extractState(newState.url); // Check hasDuplicate = oldState && oldState.id !== newState.id; // Return return hasDuplicate; }; /** * History.storeState * Store a State * @param {Object} newState * @return {Object} newState */ History.storeState = function(newState){ // Store the State History.urlToId[newState.url] = newState.id; // Push the State History.storedStates.push(History.cloneObject(newState)); // Return newState return newState; }; /** * History.isLastSavedState(newState) * Tests to see if the state is the last state * @param {Object} newState * @return {boolean} isLast */ History.isLastSavedState = function(newState){ // Prepare var isLast = false, newId, oldState, oldId; // Check if ( History.savedStates.length ) { newId = newState.id; oldState = History.getLastSavedState(); oldId = oldState.id; // Check isLast = (newId === oldId); } // Return return isLast; }; /** * History.saveState * Push a State * @param {Object} newState * @return {boolean} changed */ History.saveState = function(newState){ // Check Hash if ( History.isLastSavedState(newState) ) { return false; } // Push the State History.savedStates.push(History.cloneObject(newState)); // Return true return true; }; /** * History.getStateByIndex() * Gets a state by the index * @param {integer} index * @return {Object} */ History.getStateByIndex = function(index){ // Prepare var State = null; // Handle if ( typeof index === 'undefined' ) { // Get the last inserted State = History.savedStates[History.savedStates.length-1]; } else if ( index < 0 ) { // Get from the end State = History.savedStates[History.savedStates.length+index]; } else { // Get from the beginning State = History.savedStates[index]; } // Return State return State; }; /** * History.getCurrentIndex() * Gets the current index * @return (integer) */ History.getCurrentIndex = function(){ // Prepare var index = null; // No states saved if(History.savedStates.length < 1) { index = 0; } else { index = History.savedStates.length-1; } return index; }; // ==================================================================== // Hash Helpers /** * History.getHash() * @param {Location=} location * Gets the current document hash * Note: unlike location.hash, this is guaranteed to return the escaped hash in all browsers * @return {string} */ History.getHash = function(doc){ var url = History.getLocationHref(doc), hash; hash = History.getHashByUrl(url); return hash; }; /** * History.unescapeHash() * normalize and Unescape a Hash * @param {String} hash * @return {string} */ History.unescapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Unescape hash result = decodeURIComponent(result); // Return result return result; }; /** * History.normalizeHash() * normalize a hash across browsers * @return {string} */ History.normalizeHash = function(hash){ // Prepare var result = hash.replace(/[^#]*#/,'').replace(/#.*/, ''); // Return result return result; }; /** * History.setHash(hash) * Sets the document hash * @param {string} hash * @return {History} */ History.setHash = function(hash,queue){ // Prepare var State, pageUrl; // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.setHash: we must wait', arguments); History.pushQueue({ scope: History, callback: History.setHash, args: arguments, queue: queue }); return false; } // Log //History.debug('History.setHash: called',hash); // Make Busy + Continue History.busy(true); // Check if hash is a state State = History.extractState(hash,true); if ( State && !History.emulated.pushState ) { // Hash is a state so skip the setHash //History.debug('History.setHash: Hash is a state so skipping the hash set with a direct pushState call',arguments); // PushState History.pushState(State.data,State.title,State.url,false); } else if ( History.getHash() !== hash ) { // Hash is a proper hash, so apply it // Handle browser bugs if ( History.bugs.setHash ) { // Fix Safari Bug https://bugs.webkit.org/show_bug.cgi?id=56249 // Fetch the base page pageUrl = History.getPageUrl(); // Safari hash apply History.pushState(null,null,pageUrl+'#'+hash,false); } else { // Normal hash apply document.location.hash = hash; } } // Chain return History; }; /** * History.escape() * normalize and Escape a Hash * @return {string} */ History.escapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Escape hash result = window.encodeURIComponent(result); // IE6 Escape Bug if ( !History.bugs.hashEscape ) { // Restore common parts result = result .replace(/\%21/g,'!') .replace(/\%26/g,'&') .replace(/\%3D/g,'=') .replace(/\%3F/g,'?'); } // Return result return result; }; /** * History.getHashByUrl(url) * Extracts the Hash from a URL * @param {string} url * @return {string} url */ History.getHashByUrl = function(url){ // Extract the hash var hash = String(url) .replace(/([^#]*)#?([^#]*)#?(.*)/, '$2') ; // Unescape hash hash = History.unescapeHash(hash); // Return hash return hash; }; /** * History.setTitle(title) * Applies the title to the document * @param {State} newState * @return {Boolean} */ History.setTitle = function(newState){ // Prepare var title = newState.title, firstState; // Initial if ( !title ) { firstState = History.getStateByIndex(0); if ( firstState && firstState.url === newState.url ) { title = firstState.title||History.options.initialTitle; } } // Apply try { document.getElementsByTagName('title')[0].innerHTML = title.replace('<','<').replace('>','>').replace(' & ',' & '); } catch ( Exception ) { } document.title = title; // Chain return History; }; // ==================================================================== // Queueing /** * History.queues * The list of queues to use * First In, First Out */ History.queues = []; /** * History.busy(value) * @param {boolean} value [optional] * @return {boolean} busy */ History.busy = function(value){ // Apply if ( typeof value !== 'undefined' ) { //History.debug('History.busy: changing ['+(History.busy.flag||false)+'] to ['+(value||false)+']', History.queues.length); History.busy.flag = value; } // Default else if ( typeof History.busy.flag === 'undefined' ) { History.busy.flag = false; } // Queue if ( !History.busy.flag ) { // Execute the next item in the queue clearTimeout(History.busy.timeout); var fireNext = function(){ var i, queue, item; if ( History.busy.flag ) return; for ( i=History.queues.length-1; i >= 0; --i ) { queue = History.queues[i]; if ( queue.length === 0 ) continue; item = queue.shift(); History.fireQueueItem(item); History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } }; History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } // Return return History.busy.flag; }; /** * History.busy.flag */ History.busy.flag = false; /** * History.fireQueueItem(item) * Fire a Queue Item * @param {Object} item * @return {Mixed} result */ History.fireQueueItem = function(item){ return item.callback.apply(item.scope||History,item.args||[]); }; /** * History.pushQueue(callback,args) * Add an item to the queue * @param {Object} item [scope,callback,args,queue] */ History.pushQueue = function(item){ // Prepare the queue History.queues[item.queue||0] = History.queues[item.queue||0]||[]; // Add to the queue History.queues[item.queue||0].push(item); // Chain return History; }; /** * History.queue (item,queue), (func,queue), (func), (item) * Either firs the item now if not busy, or adds it to the queue */ History.queue = function(item,queue){ // Prepare if ( typeof item === 'function' ) { item = { callback: item }; } if ( typeof queue !== 'undefined' ) { item.queue = queue; } // Handle if ( History.busy() ) { History.pushQueue(item); } else { History.fireQueueItem(item); } // Chain return History; }; /** * History.clearQueue() * Clears the Queue */ History.clearQueue = function(){ History.busy.flag = false; History.queues = []; return History; }; // ==================================================================== // IE Bug Fix /** * History.stateChanged * States whether or not the state has changed since the last double check was initialised */ History.stateChanged = false; /** * History.doubleChecker * Contains the timeout used for the double checks */ History.doubleChecker = false; /** * History.doubleCheckComplete() * Complete a double check * @return {History} */ History.doubleCheckComplete = function(){ // Update History.stateChanged = true; // Clear History.doubleCheckClear(); // Chain return History; }; /** * History.doubleCheckClear() * Clear a double check * @return {History} */ History.doubleCheckClear = function(){ // Clear if ( History.doubleChecker ) { clearTimeout(History.doubleChecker); History.doubleChecker = false; } // Chain return History; }; /** * History.doubleCheck() * Create a double check * @return {History} */ History.doubleCheck = function(tryAgain){ // Reset History.stateChanged = false; History.doubleCheckClear(); // Fix IE6,IE7 bug where calling history.back or history.forward does not actually change the hash (whereas doing it manually does) // Fix Safari 5 bug where sometimes the state does not change: https://bugs.webkit.org/show_bug.cgi?id=42940 if ( History.bugs.ieDoubleCheck ) { // Apply Check History.doubleChecker = setTimeout( function(){ History.doubleCheckClear(); if ( !History.stateChanged ) { //History.debug('History.doubleCheck: State has not yet changed, trying again', arguments); // Re-Attempt tryAgain(); } return true; }, History.options.doubleCheckInterval ); } // Chain return History; }; // ==================================================================== // Safari Bug Fix /** * History.safariStatePoll() * Poll the current state * @return {History} */ History.safariStatePoll = function(){ // Poll the URL // Get the Last State which has the new URL var urlState = History.extractState(History.getLocationHref()), newState; // Check for a difference if ( !History.isLastSavedState(urlState) ) { newState = urlState; } else { return; } // Check if we have a state with that url // If not create it if ( !newState ) { //History.debug('History.safariStatePoll: new'); newState = History.createStateObject(); } // Apply the New State //History.debug('History.safariStatePoll: trigger'); History.Adapter.trigger(window,'popstate'); // Chain return History; }; // ==================================================================== // State Aliases /** * History.back(queue) * Send the browser history back one item * @param {Integer} queue [optional] */ History.back = function(queue){ //History.debug('History.back: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.back: we must wait', arguments); History.pushQueue({ scope: History, callback: History.back, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.back(false); }); // Go back history.go(-1); // End back closure return true; }; /** * History.forward(queue) * Send the browser history forward one item * @param {Integer} queue [optional] */ History.forward = function(queue){ //History.debug('History.forward: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.forward: we must wait', arguments); History.pushQueue({ scope: History, callback: History.forward, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.forward(false); }); // Go forward history.go(1); // End forward closure return true; }; /** * History.go(index,queue) * Send the browser history back or forward index times * @param {Integer} queue [optional] */ History.go = function(index,queue){ //History.debug('History.go: called', arguments); // Prepare var i; // Handle if ( index > 0 ) { // Forward for ( i=1; i<=index; ++i ) { History.forward(queue); } } else if ( index < 0 ) { // Backward for ( i=-1; i>=index; --i ) { History.back(queue); } } else { throw new Error('History.go: History.go requires a positive or negative integer passed.'); } // Chain return History; }; // ==================================================================== // HTML5 State Support // Non-Native pushState Implementation if ( History.emulated.pushState ) { /* * Provide Skeleton for HTML4 Browsers */ // Prepare var emptyFunction = function(){}; History.pushState = History.pushState||emptyFunction; History.replaceState = History.replaceState||emptyFunction; } // History.emulated.pushState // Native pushState Implementation else { /* * Use native HTML5 History API Implementation */ /** * History.onPopState(event,extra) * Refresh the Current State */ History.onPopState = function(event,extra){ // Prepare var stateId = false, newState = false, currentHash, currentState; // Reset the double check History.doubleCheckComplete(); // Check for a Hash, and handle apporiatly currentHash = History.getHash(); if ( currentHash ) { // Expand Hash currentState = History.extractState(currentHash||History.getLocationHref(),true); if ( currentState ) { // We were able to parse it, it must be a State! // Let's forward to replaceState //History.debug('History.onPopState: state anchor', currentHash, currentState); History.replaceState(currentState.data, currentState.title, currentState.url, false); } else { // Traditional Anchor //History.debug('History.onPopState: traditional anchor', currentHash); History.Adapter.trigger(window,'anchorchange'); History.busy(false); } // We don't care for hashes History.expectedStateId = false; return false; } // Ensure stateId = History.Adapter.extractEventData('state',event,extra) || false; // Fetch State if ( stateId ) { // Vanilla: Back/forward button was used newState = History.getStateById(stateId); } else if ( History.expectedStateId ) { // Vanilla: A new state was pushed, and popstate was called manually newState = History.getStateById(History.expectedStateId); } else { // Initial State newState = History.extractState(History.getLocationHref()); } // The State did not exist in our store if ( !newState ) { // Regenerate the State newState = History.createStateObject(null,null,History.getLocationHref()); } // Clean History.expectedStateId = false; // Check if we are the same state if ( History.isLastSavedState(newState) ) { // There has been no change (just the page's hash has finally propagated) //History.debug('History.onPopState: no change', newState, History.savedStates); History.busy(false); return false; } // Store the State History.storeState(newState); History.saveState(newState); // Force update of the title History.setTitle(newState); // Fire Our Event History.Adapter.trigger(window,'statechange'); History.busy(false); // Return true return true; }; History.Adapter.bind(window,'popstate',History.onPopState); /** * History.pushState(data,title,url) * Add a new State to the history object, become it, and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.pushState = function(data,title,url,queue){ //History.debug('History.pushState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.pushState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.pushState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.pushState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End pushState closure return true; }; /** * History.replaceState(data,title,url) * Replace the State and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.replaceState = function(data,title,url,queue){ //History.debug('History.replaceState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.replaceState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.replaceState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.replaceState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End replaceState closure return true; }; } // !History.emulated.pushState // ==================================================================== // Initialise /** * Load the Store */ if ( sessionStorage ) { // Fetch try { History.store = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { History.store = {}; } // Normalize History.normalizeStore(); } else { // Default Load History.store = {}; History.normalizeStore(); } /** * Clear Intervals on exit to prevent memory leaks */ History.Adapter.bind(window,"unload",History.clearAllIntervals); /** * Create the initial State */ History.saveState(History.storeState(History.extractState(History.getLocationHref(),true))); /** * Bind for Saving Store */ if ( sessionStorage ) { // When the page is closed History.onUnload = function(){ // Prepare var currentStore, item, currentStoreString; // Fetch try { currentStore = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { currentStore = {}; } // Ensure currentStore.idToState = currentStore.idToState || {}; currentStore.urlToId = currentStore.urlToId || {}; currentStore.stateToId = currentStore.stateToId || {}; // Sync for ( item in History.idToState ) { if ( !History.idToState.hasOwnProperty(item) ) { continue; } currentStore.idToState[item] = History.idToState[item]; } for ( item in History.urlToId ) { if ( !History.urlToId.hasOwnProperty(item) ) { continue; } currentStore.urlToId[item] = History.urlToId[item]; } for ( item in History.stateToId ) { if ( !History.stateToId.hasOwnProperty(item) ) { continue; } currentStore.stateToId[item] = History.stateToId[item]; } // Update History.store = currentStore; History.normalizeStore(); // In Safari, going into Private Browsing mode causes the // Session Storage object to still exist but if you try and use // or set any property/function of it it throws the exception // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to // add something to storage that exceeded the quota." infinitely // every second. currentStoreString = JSON.stringify(currentStore); try { // Store sessionStorage.setItem('History.store', currentStoreString); } catch (e) { if (e.code === DOMException.QUOTA_EXCEEDED_ERR) { if (sessionStorage.length) { // Workaround for a bug seen on iPads. Sometimes the quota exceeded error comes up and simply // removing/resetting the storage can work. sessionStorage.removeItem('History.store'); sessionStorage.setItem('History.store', currentStoreString); } else { // Otherwise, we're probably private browsing in Safari, so we'll ignore the exception. } } else { throw e; } } }; // For Internet Explorer History.intervalList.push(setInterval(History.onUnload,History.options.storeInterval)); // For Other Browsers History.Adapter.bind(window,'beforeunload',History.onUnload); History.Adapter.bind(window,'unload',History.onUnload); // Both are enabled for consistency } // Non-Native pushState Implementation if ( !History.emulated.pushState ) { // Be aware, the following is only for native pushState implementations // If you are wanting to include something for all browsers // Then include it above this if block /** * Setup Safari Fix */ if ( History.bugs.safariPoll ) { History.intervalList.push(setInterval(History.safariStatePoll, History.options.safariPollInterval)); } /** * Ensure Cross Browser Compatibility */ if ( navigator.vendor === 'Apple Computer, Inc.' || (navigator.appCodeName||'') === 'Mozilla' ) { /** * Fix Safari HashChange Issue */ // Setup Alias History.Adapter.bind(window,'hashchange',function(){ History.Adapter.trigger(window,'popstate'); }); // Initialise Alias if ( History.getHash() ) { History.Adapter.onDomLoad(function(){ History.Adapter.trigger(window,'hashchange'); }); } } } // !History.emulated.pushState }; // History.initCore // Try to Initialise History if (!History.options || !History.options.delayInit) { History.init(); } })(window); ================================================ FILE: scripts/compressed/history.adapter.dojo.js ================================================ (function(e,t){"use strict";var n=e.History=e.History||{},r=e.require;if(typeof n.Adapter!="undefined")throw new Error("History.js Adapter has already been loaded...");n.Adapter={handlers:{},_uid:1,uid:function(e){return e._uid||(e._uid=n.Adapter._uid++)},bind:function(e,t,r){var i=n.Adapter.uid(e);n.Adapter.handlers[i]=n.Adapter.handlers[i]||{},n.Adapter.handlers[i][t]=n.Adapter.handlers[i][t]||[],n.Adapter.handlers[i][t].push(r),e["on"+t]=function(e,t){return function(r){n.Adapter.trigger(e,t,r)}}(e,t)},trigger:function(e,t,r){r=r||{};var i=n.Adapter.uid(e),s,o;n.Adapter.handlers[i]=n.Adapter.handlers[i]||{},n.Adapter.handlers[i][t]=n.Adapter.handlers[i][t]||[];for(s=0,o=n.Adapter.handlers[i][t].length;s")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()})(window) ================================================ FILE: scripts/compressed/json2.js ================================================ typeof JSON!="object"&&(JSON={}),function(){"use strict";function f(e){return e<10?"0"+e:e}function quote(e){return escapable.lastIndex=0,escapable.test(e)?'"'+e.replace(escapable,function(e){var t=meta[e];return typeof t=="string"?t:"\\u"+("0000"+e.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+e+'"'}function str(e,t){var n,r,i,s,o=gap,u,a=t[e];a&&typeof a=="object"&&typeof a.toJSON=="function"&&(a=a.toJSON(e)),typeof rep=="function"&&(a=rep.call(t,e,a));switch(typeof a){case"string":return quote(a);case"number":return isFinite(a)?String(a):"null";case"boolean":case"null":return String(a);case"object":if(!a)return"null";gap+=indent,u=[];if(Object.prototype.toString.apply(a)==="[object Array]"){s=a.length;for(n=0;n * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ // Closure (function(window,undefined){ "use strict"; // Localise Globals var History = window.History = window.History||{}, require = window.require; // Check Existence if ( typeof History.Adapter !== 'undefined' ) { throw new Error('History.js Adapter has already been loaded...'); } // Add the Adapter History.Adapter = { /** * History.Adapter.handlers[uid][eventName] = Array */ handlers: {}, /** * History.Adapter._uid * The current element unique identifier */ _uid: 1, /** * History.Adapter.uid(element) * @param {Element} element * @return {String} uid */ uid: function(element){ return element._uid || (element._uid = History.Adapter._uid++); }, /** * History.Adapter.bind(el,event,callback) * @param {Element} element * @param {String} eventName - custom and standard events * @param {Function} callback * @return */ bind: function(element,eventName,callback){ // Prepare var uid = History.Adapter.uid(element); // Apply Listener History.Adapter.handlers[uid] = History.Adapter.handlers[uid] || {}; History.Adapter.handlers[uid][eventName] = History.Adapter.handlers[uid][eventName] || []; History.Adapter.handlers[uid][eventName].push(callback); // Bind Global Listener element['on'+eventName] = (function(element,eventName){ return function(event){ History.Adapter.trigger(element,eventName,event); }; })(element,eventName); }, /** * History.Adapter.trigger(el,event) * @param {Element} element * @param {String} eventName - custom and standard events * @param {Object} event - a object of event data * @return */ trigger: function(element,eventName,event){ // Prepare event = event || {}; var uid = History.Adapter.uid(element), i,n; // Apply Listener History.Adapter.handlers[uid] = History.Adapter.handlers[uid] || {}; History.Adapter.handlers[uid][eventName] = History.Adapter.handlers[uid][eventName] || []; // Fire Listeners for ( i=0,n=History.Adapter.handlers[uid][eventName].length; i * @copyright 2012 Sean Adkinson * @license New BSD License */ // Closure (function(window,undefined){ "use strict"; // Localise Globals var History = window.History = window.History||{}, Ext = window.Ext; window.JSON = { stringify: Ext.JSON.encode, parse: Ext.JSON.decode }; // Check Existence if ( typeof History.Adapter !== 'undefined' ) { throw new Error('History.js Adapter has already been loaded...'); } // Add the Adapter History.Adapter = { observables: {}, /** * History.Adapter.bind(el,event,callback) * @param {Element|string} el * @param {string} event - custom and standard events * @param {function} callback * @param {Object} scope * @return {void} */ bind: function(element,eventName,callback,scope){ Ext.EventManager.addListener(element, eventName, callback, scope); //bind an observable to the element that will let us "trigger" events on it var id = Ext.id(element, 'history-'), observable = this.observables[id]; if (!observable) { observable = Ext.create('Ext.util.Observable'); this.observables[id] = observable; } observable.on(eventName, callback, scope); }, /** * History.Adapter.trigger(el,event) * @param {Element|string} el * @param {string} event - custom and standard events * @param {Object=} extra - a object of extra event data (optional) * @return {void} */ trigger: function(element,eventName,extra){ var id = Ext.id(element, 'history-'), observable = this.observables[id]; if (observable) { observable.fireEvent(eventName, extra); } }, /** * History.Adapter.extractEventData(key,event,extra) * @param {string} key - key for the event data to extract * @param {string} event - custom and standard events * @param {Object=} extra - a object of extra event data (optional) * @return {mixed} */ extractEventData: function(key,event,extra){ var result = (event && event.browserEvent && event.browserEvent[key]) || (extra && extra[key]) || undefined; return result; }, /** * History.Adapter.onDomLoad(callback) * @param {function} callback * @return {void} */ onDomLoad: function(callback) { Ext.onReady(callback); } }; // Try and Initialise History if ( typeof History.init !== 'undefined' ) { History.init(); } })(window); ================================================ FILE: scripts/uncompressed/history.adapter.jquery.js ================================================ /** * History.js jQuery Adapter * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ // Closure (function(window,undefined){ "use strict"; // Localise Globals var History = window.History = window.History||{}, jQuery = window.jQuery; // Check Existence if ( typeof History.Adapter !== 'undefined' ) { throw new Error('History.js Adapter has already been loaded...'); } // Add the Adapter History.Adapter = { /** * History.Adapter.bind(el,event,callback) * @param {Element|string} el * @param {string} event - custom and standard events * @param {function} callback * @return {void} */ bind: function(el,event,callback){ jQuery(el).bind(event,callback); }, /** * History.Adapter.trigger(el,event) * @param {Element|string} el * @param {string} event - custom and standard events * @param {Object=} extra - a object of extra event data (optional) * @return {void} */ trigger: function(el,event,extra){ jQuery(el).trigger(event,extra); }, /** * History.Adapter.extractEventData(key,event,extra) * @param {string} key - key for the event data to extract * @param {string} event - custom and standard events * @param {Object=} extra - a object of extra event data (optional) * @return {mixed} */ extractEventData: function(key,event,extra){ // jQuery Native then jQuery Custom var result = (event && event.originalEvent && event.originalEvent[key]) || (extra && extra[key]) || undefined; // Return return result; }, /** * History.Adapter.onDomLoad(callback) * @param {function} callback * @return {void} */ onDomLoad: function(callback) { jQuery(callback); } }; // Try and Initialise History if ( typeof History.init !== 'undefined' ) { History.init(); } })(window); ================================================ FILE: scripts/uncompressed/history.adapter.mootools.js ================================================ /** * History.js MooTools Adapter * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ // Closure (function(window,undefined){ "use strict"; // Localise Globals var History = window.History = window.History||{}, MooTools = window.MooTools, Element = window.Element; // Check Existence if ( typeof History.Adapter !== 'undefined' ) { throw new Error('History.js Adapter has already been loaded...'); } // Make MooTools aware of History.js Events Object.append(Element.NativeEvents,{ 'popstate':2, 'hashchange':2 }); // Add the Adapter History.Adapter = { /** * History.Adapter.bind(el,event,callback) * @param {Element|string} el * @param {string} event - custom and standard events * @param {function} callback * @return {void} */ bind: function(el,event,callback){ var El = typeof el === 'string' ? document.id(el) : el; El.addEvent(event,callback); }, /** * History.Adapter.trigger(el,event) * @param {Element|string} el * @param {string} event - custom and standard events * @param {Object=} extra - a object of extra event data (optional) * @return void */ trigger: function(el,event,extra){ var El = typeof el === 'string' ? document.id(el) : el; El.fireEvent(event,extra); }, /** * History.Adapter.extractEventData(key,event,extra) * @param {string} key - key for the event data to extract * @param {string} event - custom and standard events * @return {mixed} */ extractEventData: function(key,event){ // MooTools Native then MooTools Custom var result = (event && event.event && event.event[key]) || (event && event[key]) || undefined; // Return return result; }, /** * History.Adapter.onDomLoad(callback) * @param {function} callback * @return {void} */ onDomLoad: function(callback) { window.addEvent('domready',callback); } }; // Try and Initialise History if ( typeof History.init !== 'undefined' ) { History.init(); } })(window); ================================================ FILE: scripts/uncompressed/history.adapter.native.js ================================================ /** * History.js Native Adapter * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ // Closure (function(window,undefined){ "use strict"; // Localise Globals var History = window.History = window.History||{}; // Check Existence if ( typeof History.Adapter !== 'undefined' ) { throw new Error('History.js Adapter has already been loaded...'); } // Add the Adapter History.Adapter = { /** * History.Adapter.handlers[uid][eventName] = Array */ handlers: {}, /** * History.Adapter._uid * The current element unique identifier */ _uid: 1, /** * History.Adapter.uid(element) * @param {Element} element * @return {String} uid */ uid: function(element){ return element._uid || (element._uid = History.Adapter._uid++); }, /** * History.Adapter.bind(el,event,callback) * @param {Element} element * @param {String} eventName - custom and standard events * @param {Function} callback * @return */ bind: function(element,eventName,callback){ // Prepare var uid = History.Adapter.uid(element); // Apply Listener History.Adapter.handlers[uid] = History.Adapter.handlers[uid] || {}; History.Adapter.handlers[uid][eventName] = History.Adapter.handlers[uid][eventName] || []; History.Adapter.handlers[uid][eventName].push(callback); // Bind Global Listener element['on'+eventName] = (function(element,eventName){ return function(event){ History.Adapter.trigger(element,eventName,event); }; })(element,eventName); }, /** * History.Adapter.trigger(el,event) * @param {Element} element * @param {String} eventName - custom and standard events * @param {Object} event - a object of event data * @return */ trigger: function(element,eventName,event){ // Prepare event = event || {}; var uid = History.Adapter.uid(element), i,n; // Apply Listener History.Adapter.handlers[uid] = History.Adapter.handlers[uid] || {}; History.Adapter.handlers[uid][eventName] = History.Adapter.handlers[uid][eventName] || []; // Fire Listeners for ( i=0,n=History.Adapter.handlers[uid][eventName].length; i * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ // Closure (function(window,undefined){ "use strict"; // Localise Globals var History = window.History = window.History||{}, document = window.document, RightJS = window.RightJS, $ = RightJS.$; // Check Existence if ( typeof History.Adapter !== 'undefined' ) { throw new Error('History.js Adapter has already been loaded...'); } // Add the Adapter History.Adapter = { /** * History.Adapter.bind(el,event,callback) * @param {Element|Selector} el * @param {String} event - custom and standard events * @param {Function} callback * @return */ bind: function(el,event,callback){ $(el).on(event,callback); }, /** * History.Adapter.trigger(el,event) * @param {Element|Selector} el * @param {String} event - custom and standard events * @param {Object} extraEventData - a object of extra event data * @return */ trigger: function(el,event,extraEventData){ $(el).fire(event,extraEventData); }, /** * History.Adapter.extractEventData(key,event,extra) * @param {String} key - key for the event data to extract * @param {String} event - custom and standard events * @return {mixed} */ extractEventData: function(key,event){ // Right.js Native // Right.js Custom var result = (event && event._ && event._[key]) || undefined; // Return return result; }, /** * History.Adapter.onDomLoad(callback) * @param {Function} callback * @return */ onDomLoad: function(callback) { $(document).onReady(callback); } }; // Try and Initialise History if ( typeof History.init !== 'undefined' ) { History.init(); } })(window); ================================================ FILE: scripts/uncompressed/history.adapter.zepto.js ================================================ /** * History.js Zepto Adapter * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ // Closure (function(window,undefined){ "use strict"; // Localise Globals var History = window.History = window.History||{}, Zepto = window.Zepto; // Check Existence if ( typeof History.Adapter !== 'undefined' ) { throw new Error('History.js Adapter has already been loaded...'); } // Add the Adapter History.Adapter = { /** * History.Adapter.bind(el,event,callback) * @param {Element|string} el * @param {string} event - custom and standard events * @param {function} callback * @return {void} */ bind: function(el,event,callback){ new Zepto(el).bind(event,callback); }, /** * History.Adapter.trigger(el,event) * @param {Element|string} el * @param {string} event - custom and standard events * @return {void} */ trigger: function(el,event){ new Zepto(el).trigger(event); }, /** * History.Adapter.extractEventData(key,event,extra) * @param {string} key - key for the event data to extract * @param {string} event - custom and standard events * @return {mixed} */ extractEventData: function(key,event){ // Zepto Native var result = (event && event[key]) || undefined; // Return return result; }, /** * History.Adapter.onDomLoad(callback) * @param {function} callback * @return {void} */ onDomLoad: function(callback) { new Zepto(callback); } }; // Try and Initialise History if ( typeof History.init !== 'undefined' ) { History.init(); } })(window); ================================================ FILE: scripts/uncompressed/history.html4.js ================================================ /** * History.js HTML4 Support * Depends on the HTML5 Support * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window,undefined){ "use strict"; // ======================================================================== // Initialise // Localise Globals var document = window.document, // Make sure we are using the correct document setTimeout = window.setTimeout||setTimeout, clearTimeout = window.clearTimeout||clearTimeout, setInterval = window.setInterval||setInterval, History = window.History = window.History||{}; // Public History Object // Check Existence if ( typeof History.initHtml4 !== 'undefined' ) { throw new Error('History.js HTML4 Support has already been loaded...'); } // ======================================================================== // Initialise HTML4 Support // Initialise HTML4 Support History.initHtml4 = function(){ // Initialise if ( typeof History.initHtml4.initialized !== 'undefined' ) { // Already Loaded return false; } else { History.initHtml4.initialized = true; } // ==================================================================== // Properties /** * History.enabled * Is History enabled? */ History.enabled = true; // ==================================================================== // Hash Storage /** * History.savedHashes * Store the hashes in an array */ History.savedHashes = []; /** * History.isLastHash(newHash) * Checks if the hash is the last hash * @param {string} newHash * @return {boolean} true */ History.isLastHash = function(newHash){ // Prepare var oldHash = History.getHashByIndex(), isLast; // Check isLast = newHash === oldHash; // Return isLast return isLast; }; /** * History.isHashEqual(newHash, oldHash) * Checks to see if two hashes are functionally equal * @param {string} newHash * @param {string} oldHash * @return {boolean} true */ History.isHashEqual = function(newHash, oldHash){ newHash = encodeURIComponent(newHash).replace(/%25/g, "%"); oldHash = encodeURIComponent(oldHash).replace(/%25/g, "%"); return newHash === oldHash; }; /** * History.saveHash(newHash) * Push a Hash * @param {string} newHash * @return {boolean} true */ History.saveHash = function(newHash){ // Check Hash if ( History.isLastHash(newHash) ) { return false; } // Push the Hash History.savedHashes.push(newHash); // Return true return true; }; /** * History.getHashByIndex() * Gets a hash by the index * @param {integer} index * @return {string} */ History.getHashByIndex = function(index){ // Prepare var hash = null; // Handle if ( typeof index === 'undefined' ) { // Get the last inserted hash = History.savedHashes[History.savedHashes.length-1]; } else if ( index < 0 ) { // Get from the end hash = History.savedHashes[History.savedHashes.length+index]; } else { // Get from the beginning hash = History.savedHashes[index]; } // Return hash return hash; }; // ==================================================================== // Discarded States /** * History.discardedHashes * A hashed array of discarded hashes */ History.discardedHashes = {}; /** * History.discardedStates * A hashed array of discarded states */ History.discardedStates = {}; /** * History.discardState(State) * Discards the state by ignoring it through History * @param {object} State * @return {true} */ History.discardState = function(discardedState,forwardState,backState){ //History.debug('History.discardState', arguments); // Prepare var discardedStateHash = History.getHashByState(discardedState), discardObject; // Create Discard Object discardObject = { 'discardedState': discardedState, 'backState': backState, 'forwardState': forwardState }; // Add to DiscardedStates History.discardedStates[discardedStateHash] = discardObject; // Return true return true; }; /** * History.discardHash(hash) * Discards the hash by ignoring it through History * @param {string} hash * @return {true} */ History.discardHash = function(discardedHash,forwardState,backState){ //History.debug('History.discardState', arguments); // Create Discard Object var discardObject = { 'discardedHash': discardedHash, 'backState': backState, 'forwardState': forwardState }; // Add to discardedHash History.discardedHashes[discardedHash] = discardObject; // Return true return true; }; /** * History.discardedState(State) * Checks to see if the state is discarded * @param {object} State * @return {bool} */ History.discardedState = function(State){ // Prepare var StateHash = History.getHashByState(State), discarded; // Check discarded = History.discardedStates[StateHash]||false; // Return true return discarded; }; /** * History.discardedHash(hash) * Checks to see if the state is discarded * @param {string} State * @return {bool} */ History.discardedHash = function(hash){ // Check var discarded = History.discardedHashes[hash]||false; // Return true return discarded; }; /** * History.recycleState(State) * Allows a discarded state to be used again * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.recycleState = function(State){ //History.debug('History.recycleState', arguments); // Prepare var StateHash = History.getHashByState(State); // Remove from DiscardedStates if ( History.discardedState(State) ) { delete History.discardedStates[StateHash]; } // Return true return true; }; // ==================================================================== // HTML4 HashChange Support if ( History.emulated.hashChange ) { /* * We must emulate the HTML4 HashChange Support by manually checking for hash changes */ /** * History.hashChangeInit() * Init the HashChange Emulation */ History.hashChangeInit = function(){ // Define our Checker Function History.checkerFunction = null; // Define some variables that will help in our checker function var lastDocumentHash = '', iframeId, iframe, lastIframeHash, checkerRunning, startedWithHash = Boolean(History.getHash()); // Handle depending on the browser if ( History.isInternetExplorer() ) { // IE6 and IE7 // We need to use an iframe to emulate the back and forward buttons // Create iFrame iframeId = 'historyjs-iframe'; iframe = document.createElement('iframe'); // Adjust iFarme // IE 6 requires iframe to have a src on HTTPS pages, otherwise it will throw a // "This page contains both secure and nonsecure items" warning. iframe.setAttribute('id', iframeId); iframe.setAttribute('src', '#'); iframe.style.display = 'none'; // Append iFrame document.body.appendChild(iframe); // Create initial history entry iframe.contentWindow.document.open(); iframe.contentWindow.document.close(); // Define some variables that will help in our checker function lastIframeHash = ''; checkerRunning = false; // Define the checker function History.checkerFunction = function(){ // Check Running if ( checkerRunning ) { return false; } // Update Running checkerRunning = true; // Fetch var documentHash = History.getHash(), iframeHash = History.getHash(iframe.contentWindow.document); // The Document Hash has changed (application caused) if ( documentHash !== lastDocumentHash ) { // Equalise lastDocumentHash = documentHash; // Create a history entry in the iframe if ( iframeHash !== documentHash ) { //History.debug('hashchange.checker: iframe hash change', 'documentHash (new):', documentHash, 'iframeHash (old):', iframeHash); // Equalise lastIframeHash = iframeHash = documentHash; // Create History Entry iframe.contentWindow.document.open(); iframe.contentWindow.document.close(); // Update the iframe's hash iframe.contentWindow.document.location.hash = History.escapeHash(documentHash); } // Trigger Hashchange Event History.Adapter.trigger(window,'hashchange'); } // The iFrame Hash has changed (back button caused) else if ( iframeHash !== lastIframeHash ) { //History.debug('hashchange.checker: iframe hash out of sync', 'iframeHash (new):', iframeHash, 'documentHash (old):', documentHash); // Equalise lastIframeHash = iframeHash; // If there is no iframe hash that means we're at the original // iframe state. // And if there was a hash on the original request, the original // iframe state was replaced instantly, so skip this state and take // the user back to where they came from. if (startedWithHash && iframeHash === '') { History.back(); } else { // Update the Hash History.setHash(iframeHash,false); } } // Reset Running checkerRunning = false; // Return true return true; }; } else { // We are not IE // Firefox 1 or 2, Opera // Define the checker function History.checkerFunction = function(){ // Prepare var documentHash = History.getHash()||''; // The Document Hash has changed (application caused) if ( documentHash !== lastDocumentHash ) { // Equalise lastDocumentHash = documentHash; // Trigger Hashchange Event History.Adapter.trigger(window,'hashchange'); } // Return true return true; }; } // Apply the checker function History.intervalList.push(setInterval(History.checkerFunction, History.options.hashChangeInterval)); // Done return true; }; // History.hashChangeInit // Bind hashChangeInit History.Adapter.onDomLoad(History.hashChangeInit); } // History.emulated.hashChange // ==================================================================== // HTML5 State Support // Non-Native pushState Implementation if ( History.emulated.pushState ) { /* * We must emulate the HTML5 State Management by using HTML4 HashChange */ /** * History.onHashChange(event) * Trigger HTML5's window.onpopstate via HTML4 HashChange Support */ History.onHashChange = function(event){ //History.debug('History.onHashChange', arguments); // Prepare var currentUrl = ((event && event.newURL) || History.getLocationHref()), currentHash = History.getHashByUrl(currentUrl), currentState = null, currentStateHash = null, currentStateHashExits = null, discardObject; // Check if we are the same state if ( History.isLastHash(currentHash) ) { // There has been no change (just the page's hash has finally propagated) //History.debug('History.onHashChange: no change'); History.busy(false); return false; } // Reset the double check History.doubleCheckComplete(); // Store our location for use in detecting back/forward direction History.saveHash(currentHash); // Expand Hash if ( currentHash && History.isTraditionalAnchor(currentHash) ) { //History.debug('History.onHashChange: traditional anchor', currentHash); // Traditional Anchor Hash History.Adapter.trigger(window,'anchorchange'); History.busy(false); return false; } // Create State currentState = History.extractState(History.getFullUrl(currentHash||History.getLocationHref()),true); // Check if we are the same state if ( History.isLastSavedState(currentState) ) { //History.debug('History.onHashChange: no change'); // There has been no change (just the page's hash has finally propagated) History.busy(false); return false; } // Create the state Hash currentStateHash = History.getHashByState(currentState); // Check if we are DiscardedState discardObject = History.discardedState(currentState); if ( discardObject ) { // Ignore this state as it has been discarded and go back to the state before it if ( History.getHashByIndex(-2) === History.getHashByState(discardObject.forwardState) ) { // We are going backwards //History.debug('History.onHashChange: go backwards'); History.back(false); } else { // We are going forwards //History.debug('History.onHashChange: go forwards'); History.forward(false); } return false; } // Push the new HTML5 State //History.debug('History.onHashChange: success hashchange'); History.pushState(currentState.data,currentState.title,encodeURI(currentState.url),false); // End onHashChange closure return true; }; History.Adapter.bind(window,'hashchange',History.onHashChange); /** * History.pushState(data,title,url) * Add a new State to the history object, become it, and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.pushState = function(data,title,url,queue){ //History.debug('History.pushState: called', arguments); // We assume that the URL passed in is URI-encoded, but this makes // sure that it's fully URI encoded; any '%'s that are encoded are // converted back into '%'s url = encodeURI(url).replace(/%25/g, "%"); // Check the State if ( History.getHashByUrl(url) ) { throw new Error('History.js does not support states with fragment-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.pushState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.pushState, args: arguments, queue: queue }); return false; } // Make Busy History.busy(true); // Fetch the State Object var newState = History.createStateObject(data,title,url), newStateHash = History.getHashByState(newState), oldState = History.getState(false), oldStateHash = History.getHashByState(oldState), html4Hash = History.getHash(), wasExpected = History.expectedStateId == newState.id; // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Recycle the State History.recycleState(newState); // Force update of the title History.setTitle(newState); // Check if we are the same State if ( newStateHash === oldStateHash ) { //History.debug('History.pushState: no change', newStateHash); History.busy(false); return false; } // Update HTML5 State History.saveState(newState); // Fire HTML5 Event if(!wasExpected) History.Adapter.trigger(window,'statechange'); // Update HTML4 Hash if ( !History.isHashEqual(newStateHash, html4Hash) && !History.isHashEqual(newStateHash, History.getShortUrl(History.getLocationHref())) ) { History.setHash(newStateHash,false); } History.busy(false); // End pushState closure return true; }; /** * History.replaceState(data,title,url) * Replace the State and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.replaceState = function(data,title,url,queue){ //History.debug('History.replaceState: called', arguments); // We assume that the URL passed in is URI-encoded, but this makes // sure that it's fully URI encoded; any '%'s that are encoded are // converted back into '%'s url = encodeURI(url).replace(/%25/g, "%"); // Check the State if ( History.getHashByUrl(url) ) { throw new Error('History.js does not support states with fragment-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.replaceState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.replaceState, args: arguments, queue: queue }); return false; } // Make Busy History.busy(true); // Fetch the State Objects var newState = History.createStateObject(data,title,url), newStateHash = History.getHashByState(newState), oldState = History.getState(false), oldStateHash = History.getHashByState(oldState), previousState = History.getStateByIndex(-2); // Discard Old State History.discardState(oldState,newState,previousState); // If the url hasn't changed, just store and save the state // and fire a statechange event to be consistent with the // html 5 api if ( newStateHash === oldStateHash ) { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Recycle the State History.recycleState(newState); // Force update of the title History.setTitle(newState); // Update HTML5 State History.saveState(newState); // Fire HTML5 Event //History.debug('History.pushState: trigger popstate'); History.Adapter.trigger(window,'statechange'); History.busy(false); } else { // Alias to PushState History.pushState(newState.data,newState.title,newState.url,false); } // End replaceState closure return true; }; } // History.emulated.pushState // ==================================================================== // Initialise // Non-Native pushState Implementation if ( History.emulated.pushState ) { /** * Ensure initial state is handled correctly */ if ( History.getHash() && !History.emulated.hashChange ) { History.Adapter.onDomLoad(function(){ History.Adapter.trigger(window,'hashchange'); }); } } // History.emulated.pushState }; // History.initHtml4 // Try to Initialise History if ( typeof History.init !== 'undefined' ) { History.init(); } })(window); ================================================ FILE: scripts/uncompressed/history.js ================================================ /** * History.js Core * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window,undefined){ "use strict"; // ======================================================================== // Initialise // Localise Globals var console = window.console||undefined, // Prevent a JSLint complain document = window.document, // Make sure we are using the correct document navigator = window.navigator, // Make sure we are using the correct navigator sessionStorage = false, // sessionStorage setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, setInterval = window.setInterval, clearInterval = window.clearInterval, JSON = window.JSON, alert = window.alert, History = window.History = window.History||{}, // Public History Object history = window.history; // Old History Object try { sessionStorage = window.sessionStorage; // This will throw an exception in some browsers when cookies/localStorage are explicitly disabled (i.e. Chrome) sessionStorage.setItem('TEST', '1'); sessionStorage.removeItem('TEST'); } catch(e) { sessionStorage = false; } // MooTools Compatibility JSON.stringify = JSON.stringify||JSON.encode; JSON.parse = JSON.parse||JSON.decode; // Check Existence if ( typeof History.init !== 'undefined' ) { throw new Error('History.js Core has already been loaded...'); } // Initialise History History.init = function(options){ // Check Load Status of Adapter if ( typeof History.Adapter === 'undefined' ) { return false; } // Check Load Status of Core if ( typeof History.initCore !== 'undefined' ) { History.initCore(); } // Check Load Status of HTML4 Support if ( typeof History.initHtml4 !== 'undefined' ) { History.initHtml4(); } // Return true return true; }; // ======================================================================== // Initialise Core // Initialise Core History.initCore = function(options){ // Initialise if ( typeof History.initCore.initialized !== 'undefined' ) { // Already Loaded return false; } else { History.initCore.initialized = true; } // ==================================================================== // Options /** * History.options * Configurable options */ History.options = History.options||{}; /** * History.options.hashChangeInterval * How long should the interval be before hashchange checks */ History.options.hashChangeInterval = History.options.hashChangeInterval || 100; /** * History.options.safariPollInterval * How long should the interval be before safari poll checks */ History.options.safariPollInterval = History.options.safariPollInterval || 500; /** * History.options.doubleCheckInterval * How long should the interval be before we perform a double check */ History.options.doubleCheckInterval = History.options.doubleCheckInterval || 500; /** * History.options.disableSuid * Force History not to append suid */ History.options.disableSuid = History.options.disableSuid || false; /** * History.options.storeInterval * How long should we wait between store calls */ History.options.storeInterval = History.options.storeInterval || 1000; /** * History.options.busyDelay * How long should we wait between busy events */ History.options.busyDelay = History.options.busyDelay || 250; /** * History.options.debug * If true will enable debug messages to be logged */ History.options.debug = History.options.debug || false; /** * History.options.initialTitle * What is the title of the initial state */ History.options.initialTitle = History.options.initialTitle || document.title; /** * History.options.html4Mode * If true, will force HTMl4 mode (hashtags) */ History.options.html4Mode = History.options.html4Mode || false; /** * History.options.delayInit * Want to override default options and call init manually. */ History.options.delayInit = History.options.delayInit || false; // ==================================================================== // Interval record /** * History.intervalList * List of intervals set, to be cleared when document is unloaded. */ History.intervalList = []; /** * History.clearAllIntervals * Clears all setInterval instances. */ History.clearAllIntervals = function(){ var i, il = History.intervalList; if (typeof il !== "undefined" && il !== null) { for (i = 0; i < il.length; i++) { clearInterval(il[i]); } History.intervalList = null; } }; // ==================================================================== // Debug /** * History.debug(message,...) * Logs the passed arguments if debug enabled */ History.debug = function(){ if ( (History.options.debug||false) ) { History.log.apply(History,arguments); } }; /** * History.log(message,...) * Logs the passed arguments */ History.log = function(){ // Prepare var consoleExists = !(typeof console === 'undefined' || typeof console.log === 'undefined' || typeof console.log.apply === 'undefined'), textarea = document.getElementById('log'), message, i,n, args,arg ; // Write to Console if ( consoleExists ) { args = Array.prototype.slice.call(arguments); message = args.shift(); if ( typeof console.debug !== 'undefined' ) { console.debug.apply(console,[message,args]); } else { console.log.apply(console,[message,args]); } } else { message = ("\n"+arguments[0]+"\n"); } // Write to log for ( i=1,n=arguments.length; i * @author James Padolsey */ History.getInternetExplorerMajorVersion = function(){ var result = History.getInternetExplorerMajorVersion.cached = (typeof History.getInternetExplorerMajorVersion.cached !== 'undefined') ? History.getInternetExplorerMajorVersion.cached : (function(){ var v = 3, div = document.createElement('div'), all = div.getElementsByTagName('i'); while ( (div.innerHTML = '') && all[0] ) {} return (v > 4) ? v : false; })() ; return result; }; /** * History.isInternetExplorer() * Are we using Internet Explorer? * @return {boolean} * @license Public Domain * @author Benjamin Arthur Lupton */ History.isInternetExplorer = function(){ var result = History.isInternetExplorer.cached = (typeof History.isInternetExplorer.cached !== 'undefined') ? History.isInternetExplorer.cached : Boolean(History.getInternetExplorerMajorVersion()) ; return result; }; /** * History.emulated * Which features require emulating? */ if (History.options.html4Mode) { History.emulated = { pushState : true, hashChange: true }; } else { History.emulated = { pushState: !Boolean( window.history && window.history.pushState && window.history.replaceState && !( (/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i).test(navigator.userAgent) /* disable for versions of iOS before version 4.3 (8F190) */ || (/AppleWebKit\/5([0-2]|3[0-2])/i).test(navigator.userAgent) /* disable for the mercury iOS browser, or at least older versions of the webkit engine */ ) ), hashChange: Boolean( !(('onhashchange' in window) || ('onhashchange' in document)) || (History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8) ) }; } /** * History.enabled * Is History enabled? */ History.enabled = !History.emulated.pushState; /** * History.bugs * Which bugs are present */ History.bugs = { /** * Safari 5 and Safari iOS 4 fail to return to the correct state once a hash is replaced by a `replaceState` call * https://bugs.webkit.org/show_bug.cgi?id=56249 */ setHash: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * Safari 5 and Safari iOS 4 sometimes fail to apply the state change under busy conditions * https://bugs.webkit.org/show_bug.cgi?id=42940 */ safariPoll: Boolean(!History.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)), /** * MSIE 6 and 7 sometimes do not apply a hash even it was told to (requiring a second call to the apply function) */ ieDoubleCheck: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8), /** * MSIE 6 requires the entire hash to be encoded for the hashes to trigger the onHashChange event */ hashEscape: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 7) }; /** * History.isEmptyObject(obj) * Checks to see if the Object is Empty * @param {Object} obj * @return {boolean} */ History.isEmptyObject = function(obj) { for ( var name in obj ) { if ( obj.hasOwnProperty(name) ) { return false; } } return true; }; /** * History.cloneObject(obj) * Clones a object and eliminate all references to the original contexts * @param {Object} obj * @return {Object} */ History.cloneObject = function(obj) { var hash,newObj; if ( obj ) { hash = JSON.stringify(obj); newObj = JSON.parse(hash); } else { newObj = {}; } return newObj; }; // ==================================================================== // URL Helpers /** * History.getRootUrl() * Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com" * @return {String} rootUrl */ History.getRootUrl = function(){ // Create var rootUrl = document.location.protocol+'//'+(document.location.hostname||document.location.host); if ( document.location.port||false ) { rootUrl += ':'+document.location.port; } rootUrl += '/'; // Return return rootUrl; }; /** * History.getBaseHref() * Fetches the `href` attribute of the `` element if it exists * @return {String} baseHref */ History.getBaseHref = function(){ // Create var baseElements = document.getElementsByTagName('base'), baseElement = null, baseHref = ''; // Test for Base Element if ( baseElements.length === 1 ) { // Prepare for Base Element baseElement = baseElements[0]; baseHref = baseElement.href.replace(/[^\/]+$/,''); } // Adjust trailing slash baseHref = baseHref.replace(/\/+$/,''); if ( baseHref ) baseHref += '/'; // Return return baseHref; }; /** * History.getBaseUrl() * Fetches the baseHref or basePageUrl or rootUrl (whichever one exists first) * @return {String} baseUrl */ History.getBaseUrl = function(){ // Create var baseUrl = History.getBaseHref()||History.getBasePageUrl()||History.getRootUrl(); // Return return baseUrl; }; /** * History.getPageUrl() * Fetches the URL of the current page * @return {String} pageUrl */ History.getPageUrl = function(){ // Fetch var State = History.getState(false,false), stateUrl = (State||{}).url||History.getLocationHref(), pageUrl; // Create pageUrl = stateUrl.replace(/\/+$/,'').replace(/[^\/]+$/,function(part,index,string){ return (/\./).test(part) ? part : part+'/'; }); // Return return pageUrl; }; /** * History.getBasePageUrl() * Fetches the Url of the directory of the current page * @return {String} basePageUrl */ History.getBasePageUrl = function(){ // Create var basePageUrl = (History.getLocationHref()).replace(/[#\?].*/,'').replace(/[^\/]+$/,function(part,index,string){ return (/[^\/]$/).test(part) ? '' : part; }).replace(/\/+$/,'')+'/'; // Return return basePageUrl; }; /** * History.getFullUrl(url) * Ensures that we have an absolute URL and not a relative URL * @param {string} url * @param {Boolean} allowBaseHref * @return {string} fullUrl */ History.getFullUrl = function(url,allowBaseHref){ // Prepare var fullUrl = url, firstChar = url.substring(0,1); allowBaseHref = (typeof allowBaseHref === 'undefined') ? true : allowBaseHref; // Check if ( /[a-z]+\:\/\//.test(url) ) { // Full URL } else if ( firstChar === '/' ) { // Root URL fullUrl = History.getRootUrl()+url.replace(/^\/+/,''); } else if ( firstChar === '#' ) { // Anchor URL fullUrl = History.getPageUrl().replace(/#.*/,'')+url; } else if ( firstChar === '?' ) { // Query URL fullUrl = History.getPageUrl().replace(/[\?#].*/,'')+url; } else { // Relative URL if ( allowBaseHref ) { fullUrl = History.getBaseUrl()+url.replace(/^(\.\/)+/,''); } else { fullUrl = History.getBasePageUrl()+url.replace(/^(\.\/)+/,''); } // We have an if condition above as we do not want hashes // which are relative to the baseHref in our URLs // as if the baseHref changes, then all our bookmarks // would now point to different locations // whereas the basePageUrl will always stay the same } // Return return fullUrl.replace(/\#$/,''); }; /** * History.getShortUrl(url) * Ensures that we have a relative URL and not a absolute URL * @param {string} url * @return {string} url */ History.getShortUrl = function(url){ // Prepare var shortUrl = url, baseUrl = History.getBaseUrl(), rootUrl = History.getRootUrl(); // Trim baseUrl if ( History.emulated.pushState ) { // We are in a if statement as when pushState is not emulated // The actual url these short urls are relative to can change // So within the same session, we the url may end up somewhere different shortUrl = shortUrl.replace(baseUrl,''); } // Trim rootUrl shortUrl = shortUrl.replace(rootUrl,'/'); // Ensure we can still detect it as a state if ( History.isTraditionalAnchor(shortUrl) ) { shortUrl = './'+shortUrl; } // Clean It shortUrl = shortUrl.replace(/^(\.\/)+/g,'./').replace(/\#$/,''); // Return return shortUrl; }; /** * History.getLocationHref(document) * Returns a normalized version of document.location.href * accounting for browser inconsistencies, etc. * * This URL will be URI-encoded and will include the hash * * @param {object} document * @return {string} url */ History.getLocationHref = function(doc) { doc = doc || document; // most of the time, this will be true if (doc.URL === doc.location.href) return doc.location.href; // some versions of webkit URI-decode document.location.href // but they leave document.URL in an encoded state if (doc.location.href === decodeURIComponent(doc.URL)) return doc.URL; // FF 3.6 only updates document.URL when a page is reloaded // document.location.href is updated correctly if (doc.location.hash && decodeURIComponent(doc.location.href.replace(/^[^#]+/, "")) === doc.location.hash) return doc.location.href; if (doc.URL.indexOf('#') == -1 && doc.location.href.indexOf('#') != -1) return doc.location.href; return doc.URL || doc.location.href; }; // ==================================================================== // State Storage /** * History.store * The store for all session specific data */ History.store = {}; /** * History.idToState * 1-1: State ID to State Object */ History.idToState = History.idToState||{}; /** * History.stateToId * 1-1: State String to State ID */ History.stateToId = History.stateToId||{}; /** * History.urlToId * 1-1: State URL to State ID */ History.urlToId = History.urlToId||{}; /** * History.storedStates * Store the states in an array */ History.storedStates = History.storedStates||[]; /** * History.savedStates * Saved the states in an array */ History.savedStates = History.savedStates||[]; /** * History.noramlizeStore() * Noramlize the store by adding necessary values */ History.normalizeStore = function(){ History.store.idToState = History.store.idToState||{}; History.store.urlToId = History.store.urlToId||{}; History.store.stateToId = History.store.stateToId||{}; }; /** * History.getState() * Get an object containing the data, title and url of the current state * @param {Boolean} friendly * @param {Boolean} create * @return {Object} State */ History.getState = function(friendly,create){ // Prepare if ( typeof friendly === 'undefined' ) { friendly = true; } if ( typeof create === 'undefined' ) { create = true; } // Fetch var State = History.getLastSavedState(); // Create if ( !State && create ) { State = History.createStateObject(); } // Adjust if ( friendly ) { State = History.cloneObject(State); State.url = State.cleanUrl||State.url; } // Return return State; }; /** * History.getIdByState(State) * Gets a ID for a State * @param {State} newState * @return {String} id */ History.getIdByState = function(newState){ // Fetch ID var id = History.extractId(newState.url), str; if ( !id ) { // Find ID via State String str = History.getStateString(newState); if ( typeof History.stateToId[str] !== 'undefined' ) { id = History.stateToId[str]; } else if ( typeof History.store.stateToId[str] !== 'undefined' ) { id = History.store.stateToId[str]; } else { // Generate a new ID while ( true ) { id = (new Date()).getTime() + String(Math.random()).replace(/\D/g,''); if ( typeof History.idToState[id] === 'undefined' && typeof History.store.idToState[id] === 'undefined' ) { break; } } // Apply the new State to the ID History.stateToId[str] = id; History.idToState[id] = newState; } } // Return ID return id; }; /** * History.normalizeState(State) * Expands a State Object * @param {object} State * @return {object} */ History.normalizeState = function(oldState){ // Variables var newState, dataNotEmpty; // Prepare if ( !oldState || (typeof oldState !== 'object') ) { oldState = {}; } // Check if ( typeof oldState.normalized !== 'undefined' ) { return oldState; } // Adjust if ( !oldState.data || (typeof oldState.data !== 'object') ) { oldState.data = {}; } // ---------------------------------------------------------------- // Create newState = {}; newState.normalized = true; newState.title = oldState.title||''; newState.url = History.getFullUrl(oldState.url?oldState.url:(History.getLocationHref())); newState.hash = History.getShortUrl(newState.url); newState.data = History.cloneObject(oldState.data); // Fetch ID newState.id = History.getIdByState(newState); // ---------------------------------------------------------------- // Clean the URL newState.cleanUrl = newState.url.replace(/\??\&_suid.*/,''); newState.url = newState.cleanUrl; // Check to see if we have more than just a url dataNotEmpty = !History.isEmptyObject(newState.data); // Apply if ( (newState.title || dataNotEmpty) && History.options.disableSuid !== true ) { // Add ID to Hash newState.hash = History.getShortUrl(newState.url).replace(/\??\&_suid.*/,''); if ( !/\?/.test(newState.hash) ) { newState.hash += '?'; } newState.hash += '&_suid='+newState.id; } // Create the Hashed URL newState.hashedUrl = History.getFullUrl(newState.hash); // ---------------------------------------------------------------- // Update the URL if we have a duplicate if ( (History.emulated.pushState || History.bugs.safariPoll) && History.hasUrlDuplicate(newState) ) { newState.url = newState.hashedUrl; } // ---------------------------------------------------------------- // Return return newState; }; /** * History.createStateObject(data,title,url) * Creates a object based on the data, title and url state params * @param {object} data * @param {string} title * @param {string} url * @return {object} */ History.createStateObject = function(data,title,url){ // Hashify var State = { 'data': data, 'title': title, 'url': url }; // Expand the State State = History.normalizeState(State); // Return object return State; }; /** * History.getStateById(id) * Get a state by it's UID * @param {String} id */ History.getStateById = function(id){ // Prepare id = String(id); // Retrieve var State = History.idToState[id] || History.store.idToState[id] || undefined; // Return State return State; }; /** * Get a State's String * @param {State} passedState */ History.getStateString = function(passedState){ // Prepare var State, cleanedState, str; // Fetch State = History.normalizeState(passedState); // Clean cleanedState = { data: State.data, title: passedState.title, url: passedState.url }; // Fetch str = JSON.stringify(cleanedState); // Return return str; }; /** * Get a State's ID * @param {State} passedState * @return {String} id */ History.getStateId = function(passedState){ // Prepare var State, id; // Fetch State = History.normalizeState(passedState); // Fetch id = State.id; // Return return id; }; /** * History.getHashByState(State) * Creates a Hash for the State Object * @param {State} passedState * @return {String} hash */ History.getHashByState = function(passedState){ // Prepare var State, hash; // Fetch State = History.normalizeState(passedState); // Hash hash = State.hash; // Return return hash; }; /** * History.extractId(url_or_hash) * Get a State ID by it's URL or Hash * @param {string} url_or_hash * @return {string} id */ History.extractId = function ( url_or_hash ) { // Prepare var id,parts,url, tmp; // Extract // If the URL has a #, use the id from before the # if (url_or_hash.indexOf('#') != -1) { tmp = url_or_hash.split("#")[0]; } else { tmp = url_or_hash; } parts = /(.*)\&_suid=([0-9]+)$/.exec(tmp); url = parts ? (parts[1]||url_or_hash) : url_or_hash; id = parts ? String(parts[2]||'') : ''; // Return return id||false; }; /** * History.isTraditionalAnchor * Checks to see if the url is a traditional anchor or not * @param {String} url_or_hash * @return {Boolean} */ History.isTraditionalAnchor = function(url_or_hash){ // Check var isTraditional = !(/[\/\?\.]/.test(url_or_hash)); // Return return isTraditional; }; /** * History.extractState * Get a State by it's URL or Hash * @param {String} url_or_hash * @return {State|null} */ History.extractState = function(url_or_hash,create){ // Prepare var State = null, id, url; create = create||false; // Fetch SUID id = History.extractId(url_or_hash); if ( id ) { State = History.getStateById(id); } // Fetch SUID returned no State if ( !State ) { // Fetch URL url = History.getFullUrl(url_or_hash); // Check URL id = History.getIdByUrl(url)||false; if ( id ) { State = History.getStateById(id); } // Create State if ( !State && create && !History.isTraditionalAnchor(url_or_hash) ) { State = History.createStateObject(null,null,url); } } // Return return State; }; /** * History.getIdByUrl() * Get a State ID by a State URL */ History.getIdByUrl = function(url){ // Fetch var id = History.urlToId[url] || History.store.urlToId[url] || undefined; // Return return id; }; /** * History.getLastSavedState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastSavedState = function(){ return History.savedStates[History.savedStates.length-1]||undefined; }; /** * History.getLastStoredState() * Get an object containing the data, title and url of the current state * @return {Object} State */ History.getLastStoredState = function(){ return History.storedStates[History.storedStates.length-1]||undefined; }; /** * History.hasUrlDuplicate * Checks if a Url will have a url conflict * @param {Object} newState * @return {Boolean} hasDuplicate */ History.hasUrlDuplicate = function(newState) { // Prepare var hasDuplicate = false, oldState; // Fetch oldState = History.extractState(newState.url); // Check hasDuplicate = oldState && oldState.id !== newState.id; // Return return hasDuplicate; }; /** * History.storeState * Store a State * @param {Object} newState * @return {Object} newState */ History.storeState = function(newState){ // Store the State History.urlToId[newState.url] = newState.id; // Push the State History.storedStates.push(History.cloneObject(newState)); // Return newState return newState; }; /** * History.isLastSavedState(newState) * Tests to see if the state is the last state * @param {Object} newState * @return {boolean} isLast */ History.isLastSavedState = function(newState){ // Prepare var isLast = false, newId, oldState, oldId; // Check if ( History.savedStates.length ) { newId = newState.id; oldState = History.getLastSavedState(); oldId = oldState.id; // Check isLast = (newId === oldId); } // Return return isLast; }; /** * History.saveState * Push a State * @param {Object} newState * @return {boolean} changed */ History.saveState = function(newState){ // Check Hash if ( History.isLastSavedState(newState) ) { return false; } // Push the State History.savedStates.push(History.cloneObject(newState)); // Return true return true; }; /** * History.getStateByIndex() * Gets a state by the index * @param {integer} index * @return {Object} */ History.getStateByIndex = function(index){ // Prepare var State = null; // Handle if ( typeof index === 'undefined' ) { // Get the last inserted State = History.savedStates[History.savedStates.length-1]; } else if ( index < 0 ) { // Get from the end State = History.savedStates[History.savedStates.length+index]; } else { // Get from the beginning State = History.savedStates[index]; } // Return State return State; }; /** * History.getCurrentIndex() * Gets the current index * @return (integer) */ History.getCurrentIndex = function(){ // Prepare var index = null; // No states saved if(History.savedStates.length < 1) { index = 0; } else { index = History.savedStates.length-1; } return index; }; // ==================================================================== // Hash Helpers /** * History.getHash() * @param {Location=} location * Gets the current document hash * Note: unlike location.hash, this is guaranteed to return the escaped hash in all browsers * @return {string} */ History.getHash = function(doc){ var url = History.getLocationHref(doc), hash; hash = History.getHashByUrl(url); return hash; }; /** * History.unescapeHash() * normalize and Unescape a Hash * @param {String} hash * @return {string} */ History.unescapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Unescape hash result = decodeURIComponent(result); // Return result return result; }; /** * History.normalizeHash() * normalize a hash across browsers * @return {string} */ History.normalizeHash = function(hash){ // Prepare var result = hash.replace(/[^#]*#/,'').replace(/#.*/, ''); // Return result return result; }; /** * History.setHash(hash) * Sets the document hash * @param {string} hash * @return {History} */ History.setHash = function(hash,queue){ // Prepare var State, pageUrl; // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.setHash: we must wait', arguments); History.pushQueue({ scope: History, callback: History.setHash, args: arguments, queue: queue }); return false; } // Log //History.debug('History.setHash: called',hash); // Make Busy + Continue History.busy(true); // Check if hash is a state State = History.extractState(hash,true); if ( State && !History.emulated.pushState ) { // Hash is a state so skip the setHash //History.debug('History.setHash: Hash is a state so skipping the hash set with a direct pushState call',arguments); // PushState History.pushState(State.data,State.title,State.url,false); } else if ( History.getHash() !== hash ) { // Hash is a proper hash, so apply it // Handle browser bugs if ( History.bugs.setHash ) { // Fix Safari Bug https://bugs.webkit.org/show_bug.cgi?id=56249 // Fetch the base page pageUrl = History.getPageUrl(); // Safari hash apply History.pushState(null,null,pageUrl+'#'+hash,false); } else { // Normal hash apply document.location.hash = hash; } } // Chain return History; }; /** * History.escape() * normalize and Escape a Hash * @return {string} */ History.escapeHash = function(hash){ // Prepare var result = History.normalizeHash(hash); // Escape hash result = window.encodeURIComponent(result); // IE6 Escape Bug if ( !History.bugs.hashEscape ) { // Restore common parts result = result .replace(/\%21/g,'!') .replace(/\%26/g,'&') .replace(/\%3D/g,'=') .replace(/\%3F/g,'?'); } // Return result return result; }; /** * History.getHashByUrl(url) * Extracts the Hash from a URL * @param {string} url * @return {string} url */ History.getHashByUrl = function(url){ // Extract the hash var hash = String(url) .replace(/([^#]*)#?([^#]*)#?(.*)/, '$2') ; // Unescape hash hash = History.unescapeHash(hash); // Return hash return hash; }; /** * History.setTitle(title) * Applies the title to the document * @param {State} newState * @return {Boolean} */ History.setTitle = function(newState){ // Prepare var title = newState.title, firstState; // Initial if ( !title ) { firstState = History.getStateByIndex(0); if ( firstState && firstState.url === newState.url ) { title = firstState.title||History.options.initialTitle; } } // Apply try { document.getElementsByTagName('title')[0].innerHTML = title.replace('<','<').replace('>','>').replace(' & ',' & '); } catch ( Exception ) { } document.title = title; // Chain return History; }; // ==================================================================== // Queueing /** * History.queues * The list of queues to use * First In, First Out */ History.queues = []; /** * History.busy(value) * @param {boolean} value [optional] * @return {boolean} busy */ History.busy = function(value){ // Apply if ( typeof value !== 'undefined' ) { //History.debug('History.busy: changing ['+(History.busy.flag||false)+'] to ['+(value||false)+']', History.queues.length); History.busy.flag = value; } // Default else if ( typeof History.busy.flag === 'undefined' ) { History.busy.flag = false; } // Queue if ( !History.busy.flag ) { // Execute the next item in the queue clearTimeout(History.busy.timeout); var fireNext = function(){ var i, queue, item; if ( History.busy.flag ) return; for ( i=History.queues.length-1; i >= 0; --i ) { queue = History.queues[i]; if ( queue.length === 0 ) continue; item = queue.shift(); History.fireQueueItem(item); History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } }; History.busy.timeout = setTimeout(fireNext,History.options.busyDelay); } // Return return History.busy.flag; }; /** * History.busy.flag */ History.busy.flag = false; /** * History.fireQueueItem(item) * Fire a Queue Item * @param {Object} item * @return {Mixed} result */ History.fireQueueItem = function(item){ return item.callback.apply(item.scope||History,item.args||[]); }; /** * History.pushQueue(callback,args) * Add an item to the queue * @param {Object} item [scope,callback,args,queue] */ History.pushQueue = function(item){ // Prepare the queue History.queues[item.queue||0] = History.queues[item.queue||0]||[]; // Add to the queue History.queues[item.queue||0].push(item); // Chain return History; }; /** * History.queue (item,queue), (func,queue), (func), (item) * Either firs the item now if not busy, or adds it to the queue */ History.queue = function(item,queue){ // Prepare if ( typeof item === 'function' ) { item = { callback: item }; } if ( typeof queue !== 'undefined' ) { item.queue = queue; } // Handle if ( History.busy() ) { History.pushQueue(item); } else { History.fireQueueItem(item); } // Chain return History; }; /** * History.clearQueue() * Clears the Queue */ History.clearQueue = function(){ History.busy.flag = false; History.queues = []; return History; }; // ==================================================================== // IE Bug Fix /** * History.stateChanged * States whether or not the state has changed since the last double check was initialised */ History.stateChanged = false; /** * History.doubleChecker * Contains the timeout used for the double checks */ History.doubleChecker = false; /** * History.doubleCheckComplete() * Complete a double check * @return {History} */ History.doubleCheckComplete = function(){ // Update History.stateChanged = true; // Clear History.doubleCheckClear(); // Chain return History; }; /** * History.doubleCheckClear() * Clear a double check * @return {History} */ History.doubleCheckClear = function(){ // Clear if ( History.doubleChecker ) { clearTimeout(History.doubleChecker); History.doubleChecker = false; } // Chain return History; }; /** * History.doubleCheck() * Create a double check * @return {History} */ History.doubleCheck = function(tryAgain){ // Reset History.stateChanged = false; History.doubleCheckClear(); // Fix IE6,IE7 bug where calling history.back or history.forward does not actually change the hash (whereas doing it manually does) // Fix Safari 5 bug where sometimes the state does not change: https://bugs.webkit.org/show_bug.cgi?id=42940 if ( History.bugs.ieDoubleCheck ) { // Apply Check History.doubleChecker = setTimeout( function(){ History.doubleCheckClear(); if ( !History.stateChanged ) { //History.debug('History.doubleCheck: State has not yet changed, trying again', arguments); // Re-Attempt tryAgain(); } return true; }, History.options.doubleCheckInterval ); } // Chain return History; }; // ==================================================================== // Safari Bug Fix /** * History.safariStatePoll() * Poll the current state * @return {History} */ History.safariStatePoll = function(){ // Poll the URL // Get the Last State which has the new URL var urlState = History.extractState(History.getLocationHref()), newState; // Check for a difference if ( !History.isLastSavedState(urlState) ) { newState = urlState; } else { return; } // Check if we have a state with that url // If not create it if ( !newState ) { //History.debug('History.safariStatePoll: new'); newState = History.createStateObject(); } // Apply the New State //History.debug('History.safariStatePoll: trigger'); History.Adapter.trigger(window,'popstate'); // Chain return History; }; // ==================================================================== // State Aliases /** * History.back(queue) * Send the browser history back one item * @param {Integer} queue [optional] */ History.back = function(queue){ //History.debug('History.back: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.back: we must wait', arguments); History.pushQueue({ scope: History, callback: History.back, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.back(false); }); // Go back history.go(-1); // End back closure return true; }; /** * History.forward(queue) * Send the browser history forward one item * @param {Integer} queue [optional] */ History.forward = function(queue){ //History.debug('History.forward: called', arguments); // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.forward: we must wait', arguments); History.pushQueue({ scope: History, callback: History.forward, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function(){ History.forward(false); }); // Go forward history.go(1); // End forward closure return true; }; /** * History.go(index,queue) * Send the browser history back or forward index times * @param {Integer} queue [optional] */ History.go = function(index,queue){ //History.debug('History.go: called', arguments); // Prepare var i; // Handle if ( index > 0 ) { // Forward for ( i=1; i<=index; ++i ) { History.forward(queue); } } else if ( index < 0 ) { // Backward for ( i=-1; i>=index; --i ) { History.back(queue); } } else { throw new Error('History.go: History.go requires a positive or negative integer passed.'); } // Chain return History; }; // ==================================================================== // HTML5 State Support // Non-Native pushState Implementation if ( History.emulated.pushState ) { /* * Provide Skeleton for HTML4 Browsers */ // Prepare var emptyFunction = function(){}; History.pushState = History.pushState||emptyFunction; History.replaceState = History.replaceState||emptyFunction; } // History.emulated.pushState // Native pushState Implementation else { /* * Use native HTML5 History API Implementation */ /** * History.onPopState(event,extra) * Refresh the Current State */ History.onPopState = function(event,extra){ // Prepare var stateId = false, newState = false, currentHash, currentState; // Reset the double check History.doubleCheckComplete(); // Check for a Hash, and handle apporiatly currentHash = History.getHash(); if ( currentHash ) { // Expand Hash currentState = History.extractState(currentHash||History.getLocationHref(),true); if ( currentState ) { // We were able to parse it, it must be a State! // Let's forward to replaceState //History.debug('History.onPopState: state anchor', currentHash, currentState); History.replaceState(currentState.data, currentState.title, currentState.url, false); } else { // Traditional Anchor //History.debug('History.onPopState: traditional anchor', currentHash); History.Adapter.trigger(window,'anchorchange'); History.busy(false); } // We don't care for hashes History.expectedStateId = false; return false; } // Ensure stateId = History.Adapter.extractEventData('state',event,extra) || false; // Fetch State if ( stateId ) { // Vanilla: Back/forward button was used newState = History.getStateById(stateId); } else if ( History.expectedStateId ) { // Vanilla: A new state was pushed, and popstate was called manually newState = History.getStateById(History.expectedStateId); } else { // Initial State newState = History.extractState(History.getLocationHref()); } // The State did not exist in our store if ( !newState ) { // Regenerate the State newState = History.createStateObject(null,null,History.getLocationHref()); } // Clean History.expectedStateId = false; // Check if we are the same state if ( History.isLastSavedState(newState) ) { // There has been no change (just the page's hash has finally propagated) //History.debug('History.onPopState: no change', newState, History.savedStates); History.busy(false); return false; } // Store the State History.storeState(newState); History.saveState(newState); // Force update of the title History.setTitle(newState); // Fire Our Event History.Adapter.trigger(window,'statechange'); History.busy(false); // Return true return true; }; History.Adapter.bind(window,'popstate',History.onPopState); /** * History.pushState(data,title,url) * Add a new State to the history object, become it, and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.pushState = function(data,title,url,queue){ //History.debug('History.pushState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.pushState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.pushState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.pushState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End pushState closure return true; }; /** * History.replaceState(data,title,url) * Replace the State and trigger onpopstate * We have to trigger for HTML4 compatibility * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.replaceState = function(data,title,url,queue){ //History.debug('History.replaceState: called', arguments); // Check the State if ( History.getHashByUrl(url) && History.emulated.pushState ) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // Handle Queueing if ( queue !== false && History.busy() ) { // Wait + Push to Queue //History.debug('History.replaceState: we must wait', arguments); History.pushQueue({ scope: History, callback: History.replaceState, args: arguments, queue: queue }); return false; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data,title,url); // Check it if ( History.isLastSavedState(newState) ) { // Won't be a change History.busy(false); } else { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Push the newState history.replaceState(newState.id,newState.title,newState.url); // Fire HTML5 Event History.Adapter.trigger(window,'popstate'); } // End replaceState closure return true; }; } // !History.emulated.pushState // ==================================================================== // Initialise /** * Load the Store */ if ( sessionStorage ) { // Fetch try { History.store = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { History.store = {}; } // Normalize History.normalizeStore(); } else { // Default Load History.store = {}; History.normalizeStore(); } /** * Clear Intervals on exit to prevent memory leaks */ History.Adapter.bind(window,"unload",History.clearAllIntervals); /** * Create the initial State */ History.saveState(History.storeState(History.extractState(History.getLocationHref(),true))); /** * Bind for Saving Store */ if ( sessionStorage ) { // When the page is closed History.onUnload = function(){ // Prepare var currentStore, item, currentStoreString; // Fetch try { currentStore = JSON.parse(sessionStorage.getItem('History.store'))||{}; } catch ( err ) { currentStore = {}; } // Ensure currentStore.idToState = currentStore.idToState || {}; currentStore.urlToId = currentStore.urlToId || {}; currentStore.stateToId = currentStore.stateToId || {}; // Sync for ( item in History.idToState ) { if ( !History.idToState.hasOwnProperty(item) ) { continue; } currentStore.idToState[item] = History.idToState[item]; } for ( item in History.urlToId ) { if ( !History.urlToId.hasOwnProperty(item) ) { continue; } currentStore.urlToId[item] = History.urlToId[item]; } for ( item in History.stateToId ) { if ( !History.stateToId.hasOwnProperty(item) ) { continue; } currentStore.stateToId[item] = History.stateToId[item]; } // Update History.store = currentStore; History.normalizeStore(); // In Safari, going into Private Browsing mode causes the // Session Storage object to still exist but if you try and use // or set any property/function of it it throws the exception // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to // add something to storage that exceeded the quota." infinitely // every second. currentStoreString = JSON.stringify(currentStore); try { // Store sessionStorage.setItem('History.store', currentStoreString); } catch (e) { if (e.code === DOMException.QUOTA_EXCEEDED_ERR) { if (sessionStorage.length) { // Workaround for a bug seen on iPads. Sometimes the quota exceeded error comes up and simply // removing/resetting the storage can work. sessionStorage.removeItem('History.store'); sessionStorage.setItem('History.store', currentStoreString); } else { // Otherwise, we're probably private browsing in Safari, so we'll ignore the exception. } } else { throw e; } } }; // For Internet Explorer History.intervalList.push(setInterval(History.onUnload,History.options.storeInterval)); // For Other Browsers History.Adapter.bind(window,'beforeunload',History.onUnload); History.Adapter.bind(window,'unload',History.onUnload); // Both are enabled for consistency } // Non-Native pushState Implementation if ( !History.emulated.pushState ) { // Be aware, the following is only for native pushState implementations // If you are wanting to include something for all browsers // Then include it above this if block /** * Setup Safari Fix */ if ( History.bugs.safariPoll ) { History.intervalList.push(setInterval(History.safariStatePoll, History.options.safariPollInterval)); } /** * Ensure Cross Browser Compatibility */ if ( navigator.vendor === 'Apple Computer, Inc.' || (navigator.appCodeName||'') === 'Mozilla' ) { /** * Fix Safari HashChange Issue */ // Setup Alias History.Adapter.bind(window,'hashchange',function(){ History.Adapter.trigger(window,'popstate'); }); // Initialise Alias if ( History.getHash() ) { History.Adapter.onDomLoad(function(){ History.Adapter.trigger(window,'hashchange'); }); } } } // !History.emulated.pushState }; // History.initCore // Try to Initialise History if (!History.options || !History.options.delayInit) { History.init(); } })(window); ================================================ FILE: scripts/uncompressed/json2.js ================================================ /* json2.js 2012-10-08 Public Domain. NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. See http://www.JSON.org/js.html This code should be minified before deployment. See http://javascript.crockford.com/jsmin.html USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO NOT CONTROL. This file creates a global JSON object containing two methods: stringify and parse. JSON.stringify(value, replacer, space) value any JavaScript value, usually an object or array. replacer an optional parameter that determines how object values are stringified for objects. It can be a function or an array of strings. space an optional parameter that specifies the indentation of nested structures. If it is omitted, the text will be packed without extra whitespace. If it is a number, it will specify the number of spaces to indent at each level. If it is a string (such as '\t' or ' '), it contains the characters used to indent at each level. This method produces a JSON text from a JavaScript value. When an object value is found, if the object contains a toJSON method, its toJSON method will be called and the result will be stringified. A toJSON method does not serialize: it returns the value represented by the name/value pair that should be serialized, or undefined if nothing should be serialized. The toJSON method will be passed the key associated with the value, and this will be bound to the value For example, this would serialize Dates as ISO strings. Date.prototype.toJSON = function (key) { function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } return this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z'; }; You can provide an optional replacer method. It will be passed the key and value of each member, with this bound to the containing object. The value that is returned from your method will be serialized. If your method returns undefined, then the member will be excluded from the serialization. If the replacer parameter is an array of strings, then it will be used to select the members to be serialized. It filters the results such that only members with keys listed in the replacer array are stringified. Values that do not have JSON representations, such as undefined or functions, will not be serialized. Such values in objects will be dropped; in arrays they will be replaced with null. You can use a replacer function to replace those with JSON values. JSON.stringify(undefined) returns undefined. The optional space parameter produces a stringification of the value that is filled with line breaks and indentation to make it easier to read. If the space parameter is a non-empty string, then that string will be used for indentation. If the space parameter is a number, then the indentation will be that many spaces. Example: text = JSON.stringify(['e', {pluribus: 'unum'}]); // text is '["e",{"pluribus":"unum"}]' text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' text = JSON.stringify([new Date()], function (key, value) { return this[key] instanceof Date ? 'Date(' + this[key] + ')' : value; }); // text is '["Date(---current time---)"]' JSON.parse(text, reviver) This method parses a JSON text to produce an object or array. It can throw a SyntaxError exception. The optional reviver parameter is a function that can filter and transform the results. It receives each of the keys and values, and its return value is used instead of the original value. If it returns what it received, then the structure is not modified. If it returns undefined then the member is deleted. Example: // Parse the text. Values that look like ISO date strings will // be converted to Date objects. myData = JSON.parse(text, function (key, value) { var a; if (typeof value === 'string') { a = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); if (a) { return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6])); } } return value; }); myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { var d; if (typeof value === 'string' && value.slice(0, 5) === 'Date(' && value.slice(-1) === ')') { d = new Date(value.slice(5, -1)); if (d) { return d; } } return value; }); This is a reference implementation. You are free to copy, modify, or redistribute. */ /*jslint evil: true, regexp: true */ /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, lastIndex, length, parse, prototype, push, replace, slice, stringify, test, toJSON, toString, valueOf */ // Create a JSON object only if one does not already exist. We create the // methods in a closure to avoid creating global variables. if (typeof JSON !== 'object') { JSON = {}; } (function () { 'use strict'; function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } if (typeof Date.prototype.toJSON !== 'function') { Date.prototype.toJSON = function (key) { return isFinite(this.valueOf()) ? this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z' : null; }; String.prototype.toJSON = Number.prototype.toJSON = Boolean.prototype.toJSON = function (key) { return this.valueOf(); }; } var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, gap, indent, meta = { // table of character substitutions '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\' }, rep; function quote(string) { // If the string contains no control characters, no quote characters, and no // backslash characters, then we can safely slap some quotes around it. // Otherwise we must also replace the offending characters with safe escape // sequences. escapable.lastIndex = 0; return escapable.test(string) ? '"' + string.replace(escapable, function (a) { var c = meta[a]; return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }) + '"' : '"' + string + '"'; } function str(key, holder) { // Produce a string from holder[key]. var i, // The loop counter. k, // The member key. v, // The member value. length, mind = gap, partial, value = holder[key]; // If the value has a toJSON method, call it to obtain a replacement value. if (value && typeof value === 'object' && typeof value.toJSON === 'function') { value = value.toJSON(key); } // If we were called with a replacer function, then call the replacer to // obtain a replacement value. if (typeof rep === 'function') { value = rep.call(holder, key, value); } // What happens next depends on the value's type. switch (typeof value) { case 'string': return quote(value); case 'number': // JSON numbers must be finite. Encode non-finite numbers as null. return isFinite(value) ? String(value) : 'null'; case 'boolean': case 'null': // If the value is a boolean or null, convert it to a string. Note: // typeof null does not produce 'null'. The case is included here in // the remote chance that this gets fixed someday. return String(value); // If the type is 'object', we might be dealing with an object or an array or // null. case 'object': // Due to a specification blunder in ECMAScript, typeof null is 'object', // so watch out for that case. if (!value) { return 'null'; } // Make an array to hold the partial results of stringifying this object value. gap += indent; partial = []; // Is the value an array? if (Object.prototype.toString.apply(value) === '[object Array]') { // The value is an array. Stringify every element. Use null as a placeholder // for non-JSON values. length = value.length; for (i = 0; i < length; i += 1) { partial[i] = str(i, value) || 'null'; } // Join all of the elements together, separated with commas, and wrap them in // brackets. v = partial.length === 0 ? '[]' : gap ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : '[' + partial.join(',') + ']'; gap = mind; return v; } // If the replacer is an array, use it to select the members to be stringified. if (rep && typeof rep === 'object') { length = rep.length; for (i = 0; i < length; i += 1) { if (typeof rep[i] === 'string') { k = rep[i]; v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } else { // Otherwise, iterate through all of the keys in the object. for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } // Join all of the member texts together, separated with commas, // and wrap them in braces. v = partial.length === 0 ? '{}' : gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : '{' + partial.join(',') + '}'; gap = mind; return v; } } // If the JSON object does not yet have a stringify method, give it one. if (typeof JSON.stringify !== 'function') { JSON.stringify = function (value, replacer, space) { // The stringify method takes a value and an optional replacer, and an optional // space parameter, and returns a JSON text. The replacer can be a function // that can replace values, or an array of strings that will select the keys. // A default replacer method can be provided. Use of the space parameter can // produce text that is more easily readable. var i; gap = ''; indent = ''; // If the space parameter is a number, make an indent string containing that // many spaces. if (typeof space === 'number') { for (i = 0; i < space; i += 1) { indent += ' '; } // If the space parameter is a string, it will be used as the indent string. } else if (typeof space === 'string') { indent = space; } // If there is a replacer, it must be a function or an array. // Otherwise, throw an error. rep = replacer; if (replacer && typeof replacer !== 'function' && (typeof replacer !== 'object' || typeof replacer.length !== 'number')) { throw new Error('JSON.stringify'); } // Make a fake root object containing our value under the key of ''. // Return the result of stringifying the value. return str('', {'': value}); }; } // If the JSON object does not yet have a parse method, give it one. if (typeof JSON.parse !== 'function') { JSON.parse = function (text, reviver) { // The parse method takes a text and an optional reviver function, and returns // a JavaScript value if the text is a valid JSON text. var j; function walk(holder, key) { // The walk method is used to recursively walk the resulting structure so // that modifications can be made. var k, v, value = holder[key]; if (value && typeof value === 'object') { for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = walk(value, k); if (v !== undefined) { value[k] = v; } else { delete value[k]; } } } } return reviver.call(holder, key, value); } // Parsing happens in four stages. In the first stage, we replace certain // Unicode characters with escape sequences. JavaScript handles many characters // incorrectly, either silently deleting them, or treating them as line endings. text = String(text); cx.lastIndex = 0; if (cx.test(text)) { text = text.replace(cx, function (a) { return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }); } // In the second stage, we run the text against regular expressions that look // for non-JSON patterns. We are especially concerned with '()' and 'new' // because they can cause invocation, and '=' because it can cause mutation. // But just to be safe, we want to reject all unexpected forms. // We split the second stage into 4 regexp operations in order to work around // crippling inefficiencies in IE's and Safari's regexp engines. First we // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we // replace all simple value tokens with ']' characters. Third, we delete all // open brackets that follow a colon or comma or that begin the text. Finally, // we look to see that the remaining characters are only whitespace or ']' or // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. if (/^[\],:{}\s]*$/ .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { // In the third stage we use the eval function to compile the text into a // JavaScript structure. The '{' operator is subject to a syntactic ambiguity // in JavaScript: it can begin a block or an object literal. We wrap the text // in parens to eliminate the ambiguity. j = eval('(' + text + ')'); // In the optional fourth stage, we recursively walk the new structure, passing // each name/value pair to a reviver function for possible transformation. return typeof reviver === 'function' ? walk({'': j}, '') : j; } // If the text is not JSON parseable, then a SyntaxError is thrown. throw new SyntaxError('JSON.parse'); }; } }()); ================================================ FILE: tests/.htaccess ================================================ Options +FollowSymlinks RewriteEngine On # Clean Adapter RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ([^\.]+)$ $1.html [NC,L,QSA] # Can someone smarter than me make it so: # http://localhost/history.js/tests/uncompressed-html5-persistant-jquery # Does not redirect to: # http://localhost/history.js/tests/uncompressed-html5-persistant-jquery.html # But still accesses that url ================================================ FILE: tests/html4+html5.dojo.html ================================================ History.js HTML4+HTML5 Dojo Test Suite

History.js HTML4+HTML5 Dojo Test Suite

    test markup
    ================================================ FILE: tests/html4+html5.extjs.html ================================================ History.js HTML4+HTML5 ExtJS Test Suite

    History.js HTML4+HTML5 ExtJS Test Suite

      test markup
      ================================================ FILE: tests/html4+html5.jquery.html ================================================ History.js HTML4+HTML5 Jquery Test Suite

      History.js HTML4+HTML5 Jquery Test Suite

        test markup
        ================================================ FILE: tests/html4+html5.mootools.html ================================================ History.js HTML4+HTML5 Mootools Test Suite

        History.js HTML4+HTML5 Mootools Test Suite

          test markup
          ================================================ FILE: tests/html4+html5.native.html ================================================ History.js HTML4+HTML5 Native Test Suite

          History.js HTML4+HTML5 Native Test Suite

            test markup
            ================================================ FILE: tests/html4+html5.right.html ================================================ History.js HTML4+HTML5 Right Test Suite

            History.js HTML4+HTML5 Right Test Suite

              test markup
              ================================================ FILE: tests/html4+html5.zepto.html ================================================ History.js HTML4+HTML5 Zepto Test Suite

              History.js HTML4+HTML5 Zepto Test Suite

                test markup
                ================================================ FILE: tests/html5.dojo.html ================================================ History.js HTML5 Dojo Test Suite

                History.js HTML5 Dojo Test Suite

                  test markup
                  ================================================ FILE: tests/html5.extjs.html ================================================ History.js HTML5 ExtJS Test Suite

                  History.js HTML5 ExtJS Test Suite

                    test markup
                    ================================================ FILE: tests/html5.jquery.html ================================================ History.js HTML5 Jquery Test Suite

                    History.js HTML5 Jquery Test Suite

                      test markup
                      ================================================ FILE: tests/html5.mootools.html ================================================ History.js HTML5 Mootools Test Suite

                      History.js HTML5 Mootools Test Suite

                        test markup
                        ================================================ FILE: tests/html5.native.html ================================================ History.js HTML5 Native Test Suite

                        History.js HTML5 Native Test Suite

                          test markup
                          ================================================ FILE: tests/html5.right.html ================================================ History.js HTML5 Right Test Suite

                          History.js HTML5 Right Test Suite

                            test markup
                            ================================================ FILE: tests/html5.zepto.html ================================================ History.js HTML5 Zepto Test Suite

                            History.js HTML5 Zepto Test Suite

                              test markup
                              ================================================ FILE: tests/image.php ================================================ History.js Test Suite

                              History.js Test Suite

                              HTML5 Browsers must pass the HTML4+HTML5 tests, HTML4 Browsers must pass the HTML4 tests and should fail the HTML5 tests.

                              ================================================ FILE: tests/tests.js ================================================ (function(){ var History = window.History, document = window.document, test = window.test, deepEqual = window.deepEqual; // Check if ( !History.enabled ) { throw new Error('History.js is disabled'); } // Prepare History.options.debug = false; // Variables var States = { // Home 0: { 'url': document.location.href.replace(/#.*$/,''), 'title': '' }, // One 1: { 'data': { 'state': 1, 'rand': Math.random() }, 'title': 'State 1', 'url': '?state=1' }, // Two 2: { 'data': { 'state': 2, 'rand': Math.random() }, 'title': 'State 2', 'url': '?state=2&asd=%20asd%2520asd' }, // Three 3: { 'url': '?state=3' }, // Four 4: { 'data': { 'state': 4, 'trick': true, 'rand': Math.random() }, 'title': 'State 4', 'url': '?state=3' }, // Log 5: { 'url': '?state=1#log' }, // Six 6: { 'data': { 'state': 6, 'rand': Math.random() }, 'url': 'six.html' }, // Seven 7: { 'url': 'seven' }, // Eight 8: { 'url': '/eight' } }, stateOrder = [0,1,2,3,4,3,1,0,1,3,4,3,1,0,6,7,8,1,8,7,6,0], currentTest = 0; // Original Title var title = document.title; var banner; var checkStatus = function(){ banner = banner || document.getElementById('qunit-banner'); var status = banner.className !== 'qunit-fail'; return status; }; // Check State var checkState = function(){ if ( !checkStatus() ) { throw new Error('A test has failed'); } var stateIndex = stateOrder[currentTest], expectedState = History.normalizeState(States[stateIndex]), actualState = History.getState(false); ++currentTest; document.title = title+': '+actualState.url; var testName = 'Test '+currentTest, stateName = 'State '+stateIndex; test(testName,function(){ History.log('Completed: '+testName +' / '+ stateName); deepEqual(actualState,expectedState,stateName); }); // Image Load to Stress Test Safari and Opera (new Image()).src = "image.php"; }; // Check the Initial State checkState(); // State Change History.Adapter.bind(window,'statechange',checkState); // Log var addLog = function(){ var args = arguments; History.queue(function(){ History.log.apply(History,args); }); }; // Dom Load History.Adapter.onDomLoad(function(){ setTimeout(function(){ // ---------------------------------------------------------------------- // Test State Functionality: Adding // Test 2 / State 1 (0 -> 1) // Tests HTML4 -> HTML5 Graceful Upgrade addLog('Test 2',History.queues.length,History.busy.flag); History.setHash(History.getHashByState(States[1])); // Test 3 / State 2 (1 -> 2) addLog('Test 3',History.queues.length,History.busy.flag); History.pushState(States[2].data, States[2].title, States[2].url); // Test 3-2 / State 2 (2 -> 2) / No Change addLog('Test 3-2',History.queues.length,History.busy.flag); History.pushState(States[2].data, States[2].title, States[2].url); // Test 3-3 / State 2 (2 -> 2) / No Change addLog('Test 3-3',History.queues.length,History.busy.flag); History.replaceState(States[2].data, States[2].title, States[2].url); // Test 4 / State 3 (2 -> 3) addLog('Test 4',History.queues.length,History.busy.flag); History.replaceState(States[3].data, States[3].title, States[3].url); // Test 5 / State 4 (3 -> 4) addLog('Test 5',History.queues.length,History.busy.flag); History.pushState(States[4].data, States[4].title, States[4].url); // ---------------------------------------------------------------------- // Test State Functionality: Traversing // Test 6 / State 3 (4 -> 3) // Test 7 / State 1 (3 -> 2 -> 1) addLog('Test 6,7',History.queues.length,History.busy.flag); History.go(-2); // Test 8 / State 0 (1 -> 0) // Tests Default State addLog('Test 8',History.queues.length,History.busy.flag); History.back(); // Test 9 / State 1 (0 -> 1) // Test 10 / State 3 (1 -> 2 -> 3) addLog('Test 9,10',History.queues.length,History.busy.flag); History.go(2); // Test 11 / State 4 (3 -> 4) addLog('Test 11',History.queues.length,History.busy.flag); History.forward(); // Test 12 / State 3 (4 -> 3) addLog('Test 12',History.queues.length,History.busy.flag); History.back(); // Test 13 / State 1 (3 -> 2 -> 1) addLog('Test 13',History.queues.length,History.busy.flag); History.back(); // ---------------------------------------------------------------------- // Test State Functionality: Traditional Anchors // Test 13-2 / State 1 (1 -> #log) / No Change addLog('Test 13-2',History.queues.length,History.busy.flag); History.setHash('log'); // Test 13-3 / State 1 (#log -> 1) / No Change addLog('Test 13-3',History.queues.length,History.busy.flag); History.back(); // Test 14 / State 0 (1 -> 0) addLog('Test 14',History.queues.length,History.busy.flag); History.back(); // ---------------------------------------------------------------------- // Test URL Handling: Adding // Test 15 / State 6 (1 -> 6) // Also tests data with no title addLog('Test 15',History.queues.length,History.busy.flag); History.pushState(States[6].data, States[6].title, States[6].url); // Test 16 / State 7 (6 -> 7) addLog('Test 16',History.queues.length,History.busy.flag); History.pushState(States[7].data, States[7].title, States[7].url); // Test 17 / State 7 (7 -> 8) addLog('Test 17',History.queues.length,History.busy.flag); History.pushState(States[8].data, States[8].title, States[8].url); // Test 18 / State 1 (8 -> 1) // Should be /eight?state=1 addLog('Test 18',History.queues.length,History.busy.flag); History.pushState(States[1].data, States[1].title, States[1].url); // ---------------------------------------------------------------------- // Test URL Handling: Traversing // Test 19 / State 8 (1 -> 8) addLog('Test 19',History.queues.length,History.busy.flag); History.back(); // Test 20 / State 7 (8 -> 7) addLog('Test 20',History.queues.length,History.busy.flag); History.back(); // Test 21 / State 6 (7 -> 6) addLog('Test 21',History.queues.length,History.busy.flag); History.back(); // Test 22 / State 0 (6 -> 0) addLog('Test 22',History.queues.length,History.busy.flag); History.back(); },1000); // wait for test one to complete }); })(); ================================================ FILE: tests.src/_header.php ================================================ History.js Test Suite

                              History.js Test Suite

                              HTML5 Browsers must pass the HTML4+HTML5 tests

                              HTML4 Browsers must pass the HTML4 tests and should fail the HTML5 tests

                              '; foreach ( $adapters as $adapter ) : echo '
                              '; # Url $url = "${browser}.${adapter}.html"; # Title $Browser = ucwords($browser); $Adapter = ucwords($adapter); $title = "History.js ${Browser} ${Adapter} Test Suite"; # Render ?>'; endforeach; echo '
                              '; endforeach; ?> ================================================ FILE: tests.src/each.php ================================================ <?=$title?>

                                test markup
                                ================================================ FILE: tests.src/index.php ================================================ Tests ================================================ FILE: vendor/dojo.js ================================================ /* Copyright (c) 2004-2012, The Dojo Foundation All Rights Reserved. Available via Academic Free License >= 2.1 OR the modified BSD license. see: http://dojotoolkit.org/license for details */ /* This is an optimized version of Dojo, built for deployment and not for development. To get sources and documentation, please visit: http://dojotoolkit.org */ (function( userConfig, defaultConfig ){ // summary: // This is the "source loader" and is the entry point for Dojo during development. You may also load Dojo with // any AMD-compliant loader via the package main module dojo/main. // description: // This is the "source loader" for Dojo. It provides an AMD-compliant loader that can be configured // to operate in either synchronous or asynchronous modes. After the loader is defined, dojo is loaded // IAW the package main module dojo/main. In the event you wish to use a foreign loader, you may load dojo as a package // via the package main module dojo/main and this loader is not required; see dojo/package.json for details. // // In order to keep compatibility with the v1.x line, this loader includes additional machinery that enables // the dojo.provide, dojo.require et al API. This machinery is loaded by default, but may be dynamically removed // via the has.js API and statically removed via the build system. // // This loader includes sniffing machinery to determine the environment; the following environments are supported: // // - browser // - node.js // - rhino // // This is the so-called "source loader". As such, it includes many optional features that may be discadred by // building a customized verion with the build system. // Design and Implementation Notes // // This is a dojo-specific adaption of bdLoad, donated to the dojo foundation by Altoviso LLC. // // This function defines an AMD-compliant (http://wiki.commonjs.org/wiki/Modules/AsynchronousDefinition) // loader that can be configured to operate in either synchronous or asynchronous modes. // // Since this machinery implements a loader, it does not have the luxury of using a load system and/or // leveraging a utility library. This results in an unpleasantly long file; here is a road map of the contents: // // 1. Small library for use implementing the loader. // 2. Define the has.js API; this is used throughout the loader to bracket features. // 3. Define the node.js and rhino sniffs and sniff. // 4. Define the loader's data. // 5. Define the configuration machinery. // 6. Define the script element sniffing machinery and sniff for configuration data. // 7. Configure the loader IAW the provided user, default, and sniffing data. // 8. Define the global require function. // 9. Define the module resolution machinery. // 10. Define the module and plugin module definition machinery // 11. Define the script injection machinery. // 12. Define the window load detection. // 13. Define the logging API. // 14. Define the tracing API. // 16. Define the AMD define function. // 17. Define the dojo v1.x provide/require machinery--so called "legacy" modes. // 18. Publish global variables. // // Language and Acronyms and Idioms // // moduleId: a CJS module identifier, (used for public APIs) // mid: moduleId (used internally) // packageId: a package identifier (used for public APIs) // pid: packageId (used internally); the implied system or default package has pid==="" // pack: package is used internally to reference a package object (since javascript has reserved words including "package") // prid: plugin resource identifier // The integer constant 1 is used in place of true and 0 in place of false. // define a minimal library to help build the loader var noop = function(){ }, isEmpty = function(it){ for(var p in it){ return 0; } return 1; }, toString = {}.toString, isFunction = function(it){ return toString.call(it) == "[object Function]"; }, isString = function(it){ return toString.call(it) == "[object String]"; }, isArray = function(it){ return toString.call(it) == "[object Array]"; }, forEach = function(vector, callback){ if(vector){ for(var i = 0; i < vector.length;){ callback(vector[i++]); } } }, mix = function(dest, src){ for(var p in src){ dest[p] = src[p]; } return dest; }, makeError = function(error, info){ return mix(new Error(error), {src:"dojoLoader", info:info}); }, uidSeed = 1, uid = function(){ // Returns a unique indentifier (within the lifetime of the document) of the form /_d+/. return "_" + uidSeed++; }, // FIXME: how to doc window.require() api // this will be the global require function; define it immediately so we can start hanging things off of it req = function( config, //(object, optional) hash of configuration properties dependencies, //(array of commonjs.moduleId, optional) list of modules to be loaded before applying callback callback //(function, optional) lamda expression to apply to module values implied by dependencies ){ return contextRequire(config, dependencies, callback, 0, req); }, // the loader uses the has.js API to control feature inclusion/exclusion; define then use throughout global = this, doc = global.document, element = doc && doc.createElement("DiV"), has = req.has = function(name){ return isFunction(hasCache[name]) ? (hasCache[name] = hasCache[name](global, doc, element)) : hasCache[name]; }, hasCache = has.cache = defaultConfig.hasCache; has.add = function(name, test, now, force){ (hasCache[name]===undefined || force) && (hasCache[name] = test); return now && has(name); }; 0 && has.add("host-node", userConfig.has && "host-node" in userConfig.has ? userConfig.has["host-node"] : (typeof process == "object" && process.versions && process.versions.node && process.versions.v8)); if( 0 ){ // fixup the default config for node.js environment require("./_base/configNode.js").config(defaultConfig); // remember node's require (with respect to baseUrl==dojo's root) defaultConfig.loaderPatch.nodeRequire = require; } 0 && has.add("host-rhino", userConfig.has && "host-rhino" in userConfig.has ? userConfig.has["host-rhino"] : (typeof load == "function" && (typeof Packages == "function" || typeof Packages == "object"))); if( 0 ){ // owing to rhino's lame feature that hides the source of the script, give the user a way to specify the baseUrl... for(var baseUrl = userConfig.baseUrl || ".", arg, rhinoArgs = this.arguments, i = 0; i < rhinoArgs.length;){ arg = (rhinoArgs[i++] + "").split("="); if(arg[0] == "baseUrl"){ baseUrl = arg[1]; break; } } load(baseUrl + "/_base/configRhino.js"); rhinoDojoConfig(defaultConfig, baseUrl, rhinoArgs); } // userConfig has tests override defaultConfig has tests; do this after the environment detection because // the environment detection usually sets some has feature values in the hasCache. for(var p in userConfig.has){ has.add(p, userConfig.has[p], 0, 1); } // // define the loader data // // the loader will use these like symbols if the loader has the traceApi; otherwise // define magic numbers so that modules can be provided as part of defaultConfig var requested = 1, arrived = 2, nonmodule = 3, executing = 4, executed = 5; if( 0 ){ // these make debugging nice; but using strings for symbols is a gross rookie error; don't do it for production code requested = "requested"; arrived = "arrived"; nonmodule = "not-a-module"; executing = "executing"; executed = "executed"; } var legacyMode = 0, sync = "sync", xd = "xd", syncExecStack = [], dojoRequirePlugin = 0, checkDojoRequirePlugin = noop, transformToAmd = noop, getXhr; if( 1 ){ req.isXdUrl = noop; req.initSyncLoader = function(dojoRequirePlugin_, checkDojoRequirePlugin_, transformToAmd_){ // the first dojo/_base/loader loaded gets to define these variables; they are designed to work // in the presense of zero to many mapped dojo/_base/loaders if(!dojoRequirePlugin){ dojoRequirePlugin = dojoRequirePlugin_; checkDojoRequirePlugin = checkDojoRequirePlugin_; transformToAmd = transformToAmd_; } return { sync:sync, requested:requested, arrived:arrived, nonmodule:nonmodule, executing:executing, executed:executed, syncExecStack:syncExecStack, modules:modules, execQ:execQ, getModule:getModule, injectModule:injectModule, setArrived:setArrived, signal:signal, finishExec:finishExec, execModule:execModule, dojoRequirePlugin:dojoRequirePlugin, getLegacyMode:function(){return legacyMode;}, guardCheckComplete:guardCheckComplete }; }; if( 1 ){ // in legacy sync mode, the loader needs a minimal XHR library var locationProtocol = location.protocol, locationHost = location.host; req.isXdUrl = function(url){ if(/^\./.test(url)){ // begins with a dot is always relative to page URL; therefore not xdomain return false; } if(/^\/\//.test(url)){ // for v1.6- backcompat, url starting with // indicates xdomain return true; } // get protocol and host // \/+ takes care of the typical file protocol that looks like file:///drive/path/to/file // locationHost is falsy if file protocol => if locationProtocol matches and is "file:", || will return false var match = url.match(/^([^\/\:]+\:)\/+([^\/]+)/); return match && (match[1] != locationProtocol || (locationHost && match[2] != locationHost)); }; // note: to get the file:// protocol to work in FF, you must set security.fileuri.strict_origin_policy to false in about:config 1 || has.add("dojo-xhr-factory", 1); has.add("dojo-force-activex-xhr", 1 && !doc.addEventListener && window.location.protocol == "file:"); has.add("native-xhr", typeof XMLHttpRequest != "undefined"); if(has("native-xhr") && !has("dojo-force-activex-xhr")){ getXhr = function(){ return new XMLHttpRequest(); }; }else{ // if in the browser an old IE; find an xhr for(var XMLHTTP_PROGIDS = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'], progid, i = 0; i < 3;){ try{ progid = XMLHTTP_PROGIDS[i++]; if(new ActiveXObject(progid)){ // this progid works; therefore, use it from now on break; } }catch(e){ // squelch; we're just trying to find a good ActiveX progid // if they all fail, then progid ends up as the last attempt and that will signal the error // the first time the client actually tries to exec an xhr } } getXhr = function(){ return new ActiveXObject(progid); }; } req.getXhr = getXhr; has.add("dojo-gettext-api", 1); req.getText = function(url, async, onLoad){ var xhr = getXhr(); xhr.open('GET', fixupUrl(url), false); xhr.send(null); if(xhr.status == 200 || (!location.host && !xhr.status)){ if(onLoad){ onLoad(xhr.responseText, async); } }else{ throw makeError("xhrFailed", xhr.status); } return xhr.responseText; }; } }else{ req.async = 1; } // // loader eval // var eval_ = // use the function constructor so our eval is scoped close to (but not in) in the global space with minimal pollution new Function('return eval(arguments[0]);'); req.eval = function(text, hint){ return eval_(text + "\r\n////@ sourceURL=" + hint); }; // // loader micro events API // var listenerQueues = {}, error = "error", signal = req.signal = function(type, args){ var queue = listenerQueues[type]; // notice we run a copy of the queue; this allows listeners to add/remove // other listeners without affecting this particular signal forEach(queue && queue.slice(0), function(listener){ listener.apply(null, isArray(args) ? args : [args]); }); }, on = req.on = function(type, listener){ // notice a queue is not created until a client actually connects var queue = listenerQueues[type] || (listenerQueues[type] = []); queue.push(listener); return { remove:function(){ for(var i = 0; i (alias, actual) = [], paths // CommonJS paths = {}, pathsMapProg // list of (from-path, to-path, regex, length) derived from paths; // a "program" to apply paths; see computeMapProg = [], packs // a map from packageId to package configuration object; see fixupPackageInfo = {}, map = req.map // AMD map config variable; dojo/_base/kernel needs req.map to figure out the scope map = {}, mapProgs // vector of quads as described by computeMapProg; map-key is AMD map key, map-value is AMD map value = [], modules // A hash:(mid) --> (module-object) the module namespace // // pid: the package identifier to which the module belongs (e.g., "dojo"); "" indicates the system or default package // mid: the fully-resolved (i.e., mappings have been applied) module identifier without the package identifier (e.g., "dojo/io/script") // url: the URL from which the module was retrieved // pack: the package object of the package to which the module belongs // executed: 0 => not executed; executing => in the process of tranversing deps and running factory; executed => factory has been executed // deps: the dependency vector for this module (vector of modules objects) // def: the factory for this module // result: the result of the running the factory for this module // injected: (0 | requested | arrived) the status of the module; nonmodule means the resource did not call define // load: plugin load function; applicable only for plugins // // Modules go through several phases in creation: // // 1. Requested: some other module's definition or a require application contained the requested module in // its dependency vector or executing code explicitly demands a module via req.require. // // 2. Injected: a script element has been appended to the insert-point element demanding the resource implied by the URL // // 3. Loaded: the resource injected in [2] has been evalated. // // 4. Defined: the resource contained a define statement that advised the loader about the module. Notice that some // resources may just contain a bundle of code and never formally define a module via define // // 5. Evaluated: the module was defined via define and the loader has evaluated the factory and computed a result. = {}, cacheBust // query string to append to module URLs to bust browser cache = "", cache // hash:(mid | url)-->(function | string) // // A cache of resources. The resources arrive via a config.cache object, which is a hash from either mid --> function or // url --> string. The url key is distinguished from the mid key by always containing the prefix "url:". url keys as provided // by config.cache always have a string value that represents the contents of the resource at the given url. mid keys as provided // by configl.cache always have a function value that causes the same code to execute as if the module was script injected. // // Both kinds of key-value pairs are entered into cache via the function consumePendingCache, which may relocate keys as given // by any mappings *iff* the config.cache was received as part of a module resource request. // // Further, for mid keys, the implied url is computed and the value is entered into that key as well. This allows mapped modules // to retrieve cached items that may have arrived consequent to another namespace. // = {}, urlKeyPrefix // the prefix to prepend to a URL key in the cache. = "url:", pendingCacheInsert // hash:(mid)-->(function) // // Gives a set of cache modules pending entry into cache. When cached modules are published to the loader, they are // entered into pendingCacheInsert; modules are then pressed into cache upon (1) AMD define or (2) upon receiving another // independent set of cached modules. (1) is the usual case, and this case allows normalizing mids given in the pending // cache for the local configuration, possibly relocating modules. = {}, dojoSniffConfig // map of configuration variables // give the data-dojo-config as sniffed from the document (if any) = {}; if( 1 ){ var consumePendingCacheInsert = function(referenceModule){ var p, item, match, now, m; for(p in pendingCacheInsert){ item = pendingCacheInsert[p]; match = p.match(/^url\:(.+)/); if(match){ cache[urlKeyPrefix + toUrl(match[1], referenceModule)] = item; }else if(p=="*now"){ now = item; }else if(p!="*noref"){ m = getModuleInfo(p, referenceModule); cache[m.mid] = cache[urlKeyPrefix + m.url] = item; } } if(now){ now(createRequire(referenceModule)); } pendingCacheInsert = {}; }, escapeString = function(s){ return s.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, function(c){ return "\\" + c; }); }, computeMapProg = function(map, dest){ // This routine takes a map as represented by a JavaScript object and initializes dest, a vector of // quads of (map-key, map-value, refex-for-map-key, length-of-map-key), sorted decreasing by length- // of-map-key. The regex looks for the map-key followed by either "/" or end-of-string at the beginning // of a the search source. Notice the map-value is irrelevent to the algorithm dest.splice(0, dest.length); for(var p in map){ dest.push([ p, map[p], new RegExp("^" + escapeString(p) + "(\/|$)"), p.length]); } dest.sort(function(lhs, rhs){ return rhs[3] - lhs[3]; }); return dest; }, fixupPackageInfo = function(packageInfo){ // calculate the precise (name, location, main, mappings) for a package var name = packageInfo.name; if(!name){ // packageInfo must be a string that gives the name name = packageInfo; packageInfo = {name:name}; } packageInfo = mix({main:"main"}, packageInfo); packageInfo.location = packageInfo.location ? packageInfo.location : name; // packageMap is depricated in favor of AMD map if(packageInfo.packageMap){ map[name] = packageInfo.packageMap; } if(!packageInfo.main.indexOf("./")){ packageInfo.main = packageInfo.main.substring(2); } // now that we've got a fully-resolved package object, push it into the configuration packs[name] = packageInfo; }, delayedModuleConfig // module config cannot be consummed until the loader is completely initialized; therefore, all // module config detected during booting is memorized and applied at the end of loader initialization // TODO: this is a bit of a kludge; all config should be moved to end of loader initialization, but // we'll delay this chore and do it with a final loader 1.x cleanup after the 2.x loader prototyping is complete = [], config = function(config, booting, referenceModule){ for(var p in config){ if(p=="waitSeconds"){ req.waitms = (config[p] || 0) * 1000; } if(p=="cacheBust"){ cacheBust = config[p] ? (isString(config[p]) ? config[p] : (new Date()).getTime() + "") : ""; } if(p=="baseUrl" || p=="combo"){ req[p] = config[p]; } if( 1 && p=="async"){ // falsy or "sync" => legacy sync loader // "xd" => sync but loading xdomain tree and therefore loading asynchronously (not configurable, set automatically by the loader) // "legacyAsync" => permanently in "xd" by choice // "debugAtAllCosts" => trying to load everything via script injection (not implemented) // otherwise, must be truthy => AMD // legacyMode: sync | legacyAsync | xd | false var mode = config[p]; req.legacyMode = legacyMode = (isString(mode) && /sync|legacyAsync/.test(mode) ? mode : (!mode ? sync : false)); req.async = !legacyMode; } if(config[p]!==hasCache){ // accumulate raw config info for client apps which can use this to pass their own config req.rawConfig[p] = config[p]; p!="has" && has.add("config-"+p, config[p], 0, booting); } } // make sure baseUrl exists if(!req.baseUrl){ req.baseUrl = "./"; } // make sure baseUrl ends with a slash if(!/\/$/.test(req.baseUrl)){ req.baseUrl += "/"; } // now do the special work for has, packages, packagePaths, paths, aliases, and cache for(p in config.has){ has.add(p, config.has[p], 0, booting); } // for each package found in any packages config item, augment the packs map owned by the loader forEach(config.packages, fixupPackageInfo); // for each packagePath found in any packagePaths config item, augment the packageConfig // packagePaths is depricated; remove in 2.0 for(baseUrl in config.packagePaths){ forEach(config.packagePaths[baseUrl], function(packageInfo){ var location = baseUrl + "/" + packageInfo; if(isString(packageInfo)){ packageInfo = {name:packageInfo}; } packageInfo.location = location; fixupPackageInfo(packageInfo); }); } // notice that computeMapProg treats the dest as a reference; therefore, if/when that variable // is published (see dojo-publish-privates), the published variable will always hold a valid value. // this must come after all package processing since package processing may mutate map computeMapProg(mix(map, config.map), mapProgs); forEach(mapProgs, function(item){ item[1] = computeMapProg(item[1], []); if(item[0]=="*"){ mapProgs.star = item[1]; } }); // push in any paths and recompute the internal pathmap computeMapProg(mix(paths, config.paths), pathsMapProg); // aliases forEach(config.aliases, function(pair){ if(isString(pair[0])){ pair[0] = new RegExp("^" + escapeString(pair[0]) + "$"); } aliases.push(pair); }); if(booting){ delayedModuleConfig.push({config:config.config}); }else{ for(p in config.config){ var module = getModule(p, referenceModule); module.config = mix(module.config || {}, config.config[p]); } } // push in any new cache values if(config.cache){ consumePendingCacheInsert(); pendingCacheInsert = config.cache; if(config.cache["*noref"]){ consumePendingCacheInsert(); } } signal("config", [config, req.rawConfig]); }; // // execute the various sniffs; userConfig can override and value // if(has("dojo-cdn") || 1 ){ // the sniff regex looks for a src attribute ending in dojo.js, optionally preceeded with a path. // match[3] returns the path to dojo.js (if any) without the trailing slash. This is used for the // dojo location on CDN deployments and baseUrl when either/both of these are not provided // explicitly in the config data; this is the 1.6- behavior. var scripts = doc.getElementsByTagName("script"), i = 0, script, dojoDir, src, match; while(i < scripts.length){ script = scripts[i++]; if((src = script.getAttribute("src")) && (match = src.match(/(((.*)\/)|^)dojo\.js(\W|$)/i))){ // sniff dojoDir and baseUrl dojoDir = match[3] || ""; defaultConfig.baseUrl = defaultConfig.baseUrl || dojoDir; // sniff configuration on attribute in script element src = (script.getAttribute("data-dojo-config") || script.getAttribute("djConfig")); if(src){ dojoSniffConfig = req.eval("({ " + src + " })", "data-dojo-config"); } // sniff requirejs attribute if( 0 ){ var dataMain = script.getAttribute("data-main"); if(dataMain){ dojoSniffConfig.deps = dojoSniffConfig.deps || [dataMain]; } } break; } } } if( 0 ){ // pass down doh.testConfig from parent as if it were a data-dojo-config try{ if(window.parent != window && window.parent.require){ var doh = window.parent.require("doh"); doh && mix(dojoSniffConfig, doh.testConfig); } }catch(e){} } // configure the loader; let the user override defaults req.rawConfig = {}; config(defaultConfig, 1); // do this before setting userConfig/sniffConfig to allow userConfig/sniff overrides if(has("dojo-cdn")){ packs.dojo.location = dojoDir; if(dojoDir){ dojoDir += "/"; } packs.dijit.location = dojoDir + "../dijit/"; packs.dojox.location = dojoDir + "../dojox/"; } config(userConfig, 1); config(dojoSniffConfig, 1); }else{ // no config API, assume defaultConfig has everything the loader needs...for the entire lifetime of the application paths = defaultConfig.paths; pathsMapProg = defaultConfig.pathsMapProg; packs = defaultConfig.packs; aliases = defaultConfig.aliases; mapProgs = defaultConfig.mapProgs; modules = defaultConfig.modules; cache = defaultConfig.cache; cacheBust = defaultConfig.cacheBust; // remember the default config for other processes (e.g., dojo/config) req.rawConfig = defaultConfig; } if( 0 ){ req.combo = req.combo || {add:noop}; var comboPending = 0, combosPending = [], comboPendingTimer = null; } // build the loader machinery iaw configuration, including has feature tests var injectDependencies = function(module){ // checkComplete!=0 holds the idle signal; we're not idle if we're injecting dependencies guardCheckComplete(function(){ forEach(module.deps, injectModule); if( 0 && comboPending && !comboPendingTimer){ comboPendingTimer = setTimeout(function() { comboPending = 0; comboPendingTimer = null; req.combo.done(function(mids, url) { var onLoadCallback= function(){ // defQ is a vector of module definitions 1-to-1, onto mids runDefQ(0, mids); checkComplete(); }; combosPending.push(mids); injectingModule = mids; req.injectUrl(url, onLoadCallback, mids); injectingModule = 0; }, req); }, 0); } }); }, contextRequire = function(a1, a2, a3, referenceModule, contextRequire){ var module, syntheticMid; if(isString(a1)){ // signature is (moduleId) module = getModule(a1, referenceModule, true); if(module && module.executed){ return module.result; } throw makeError("undefinedModule", a1); } if(!isArray(a1)){ // a1 is a configuration config(a1, 0, referenceModule); // juggle args; (a2, a3) may be (dependencies, callback) a1 = a2; a2 = a3; } if(isArray(a1)){ // signature is (requestList [,callback]) if(!a1.length){ a2 && a2(); }else{ syntheticMid = "require*" + uid(); // resolve the request list with respect to the reference module for(var mid, deps = [], i = 0; i < a1.length;){ mid = a1[i++]; deps.push(getModule(mid, referenceModule)); } // construct a synthetic module to control execution of the requestList, and, optionally, callback module = mix(makeModuleInfo("", syntheticMid, 0, ""), { injected: arrived, deps: deps, def: a2 || noop, require: referenceModule ? referenceModule.require : req, gc: 1 //garbage collect }); modules[module.mid] = module; // checkComplete!=0 holds the idle signal; we're not idle if we're injecting dependencies injectDependencies(module); // try to immediately execute // if already traversing a factory tree, then strict causes circular dependency to abort the execution; maybe // it's possible to execute this require later after the current traversal completes and avoid the circular dependency. // ...but *always* insist on immediate in synch mode var strict = checkCompleteGuard && legacyMode!=sync; guardCheckComplete(function(){ execModule(module, strict); }); if(!module.executed){ // some deps weren't on board or circular dependency detected and strict; therefore, push into the execQ execQ.push(module); } checkComplete(); } } return contextRequire; }, createRequire = function(module){ if(!module){ return req; } var result = module.require; if(!result){ result = function(a1, a2, a3){ return contextRequire(a1, a2, a3, module, result); }; module.require = mix(result, req); result.module = module; result.toUrl = function(name){ return toUrl(name, module); }; result.toAbsMid = function(mid){ return toAbsMid(mid, module); }; if( 0 ){ result.undef = function(mid){ req.undef(mid, module); }; } if( 1 ){ result.syncLoadNls = function(mid){ var nlsModuleInfo = getModuleInfo(mid, module), nlsModule = modules[nlsModuleInfo.mid]; if(!nlsModule || !nlsModule.executed){ cached = cache[nlsModuleInfo.mid] || cache[urlKeyPrefix + nlsModuleInfo.url]; if(cached){ evalModuleText(cached); nlsModule = modules[nlsModuleInfo.mid]; } } return nlsModule && nlsModule.executed && nlsModule.result; }; } } return result; }, execQ = // The list of modules that need to be evaluated. [], defQ = // The queue of define arguments sent to loader. [], waiting = // The set of modules upon which the loader is waiting for definition to arrive {}, setRequested = function(module){ module.injected = requested; waiting[module.mid] = 1; if(module.url){ waiting[module.url] = module.pack || 1; } startTimer(); }, setArrived = function(module){ module.injected = arrived; delete waiting[module.mid]; if(module.url){ delete waiting[module.url]; } if(isEmpty(waiting)){ clearTimer(); 1 && legacyMode==xd && (legacyMode = sync); } }, execComplete = req.idle = // says the loader has completed (or not) its work function(){ return !defQ.length && isEmpty(waiting) && !execQ.length && !checkCompleteGuard; }, runMapProg = function(targetMid, map){ // search for targetMid in map; return the map item if found; falsy otherwise if(map){ for(var i = 0; i < map.length; i++){ if(map[i][2].test(targetMid)){ return map[i]; } } } return 0; }, compactPath = function(path){ var result = [], segment, lastSegment; path = path.replace(/\\/g, '/').split('/'); while(path.length){ segment = path.shift(); if(segment==".." && result.length && lastSegment!=".."){ result.pop(); lastSegment = result[result.length - 1]; }else if(segment!="."){ result.push(lastSegment= segment); } // else ignore "." } return result.join("/"); }, makeModuleInfo = function(pid, mid, pack, url){ if( 1 ){ var xd= req.isXdUrl(url); return {pid:pid, mid:mid, pack:pack, url:url, executed:0, def:0, isXd:xd, isAmd:!!(xd || (packs[pid] && packs[pid].isAmd))}; }else{ return {pid:pid, mid:mid, pack:pack, url:url, executed:0, def:0}; } }, getModuleInfo_ = function(mid, referenceModule, packs, modules, baseUrl, mapProgs, pathsMapProg, alwaysCreate){ // arguments are passed instead of using lexical variables so that this function my be used independent of the loader (e.g., the builder) // alwaysCreate is useful in this case so that getModuleInfo never returns references to real modules owned by the loader var pid, pack, midInPackage, mapProg, mapItem, url, result, isRelative, requestedMid; requestedMid = mid; isRelative = /^\./.test(mid); if(/(^\/)|(\:)|(\.js$)/.test(mid) || (isRelative && !referenceModule)){ // absolute path or protocol of .js filetype, or relative path but no reference module and therefore relative to page // whatever it is, it's not a module but just a URL of some sort // note: pid===0 indicates the routine is returning an unmodified mid return makeModuleInfo(0, mid, 0, mid); }else{ // relative module ids are relative to the referenceModule; get rid of any dots mid = compactPath(isRelative ? (referenceModule.mid + "/../" + mid) : mid); if(/^\./.test(mid)){ throw makeError("irrationalPath", mid); } // at this point, mid is an absolute mid // map the mid if(referenceModule){ mapItem = runMapProg(referenceModule.mid, mapProgs); } mapItem = mapItem || mapProgs.star; mapItem = mapItem && runMapProg(mid, mapItem[1]); if(mapItem){ mid = mapItem[1] + mid.substring(mapItem[3]); } match = mid.match(/^([^\/]+)(\/(.+))?$/); pid = match ? match[1] : ""; if((pack = packs[pid])){ mid = pid + "/" + (midInPackage = (match[3] || pack.main)); }else{ pid = ""; } // search aliases var candidateLength = 0, candidate = 0; forEach(aliases, function(pair){ var match = mid.match(pair[0]); if(match && match.length>candidateLength){ candidate = isFunction(pair[1]) ? mid.replace(pair[0], pair[1]) : pair[1]; } }); if(candidate){ return getModuleInfo_(candidate, 0, packs, modules, baseUrl, mapProgs, pathsMapProg, alwaysCreate); } result = modules[mid]; if(result){ return alwaysCreate ? makeModuleInfo(result.pid, result.mid, result.pack, result.url) : modules[mid]; } } // get here iff the sought-after module does not yet exist; therefore, we need to compute the URL given the // fully resolved (i.e., all relative indicators and package mapping resolved) module id // note: pid!==0 indicates the routine is returning a url that has .js appended unmodified mid mapItem = runMapProg(mid, pathsMapProg); if(mapItem){ url = mapItem[1] + mid.substring(mapItem[3]); }else if(pid){ url = pack.location + "/" + midInPackage; }else if(has("config-tlmSiblingOfDojo")){ url = "../" + mid; }else{ url = mid; } // if result is not absolute, add baseUrl if(!(/(^\/)|(\:)/.test(url))){ url = baseUrl + url; } url += ".js"; return makeModuleInfo(pid, mid, pack, compactPath(url)); }, getModuleInfo = function(mid, referenceModule){ return getModuleInfo_(mid, referenceModule, packs, modules, req.baseUrl, mapProgs, pathsMapProg); }, resolvePluginResourceId = function(plugin, prid, referenceModule){ return plugin.normalize ? plugin.normalize(prid, function(mid){return toAbsMid(mid, referenceModule);}) : toAbsMid(prid, referenceModule); }, dynamicPluginUidGenerator = 0, getModule = function(mid, referenceModule, immediate){ // compute and optionally construct (if necessary) the module implied by the mid with respect to referenceModule var match, plugin, prid, result; match = mid.match(/^(.+?)\!(.*)$/); if(match){ // name was ! plugin = getModule(match[1], referenceModule, immediate); if( 1 && legacyMode == sync && !plugin.executed){ injectModule(plugin); if(plugin.injected===arrived && !plugin.executed){ guardCheckComplete(function(){ execModule(plugin); }); } if(plugin.executed){ promoteModuleToPlugin(plugin); }else{ // we are in xdomain mode for some reason execQ.unshift(plugin); } } if(plugin.executed === executed && !plugin.load){ // executed the module not knowing it was a plugin promoteModuleToPlugin(plugin); } // if the plugin has not been loaded, then can't resolve the prid and must assume this plugin is dynamic until we find out otherwise if(plugin.load){ prid = resolvePluginResourceId(plugin, match[2], referenceModule); mid = (plugin.mid + "!" + (plugin.dynamic ? ++dynamicPluginUidGenerator + "!" : "") + prid); }else{ prid = match[2]; mid = plugin.mid + "!" + (++dynamicPluginUidGenerator) + "!waitingForPlugin"; } result = {plugin:plugin, mid:mid, req:createRequire(referenceModule), prid:prid}; }else{ result = getModuleInfo(mid, referenceModule); } return modules[result.mid] || (!immediate && (modules[result.mid] = result)); }, toAbsMid = req.toAbsMid = function(mid, referenceModule){ return getModuleInfo(mid, referenceModule).mid; }, toUrl = req.toUrl = function(name, referenceModule){ var moduleInfo = getModuleInfo(name+"/x", referenceModule), url= moduleInfo.url; return fixupUrl(moduleInfo.pid===0 ? // if pid===0, then name had a protocol or absolute path; either way, toUrl is the identify function in such cases name : // "/x.js" since getModuleInfo automatically appends ".js" and we appended "/x" to make name look likde a module id url.substring(0, url.length-5) ); }, nonModuleProps = { injected: arrived, executed: executed, def: nonmodule, result: nonmodule }, makeCjs = function(mid){ return modules[mid] = mix({mid:mid}, nonModuleProps); }, cjsRequireModule = makeCjs("require"), cjsExportsModule = makeCjs("exports"), cjsModuleModule = makeCjs("module"), runFactory = function(module, args){ req.trace("loader-run-factory", [module.mid]); var factory = module.def, result; 1 && syncExecStack.unshift(module); if(has("config-dojo-loader-catches")){ try{ result= isFunction(factory) ? factory.apply(null, args) : factory; }catch(e){ signal(error, module.result = makeError("factoryThrew", [module, e])); } }else{ result= isFunction(factory) ? factory.apply(null, args) : factory; } module.result = result===undefined && module.cjs ? module.cjs.exports : result; 1 && syncExecStack.shift(module); }, abortExec = {}, defOrder = 0, promoteModuleToPlugin = function(pluginModule){ var plugin = pluginModule.result; pluginModule.dynamic = plugin.dynamic; pluginModule.normalize = plugin.normalize; pluginModule.load = plugin.load; return pluginModule; }, resolvePluginLoadQ = function(plugin){ // plugins is a newly executed module that has a loadQ waiting to run // step 1: traverse the loadQ and fixup the mid and prid; remember the map from original mid to new mid // recall the original mid was created before the plugin was on board and therefore it was impossible to // compute the final mid; accordingly, prid may or may not change, but the mid will definitely change var map = {}; forEach(plugin.loadQ, function(pseudoPluginResource){ // manufacture and insert the real module in modules var prid = resolvePluginResourceId(plugin, pseudoPluginResource.prid, pseudoPluginResource.req.module), mid = plugin.dynamic ? pseudoPluginResource.mid.replace(/waitingForPlugin$/, prid) : (plugin.mid + "!" + prid), pluginResource = mix(mix({}, pseudoPluginResource), {mid:mid, prid:prid, injected:0}); if(!modules[mid]){ // create a new (the real) plugin resource and inject it normally now that the plugin is on board injectPlugin(modules[mid] = pluginResource); } // else this was a duplicate request for the same (plugin, rid) for a nondynamic plugin // pluginResource is really just a placeholder with the wrong mid (because we couldn't calculate it until the plugin was on board) // mark is as arrived and delete it from modules; the real module was requested above map[pseudoPluginResource.mid] = modules[mid]; setArrived(pseudoPluginResource); delete modules[pseudoPluginResource.mid]; }); plugin.loadQ = 0; // step2: replace all references to any placeholder modules with real modules var substituteModules = function(module){ for(var replacement, deps = module.deps || [], i = 0; i")]); return (!module.def || strict) ? abortExec : (module.cjs && module.cjs.exports); } // at this point the module is either not executed or fully executed if(!module.executed){ if(!module.def){ return abortExec; } var mid = module.mid, deps = module.deps || [], arg, argResult, args = [], i = 0; if( 0 ){ circleTrace.push(mid); req.trace("loader-exec-module", ["exec", circleTrace.length, mid]); } // for circular dependencies, assume the first module encountered was executed OK // modules that circularly depend on a module that has not run its factory will get // the premade cjs.exports===module.result. They can take a reference to this object and/or // add properties to it. When the module finally runs its factory, the factory can // read/write/replace this object. Notice that so long as the object isn't replaced, any // reference taken earlier while walking the deps list is still valid. module.executed = executing; while(i < deps.length){ arg = deps[i++]; argResult = ((arg === cjsRequireModule) ? createRequire(module) : ((arg === cjsExportsModule) ? module.cjs.exports : ((arg === cjsModuleModule) ? module.cjs : execModule(arg, strict)))); if(argResult === abortExec){ module.executed = 0; req.trace("loader-exec-module", ["abort", mid]); 0 && circleTrace.pop(); return abortExec; } args.push(argResult); } runFactory(module, args); finishExec(module); 0 && circleTrace.pop(); } // at this point the module is guaranteed fully executed return module.result; }, checkCompleteGuard = 0, guardCheckComplete = function(proc){ try{ checkCompleteGuard++; proc(); }finally{ checkCompleteGuard--; } if(execComplete()){ signal("idle", []); } }, checkComplete = function(){ // keep going through the execQ as long as at least one factory is executed // plugins, recursion, cached modules all make for many execution path possibilities if(checkCompleteGuard){ return; } guardCheckComplete(function(){ checkDojoRequirePlugin(); for(var currentDefOrder, module, i = 0; i < execQ.length;){ currentDefOrder = defOrder; module = execQ[i]; execModule(module); if(currentDefOrder!=defOrder){ // defOrder was bumped one or more times indicating something was executed (note, this indicates // the execQ was modified, maybe a lot (for example a later module causes an earlier module to execute) checkDojoRequirePlugin(); i = 0; }else{ // nothing happened; check the next module in the exec queue i++; } } }); }; if( 0 ){ req.undef = function(moduleId, referenceModule){ // In order to reload a module, it must be undefined (this routine) and then re-requested. // This is useful for testing frameworks (at least). var module = getModule(moduleId, referenceModule); setArrived(module); delete modules[module.mid]; }; } if( 1 ){ if(has("dojo-loader-eval-hint-url")===undefined){ has.add("dojo-loader-eval-hint-url", 1); } var fixupUrl= function(url){ url += ""; // make sure url is a Javascript string (some paths may be a Java string) return url + (cacheBust ? ((/\?/.test(url) ? "&" : "?") + cacheBust) : ""); }, injectPlugin = function( module ){ // injects the plugin module given by module; may have to inject the plugin itself var plugin = module.plugin; if(plugin.executed === executed && !plugin.load){ // executed the module not knowing it was a plugin promoteModuleToPlugin(plugin); } var onLoad = function(def){ module.result = def; setArrived(module); finishExec(module); checkComplete(); }; if(plugin.load){ plugin.load(module.prid, module.req, onLoad); }else if(plugin.loadQ){ plugin.loadQ.push(module); }else{ // the unshift instead of push is important: we don't want plugins to execute as // dependencies of some other module because this may cause circles when the plugin // loadQ is run; also, generally, we want plugins to run early since they may load // several other modules and therefore can potentially unblock many modules plugin.loadQ = [module]; execQ.unshift(plugin); injectModule(plugin); } }, // for IE, injecting a module may result in a recursive execution if the module is in the cache cached = 0, injectingModule = 0, injectingCachedModule = 0, evalModuleText = function(text, module){ // see def() for the injectingCachedModule bracket; it simply causes a short, safe curcuit if(has("config-stripStrict")){ text = text.replace(/"use strict"/g, ''); } injectingCachedModule = 1; if(has("config-dojo-loader-catches")){ try{ if(text===cached){ cached.call(null); }else{ req.eval(text, has("dojo-loader-eval-hint-url") ? module.url : module.mid); } }catch(e){ signal(error, makeError("evalModuleThrew", module)); } }else{ if(text===cached){ cached.call(null); }else{ req.eval(text, has("dojo-loader-eval-hint-url") ? module.url : module.mid); } } injectingCachedModule = 0; }, injectModule = function(module){ // Inject the module. In the browser environment, this means appending a script element into // the document; in other environments, it means loading a file. // // If in synchronous mode, then get the module synchronously if it's not xdomainLoading. var mid = module.mid, url = module.url; if(module.executed || module.injected || waiting[mid] || (module.url && ((module.pack && waiting[module.url]===module.pack) || waiting[module.url]==1))){ return; } setRequested(module); if( 0 ){ var viaCombo = 0; if(module.plugin && module.plugin.isCombo){ // a combo plugin; therefore, must be handled by combo service // the prid should have already been converted to a URL (if required by the plugin) during // the normalze process; in any event, there is no way for the loader to know how to // to the conversion; therefore the third argument is zero req.combo.add(module.plugin.mid, module.prid, 0, req); viaCombo = 1; }else if(!module.plugin){ viaCombo = req.combo.add(0, module.mid, module.url, req); } if(viaCombo){ comboPending= 1; return; } } if(module.plugin){ injectPlugin(module); return; } // else a normal module (not a plugin) var onLoadCallback = function(){ runDefQ(module); if(module.injected !== arrived){ // the script that contained the module arrived and has been executed yet // nothing was added to the defQ (so it wasn't an AMD module) and the module // wasn't marked as arrived by dojo.provide (so it wasn't a v1.6- module); // therefore, it must not have been a module; adjust state accordingly setArrived(module); mix(module, nonModuleProps); req.trace("loader-define-nonmodule", [module.url]); } if( 1 && legacyMode){ // must call checkComplete even in for sync loader because we may be in xdomainLoading mode; // but, if xd loading, then don't call checkComplete until out of the current sync traversal // in order to preserve order of execution of the dojo.required modules !syncExecStack.length && checkComplete(); }else{ checkComplete(); } }; cached = cache[mid] || cache[urlKeyPrefix + module.url]; if(cached){ req.trace("loader-inject", ["cache", module.mid, url]); evalModuleText(cached, module); onLoadCallback(); return; } if( 1 && legacyMode){ if(module.isXd){ // switch to async mode temporarily; if current legacyMode!=sync, then is must be one of {legacyAsync, xd, false} legacyMode==sync && (legacyMode = xd); // fall through and load via script injection }else if(module.isAmd && legacyMode!=sync){ // fall through and load via script injection }else{ // mode may be sync, xd/legacyAsync, or async; module may be AMD or legacy; but module is always located on the same domain var xhrCallback = function(text){ if(legacyMode==sync){ // the top of syncExecStack gives the current synchronously executing module; the loader needs // to know this if it has to switch to async loading in the middle of evaluating a legacy module // this happens when a modules dojo.require's a module that must be loaded async because it's xdomain // (using unshift/shift because there is no back() methods for Javascript arrays) syncExecStack.unshift(module); evalModuleText(text, module); syncExecStack.shift(); // maybe the module was an AMD module runDefQ(module); // legacy modules never get to defineModule() => cjs and injected never set; also evaluation implies executing if(!module.cjs){ setArrived(module); finishExec(module); } if(module.finish){ // while synchronously evaluating this module, dojo.require was applied referencing a module // that had to be loaded async; therefore, the loader stopped answering all dojo.require // requests so they could be answered completely in the correct sequence; module.finish gives // the list of dojo.requires that must be re-applied once all target modules are available; // make a synthetic module to execute the dojo.require's in the correct order // compute a guarnateed-unique mid for the synthetic finish module; remember the finish vector; remove it from the reference module // TODO: can we just leave the module.finish...what's it hurting? var finishMid = mid + "*finish", finish = module.finish; delete module.finish; def(finishMid, ["dojo", ("dojo/require!" + finish.join(",")).replace(/\./g, "/")], function(dojo){ forEach(finish, function(mid){ dojo.require(mid); }); }); // unshift, not push, which causes the current traversal to be reattempted from the top execQ.unshift(getModule(finishMid)); } onLoadCallback(); }else{ text = transformToAmd(module, text); if(text){ evalModuleText(text, module); onLoadCallback(); }else{ // if transformToAmd returned falsy, then the module was already AMD and it can be script-injected // do so to improve debugability(even though it means another download...which probably won't happen with a good browser cache) injectingModule = module; req.injectUrl(fixupUrl(url), onLoadCallback, module); injectingModule = 0; } } }; req.trace("loader-inject", ["xhr", module.mid, url, legacyMode!=sync]); if(has("config-dojo-loader-catches")){ try{ req.getText(url, legacyMode!=sync, xhrCallback); }catch(e){ signal(error, makeError("xhrInjectFailed", [module, e])); } }else{ req.getText(url, legacyMode!=sync, xhrCallback); } return; } } // else async mode or fell through in xdomain loading mode; either way, load by script injection req.trace("loader-inject", ["script", module.mid, url]); injectingModule = module; req.injectUrl(fixupUrl(url), onLoadCallback, module); injectingModule = 0; }, defineModule = function(module, deps, def){ req.trace("loader-define-module", [module.mid, deps]); if( 0 && module.plugin && module.plugin.isCombo){ // the module is a plugin resource loaded by the combo service // note: check for module.plugin should be enough since normal plugin resources should // not follow this path; module.plugin.isCombo is future-proofing belt and suspenders module.result = isFunction(def) ? def() : def; setArrived(module); finishExec(module); return module; }; var mid = module.mid; if(module.injected === arrived){ signal(error, makeError("multipleDefine", module)); return module; } mix(module, { deps: deps, def: def, cjs: { id: module.mid, uri: module.url, exports: (module.result = {}), setExports: function(exports){ module.cjs.exports = exports; }, config:function(){ return module.config; } } }); // resolve deps with respect to this module for(var i = 0; i < deps.length; i++){ deps[i] = getModule(deps[i], module); } if( 1 && legacyMode && !waiting[mid]){ // the module showed up without being asked for; it was probably in a // return new NodeList(); // dojo/NodeList }; =====*/ // the query that is returned from this module is slightly different than dojo.query, // because dojo.query has to maintain backwards compatibility with returning a // true array which has performance problems. The query returned from the module // does not use true arrays, but rather inherits from Array, making it much faster to // instantiate. dojo.query = queryForEngine(defaultEngine, function(array){ // call it without the new operator to invoke the back-compat behavior that returns a true array return NodeList(array); // dojo/NodeList }); query.load = function(id, parentRequire, loaded){ // summary: // can be used as AMD plugin to conditionally load new query engine // example: // | require(["dojo/query!custom"], function(qsa){ // | // loaded selector/custom.js as engine // | qsa("#foobar").forEach(...); // | }); loader.load(id, parentRequire, function(engine){ loaded(queryForEngine(engine, NodeList)); }); }; dojo._filterQueryResult = query._filterResult = function(nodes, selector, root){ return new NodeList(query.filter(nodes, selector, root)); }; dojo.NodeList = query.NodeList = NodeList; return query; }); }, 'dojo/has':function(){ define(["require", "module"], function(require, module){ // module: // dojo/has // summary: // Defines the has.js API and several feature tests used by dojo. // description: // This module defines the has API as described by the project has.js with the following additional features: // // - the has test cache is exposed at has.cache. // - the method has.add includes a forth parameter that controls whether or not existing tests are replaced // - the loader's has cache may be optionally copied into this module's has cahce. // // This module adopted from https://github.com/phiggins42/has.js; thanks has.js team! // try to pull the has implementation from the loader; both the dojo loader and bdLoad provide one // if using a foreign loader, then the has cache may be initialized via the config object for this module // WARNING: if a foreign loader defines require.has to be something other than the has.js API, then this implementation fail var has = require.has || function(){}; if(! 1 ){ var isBrowser = // the most fundamental decision: are we in the browser? typeof window != "undefined" && typeof location != "undefined" && typeof document != "undefined" && window.location == location && window.document == document, // has API variables global = this, doc = isBrowser && document, element = doc && doc.createElement("DiV"), cache = (module.config && module.config()) || {}; has = function(name){ // summary: // Return the current value of the named feature. // // name: String|Integer // The name (if a string) or identifier (if an integer) of the feature to test. // // description: // Returns the value of the feature named by name. The feature must have been // previously added to the cache by has.add. return typeof cache[name] == "function" ? (cache[name] = cache[name](global, doc, element)) : cache[name]; // Boolean }; has.cache = cache; has.add = function(name, test, now, force){ // summary: // Register a new feature test for some named feature. // name: String|Integer // The name (if a string) or identifier (if an integer) of the feature to test. // test: Function // A test function to register. If a function, queued for testing until actually // needed. The test function should return a boolean indicating // the presence of a feature or bug. // now: Boolean? // Optional. Omit if `test` is not a function. Provides a way to immediately // run the test and cache the result. // force: Boolean? // Optional. If the test already exists and force is truthy, then the existing // test will be replaced; otherwise, add does not replace an existing test (that // is, by default, the first test advice wins). // example: // A redundant test, testFn with immediate execution: // | has.add("javascript", function(){ return true; }, true); // // example: // Again with the redundantness. You can do this in your tests, but we should // not be doing this in any internal has.js tests // | has.add("javascript", true); // // example: // Three things are passed to the testFunction. `global`, `document`, and a generic element // from which to work your test should the need arise. // | has.add("bug-byid", function(g, d, el){ // | // g == global, typically window, yadda yadda // | // d == document object // | // el == the generic element. a `has` element. // | return false; // fake test, byid-when-form-has-name-matching-an-id is slightly longer // | }); (typeof cache[name]=="undefined" || force) && (cache[name]= test); return now && has(name); }; // since we're operating under a loader that doesn't provide a has API, we must explicitly initialize // has as it would have otherwise been initialized by the dojo loader; use has.add to the builder // can optimize these away iff desired 1 || has.add("host-browser", isBrowser); 1 || has.add("dom", isBrowser); 1 || has.add("dojo-dom-ready-api", 1); 1 || has.add("dojo-sniff", 1); } if( 1 ){ // Common application level tests has.add("dom-addeventlistener", !!document.addEventListener); has.add("touch", "ontouchstart" in document); // I don't know if any of these tests are really correct, just a rough guess has.add("device-width", screen.availWidth || innerWidth); // Tests for DOMNode.attributes[] behavior: // - dom-attributes-explicit - attributes[] only lists explicitly user specified attributes // - dom-attributes-specified-flag (IE8) - need to check attr.specified flag to skip attributes user didn't specify // - Otherwise, in IE6-7. attributes[] will list hundreds of values, so need to do outerHTML to get attrs instead. var form = document.createElement("form"); has.add("dom-attributes-explicit", form.attributes.length == 0); // W3C has.add("dom-attributes-specified-flag", form.attributes.length > 0 && form.attributes.length < 40); // IE8 } has.clearElement = function(element){ // summary: // Deletes the contents of the element passed to test functions. element.innerHTML= ""; return element; }; has.normalize = function(id, toAbsMid){ // summary: // Resolves id into a module id based on possibly-nested tenary expression that branches on has feature test value(s). // // toAbsMid: Function // Resolves a relative module id into an absolute module id var tokens = id.match(/[\?:]|[^:\?]*/g), i = 0, get = function(skip){ var term = tokens[i++]; if(term == ":"){ // empty string module name, resolves to 0 return 0; }else{ // postfixed with a ? means it is a feature to branch on, the term is the name of the feature if(tokens[i++] == "?"){ if(!skip && has(term)){ // matched the feature, get the first value from the options return get(); }else{ // did not match, get the second value, passing over the first get(true); return get(skip); } } // a module return term || 0; } }; id = get(); return id && toAbsMid(id); }; has.load = function(id, parentRequire, loaded){ // summary: // Conditional loading of AMD modules based on a has feature test value. // id: String // Gives the resolved module id to load. // parentRequire: Function // The loader require function with respect to the module that contained the plugin resource in it's // dependency list. // loaded: Function // Callback to loader that consumes result of plugin demand. if(id){ parentRequire([id], loaded); }else{ loaded(); } }; return has; }); }, 'dojo/_base/loader':function(){ define(["./kernel", "../has", "require", "module", "./json", "./lang", "./array"], function(dojo, has, require, thisModule, json, lang, array) { // module: // dojo/_base/loader // This module defines the v1.x synchronous loader API. // signal the loader in sync mode... //>>pure-amd if (! 1 ){ console.error("cannot load the Dojo v1.x loader with a foreign loader"); return 0; } 1 || has.add("dojo-fast-sync-require", 1); var makeErrorToken = function(id){ return {src:thisModule.id, id:id}; }, slashName = function(name){ return name.replace(/\./g, "/"); }, buildDetectRe = /\/\/>>built/, dojoRequireCallbacks = [], dojoRequireModuleStack = [], dojoRequirePlugin = function(mid, require, loaded){ dojoRequireCallbacks.push(loaded); array.forEach(mid.split(","), function(mid){ var module = getModule(mid, require.module); dojoRequireModuleStack.push(module); injectModule(module); }); checkDojoRequirePlugin(); }, checkDojoRequirePlugin = ( 1 ? // This version of checkDojoRequirePlugin makes the observation that all dojoRequireCallbacks can be released // when all *non-dojo/require!, dojo/loadInit!* modules are either executed, not requested, or arrived. This is // the case since there are no more modules the loader is waiting for, therefore, dojo/require! must have // everything it needs on board. // // The potential weakness of this algorithm is that dojo/require will not execute callbacks until *all* dependency // trees are ready. It is possible that some trees may be ready earlier than others, and this extra wait is non-optimal. // Still, for big projects, this seems better than the original algorithm below that proved slow in some cases. // Note, however, the original algorithm had the potential to execute partial trees, but that potential was never enabled. // There are also other optimization available with the original algorithm that have not been explored. function(){ var module, mid; for(mid in modules){ module = modules[mid]; if(module.noReqPluginCheck===undefined){ // tag the module as either a loadInit or require plugin or not for future reference module.noReqPluginCheck = /loadInit\!/.test(mid) || /require\!/.test(mid) ? 1 : 0; } if(!module.executed && !module.noReqPluginCheck && module.injected==requested){ return; } } guardCheckComplete(function(){ var oldCallbacks = dojoRequireCallbacks; dojoRequireCallbacks = []; array.forEach(oldCallbacks, function(cb){cb(1);}); }); } : (function(){ // Note: this is the original checkDojoRequirePlugin that is much slower than the algorithm above. However, we know it // works, so we leave it here in case the algorithm above fails in some corner case. // // checkDojoRequirePlugin inspects all of the modules demanded by a dojo/require! dependency // to see if they have arrived. The loader does not release *any* of these modules to be instantiated // until *all* of these modules are on board, thereby preventing the evaluation of a module with dojo.require's // that reference modules that are not available. // // The algorithm works by traversing the dependency graphs (remember, there can be cycles so they are not trees) // of each module in the dojoRequireModuleStack array (which contains the list of modules demanded by dojo/require!). // The moment a single module is discovered that is missing, the algorithm gives up and indicates that not all // modules are on board. dojo/loadInit! and dojo/require! are ignored because there dependencies are inserted // directly in dojoRequireModuleStack. For example, if "your/module" module depends on "dojo/require!my/module", then // *both* "dojo/require!my/module" and "my/module" will be in dojoRequireModuleStack. Obviously, if "my/module" // is on board, then "dojo/require!my/module" is also satisfied, so the algorithm doesn't check for "dojo/require!my/module". // // Note: inserting a dojo/require! dependency in the dojoRequireModuleStack achieves nothing // with the current algorithm; however, having such modules present makes it possible to optimize the algorithm // // Note: prior versions of this algorithm had an optimization that signaled loaded on dojo/require! dependencies // individually (rather than waiting for them all to be resolved). The implementation proved problematic with cycles // and plugins. However, it is possible to reattach that strategy in the future. // a set from module-id to {undefined | 1 | 0}, where... // undefined => the module has not been inspected // 0 => the module or at least one of its dependencies has not arrived // 1 => the module is a loadInit! or require! plugin resource, or is currently being traversed (therefore, assume // OK until proven otherwise), or has been completely traversed and all dependencies have arrived var touched, traverse = function(m){ touched[m.mid] = 1; for(var t, module, deps = m.deps || [], i= 0; i a built module, always AMD // extractResult==0 => no sync API return 0; } // manufacture a synthetic module id that can never be a real mdule id (just like require does) id = module.mid + "-*loadInit"; // construct the dojo/loadInit names vector which causes any relocated names to be defined as lexical variables under their not-relocated name // the dojo/loadInit plugin assumes the first name in names is "dojo" for(var p in getModule("dojo", module).result.scopeMap){ names.push(p); namesAsStrings.push('"' + p + '"'); } // rewrite the module as a synthetic dojo/loadInit plugin resource + the module expressed as an AMD module that depends on this synthetic resource // don't have to map dojo/init since that will occur when the dependency is resolved return "// xdomain rewrite of " + module.mid + "\n" + "define('" + id + "',{\n" + "\tnames:" + dojo.toJson(names) + ",\n" + "\tdef:function(" + names.join(",") + "){" + extractResult[1] + "}" + "});\n\n" + "define(" + dojo.toJson(names.concat(["dojo/loadInit!"+id])) + ", function(" + names.join(",") + "){\n" + extractResult[0] + "});"; }, loaderVars = require.initSyncLoader(dojoRequirePlugin, checkDojoRequirePlugin, transformToAmd), sync = loaderVars.sync, requested = loaderVars.requested, arrived = loaderVars.arrived, nonmodule = loaderVars.nonmodule, executing = loaderVars.executing, executed = loaderVars.executed, syncExecStack = loaderVars.syncExecStack, modules = loaderVars.modules, execQ = loaderVars.execQ, getModule = loaderVars.getModule, injectModule = loaderVars.injectModule, setArrived = loaderVars.setArrived, signal = loaderVars.signal, finishExec = loaderVars.finishExec, execModule = loaderVars.execModule, getLegacyMode = loaderVars.getLegacyMode, guardCheckComplete = loaderVars.guardCheckComplete; // there is exactly one dojoRequirePlugin among possibly-many dojo/_base/loader's (owing to mapping) dojoRequirePlugin = loaderVars.dojoRequirePlugin; dojo.provide = function(mid){ var executingModule = syncExecStack[0], module = lang.mixin(getModule(slashName(mid), require.module), { executed:executing, result:lang.getObject(mid, true) }); setArrived(module); if(executingModule){ (executingModule.provides || (executingModule.provides = [])).push(function(){ module.result = lang.getObject(mid); delete module.provides; module.executed!==executed && finishExec(module); }); }// else dojo.provide called not consequent to loading; therefore, give up trying to publish module value to loader namespace return module.result; }; has.add("config-publishRequireResult", 1, 0, 0); dojo.require = function(moduleName, omitModuleCheck) { // summary: // loads a Javascript module from the appropriate URI // // moduleName: String // module name to load, using periods for separators, // e.g. "dojo.date.locale". Module paths are de-referenced by dojo's // internal mapping of locations to names and are disambiguated by // longest prefix. See `dojo.registerModulePath()` for details on // registering new modules. // // omitModuleCheck: Boolean? // if `true`, omitModuleCheck skips the step of ensuring that the // loaded file actually defines the symbol it is referenced by. // For example if it called as `dojo.require("a.b.c")` and the // file located at `a/b/c.js` does not define an object `a.b.c`, // and exception will be throws whereas no exception is raised // when called as `dojo.require("a.b.c", true)` // // description: // Modules are loaded via dojo.require by using one of two loaders: the normal loader // and the xdomain loader. The xdomain loader is used when dojo was built with a // custom build that specified loader=xdomain and the module lives on a modulePath // that is a whole URL, with protocol and a domain. The versions of Dojo that are on // the Google and AOL CDNs use the xdomain loader. // // If the module is loaded via the xdomain loader, it is an asynchronous load, since // the module is added via a dynamically created script tag. This // means that dojo.require() can return before the module has loaded. However, this // should only happen in the case where you do dojo.require calls in the top-level // HTML page, or if you purposely avoid the loader checking for dojo.require // dependencies in your module by using a syntax like dojo["require"] to load the module. // // Sometimes it is useful to not have the loader detect the dojo.require calls in the // module so that you can dynamically load the modules as a result of an action on the // page, instead of right at module load time. // // Also, for script blocks in an HTML page, the loader does not pre-process them, so // it does not know to download the modules before the dojo.require calls occur. // // So, in those two cases, when you want on-the-fly module loading or for script blocks // in the HTML page, special care must be taken if the dojo.required code is loaded // asynchronously. To make sure you can execute code that depends on the dojo.required // modules, be sure to add the code that depends on the modules in a dojo.addOnLoad() // callback. dojo.addOnLoad waits for all outstanding modules to finish loading before // executing. // // This type of syntax works with both xdomain and normal loaders, so it is good // practice to always use this idiom for on-the-fly code loading and in HTML script // blocks. If at some point you change loaders and where the code is loaded from, // it will all still work. // // More on how dojo.require // `dojo.require("A.B")` first checks to see if symbol A.B is // defined. If it is, it is simply returned (nothing to do). // // If it is not defined, it will look for `A/B.js` in the script root // directory. // // `dojo.require` throws an exception if it cannot find a file // to load, or if the symbol `A.B` is not defined after loading. // // It returns the object `A.B`, but note the caveats above about on-the-fly loading and // HTML script blocks when the xdomain loader is loading a module. // // `dojo.require()` does nothing about importing symbols into // the current namespace. It is presumed that the caller will // take care of that. // // example: // To use dojo.require in conjunction with dojo.ready: // // | dojo.require("foo"); // | dojo.require("bar"); // | dojo.addOnLoad(function(){ // | //you can now safely do something with foo and bar // | }); // // example: // For example, to import all symbols into a local block, you might write: // // | with (dojo.require("A.B")) { // | ... // | } // // And to import just the leaf symbol to a local variable: // // | var B = dojo.require("A.B"); // | ... // // returns: // the required namespace object function doRequire(mid, omitModuleCheck){ var module = getModule(slashName(mid), require.module); if(syncExecStack.length && syncExecStack[0].finish){ // switched to async loading in the middle of evaluating a legacy module; stop // applying dojo.require so the remaining dojo.requires are applied in order syncExecStack[0].finish.push(mid); return undefined; } // recall module.executed has values {0, executing, executed}; therefore, truthy indicates executing or executed if(module.executed){ return module.result; } omitModuleCheck && (module.result = nonmodule); // rcg...why here and in two lines?? var currentMode = getLegacyMode(); // recall, in sync mode to inject is to *eval* the module text // if the module is a legacy module, this is the same as executing // but if the module is an AMD module, this means defining, not executing injectModule(module); // the inject may have changed the mode currentMode = getLegacyMode(); // in sync mode to dojo.require is to execute if(module.executed!==executed && module.injected===arrived){ // the module was already here before injectModule was called probably finishing up a xdomain // load, but maybe a module given to the loader directly rather than having the loader retrieve it loaderVars.guardCheckComplete(function(){ execModule(module); }); } if(module.executed){ return module.result; } if(currentMode==sync){ // the only way to get here is in sync mode and dojo.required a module that // * was loaded async in the injectModule application a few lines up // * was an AMD module that had deps that are being loaded async and therefore couldn't execute if(module.cjs){ // the module was an AMD module; unshift, not push, which causes the current traversal to be reattempted from the top execQ.unshift(module); }else{ // the module was a legacy module syncExecStack.length && (syncExecStack[0].finish= [mid]); } }else{ // the loader wasn't in sync mode on entry; probably async mode; therefore, no expectation of getting // the module value synchronously; make sure it gets executed though execQ.push(module); } return undefined; } var result = doRequire(moduleName, omitModuleCheck); if(has("config-publishRequireResult") && !lang.exists(moduleName) && result!==undefined){ lang.setObject(moduleName, result); } return result; }; dojo.loadInit = function(f) { f(); }; dojo.registerModulePath = function(/*String*/moduleName, /*String*/prefix){ // summary: // Maps a module name to a path // description: // An unregistered module is given the default path of ../[module], // relative to Dojo root. For example, module acme is mapped to // ../acme. If you want to use a different module name, use // dojo.registerModulePath. // example: // If your dojo.js is located at this location in the web root: // | /myapp/js/dojo/dojo/dojo.js // and your modules are located at: // | /myapp/js/foo/bar.js // | /myapp/js/foo/baz.js // | /myapp/js/foo/thud/xyzzy.js // Your application can tell Dojo to locate the "foo" namespace by calling: // | dojo.registerModulePath("foo", "../../foo"); // At which point you can then use dojo.require() to load the // modules (assuming they provide() the same things which are // required). The full code might be: // | // | var paths = {}; paths[moduleName.replace(/\./g, "/")] = prefix; require({paths:paths}); }; dojo.platformRequire = function(/*Object*/modMap){ // summary: // require one or more modules based on which host environment // Dojo is currently operating in // description: // This method takes a "map" of arrays which one can use to // optionally load dojo modules. The map is indexed by the // possible dojo.name_ values, with two additional values: // "default" and "common". The items in the "default" array will // be loaded if none of the other items have been choosen based on // dojo.name_, set by your host environment. The items in the // "common" array will *always* be loaded, regardless of which // list is chosen. // example: // | dojo.platformRequire({ // | browser: [ // | "foo.sample", // simple module // | "foo.test", // | ["foo.bar.baz", true] // skip object check in _loadModule (dojo.require) // | ], // | default: [ "foo.sample._base" ], // | common: [ "important.module.common" ] // | }); var result = (modMap.common || []).concat(modMap[dojo._name] || modMap["default"] || []), temp; while(result.length){ if(lang.isArray(temp = result.shift())){ dojo.require.apply(dojo, temp); }else{ dojo.require(temp); } } }; dojo.requireIf = dojo.requireAfterIf = function(/*Boolean*/ condition, /*String*/ moduleName, /*Boolean?*/omitModuleCheck){ // summary: // If the condition is true then call `dojo.require()` for the specified // resource // // example: // | dojo.requireIf(dojo.isBrowser, "my.special.Module"); if(condition){ dojo.require(moduleName, omitModuleCheck); } }; dojo.requireLocalization = function(/*String*/moduleName, /*String*/bundleName, /*String?*/locale){ require(["../i18n"], function(i18n){ i18n.getLocalization(moduleName, bundleName, locale); }); }; return { // summary: // This module defines the v1.x synchronous loader API. extractLegacyApiApplications:extractLegacyApiApplications, require:dojoRequirePlugin, loadInit:dojoLoadInitPlugin }; }); }, 'dojo/json':function(){ define(["./has"], function(has){ "use strict"; var hasJSON = typeof JSON != "undefined"; has.add("json-parse", hasJSON); // all the parsers work fine // Firefox 3.5/Gecko 1.9 fails to use replacer in stringify properly https://bugzilla.mozilla.org/show_bug.cgi?id=509184 has.add("json-stringify", hasJSON && JSON.stringify({a:0}, function(k,v){return v||1;}) == '{"a":1}'); /*===== return { // summary: // Functions to parse and serialize JSON parse: function(str, strict){ // summary: // Parses a [JSON](http://json.org) string to return a JavaScript object. // description: // This function follows [native JSON API](https://developer.mozilla.org/en/JSON) // Throws for invalid JSON strings. This delegates to eval() if native JSON // support is not available. By default this will evaluate any valid JS expression. // With the strict parameter set to true, the parser will ensure that only // valid JSON strings are parsed (otherwise throwing an error). Without the strict // parameter, the content passed to this method must come // from a trusted source. // str: // a string literal of a JSON item, for instance: // `'{ "foo": [ "bar", 1, { "baz": "thud" } ] }'` // strict: // When set to true, this will ensure that only valid, secure JSON is ever parsed. // Make sure this is set to true for untrusted content. Note that on browsers/engines // without native JSON support, setting this to true will run slower. }, stringify: function(value, replacer, spacer){ // summary: // Returns a [JSON](http://json.org) serialization of an object. // description: // Returns a [JSON](http://json.org) serialization of an object. // This function follows [native JSON API](https://developer.mozilla.org/en/JSON) // Note that this doesn't check for infinite recursion, so don't do that! // value: // A value to be serialized. // replacer: // A replacer function that is called for each value and can return a replacement // spacer: // A spacer string to be used for pretty printing of JSON // example: // simple serialization of a trivial object // | define(["dojo/json"], function(JSON){ // | var jsonStr = JSON.stringify({ howdy: "stranger!", isStrange: true }); // | doh.is('{"howdy":"stranger!","isStrange":true}', jsonStr); } }; =====*/ if(has("json-stringify")){ return JSON; }else{ var escapeString = function(/*String*/str){ // summary: // Adds escape sequences for non-visual characters, double quote and // backslash and surrounds with double quotes to form a valid string // literal. return ('"' + str.replace(/(["\\])/g, '\\$1') + '"'). replace(/[\f]/g, "\\f").replace(/[\b]/g, "\\b").replace(/[\n]/g, "\\n"). replace(/[\t]/g, "\\t").replace(/[\r]/g, "\\r"); // string }; return { parse: has("json-parse") ? JSON.parse : function(str, strict){ if(strict && !/^([\s\[\{]*(?:"(?:\\.|[^"])+"|-?\d[\d\.]*(?:[Ee][+-]?\d+)?|null|true|false|)[\s\]\}]*(?:,|:|$))+$/.test(str)){ throw new SyntaxError("Invalid characters in JSON"); } return eval('(' + str + ')'); }, stringify: function(value, replacer, spacer){ var undef; if(typeof replacer == "string"){ spacer = replacer; replacer = null; } function stringify(it, indent, key){ if(replacer){ it = replacer(key, it); } var val, objtype = typeof it; if(objtype == "number"){ return isFinite(it) ? it + "" : "null"; } if(objtype == "boolean"){ return it + ""; } if(it === null){ return "null"; } if(typeof it == "string"){ return escapeString(it); } if(objtype == "function" || objtype == "undefined"){ return undef; // undefined } // short-circuit for objects that support "json" serialization // if they return "self" then just pass-through... if(typeof it.toJSON == "function"){ return stringify(it.toJSON(key), indent, key); } if(it instanceof Date){ return '"{FullYear}-{Month+}-{Date}T{Hours}:{Minutes}:{Seconds}Z"'.replace(/\{(\w+)(\+)?\}/g, function(t, prop, plus){ var num = it["getUTC" + prop]() + (plus ? 1 : 0); return num < 10 ? "0" + num : num; }); } if(it.valueOf() !== it){ // primitive wrapper, try again unwrapped: return stringify(it.valueOf(), indent, key); } var nextIndent= spacer ? (indent + spacer) : ""; /* we used to test for DOM nodes and throw, but FF serializes them as {}, so cross-browser consistency is probably not efficiently attainable */ var sep = spacer ? " " : ""; var newLine = spacer ? "\n" : ""; // array if(it instanceof Array){ var itl = it.length, res = []; for(key = 0; key < itl; key++){ var obj = it[key]; val = stringify(obj, nextIndent, key); if(typeof val != "string"){ val = "null"; } res.push(newLine + nextIndent + val); } return "[" + res.join(",") + newLine + indent + "]"; } // generic object code path var output = []; for(key in it){ var keyStr; if(it.hasOwnProperty(key)){ if(typeof key == "number"){ keyStr = '"' + key + '"'; }else if(typeof key == "string"){ keyStr = escapeString(key); }else{ // skip non-string or number keys continue; } val = stringify(it[key], nextIndent, key); if(typeof val != "string"){ // skip non-serializable values continue; } // At this point, the most non-IE browsers don't get in this branch // (they have native JSON), so push is definitely the way to output.push(newLine + nextIndent + keyStr + ":" + sep + val); } } return "{" + output.join(",") + newLine + indent + "}"; // String } return stringify(value, "", ""); } }; } }); }, 'dojo/_base/declare':function(){ define(["./kernel", "../has", "./lang"], function(dojo, has, lang){ // module: // dojo/_base/declare var mix = lang.mixin, op = Object.prototype, opts = op.toString, xtor = new Function, counter = 0, cname = "constructor"; function err(msg, cls){ throw new Error("declare" + (cls ? " " + cls : "") + ": " + msg); } // C3 Method Resolution Order (see http://www.python.org/download/releases/2.3/mro/) function c3mro(bases, className){ var result = [], roots = [{cls: 0, refs: []}], nameMap = {}, clsCount = 1, l = bases.length, i = 0, j, lin, base, top, proto, rec, name, refs; // build a list of bases naming them if needed for(; i < l; ++i){ base = bases[i]; if(!base){ err("mixin #" + i + " is unknown. Did you use dojo.require to pull it in?", className); }else if(opts.call(base) != "[object Function]"){ err("mixin #" + i + " is not a callable constructor.", className); } lin = base._meta ? base._meta.bases : [base]; top = 0; // add bases to the name map for(j = lin.length - 1; j >= 0; --j){ proto = lin[j].prototype; if(!proto.hasOwnProperty("declaredClass")){ proto.declaredClass = "uniqName_" + (counter++); } name = proto.declaredClass; if(!nameMap.hasOwnProperty(name)){ nameMap[name] = {count: 0, refs: [], cls: lin[j]}; ++clsCount; } rec = nameMap[name]; if(top && top !== rec){ rec.refs.push(top); ++top.count; } top = rec; } ++top.count; roots[0].refs.push(top); } // remove classes without external references recursively while(roots.length){ top = roots.pop(); result.push(top.cls); --clsCount; // optimization: follow a single-linked chain while(refs = top.refs, refs.length == 1){ top = refs[0]; if(!top || --top.count){ // branch or end of chain => do not end to roots top = 0; break; } result.push(top.cls); --clsCount; } if(top){ // branch for(i = 0, l = refs.length; i < l; ++i){ top = refs[i]; if(!--top.count){ roots.push(top); } } } } if(clsCount){ err("can't build consistent linearization", className); } // calculate the superclass offset base = bases[0]; result[0] = base ? base._meta && base === result[result.length - base._meta.bases.length] ? base._meta.bases.length : 1 : 0; return result; } function inherited(args, a, f){ var name, chains, bases, caller, meta, base, proto, opf, pos, cache = this._inherited = this._inherited || {}; // crack arguments if(typeof args == "string"){ name = args; args = a; a = f; } f = 0; caller = args.callee; name = name || caller.nom; if(!name){ err("can't deduce a name to call inherited()", this.declaredClass); } meta = this.constructor._meta; bases = meta.bases; pos = cache.p; if(name != cname){ // method if(cache.c !== caller){ // cache bust pos = 0; base = bases[0]; meta = base._meta; if(meta.hidden[name] !== caller){ // error detection chains = meta.chains; if(chains && typeof chains[name] == "string"){ err("calling chained method with inherited: " + name, this.declaredClass); } // find caller do{ meta = base._meta; proto = base.prototype; if(meta && (proto[name] === caller && proto.hasOwnProperty(name) || meta.hidden[name] === caller)){ break; } }while(base = bases[++pos]); // intentional assignment pos = base ? pos : -1; } } // find next base = bases[++pos]; if(base){ proto = base.prototype; if(base._meta && proto.hasOwnProperty(name)){ f = proto[name]; }else{ opf = op[name]; do{ proto = base.prototype; f = proto[name]; if(f && (base._meta ? proto.hasOwnProperty(name) : f !== opf)){ break; } }while(base = bases[++pos]); // intentional assignment } } f = base && f || op[name]; }else{ // constructor if(cache.c !== caller){ // cache bust pos = 0; meta = bases[0]._meta; if(meta && meta.ctor !== caller){ // error detection chains = meta.chains; if(!chains || chains.constructor !== "manual"){ err("calling chained constructor with inherited", this.declaredClass); } // find caller while(base = bases[++pos]){ // intentional assignment meta = base._meta; if(meta && meta.ctor === caller){ break; } } pos = base ? pos : -1; } } // find next while(base = bases[++pos]){ // intentional assignment meta = base._meta; f = meta ? meta.ctor : base; if(f){ break; } } f = base && f; } // cache the found super method cache.c = f; cache.p = pos; // now we have the result if(f){ return a === true ? f : f.apply(this, a || args); } // intentionally no return if a super method was not found } function getInherited(name, args){ if(typeof name == "string"){ return this.__inherited(name, args, true); } return this.__inherited(name, true); } function inherited__debug(args, a1, a2){ var f = this.getInherited(args, a1); if(f){ return f.apply(this, a2 || a1 || args); } // intentionally no return if a super method was not found } var inheritedImpl = dojo.config.isDebug ? inherited__debug : inherited; // emulation of "instanceof" function isInstanceOf(cls){ var bases = this.constructor._meta.bases; for(var i = 0, l = bases.length; i < l; ++i){ if(bases[i] === cls){ return true; } } return this instanceof cls; } function mixOwn(target, source){ // add props adding metadata for incoming functions skipping a constructor for(var name in source){ if(name != cname && source.hasOwnProperty(name)){ target[name] = source[name]; } } if(has("bug-for-in-skips-shadowed")){ for(var extraNames= lang._extraNames, i= extraNames.length; i;){ name = extraNames[--i]; if(name != cname && source.hasOwnProperty(name)){ target[name] = source[name]; } } } } // implementation of safe mixin function function safeMixin(target, source){ // summary: // Mix in properties skipping a constructor and decorating functions // like it is done by declare(). // target: Object // Target object to accept new properties. // source: Object // Source object for new properties. // description: // This function is used to mix in properties like lang.mixin does, // but it skips a constructor property and decorates functions like // declare() does. // // It is meant to be used with classes and objects produced with // declare. Functions mixed in with dojo.safeMixin can use // this.inherited() like normal methods. // // This function is used to implement extend() method of a constructor // produced with declare(). // // example: // | var A = declare(null, { // | m1: function(){ // | console.log("A.m1"); // | }, // | m2: function(){ // | console.log("A.m2"); // | } // | }); // | var B = declare(A, { // | m1: function(){ // | this.inherited(arguments); // | console.log("B.m1"); // | } // | }); // | B.extend({ // | m2: function(){ // | this.inherited(arguments); // | console.log("B.m2"); // | } // | }); // | var x = new B(); // | dojo.safeMixin(x, { // | m1: function(){ // | this.inherited(arguments); // | console.log("X.m1"); // | }, // | m2: function(){ // | this.inherited(arguments); // | console.log("X.m2"); // | } // | }); // | x.m2(); // | // prints: // | // A.m1 // | // B.m1 // | // X.m1 var name, t; // add props adding metadata for incoming functions skipping a constructor for(name in source){ t = source[name]; if((t !== op[name] || !(name in op)) && name != cname){ if(opts.call(t) == "[object Function]"){ // non-trivial function method => attach its name t.nom = name; } target[name] = t; } } if(has("bug-for-in-skips-shadowed")){ for(var extraNames= lang._extraNames, i= extraNames.length; i;){ name = extraNames[--i]; t = source[name]; if((t !== op[name] || !(name in op)) && name != cname){ if(opts.call(t) == "[object Function]"){ // non-trivial function method => attach its name t.nom = name; } target[name] = t; } } } return target; } function extend(source){ declare.safeMixin(this.prototype, source); return this; } function createSubclass(mixins){ return declare([this].concat(mixins)); } // chained constructor compatible with the legacy declare() function chainedConstructor(bases, ctorSpecial){ return function(){ var a = arguments, args = a, a0 = a[0], f, i, m, l = bases.length, preArgs; if(!(this instanceof a.callee)){ // not called via new, so force it return applyNew(a); } //this._inherited = {}; // perform the shaman's rituals of the original declare() // 1) call two types of the preamble if(ctorSpecial && (a0 && a0.preamble || this.preamble)){ // full blown ritual preArgs = new Array(bases.length); // prepare parameters preArgs[0] = a; for(i = 0;;){ // process the preamble of the 1st argument a0 = a[0]; if(a0){ f = a0.preamble; if(f){ a = f.apply(this, a) || a; } } // process the preamble of this class f = bases[i].prototype; f = f.hasOwnProperty("preamble") && f.preamble; if(f){ a = f.apply(this, a) || a; } // one peculiarity of the preamble: // it is called if it is not needed, // e.g., there is no constructor to call // let's watch for the last constructor // (see ticket #9795) if(++i == l){ break; } preArgs[i] = a; } } // 2) call all non-trivial constructors using prepared arguments for(i = l - 1; i >= 0; --i){ f = bases[i]; m = f._meta; f = m ? m.ctor : f; if(f){ f.apply(this, preArgs ? preArgs[i] : a); } } // 3) continue the original ritual: call the postscript f = this.postscript; if(f){ f.apply(this, args); } }; } // chained constructor compatible with the legacy declare() function singleConstructor(ctor, ctorSpecial){ return function(){ var a = arguments, t = a, a0 = a[0], f; if(!(this instanceof a.callee)){ // not called via new, so force it return applyNew(a); } //this._inherited = {}; // perform the shaman's rituals of the original declare() // 1) call two types of the preamble if(ctorSpecial){ // full blown ritual if(a0){ // process the preamble of the 1st argument f = a0.preamble; if(f){ t = f.apply(this, t) || t; } } f = this.preamble; if(f){ // process the preamble of this class f.apply(this, t); // one peculiarity of the preamble: // it is called even if it is not needed, // e.g., there is no constructor to call // let's watch for the last constructor // (see ticket #9795) } } // 2) call a constructor if(ctor){ ctor.apply(this, a); } // 3) continue the original ritual: call the postscript f = this.postscript; if(f){ f.apply(this, a); } }; } // plain vanilla constructor (can use inherited() to call its base constructor) function simpleConstructor(bases){ return function(){ var a = arguments, i = 0, f, m; if(!(this instanceof a.callee)){ // not called via new, so force it return applyNew(a); } //this._inherited = {}; // perform the shaman's rituals of the original declare() // 1) do not call the preamble // 2) call the top constructor (it can use this.inherited()) for(; f = bases[i]; ++i){ // intentional assignment m = f._meta; f = m ? m.ctor : f; if(f){ f.apply(this, a); break; } } // 3) call the postscript f = this.postscript; if(f){ f.apply(this, a); } }; } function chain(name, bases, reversed){ return function(){ var b, m, f, i = 0, step = 1; if(reversed){ i = bases.length - 1; step = -1; } for(; b = bases[i]; i += step){ // intentional assignment m = b._meta; f = (m ? m.hidden : b.prototype)[name]; if(f){ f.apply(this, arguments); } } }; } // forceNew(ctor) // return a new object that inherits from ctor.prototype but // without actually running ctor on the object. function forceNew(ctor){ // create object with correct prototype using a do-nothing // constructor xtor.prototype = ctor.prototype; var t = new xtor; xtor.prototype = null; // clean up return t; } // applyNew(args) // just like 'new ctor()' except that the constructor and its arguments come // from args, which must be an array or an arguments object function applyNew(args){ // create an object with ctor's prototype but without // calling ctor on it. var ctor = args.callee, t = forceNew(ctor); // execute the real constructor on the new object ctor.apply(t, args); return t; } function declare(className, superclass, props){ // summary: // Create a feature-rich constructor from compact notation. // className: String? // The optional name of the constructor (loosely, a "class") // stored in the "declaredClass" property in the created prototype. // It will be used as a global name for a created constructor. // superclass: Function|Function[] // May be null, a Function, or an Array of Functions. This argument // specifies a list of bases (the left-most one is the most deepest // base). // props: Object // An object whose properties are copied to the created prototype. // Add an instance-initialization function by making it a property // named "constructor". // returns: dojo/_base/declare.__DeclareCreatedObject // New constructor function. // description: // Create a constructor using a compact notation for inheritance and // prototype extension. // // Mixin ancestors provide a type of multiple inheritance. // Prototypes of mixin ancestors are copied to the new class: // changes to mixin prototypes will not affect classes to which // they have been mixed in. // // Ancestors can be compound classes created by this version of // declare(). In complex cases all base classes are going to be // linearized according to C3 MRO algorithm // (see http://www.python.org/download/releases/2.3/mro/ for more // details). // // "className" is cached in "declaredClass" property of the new class, // if it was supplied. The immediate super class will be cached in // "superclass" property of the new class. // // Methods in "props" will be copied and modified: "nom" property // (the declared name of the method) will be added to all copied // functions to help identify them for the internal machinery. Be // very careful, while reusing methods: if you use the same // function under different names, it can produce errors in some // cases. // // It is possible to use constructors created "manually" (without // declare()) as bases. They will be called as usual during the // creation of an instance, their methods will be chained, and even // called by "this.inherited()". // // Special property "-chains-" governs how to chain methods. It is // a dictionary, which uses method names as keys, and hint strings // as values. If a hint string is "after", this method will be // called after methods of its base classes. If a hint string is // "before", this method will be called before methods of its base // classes. // // If "constructor" is not mentioned in "-chains-" property, it will // be chained using the legacy mode: using "after" chaining, // calling preamble() method before each constructor, if available, // and calling postscript() after all constructors were executed. // If the hint is "after", it is chained as a regular method, but // postscript() will be called after the chain of constructors. // "constructor" cannot be chained "before", but it allows // a special hint string: "manual", which means that constructors // are not going to be chained in any way, and programmer will call // them manually using this.inherited(). In the latter case // postscript() will be called after the construction. // // All chaining hints are "inherited" from base classes and // potentially can be overridden. Be very careful when overriding // hints! Make sure that all chained methods can work in a proposed // manner of chaining. // // Once a method was chained, it is impossible to unchain it. The // only exception is "constructor". You don't need to define a // method in order to supply a chaining hint. // // If a method is chained, it cannot use this.inherited() because // all other methods in the hierarchy will be called automatically. // // Usually constructors and initializers of any kind are chained // using "after" and destructors of any kind are chained as // "before". Note that chaining assumes that chained methods do not // return any value: any returned value will be discarded. // // example: // | declare("my.classes.bar", my.classes.foo, { // | // properties to be added to the class prototype // | someValue: 2, // | // initialization function // | constructor: function(){ // | this.myComplicatedObject = new ReallyComplicatedObject(); // | }, // | // other functions // | someMethod: function(){ // | doStuff(); // | } // | }); // // example: // | var MyBase = declare(null, { // | // constructor, properties, and methods go here // | // ... // | }); // | var MyClass1 = declare(MyBase, { // | // constructor, properties, and methods go here // | // ... // | }); // | var MyClass2 = declare(MyBase, { // | // constructor, properties, and methods go here // | // ... // | }); // | var MyDiamond = declare([MyClass1, MyClass2], { // | // constructor, properties, and methods go here // | // ... // | }); // // example: // | var F = function(){ console.log("raw constructor"); }; // | F.prototype.method = function(){ // | console.log("raw method"); // | }; // | var A = declare(F, { // | constructor: function(){ // | console.log("A.constructor"); // | }, // | method: function(){ // | console.log("before calling F.method..."); // | this.inherited(arguments); // | console.log("...back in A"); // | } // | }); // | new A().method(); // | // will print: // | // raw constructor // | // A.constructor // | // before calling F.method... // | // raw method // | // ...back in A // // example: // | var A = declare(null, { // | "-chains-": { // | destroy: "before" // | } // | }); // | var B = declare(A, { // | constructor: function(){ // | console.log("B.constructor"); // | }, // | destroy: function(){ // | console.log("B.destroy"); // | } // | }); // | var C = declare(B, { // | constructor: function(){ // | console.log("C.constructor"); // | }, // | destroy: function(){ // | console.log("C.destroy"); // | } // | }); // | new C().destroy(); // | // prints: // | // B.constructor // | // C.constructor // | // C.destroy // | // B.destroy // // example: // | var A = declare(null, { // | "-chains-": { // | constructor: "manual" // | } // | }); // | var B = declare(A, { // | constructor: function(){ // | // ... // | // call the base constructor with new parameters // | this.inherited(arguments, [1, 2, 3]); // | // ... // | } // | }); // // example: // | var A = declare(null, { // | "-chains-": { // | m1: "before" // | }, // | m1: function(){ // | console.log("A.m1"); // | }, // | m2: function(){ // | console.log("A.m2"); // | } // | }); // | var B = declare(A, { // | "-chains-": { // | m2: "after" // | }, // | m1: function(){ // | console.log("B.m1"); // | }, // | m2: function(){ // | console.log("B.m2"); // | } // | }); // | var x = new B(); // | x.m1(); // | // prints: // | // B.m1 // | // A.m1 // | x.m2(); // | // prints: // | // A.m2 // | // B.m2 // crack parameters if(typeof className != "string"){ props = superclass; superclass = className; className = ""; } props = props || {}; var proto, i, t, ctor, name, bases, chains, mixins = 1, parents = superclass; // build a prototype if(opts.call(superclass) == "[object Array]"){ // C3 MRO bases = c3mro(superclass, className); t = bases[0]; mixins = bases.length - t; superclass = bases[mixins]; }else{ bases = [0]; if(superclass){ if(opts.call(superclass) == "[object Function]"){ t = superclass._meta; bases = bases.concat(t ? t.bases : superclass); }else{ err("base class is not a callable constructor.", className); } }else if(superclass !== null){ err("unknown base class. Did you use dojo.require to pull it in?", className); } } if(superclass){ for(i = mixins - 1;; --i){ proto = forceNew(superclass); if(!i){ // stop if nothing to add (the last base) break; } // mix in properties t = bases[i]; (t._meta ? mixOwn : mix)(proto, t.prototype); // chain in new constructor ctor = new Function; ctor.superclass = superclass; ctor.prototype = proto; superclass = proto.constructor = ctor; } }else{ proto = {}; } // add all properties declare.safeMixin(proto, props); // add constructor t = props.constructor; if(t !== op.constructor){ t.nom = cname; proto.constructor = t; } // collect chains and flags for(i = mixins - 1; i; --i){ // intentional assignment t = bases[i]._meta; if(t && t.chains){ chains = mix(chains || {}, t.chains); } } if(proto["-chains-"]){ chains = mix(chains || {}, proto["-chains-"]); } // build ctor t = !chains || !chains.hasOwnProperty(cname); bases[0] = ctor = (chains && chains.constructor === "manual") ? simpleConstructor(bases) : (bases.length == 1 ? singleConstructor(props.constructor, t) : chainedConstructor(bases, t)); // add meta information to the constructor ctor._meta = {bases: bases, hidden: props, chains: chains, parents: parents, ctor: props.constructor}; ctor.superclass = superclass && superclass.prototype; ctor.extend = extend; ctor.createSubclass = createSubclass; ctor.prototype = proto; proto.constructor = ctor; // add "standard" methods to the prototype proto.getInherited = getInherited; proto.isInstanceOf = isInstanceOf; proto.inherited = inheritedImpl; proto.__inherited = inherited; // add name if specified if(className){ proto.declaredClass = className; lang.setObject(className, ctor); } // build chains and add them to the prototype if(chains){ for(name in chains){ if(proto[name] && typeof chains[name] == "string" && name != cname){ t = proto[name] = chain(name, bases, chains[name] === "after"); t.nom = name; } } } // chained methods do not return values // no need to chain "invisible" functions return ctor; // Function } /*===== declare.__DeclareCreatedObject = { // summary: // dojo/_base/declare() returns a constructor `C`. `new C()` returns an Object with the following // methods, in addition to the methods and properties specified via the arguments passed to declare(). inherited: function(name, args, newArgs){ // summary: // Calls a super method. // name: String? // The optional method name. Should be the same as the caller's // name. Usually "name" is specified in complex dynamic cases, when // the calling method was dynamically added, undecorated by // declare(), and it cannot be determined. // args: Arguments // The caller supply this argument, which should be the original // "arguments". // newArgs: Object? // If "true", the found function will be returned without // executing it. // If Array, it will be used to call a super method. Otherwise // "args" will be used. // returns: // Whatever is returned by a super method, or a super method itself, // if "true" was specified as newArgs. // description: // This method is used inside method of classes produced with // declare() to call a super method (next in the chain). It is // used for manually controlled chaining. Consider using the regular // chaining, because it is faster. Use "this.inherited()" only in // complex cases. // // This method cannot me called from automatically chained // constructors including the case of a special (legacy) // constructor chaining. It cannot be called from chained methods. // // If "this.inherited()" cannot find the next-in-chain method, it // does nothing and returns "undefined". The last method in chain // can be a default method implemented in Object, which will be // called last. // // If "name" is specified, it is assumed that the method that // received "args" is the parent method for this call. It is looked // up in the chain list and if it is found the next-in-chain method // is called. If it is not found, the first-in-chain method is // called. // // If "name" is not specified, it will be derived from the calling // method (using a methoid property "nom"). // // example: // | var B = declare(A, { // | method1: function(a, b, c){ // | this.inherited(arguments); // | }, // | method2: function(a, b){ // | return this.inherited(arguments, [a + b]); // | } // | }); // | // next method is not in the chain list because it is added // | // manually after the class was created. // | B.prototype.method3 = function(){ // | console.log("This is a dynamically-added method."); // | this.inherited("method3", arguments); // | }; // example: // | var B = declare(A, { // | method: function(a, b){ // | var super = this.inherited(arguments, true); // | // ... // | if(!super){ // | console.log("there is no super method"); // | return 0; // | } // | return super.apply(this, arguments); // | } // | }); return {}; // Object }, getInherited: function(name, args){ // summary: // Returns a super method. // name: String? // The optional method name. Should be the same as the caller's // name. Usually "name" is specified in complex dynamic cases, when // the calling method was dynamically added, undecorated by // declare(), and it cannot be determined. // args: Arguments // The caller supply this argument, which should be the original // "arguments". // returns: // Returns a super method (Function) or "undefined". // description: // This method is a convenience method for "this.inherited()". // It uses the same algorithm but instead of executing a super // method, it returns it, or "undefined" if not found. // // example: // | var B = declare(A, { // | method: function(a, b){ // | var super = this.getInherited(arguments); // | // ... // | if(!super){ // | console.log("there is no super method"); // | return 0; // | } // | return super.apply(this, arguments); // | } // | }); return {}; // Object }, isInstanceOf: function(cls){ // summary: // Checks the inheritance chain to see if it is inherited from this // class. // cls: Function // Class constructor. // returns: // "true", if this object is inherited from this class, "false" // otherwise. // description: // This method is used with instances of classes produced with // declare() to determine of they support a certain interface or // not. It models "instanceof" operator. // // example: // | var A = declare(null, { // | // constructor, properties, and methods go here // | // ... // | }); // | var B = declare(null, { // | // constructor, properties, and methods go here // | // ... // | }); // | var C = declare([A, B], { // | // constructor, properties, and methods go here // | // ... // | }); // | var D = declare(A, { // | // constructor, properties, and methods go here // | // ... // | }); // | // | var a = new A(), b = new B(), c = new C(), d = new D(); // | // | console.log(a.isInstanceOf(A)); // true // | console.log(b.isInstanceOf(A)); // false // | console.log(c.isInstanceOf(A)); // true // | console.log(d.isInstanceOf(A)); // true // | // | console.log(a.isInstanceOf(B)); // false // | console.log(b.isInstanceOf(B)); // true // | console.log(c.isInstanceOf(B)); // true // | console.log(d.isInstanceOf(B)); // false // | // | console.log(a.isInstanceOf(C)); // false // | console.log(b.isInstanceOf(C)); // false // | console.log(c.isInstanceOf(C)); // true // | console.log(d.isInstanceOf(C)); // false // | // | console.log(a.isInstanceOf(D)); // false // | console.log(b.isInstanceOf(D)); // false // | console.log(c.isInstanceOf(D)); // false // | console.log(d.isInstanceOf(D)); // true return {}; // Object }, extend: function(source){ // summary: // Adds all properties and methods of source to constructor's // prototype, making them available to all instances created with // constructor. This method is specific to constructors created with // declare(). // source: Object // Source object which properties are going to be copied to the // constructor's prototype. // description: // Adds source properties to the constructor's prototype. It can // override existing properties. // // This method is similar to dojo.extend function, but it is specific // to constructors produced by declare(). It is implemented // using dojo.safeMixin, and it skips a constructor property, // and properly decorates copied functions. // // example: // | var A = declare(null, { // | m1: function(){}, // | s1: "Popokatepetl" // | }); // | A.extend({ // | m1: function(){}, // | m2: function(){}, // | f1: true, // | d1: 42 // | }); } }; =====*/ // For back-compat, remove for 2.0 dojo.safeMixin = declare.safeMixin = safeMixin; dojo.declare = declare; return declare; }); }, 'dojo/dom':function(){ define(["./sniff", "./_base/lang", "./_base/window"], function(has, lang, win){ // module: // dojo/dom // FIXME: need to add unit tests for all the semi-public methods if(has("ie") <= 7){ try{ document.execCommand("BackgroundImageCache", false, true); }catch(e){ // sane browsers don't have cache "issues" } } // ============================= // DOM Functions // ============================= // the result object var dom = { // summary: // This module defines the core dojo DOM API. }; if(has("ie")){ dom.byId = function(id, doc){ if(typeof id != "string"){ return id; } var _d = doc || win.doc, te = id && _d.getElementById(id); // attributes.id.value is better than just id in case the // user has a name=id inside a form if(te && (te.attributes.id.value == id || te.id == id)){ return te; }else{ var eles = _d.all[id]; if(!eles || eles.nodeName){ eles = [eles]; } // if more than 1, choose first with the correct id var i = 0; while((te = eles[i++])){ if((te.attributes && te.attributes.id && te.attributes.id.value == id) || te.id == id){ return te; } } } }; }else{ dom.byId = function(id, doc){ // inline'd type check. // be sure to return null per documentation, to match IE branch. return ((typeof id == "string") ? (doc || win.doc).getElementById(id) : id) || null; // DOMNode }; } /*===== dom.byId = function(id, doc){ // summary: // Returns DOM node with matching `id` attribute or falsy value (ex: null or undefined) // if not found. If `id` is a DomNode, this function is a no-op. // // id: String|DOMNode // A string to match an HTML id attribute or a reference to a DOM Node // // doc: Document? // Document to work in. Defaults to the current value of // dojo.doc. Can be used to retrieve // node references from other documents. // // example: // Look up a node by ID: // | var n = dojo.byId("foo"); // // example: // Check if a node exists, and use it. // | var n = dojo.byId("bar"); // | if(n){ doStuff() ... } // // example: // Allow string or DomNode references to be passed to a custom function: // | var foo = function(nodeOrId){ // | nodeOrId = dojo.byId(nodeOrId); // | // ... more stuff // | } }; =====*/ dom.isDescendant = function(/*DOMNode|String*/ node, /*DOMNode|String*/ ancestor){ // summary: // Returns true if node is a descendant of ancestor // node: DOMNode|String // string id or node reference to test // ancestor: DOMNode|String // string id or node reference of potential parent to test against // // example: // Test is node id="bar" is a descendant of node id="foo" // | if(dojo.isDescendant("bar", "foo")){ ... } try{ node = dom.byId(node); ancestor = dom.byId(ancestor); while(node){ if(node == ancestor){ return true; // Boolean } node = node.parentNode; } }catch(e){ /* squelch, return false */ } return false; // Boolean }; // TODO: do we need this function in the base? dom.setSelectable = function(/*DOMNode|String*/ node, /*Boolean*/ selectable){ // summary: // Enable or disable selection on a node // node: DOMNode|String // id or reference to node // selectable: Boolean // state to put the node in. false indicates unselectable, true // allows selection. // example: // Make the node id="bar" unselectable // | dojo.setSelectable("bar"); // example: // Make the node id="bar" selectable // | dojo.setSelectable("bar", true); node = dom.byId(node); if(has("mozilla")){ node.style.MozUserSelect = selectable ? "" : "none"; }else if(has("khtml") || has("webkit")){ node.style.KhtmlUserSelect = selectable ? "auto" : "none"; }else if(has("ie")){ var v = (node.unselectable = selectable ? "" : "on"), cs = node.getElementsByTagName("*"), i = 0, l = cs.length; for(; i < l; ++i){ cs.item(i).unselectable = v; } } //FIXME: else? Opera? }; return dom; }); }, 'dojo/_base/browser':function(){ if(require.has){ require.has.add("config-selectorEngine", "acme"); } define([ "../ready", "./kernel", "./connect", // until we decide if connect is going back into non-browser environments "./unload", "./window", "./event", "./html", "./NodeList", "../query", "./xhr", "./fx"], function(dojo){ // module: // dojo/_base/browser /*===== return { // summary: // This module causes the browser-only base modules to be loaded. }; =====*/ return dojo; }); }, 'dojo/selector/acme':function(){ define([ "../dom", "../sniff", "../_base/array", "../_base/lang", "../_base/window" ], function(dom, has, array, lang, win){ // module: // dojo/selector/acme /* acme architectural overview: acme is a relatively full-featured CSS3 query library. It is designed to take any valid CSS3 selector and return the nodes matching the selector. To do this quickly, it processes queries in several steps, applying caching where profitable. The steps (roughly in reverse order of the way they appear in the code): 1.) check to see if we already have a "query dispatcher" - if so, use that with the given parameterization. Skip to step 4. 2.) attempt to determine which branch to dispatch the query to: - JS (optimized DOM iteration) - native (FF3.1+, Safari 3.1+, IE 8+) 3.) tokenize and convert to executable "query dispatcher" - this is where the lion's share of the complexity in the system lies. In the DOM version, the query dispatcher is assembled as a chain of "yes/no" test functions pertaining to a section of a simple query statement (".blah:nth-child(odd)" but not "div div", which is 2 simple statements). Individual statement dispatchers are cached (to prevent re-definition) as are entire dispatch chains (to make re-execution of the same query fast) 4.) the resulting query dispatcher is called in the passed scope (by default the top-level document) - for DOM queries, this results in a recursive, top-down evaluation of nodes based on each simple query section - for native implementations, this may mean working around spec bugs. So be it. 5.) matched nodes are pruned to ensure they are unique (if necessary) */ //////////////////////////////////////////////////////////////////////// // Toolkit aliases //////////////////////////////////////////////////////////////////////// // if you are extracting acme for use in your own system, you will // need to provide these methods and properties. No other porting should be // necessary, save for configuring the system to use a class other than // dojo/NodeList as the return instance instantiator var trim = lang.trim; var each = array.forEach; var getDoc = function(){ return win.doc; }; // NOTE(alex): the spec is idiotic. CSS queries should ALWAYS be case-sensitive, but nooooooo var cssCaseBug = (getDoc().compatMode) == "BackCompat"; //////////////////////////////////////////////////////////////////////// // Global utilities //////////////////////////////////////////////////////////////////////// var specials = ">~+"; // global thunk to determine whether we should treat the current query as // case sensitive or not. This switch is flipped by the query evaluator // based on the document passed as the context to search. var caseSensitive = false; // how high? var yesman = function(){ return true; }; //////////////////////////////////////////////////////////////////////// // Tokenizer //////////////////////////////////////////////////////////////////////// var getQueryParts = function(query){ // summary: // state machine for query tokenization // description: // instead of using a brittle and slow regex-based CSS parser, // acme implements an AST-style query representation. This // representation is only generated once per query. For example, // the same query run multiple times or under different root nodes // does not re-parse the selector expression but instead uses the // cached data structure. The state machine implemented here // terminates on the last " " (space) character and returns an // ordered array of query component structures (or "parts"). Each // part represents an operator or a simple CSS filtering // expression. The structure for parts is documented in the code // below. // NOTE: // this code is designed to run fast and compress well. Sacrifices // to readability and maintainability have been made. Your best // bet when hacking the tokenizer is to put The Donnas on *really* // loud (may we recommend their "Spend The Night" release?) and // just assume you're gonna make mistakes. Keep the unit tests // open and run them frequently. Knowing is half the battle ;-) if(specials.indexOf(query.slice(-1)) >= 0){ // if we end with a ">", "+", or "~", that means we're implicitly // searching all children, so make it explicit query += " * "; }else{ // if you have not provided a terminator, one will be provided for // you... query += " "; } var ts = function(/*Integer*/ s, /*Integer*/ e){ // trim and slice. // take an index to start a string slice from and an end position // and return a trimmed copy of that sub-string return trim(query.slice(s, e)); }; // the overall data graph of the full query, as represented by queryPart objects var queryParts = []; // state keeping vars var inBrackets = -1, inParens = -1, inMatchFor = -1, inPseudo = -1, inClass = -1, inId = -1, inTag = -1, currentQuoteChar, lc = "", cc = "", pStart; // iteration vars var x = 0, // index in the query ql = query.length, currentPart = null, // data structure representing the entire clause _cp = null; // the current pseudo or attr matcher // several temporary variables are assigned to this structure during a // potential sub-expression match: // attr: // a string representing the current full attribute match in a // bracket expression // type: // if there's an operator in a bracket expression, this is // used to keep track of it // value: // the internals of parenthetical expression for a pseudo. for // :nth-child(2n+1), value might be "2n+1" var endTag = function(){ // called when the tokenizer hits the end of a particular tag name. // Re-sets state variables for tag matching and sets up the matcher // to handle the next type of token (tag or operator). if(inTag >= 0){ var tv = (inTag == x) ? null : ts(inTag, x); // .toLowerCase(); currentPart[ (specials.indexOf(tv) < 0) ? "tag" : "oper" ] = tv; inTag = -1; } }; var endId = function(){ // called when the tokenizer might be at the end of an ID portion of a match if(inId >= 0){ currentPart.id = ts(inId, x).replace(/\\/g, ""); inId = -1; } }; var endClass = function(){ // called when the tokenizer might be at the end of a class name // match. CSS allows for multiple classes, so we augment the // current item with another class in its list if(inClass >= 0){ currentPart.classes.push(ts(inClass + 1, x).replace(/\\/g, "")); inClass = -1; } }; var endAll = function(){ // at the end of a simple fragment, so wall off the matches endId(); endTag(); endClass(); }; var endPart = function(){ endAll(); if(inPseudo >= 0){ currentPart.pseudos.push({ name: ts(inPseudo + 1, x) }); } // hint to the selector engine to tell it whether or not it // needs to do any iteration. Many simple selectors don't, and // we can avoid significant construction-time work by advising // the system to skip them currentPart.loops = ( currentPart.pseudos.length || currentPart.attrs.length || currentPart.classes.length ); currentPart.oquery = currentPart.query = ts(pStart, x); // save the full expression as a string // otag/tag are hints to suggest to the system whether or not // it's an operator or a tag. We save a copy of otag since the // tag name is cast to upper-case in regular HTML matches. The // system has a global switch to figure out if the current // expression needs to be case sensitive or not and it will use // otag or tag accordingly currentPart.otag = currentPart.tag = (currentPart["oper"]) ? null : (currentPart.tag || "*"); if(currentPart.tag){ // if we're in a case-insensitive HTML doc, we likely want // the toUpperCase when matching on element.tagName. If we // do it here, we can skip the string op per node // comparison currentPart.tag = currentPart.tag.toUpperCase(); } // add the part to the list if(queryParts.length && (queryParts[queryParts.length-1].oper)){ // operators are always infix, so we remove them from the // list and attach them to the next match. The evaluator is // responsible for sorting out how to handle them. currentPart.infixOper = queryParts.pop(); currentPart.query = currentPart.infixOper.query + " " + currentPart.query; /* console.debug( "swapping out the infix", currentPart.infixOper, "and attaching it to", currentPart); */ } queryParts.push(currentPart); currentPart = null; }; // iterate over the query, character by character, building up a // list of query part objects for(; lc=cc, cc=query.charAt(x), x < ql; x++){ // cc: the current character in the match // lc: the last character (if any) // someone is trying to escape something, so don't try to match any // fragments. We assume we're inside a literal. if(lc == "\\"){ continue; } if(!currentPart){ // a part was just ended or none has yet been created // NOTE: I hate all this alloc, but it's shorter than writing tons of if's pStart = x; // rules describe full CSS sub-expressions, like: // #someId // .className:first-child // but not: // thinger > div.howdy[type=thinger] // the indidual components of the previous query would be // split into 3 parts that would be represented a structure like: // [ // { // query: "thinger", // tag: "thinger", // }, // { // query: "div.howdy[type=thinger]", // classes: ["howdy"], // infixOper: { // query: ">", // oper: ">", // } // }, // ] currentPart = { query: null, // the full text of the part's rule pseudos: [], // CSS supports multiple pseud-class matches in a single rule attrs: [], // CSS supports multi-attribute match, so we need an array classes: [], // class matches may be additive, e.g.: .thinger.blah.howdy tag: null, // only one tag... oper: null, // ...or operator per component. Note that these wind up being exclusive. id: null, // the id component of a rule getTag: function(){ return caseSensitive ? this.otag : this.tag; } }; // if we don't have a part, we assume we're going to start at // the beginning of a match, which should be a tag name. This // might fault a little later on, but we detect that and this // iteration will still be fine. inTag = x; } // Skip processing all quoted characters. // If we are inside quoted text then currentQuoteChar stores the character that began the quote, // thus that character that will end it. if(currentQuoteChar){ if(cc == currentQuoteChar){ currentQuoteChar = null; } continue; }else if (cc == "'" || cc == '"'){ currentQuoteChar = cc; continue; } if(inBrackets >= 0){ // look for a the close first if(cc == "]"){ // if we're in a [...] clause and we end, do assignment if(!_cp.attr){ // no attribute match was previously begun, so we // assume this is an attribute existence match in the // form of [someAttributeName] _cp.attr = ts(inBrackets+1, x); }else{ // we had an attribute already, so we know that we're // matching some sort of value, as in [attrName=howdy] _cp.matchFor = ts((inMatchFor||inBrackets+1), x); } var cmf = _cp.matchFor; if(cmf){ // try to strip quotes from the matchFor value. We want // [attrName=howdy] to match the same // as [attrName = 'howdy' ] if( (cmf.charAt(0) == '"') || (cmf.charAt(0) == "'") ){ _cp.matchFor = cmf.slice(1, -1); } } // remove backslash escapes from an attribute match, since DOM // querying will get attribute values without backslashes if(_cp.matchFor){ _cp.matchFor = _cp.matchFor.replace(/\\/g, ""); } // end the attribute by adding it to the list of attributes. currentPart.attrs.push(_cp); _cp = null; // necessary? inBrackets = inMatchFor = -1; }else if(cc == "="){ // if the last char was an operator prefix, make sure we // record it along with the "=" operator. var addToCc = ("|~^$*".indexOf(lc) >=0 ) ? lc : ""; _cp.type = addToCc+cc; _cp.attr = ts(inBrackets+1, x-addToCc.length); inMatchFor = x+1; } // now look for other clause parts }else if(inParens >= 0){ // if we're in a parenthetical expression, we need to figure // out if it's attached to a pseudo-selector rule like // :nth-child(1) if(cc == ")"){ if(inPseudo >= 0){ _cp.value = ts(inParens+1, x); } inPseudo = inParens = -1; } }else if(cc == "#"){ // start of an ID match endAll(); inId = x+1; }else if(cc == "."){ // start of a class match endAll(); inClass = x; }else if(cc == ":"){ // start of a pseudo-selector match endAll(); inPseudo = x; }else if(cc == "["){ // start of an attribute match. endAll(); inBrackets = x; // provide a new structure for the attribute match to fill-in _cp = { /*===== attr: null, type: null, matchFor: null =====*/ }; }else if(cc == "("){ // we really only care if we've entered a parenthetical // expression if we're already inside a pseudo-selector match if(inPseudo >= 0){ // provide a new structure for the pseudo match to fill-in _cp = { name: ts(inPseudo+1, x), value: null }; currentPart.pseudos.push(_cp); } inParens = x; }else if( (cc == " ") && // if it's a space char and the last char is too, consume the // current one without doing more work (lc != cc) ){ endPart(); } } return queryParts; }; //////////////////////////////////////////////////////////////////////// // DOM query infrastructure //////////////////////////////////////////////////////////////////////// var agree = function(first, second){ // the basic building block of the yes/no chaining system. agree(f1, // f2) generates a new function which returns the boolean results of // both of the passed functions to a single logical-anded result. If // either are not passed, the other is used exclusively. if(!first){ return second; } if(!second){ return first; } return function(){ return first.apply(window, arguments) && second.apply(window, arguments); }; }; var getArr = function(i, arr){ // helps us avoid array alloc when we don't need it var r = arr||[]; // FIXME: should this be 'new d._NodeListCtor()' ? if(i){ r.push(i); } return r; }; var _isElement = function(n){ return (1 == n.nodeType); }; // FIXME: need to coalesce _getAttr with defaultGetter var blank = ""; var _getAttr = function(elem, attr){ if(!elem){ return blank; } if(attr == "class"){ return elem.className || blank; } if(attr == "for"){ return elem.htmlFor || blank; } if(attr == "style"){ return elem.style.cssText || blank; } return (caseSensitive ? elem.getAttribute(attr) : elem.getAttribute(attr, 2)) || blank; }; var attrs = { "*=": function(attr, value){ return function(elem){ // E[foo*="bar"] // an E element whose "foo" attribute value contains // the substring "bar" return (_getAttr(elem, attr).indexOf(value)>=0); }; }, "^=": function(attr, value){ // E[foo^="bar"] // an E element whose "foo" attribute value begins exactly // with the string "bar" return function(elem){ return (_getAttr(elem, attr).indexOf(value)==0); }; }, "$=": function(attr, value){ // E[foo$="bar"] // an E element whose "foo" attribute value ends exactly // with the string "bar" return function(elem){ var ea = " "+_getAttr(elem, attr); var lastIndex = ea.lastIndexOf(value); return lastIndex > -1 && (lastIndex==(ea.length-value.length)); }; }, "~=": function(attr, value){ // E[foo~="bar"] // an E element whose "foo" attribute value is a list of // space-separated values, one of which is exactly equal // to "bar" // return "[contains(concat(' ',@"+attr+",' '), ' "+ value +" ')]"; var tval = " "+value+" "; return function(elem){ var ea = " "+_getAttr(elem, attr)+" "; return (ea.indexOf(tval)>=0); }; }, "|=": function(attr, value){ // E[hreflang|="en"] // an E element whose "hreflang" attribute has a // hyphen-separated list of values beginning (from the // left) with "en" var valueDash = value+"-"; return function(elem){ var ea = _getAttr(elem, attr); return ( (ea == value) || (ea.indexOf(valueDash)==0) ); }; }, "=": function(attr, value){ return function(elem){ return (_getAttr(elem, attr) == value); }; } }; // avoid testing for node type if we can. Defining this in the negative // here to avoid negation in the fast path. var _noNES = (typeof getDoc().firstChild.nextElementSibling == "undefined"); var _ns = !_noNES ? "nextElementSibling" : "nextSibling"; var _ps = !_noNES ? "previousElementSibling" : "previousSibling"; var _simpleNodeTest = (_noNES ? _isElement : yesman); var _lookLeft = function(node){ // look left while(node = node[_ps]){ if(_simpleNodeTest(node)){ return false; } } return true; }; var _lookRight = function(node){ // look right while(node = node[_ns]){ if(_simpleNodeTest(node)){ return false; } } return true; }; var getNodeIndex = function(node){ var root = node.parentNode; root = root.nodeType != 7 ? root : root.nextSibling; // PROCESSING_INSTRUCTION_NODE var i = 0, tret = root.children || root.childNodes, ci = (node["_i"]||node.getAttribute("_i")||-1), cl = (root["_l"]|| (typeof root.getAttribute !== "undefined" ? root.getAttribute("_l") : -1)); if(!tret){ return -1; } var l = tret.length; // we calculate the parent length as a cheap way to invalidate the // cache. It's not 100% accurate, but it's much more honest than what // other libraries do if( cl == l && ci >= 0 && cl >= 0 ){ // if it's legit, tag and release return ci; } // else re-key things if(has("ie") && typeof root.setAttribute !== "undefined"){ root.setAttribute("_l", l); }else{ root["_l"] = l; } ci = -1; for(var te = root["firstElementChild"]||root["firstChild"]; te; te = te[_ns]){ if(_simpleNodeTest(te)){ if(has("ie")){ te.setAttribute("_i", ++i); }else{ te["_i"] = ++i; } if(node === te){ // NOTE: // shortcutting the return at this step in indexing works // very well for benchmarking but we avoid it here since // it leads to potential O(n^2) behavior in sequential // getNodexIndex operations on a previously un-indexed // parent. We may revisit this at a later time, but for // now we just want to get the right answer more often // than not. ci = i; } } } return ci; }; var isEven = function(elem){ return !((getNodeIndex(elem)) % 2); }; var isOdd = function(elem){ return ((getNodeIndex(elem)) % 2); }; var pseudos = { "checked": function(name, condition){ return function(elem){ return !!("checked" in elem ? elem.checked : elem.selected); }; }, "disabled": function(name, condition){ return function(elem){ return elem.disabled; }; }, "enabled": function(name, condition){ return function(elem){ return !elem.disabled; }; }, "first-child": function(){ return _lookLeft; }, "last-child": function(){ return _lookRight; }, "only-child": function(name, condition){ return function(node){ return _lookLeft(node) && _lookRight(node); }; }, "empty": function(name, condition){ return function(elem){ // DomQuery and jQuery get this wrong, oddly enough. // The CSS 3 selectors spec is pretty explicit about it, too. var cn = elem.childNodes; var cnl = elem.childNodes.length; // if(!cnl){ return true; } for(var x=cnl-1; x >= 0; x--){ var nt = cn[x].nodeType; if((nt === 1)||(nt == 3)){ return false; } } return true; }; }, "contains": function(name, condition){ var cz = condition.charAt(0); if( cz == '"' || cz == "'" ){ //remove quote condition = condition.slice(1, -1); } return function(elem){ return (elem.innerHTML.indexOf(condition) >= 0); }; }, "not": function(name, condition){ var p = getQueryParts(condition)[0]; var ignores = { el: 1 }; if(p.tag != "*"){ ignores.tag = 1; } if(!p.classes.length){ ignores.classes = 1; } var ntf = getSimpleFilterFunc(p, ignores); return function(elem){ return (!ntf(elem)); }; }, "nth-child": function(name, condition){ var pi = parseInt; // avoid re-defining function objects if we can if(condition == "odd"){ return isOdd; }else if(condition == "even"){ return isEven; } // FIXME: can we shorten this? if(condition.indexOf("n") != -1){ var tparts = condition.split("n", 2); var pred = tparts[0] ? ((tparts[0] == '-') ? -1 : pi(tparts[0])) : 1; var idx = tparts[1] ? pi(tparts[1]) : 0; var lb = 0, ub = -1; if(pred > 0){ if(idx < 0){ idx = (idx % pred) && (pred + (idx % pred)); }else if(idx>0){ if(idx >= pred){ lb = idx - idx % pred; } idx = idx % pred; } }else if(pred<0){ pred *= -1; // idx has to be greater than 0 when pred is negative; // shall we throw an error here? if(idx > 0){ ub = idx; idx = idx % pred; } } if(pred > 0){ return function(elem){ var i = getNodeIndex(elem); return (i>=lb) && (ub<0 || i<=ub) && ((i % pred) == idx); }; }else{ condition = idx; } } var ncount = pi(condition); return function(elem){ return (getNodeIndex(elem) == ncount); }; } }; var defaultGetter = (has("ie") && (has("ie") < 9 || has("quirks"))) ? function(cond){ var clc = cond.toLowerCase(); if(clc == "class"){ cond = "className"; } return function(elem){ return (caseSensitive ? elem.getAttribute(cond) : elem[cond]||elem[clc]); }; } : function(cond){ return function(elem){ return (elem && elem.getAttribute && elem.hasAttribute(cond)); }; }; var getSimpleFilterFunc = function(query, ignores){ // generates a node tester function based on the passed query part. The // query part is one of the structures generated by the query parser // when it creates the query AST. The "ignores" object specifies which // (if any) tests to skip, allowing the system to avoid duplicating // work where it may have already been taken into account by other // factors such as how the nodes to test were fetched in the first // place if(!query){ return yesman; } ignores = ignores||{}; var ff = null; if(!("el" in ignores)){ ff = agree(ff, _isElement); } if(!("tag" in ignores)){ if(query.tag != "*"){ ff = agree(ff, function(elem){ return (elem && ((caseSensitive ? elem.tagName : elem.tagName.toUpperCase()) == query.getTag())); }); } } if(!("classes" in ignores)){ each(query.classes, function(cname, idx, arr){ // get the class name /* var isWildcard = cname.charAt(cname.length-1) == "*"; if(isWildcard){ cname = cname.substr(0, cname.length-1); } // I dislike the regex thing, even if memoized in a cache, but it's VERY short var re = new RegExp("(?:^|\\s)" + cname + (isWildcard ? ".*" : "") + "(?:\\s|$)"); */ var re = new RegExp("(?:^|\\s)" + cname + "(?:\\s|$)"); ff = agree(ff, function(elem){ return re.test(elem.className); }); ff.count = idx; }); } if(!("pseudos" in ignores)){ each(query.pseudos, function(pseudo){ var pn = pseudo.name; if(pseudos[pn]){ ff = agree(ff, pseudos[pn](pn, pseudo.value)); } }); } if(!("attrs" in ignores)){ each(query.attrs, function(attr){ var matcher; var a = attr.attr; // type, attr, matchFor if(attr.type && attrs[attr.type]){ matcher = attrs[attr.type](a, attr.matchFor); }else if(a.length){ matcher = defaultGetter(a); } if(matcher){ ff = agree(ff, matcher); } }); } if(!("id" in ignores)){ if(query.id){ ff = agree(ff, function(elem){ return (!!elem && (elem.id == query.id)); }); } } if(!ff){ if(!("default" in ignores)){ ff = yesman; } } return ff; }; var _nextSibling = function(filterFunc){ return function(node, ret, bag){ while(node = node[_ns]){ if(_noNES && (!_isElement(node))){ continue; } if( (!bag || _isUnique(node, bag)) && filterFunc(node) ){ ret.push(node); } break; } return ret; }; }; var _nextSiblings = function(filterFunc){ return function(root, ret, bag){ var te = root[_ns]; while(te){ if(_simpleNodeTest(te)){ if(bag && !_isUnique(te, bag)){ break; } if(filterFunc(te)){ ret.push(te); } } te = te[_ns]; } return ret; }; }; // get an array of child *elements*, skipping text and comment nodes var _childElements = function(filterFunc){ filterFunc = filterFunc||yesman; return function(root, ret, bag){ // get an array of child elements, skipping text and comment nodes var te, x = 0, tret = root.children || root.childNodes; while(te = tret[x++]){ if( _simpleNodeTest(te) && (!bag || _isUnique(te, bag)) && (filterFunc(te, x)) ){ ret.push(te); } } return ret; }; }; // test to see if node is below root var _isDescendant = function(node, root){ var pn = node.parentNode; while(pn){ if(pn == root){ break; } pn = pn.parentNode; } return !!pn; }; var _getElementsFuncCache = {}; var getElementsFunc = function(query){ var retFunc = _getElementsFuncCache[query.query]; // if we've got a cached dispatcher, just use that if(retFunc){ return retFunc; } // else, generate a new on // NOTE: // this function returns a function that searches for nodes and // filters them. The search may be specialized by infix operators // (">", "~", or "+") else it will default to searching all // descendants (the " " selector). Once a group of children is // found, a test function is applied to weed out the ones we // don't want. Many common cases can be fast-pathed. We spend a // lot of cycles to create a dispatcher that doesn't do more work // than necessary at any point since, unlike this function, the // dispatchers will be called every time. The logic of generating // efficient dispatchers looks like this in pseudo code: // // # if it's a purely descendant query (no ">", "+", or "~" modifiers) // if infixOperator == " ": // if only(id): // return def(root): // return d.byId(id, root); // // elif id: // return def(root): // return filter(d.byId(id, root)); // // elif cssClass && getElementsByClassName: // return def(root): // return filter(root.getElementsByClassName(cssClass)); // // elif only(tag): // return def(root): // return root.getElementsByTagName(tagName); // // else: // # search by tag name, then filter // return def(root): // return filter(root.getElementsByTagName(tagName||"*")); // // elif infixOperator == ">": // # search direct children // return def(root): // return filter(root.children); // // elif infixOperator == "+": // # search next sibling // return def(root): // return filter(root.nextElementSibling); // // elif infixOperator == "~": // # search rightward siblings // return def(root): // return filter(nextSiblings(root)); var io = query.infixOper; var oper = (io ? io.oper : ""); // the default filter func which tests for all conditions in the query // part. This is potentially inefficient, so some optimized paths may // re-define it to test fewer things. var filterFunc = getSimpleFilterFunc(query, { el: 1 }); var qt = query.tag; var wildcardTag = ("*" == qt); var ecs = getDoc()["getElementsByClassName"]; if(!oper){ // if there's no infix operator, then it's a descendant query. ID // and "elements by class name" variants can be accelerated so we // call them out explicitly: if(query.id){ // testing shows that the overhead of yesman() is acceptable // and can save us some bytes vs. re-defining the function // everywhere. filterFunc = (!query.loops && wildcardTag) ? yesman : getSimpleFilterFunc(query, { el: 1, id: 1 }); retFunc = function(root, arr){ var te = dom.byId(query.id, (root.ownerDocument||root)); if(!te || !filterFunc(te)){ return; } if(9 == root.nodeType){ // if root's a doc, we just return directly return getArr(te, arr); }else{ // otherwise check ancestry if(_isDescendant(te, root)){ return getArr(te, arr); } } }; }else if( ecs && // isAlien check. Workaround for Prototype.js being totally evil/dumb. /\{\s*\[native code\]\s*\}/.test(String(ecs)) && query.classes.length && !cssCaseBug ){ // it's a class-based query and we've got a fast way to run it. // ignore class and ID filters since we will have handled both filterFunc = getSimpleFilterFunc(query, { el: 1, classes: 1, id: 1 }); var classesString = query.classes.join(" "); retFunc = function(root, arr, bag){ var ret = getArr(0, arr), te, x=0; var tret = root.getElementsByClassName(classesString); while((te = tret[x++])){ if(filterFunc(te, root) && _isUnique(te, bag)){ ret.push(te); } } return ret; }; }else if(!wildcardTag && !query.loops){ // it's tag only. Fast-path it. retFunc = function(root, arr, bag){ var ret = getArr(0, arr), te, x=0; var tag = query.getTag(), tret = tag ? root.getElementsByTagName(tag) : []; while((te = tret[x++])){ if(_isUnique(te, bag)){ ret.push(te); } } return ret; }; }else{ // the common case: // a descendant selector without a fast path. By now it's got // to have a tag selector, even if it's just "*" so we query // by that and filter filterFunc = getSimpleFilterFunc(query, { el: 1, tag: 1, id: 1 }); retFunc = function(root, arr, bag){ var ret = getArr(0, arr), te, x=0; // we use getTag() to avoid case sensitivity issues var tag = query.getTag(), tret = tag ? root.getElementsByTagName(tag) : []; while((te = tret[x++])){ if(filterFunc(te, root) && _isUnique(te, bag)){ ret.push(te); } } return ret; }; } }else{ // the query is scoped in some way. Instead of querying by tag we // use some other collection to find candidate nodes var skipFilters = { el: 1 }; if(wildcardTag){ skipFilters.tag = 1; } filterFunc = getSimpleFilterFunc(query, skipFilters); if("+" == oper){ retFunc = _nextSibling(filterFunc); }else if("~" == oper){ retFunc = _nextSiblings(filterFunc); }else if(">" == oper){ retFunc = _childElements(filterFunc); } } // cache it and return return _getElementsFuncCache[query.query] = retFunc; }; var filterDown = function(root, queryParts){ // NOTE: // this is the guts of the DOM query system. It takes a list of // parsed query parts and a root and finds children which match // the selector represented by the parts var candidates = getArr(root), qp, x, te, qpl = queryParts.length, bag, ret; for(var i = 0; i < qpl; i++){ ret = []; qp = queryParts[i]; x = candidates.length - 1; if(x > 0){ // if we have more than one root at this level, provide a new // hash to use for checking group membership but tell the // system not to post-filter us since we will already have been // guaranteed to be unique bag = {}; ret.nozip = true; } var gef = getElementsFunc(qp); for(var j = 0; (te = candidates[j]); j++){ // for every root, get the elements that match the descendant // selector, adding them to the "ret" array and filtering them // via membership in this level's bag. If there are more query // parts, then this level's return will be used as the next // level's candidates gef(te, ret, bag); } if(!ret.length){ break; } candidates = ret; } return ret; }; //////////////////////////////////////////////////////////////////////// // the query runner //////////////////////////////////////////////////////////////////////// // these are the primary caches for full-query results. The query // dispatcher functions are generated then stored here for hash lookup in // the future var _queryFuncCacheDOM = {}, _queryFuncCacheQSA = {}; // this is the second level of splitting, from full-length queries (e.g., // "div.foo .bar") into simple query expressions (e.g., ["div.foo", // ".bar"]) var getStepQueryFunc = function(query){ var qparts = getQueryParts(trim(query)); // if it's trivial, avoid iteration and zipping costs if(qparts.length == 1){ // we optimize this case here to prevent dispatch further down the // chain, potentially slowing things down. We could more elegantly // handle this in filterDown(), but it's slower for simple things // that need to be fast (e.g., "#someId"). var tef = getElementsFunc(qparts[0]); return function(root){ var r = tef(root, []); if(r){ r.nozip = true; } return r; }; } // otherwise, break it up and return a runner that iterates over the parts recursively return function(root){ return filterDown(root, qparts); }; }; // NOTES: // * we can't trust QSA for anything but document-rooted queries, so // caching is split into DOM query evaluators and QSA query evaluators // * caching query results is dirty and leak-prone (or, at a minimum, // prone to unbounded growth). Other toolkits may go this route, but // they totally destroy their own ability to manage their memory // footprint. If we implement it, it should only ever be with a fixed // total element reference # limit and an LRU-style algorithm since JS // has no weakref support. Caching compiled query evaluators is also // potentially problematic, but even on large documents the size of the // query evaluators is often < 100 function objects per evaluator (and // LRU can be applied if it's ever shown to be an issue). // * since IE's QSA support is currently only for HTML documents and even // then only in IE 8's "standards mode", we have to detect our dispatch // route at query time and keep 2 separate caches. Ugg. // we need to determine if we think we can run a given query via // querySelectorAll or if we'll need to fall back on DOM queries to get // there. We need a lot of information about the environment and the query // to make the determination (e.g. does it support QSA, does the query in // question work in the native QSA impl, etc.). // IE QSA queries may incorrectly include comment nodes, so we throw the // zipping function into "remove" comments mode instead of the normal "skip // it" which every other QSA-clued browser enjoys var noZip = has("ie") ? "commentStrip" : "nozip"; var qsa = "querySelectorAll"; var qsaAvail = !!getDoc()[qsa]; //Don't bother with n+3 type of matches, IE complains if we modify those. var infixSpaceRe = /\\[>~+]|n\+\d|([^ \\])?([>~+])([^ =])?/g; var infixSpaceFunc = function(match, pre, ch, post){ return ch ? (pre ? pre + " " : "") + ch + (post ? " " + post : "") : /*n+3*/ match; }; //Don't apply the infixSpaceRe to attribute value selectors var attRe = /([^[]*)([^\]]*])?/g; var attFunc = function(match, nonAtt, att){ return nonAtt.replace(infixSpaceRe, infixSpaceFunc) + (att||""); }; var getQueryFunc = function(query, forceDOM){ //Normalize query. The CSS3 selectors spec allows for omitting spaces around //infix operators, >, ~ and + //Do the work here since detection for spaces is used as a simple "not use QSA" //test below. query = query.replace(attRe, attFunc); if(qsaAvail){ // if we've got a cached variant and we think we can do it, run it! var qsaCached = _queryFuncCacheQSA[query]; if(qsaCached && !forceDOM){ return qsaCached; } } // else if we've got a DOM cached variant, assume that we already know // all we need to and use it var domCached = _queryFuncCacheDOM[query]; if(domCached){ return domCached; } // TODO: // today we're caching DOM and QSA branches separately so we // recalc useQSA every time. If we had a way to tag root+query // efficiently, we'd be in good shape to do a global cache. var qcz = query.charAt(0); var nospace = (-1 == query.indexOf(" ")); // byId searches are wicked fast compared to QSA, even when filtering // is required if( (query.indexOf("#") >= 0) && (nospace) ){ forceDOM = true; } var useQSA = ( qsaAvail && (!forceDOM) && // as per CSS 3, we can't currently start w/ combinator: // http://www.w3.org/TR/css3-selectors/#w3cselgrammar (specials.indexOf(qcz) == -1) && // IE's QSA impl sucks on pseudos (!has("ie") || (query.indexOf(":") == -1)) && (!(cssCaseBug && (query.indexOf(".") >= 0))) && // FIXME: // need to tighten up browser rules on ":contains" and "|=" to // figure out which aren't good // Latest webkit (around 531.21.8) does not seem to do well with :checked on option // elements, even though according to spec, selected options should // match :checked. So go nonQSA for it: // http://bugs.dojotoolkit.org/ticket/5179 (query.indexOf(":contains") == -1) && (query.indexOf(":checked") == -1) && (query.indexOf("|=") == -1) // some browsers don't grok it ); // TODO: // if we've got a descendant query (e.g., "> .thinger" instead of // just ".thinger") in a QSA-able doc, but are passed a child as a // root, it should be possible to give the item a synthetic ID and // trivially rewrite the query to the form "#synid > .thinger" to // use the QSA branch if(useQSA){ var tq = (specials.indexOf(query.charAt(query.length-1)) >= 0) ? (query + " *") : query; return _queryFuncCacheQSA[query] = function(root){ try{ // the QSA system contains an egregious spec bug which // limits us, effectively, to only running QSA queries over // entire documents. See: // http://ejohn.org/blog/thoughts-on-queryselectorall/ // despite this, we can also handle QSA runs on simple // selectors, but we don't want detection to be expensive // so we're just checking for the presence of a space char // right now. Not elegant, but it's cheaper than running // the query parser when we might not need to if(!((9 == root.nodeType) || nospace)){ throw ""; } var r = root[qsa](tq); // skip expensive duplication checks and just wrap in a NodeList r[noZip] = true; return r; }catch(e){ // else run the DOM branch on this query, ensuring that we // default that way in the future return getQueryFunc(query, true)(root); } }; }else{ // DOM branch var parts = query.match(/([^\s,](?:"(?:\\.|[^"])+"|'(?:\\.|[^'])+'|[^,])*)/g); return _queryFuncCacheDOM[query] = ((parts.length < 2) ? // if not a compound query (e.g., ".foo, .bar"), cache and return a dispatcher getStepQueryFunc(query) : // if it *is* a complex query, break it up into its // constituent parts and return a dispatcher that will // merge the parts when run function(root){ var pindex = 0, // avoid array alloc for every invocation ret = [], tp; while((tp = parts[pindex++])){ ret = ret.concat(getStepQueryFunc(tp)(root)); } return ret; } ); } }; var _zipIdx = 0; // NOTE: // this function is Moo inspired, but our own impl to deal correctly // with XML in IE var _nodeUID = has("ie") ? function(node){ if(caseSensitive){ // XML docs don't have uniqueID on their nodes return (node.getAttribute("_uid") || node.setAttribute("_uid", ++_zipIdx) || _zipIdx); }else{ return node.uniqueID; } } : function(node){ return (node._uid || (node._uid = ++_zipIdx)); }; // determine if a node in is unique in a "bag". In this case we don't want // to flatten a list of unique items, but rather just tell if the item in // question is already in the bag. Normally we'd just use hash lookup to do // this for us but IE's DOM is busted so we can't really count on that. On // the upside, it gives us a built in unique ID function. var _isUnique = function(node, bag){ if(!bag){ return 1; } var id = _nodeUID(node); if(!bag[id]){ return bag[id] = 1; } return 0; }; // attempt to efficiently determine if an item in a list is a dupe, // returning a list of "uniques", hopefully in document order var _zipIdxName = "_zipIdx"; var _zip = function(arr){ if(arr && arr.nozip){ return arr; } var ret = []; if(!arr || !arr.length){ return ret; } if(arr[0]){ ret.push(arr[0]); } if(arr.length < 2){ return ret; } _zipIdx++; // we have to fork here for IE and XML docs because we can't set // expandos on their nodes (apparently). *sigh* var x, te; if(has("ie") && caseSensitive){ var szidx = _zipIdx+""; arr[0].setAttribute(_zipIdxName, szidx); for(x = 1; te = arr[x]; x++){ if(arr[x].getAttribute(_zipIdxName) != szidx){ ret.push(te); } te.setAttribute(_zipIdxName, szidx); } }else if(has("ie") && arr.commentStrip){ try{ for(x = 1; te = arr[x]; x++){ if(_isElement(te)){ ret.push(te); } } }catch(e){ /* squelch */ } }else{ if(arr[0]){ arr[0][_zipIdxName] = _zipIdx; } for(x = 1; te = arr[x]; x++){ if(arr[x][_zipIdxName] != _zipIdx){ ret.push(te); } te[_zipIdxName] = _zipIdx; } } return ret; }; // the main executor var query = function(/*String*/ query, /*String|DOMNode?*/ root){ // summary: // Returns nodes which match the given CSS3 selector, searching the // entire document by default but optionally taking a node to scope // the search by. Returns an array. // description: // dojo.query() is the swiss army knife of DOM node manipulation in // Dojo. Much like Prototype's "$$" (bling-bling) function or JQuery's // "$" function, dojo.query provides robust, high-performance // CSS-based node selector support with the option of scoping searches // to a particular sub-tree of a document. // // Supported Selectors: // -------------------- // // acme supports a rich set of CSS3 selectors, including: // // - class selectors (e.g., `.foo`) // - node type selectors like `span` // - ` ` descendant selectors // - `>` child element selectors // - `#foo` style ID selectors // - `*` universal selector // - `~`, the preceded-by sibling selector // - `+`, the immediately preceded-by sibling selector // - attribute queries: // - `[foo]` attribute presence selector // - `[foo='bar']` attribute value exact match // - `[foo~='bar']` attribute value list item match // - `[foo^='bar']` attribute start match // - `[foo$='bar']` attribute end match // - `[foo*='bar']` attribute substring match // - `:first-child`, `:last-child`, and `:only-child` positional selectors // - `:empty` content emtpy selector // - `:checked` pseudo selector // - `:nth-child(n)`, `:nth-child(2n+1)` style positional calculations // - `:nth-child(even)`, `:nth-child(odd)` positional selectors // - `:not(...)` negation pseudo selectors // // Any legal combination of these selectors will work with // `dojo.query()`, including compound selectors ("," delimited). // Very complex and useful searches can be constructed with this // palette of selectors and when combined with functions for // manipulation presented by dojo/NodeList, many types of DOM // manipulation operations become very straightforward. // // Unsupported Selectors: // ---------------------- // // While dojo.query handles many CSS3 selectors, some fall outside of // what's reasonable for a programmatic node querying engine to // handle. Currently unsupported selectors include: // // - namespace-differentiated selectors of any form // - all `::` pseduo-element selectors // - certain pseudo-selectors which don't get a lot of day-to-day use: // - `:root`, `:lang()`, `:target`, `:focus` // - all visual and state selectors: // - `:root`, `:active`, `:hover`, `:visited`, `:link`, // `:enabled`, `:disabled` // - `:*-of-type` pseudo selectors // // dojo.query and XML Documents: // ----------------------------- // // `dojo.query` (as of dojo 1.2) supports searching XML documents // in a case-sensitive manner. If an HTML document is served with // a doctype that forces case-sensitivity (e.g., XHTML 1.1 // Strict), dojo.query() will detect this and "do the right // thing". Case sensitivity is dependent upon the document being // searched and not the query used. It is therefore possible to // use case-sensitive queries on strict sub-documents (iframes, // etc.) or XML documents while still assuming case-insensitivity // for a host/root document. // // Non-selector Queries: // --------------------- // // If something other than a String is passed for the query, // `dojo.query` will return a new `dojo/NodeList` instance // constructed from that parameter alone and all further // processing will stop. This means that if you have a reference // to a node or NodeList, you can quickly construct a new NodeList // from the original by calling `dojo.query(node)` or // `dojo.query(list)`. // // query: // The CSS3 expression to match against. For details on the syntax of // CSS3 selectors, see // root: // A DOMNode (or node id) to scope the search from. Optional. // returns: Array // example: // search the entire document for elements with the class "foo": // | dojo.query(".foo"); // these elements will match: // | // | // |

                                // example: // search the entire document for elements with the classes "foo" *and* "bar": // | dojo.query(".foo.bar"); // these elements will match: // | // while these will not: // | // |

                                // example: // find `` elements which are descendants of paragraphs and // which have a "highlighted" class: // | dojo.query("p span.highlighted"); // the innermost span in this fragment matches: // |

                                // | ... // | ... // | // |

                                // example: // set an "odd" class on all odd table rows inside of the table // `#tabular_data`, using the `>` (direct child) selector to avoid // affecting any nested tables: // | dojo.query("#tabular_data > tbody > tr:nth-child(odd)").addClass("odd"); // example: // remove all elements with the class "error" from the document // and store them in a list: // | var errors = dojo.query(".error").orphan(); // example: // add an onclick handler to every submit button in the document // which causes the form to be sent via Ajax instead: // | dojo.query("input[type='submit']").onclick(function(e){ // | dojo.stopEvent(e); // prevent sending the form // | var btn = e.target; // | dojo.xhrPost({ // | form: btn.form, // | load: function(data){ // | // replace the form with the response // | var div = dojo.doc.createElement("div"); // | dojo.place(div, btn.form, "after"); // | div.innerHTML = data; // | dojo.style(btn.form, "display", "none"); // | } // | }); // | }); root = root || getDoc(); // throw the big case sensitivity switch var od = root.ownerDocument || root; // root is either Document or a node inside the document caseSensitive = (od.createElement("div").tagName === "div"); // NOTE: // adding "true" as the 2nd argument to getQueryFunc is useful for // testing the DOM branch without worrying about the // behavior/performance of the QSA branch. var r = getQueryFunc(query)(root); // FIXME: // need to investigate this branch WRT #8074 and #8075 if(r && r.nozip){ return r; } return _zip(r); // dojo/NodeList }; query.filter = function(/*Node[]*/ nodeList, /*String*/ filter, /*String|DOMNode?*/ root){ // summary: // function for filtering a NodeList based on a selector, optimized for simple selectors var tmpNodeList = [], parts = getQueryParts(filter), filterFunc = (parts.length == 1 && !/[^\w#\.]/.test(filter)) ? getSimpleFilterFunc(parts[0]) : function(node){ return array.indexOf(query(filter, dom.byId(root)), node) != -1; }; for(var x = 0, te; te = nodeList[x]; x++){ if(filterFunc(te)){ tmpNodeList.push(te); } } return tmpNodeList; }; return query; }); }, 'dojo/errors/RequestTimeoutError':function(){ define("dojo/errors/RequestTimeoutError", ['./create', './RequestError'], function(create, RequestError){ // module: // dojo/errors/RequestTimeoutError /*===== return function(){ // summary: // TODOC }; =====*/ return create("RequestTimeoutError", null, RequestError, { dojoType: "timeout" }); }); }, 'dojo/dom-style':function(){ define("dojo/dom-style", ["./sniff", "./dom"], function(has, dom){ // module: // dojo/dom-style // ============================= // Style Functions // ============================= // getComputedStyle drives most of the style code. // Wherever possible, reuse the returned object. // // API functions below that need to access computed styles accept an // optional computedStyle parameter. // If this parameter is omitted, the functions will call getComputedStyle themselves. // This way, calling code can access computedStyle once, and then pass the reference to // multiple API functions. // Although we normally eschew argument validation at this // level, here we test argument 'node' for (duck)type, // by testing nodeType, ecause 'document' is the 'parentNode' of 'body' // it is frequently sent to this function even // though it is not Element. var getComputedStyle, style = { // summary: // This module defines the core dojo DOM style API. }; if(has("webkit")){ getComputedStyle = function(/*DomNode*/ node){ var s; if(node.nodeType == 1){ var dv = node.ownerDocument.defaultView; s = dv.getComputedStyle(node, null); if(!s && node.style){ node.style.display = ""; s = dv.getComputedStyle(node, null); } } return s || {}; }; }else if(has("ie") && (has("ie") < 9 || has("quirks"))){ getComputedStyle = function(node){ // IE (as of 7) doesn't expose Element like sane browsers // currentStyle can be null on IE8! return node.nodeType == 1 /* ELEMENT_NODE*/ && node.currentStyle ? node.currentStyle : {}; }; }else{ getComputedStyle = function(node){ return node.nodeType == 1 /* ELEMENT_NODE*/ ? node.ownerDocument.defaultView.getComputedStyle(node, null) : {}; }; } style.getComputedStyle = getComputedStyle; /*===== style.getComputedStyle = function(node){ // summary: // Returns a "computed style" object. // // description: // Gets a "computed style" object which can be used to gather // information about the current state of the rendered node. // // Note that this may behave differently on different browsers. // Values may have different formats and value encodings across // browsers. // // Note also that this method is expensive. Wherever possible, // reuse the returned object. // // Use the dojo.style() method for more consistent (pixelized) // return values. // // node: DOMNode // A reference to a DOM node. Does NOT support taking an // ID string for speed reasons. // example: // | dojo.getComputedStyle(dojo.byId('foo')).borderWidth; // // example: // Reusing the returned object, avoiding multiple lookups: // | var cs = dojo.getComputedStyle(dojo.byId("someNode")); // | var w = cs.width, h = cs.height; return; // CSS2Properties }; =====*/ var toPixel; if(!has("ie")){ toPixel = function(element, value){ // style values can be floats, client code may want // to round for integer pixels. return parseFloat(value) || 0; }; }else{ toPixel = function(element, avalue){ if(!avalue){ return 0; } // on IE7, medium is usually 4 pixels if(avalue == "medium"){ return 4; } // style values can be floats, client code may // want to round this value for integer pixels. if(avalue.slice && avalue.slice(-2) == 'px'){ return parseFloat(avalue); } var s = element.style, rs = element.runtimeStyle, cs = element.currentStyle, sLeft = s.left, rsLeft = rs.left; rs.left = cs.left; try{ // 'avalue' may be incompatible with style.left, which can cause IE to throw // this has been observed for border widths using "thin", "medium", "thick" constants // those particular constants could be trapped by a lookup // but perhaps there are more s.left = avalue; avalue = s.pixelLeft; }catch(e){ avalue = 0; } s.left = sLeft; rs.left = rsLeft; return avalue; }; } style.toPixelValue = toPixel; /*===== style.toPixelValue = function(node, value){ // summary: // converts style value to pixels on IE or return a numeric value. // node: DOMNode // value: String // returns: Number }; =====*/ // FIXME: there opacity quirks on FF that we haven't ported over. Hrm. var astr = "DXImageTransform.Microsoft.Alpha"; var af = function(n, f){ try{ return n.filters.item(astr); }catch(e){ return f ? {} : null; } }; var _getOpacity = has("ie") < 9 || (has("ie") && has("quirks")) ? function(node){ try{ return af(node).Opacity / 100; // Number }catch(e){ return 1; // Number } } : function(node){ return getComputedStyle(node).opacity; }; var _setOpacity = has("ie") < 9 || (has("ie") && has("quirks")) ? function(/*DomNode*/ node, /*Number*/ opacity){ var ov = opacity * 100, opaque = opacity == 1; node.style.zoom = opaque ? "" : 1; if(!af(node)){ if(opaque){ return opacity; } node.style.filter += " progid:" + astr + "(Opacity=" + ov + ")"; }else{ af(node, 1).Opacity = ov; } // on IE7 Alpha(Filter opacity=100) makes text look fuzzy so disable it altogether (bug #2661), //but still update the opacity value so we can get a correct reading if it is read later. af(node, 1).Enabled = !opaque; if(node.tagName.toLowerCase() == "tr"){ for(var td = node.firstChild; td; td = td.nextSibling){ if(td.tagName.toLowerCase() == "td"){ _setOpacity(td, opacity); } } } return opacity; } : function(node, opacity){ return node.style.opacity = opacity; }; var _pixelNamesCache = { left: true, top: true }; var _pixelRegExp = /margin|padding|width|height|max|min|offset/; // |border function _toStyleValue(node, type, value){ //TODO: should we really be doing string case conversion here? Should we cache it? Need to profile! type = type.toLowerCase(); if(has("ie")){ if(value == "auto"){ if(type == "height"){ return node.offsetHeight; } if(type == "width"){ return node.offsetWidth; } } if(type == "fontweight"){ switch(value){ case 700: return "bold"; case 400: default: return "normal"; } } } if(!(type in _pixelNamesCache)){ _pixelNamesCache[type] = _pixelRegExp.test(type); } return _pixelNamesCache[type] ? toPixel(node, value) : value; } var _floatStyle = has("ie") ? "styleFloat" : "cssFloat", _floatAliases = {"cssFloat": _floatStyle, "styleFloat": _floatStyle, "float": _floatStyle}; // public API style.get = function getStyle(/*DOMNode|String*/ node, /*String?*/ name){ // summary: // Accesses styles on a node. // description: // Getting the style value uses the computed style for the node, so the value // will be a calculated value, not just the immediate node.style value. // Also when getting values, use specific style names, // like "borderBottomWidth" instead of "border" since compound values like // "border" are not necessarily reflected as expected. // If you want to get node dimensions, use `dojo.marginBox()`, // `dojo.contentBox()` or `dojo.position()`. // node: DOMNode|String // id or reference to node to get style for // name: String? // the style property to get // example: // Passing only an ID or node returns the computed style object of // the node: // | dojo.getStyle("thinger"); // example: // Passing a node and a style property returns the current // normalized, computed value for that property: // | dojo.getStyle("thinger", "opacity"); // 1 by default var n = dom.byId(node), l = arguments.length, op = (name == "opacity"); if(l == 2 && op){ return _getOpacity(n); } name = _floatAliases[name] || name; var s = style.getComputedStyle(n); return (l == 1) ? s : _toStyleValue(n, name, s[name] || n.style[name]); /* CSS2Properties||String||Number */ }; style.set = function setStyle(/*DOMNode|String*/ node, /*String|Object*/ name, /*String?*/ value){ // summary: // Sets styles on a node. // node: DOMNode|String // id or reference to node to set style for // name: String|Object // the style property to set in DOM-accessor format // ("borderWidth", not "border-width") or an object with key/value // pairs suitable for setting each property. // value: String? // If passed, sets value on the node for style, handling // cross-browser concerns. When setting a pixel value, // be sure to include "px" in the value. For instance, top: "200px". // Otherwise, in some cases, some browsers will not apply the style. // // example: // Passing a node, a style property, and a value changes the // current display of the node and returns the new computed value // | dojo.setStyle("thinger", "opacity", 0.5); // == 0.5 // // example: // Passing a node, an object-style style property sets each of the values in turn and returns the computed style object of the node: // | dojo.setStyle("thinger", { // | "opacity": 0.5, // | "border": "3px solid black", // | "height": "300px" // | }); // // example: // When the CSS style property is hyphenated, the JavaScript property is camelCased. // font-size becomes fontSize, and so on. // | dojo.setStyle("thinger",{ // | fontSize:"14pt", // | letterSpacing:"1.2em" // | }); // // example: // dojo/NodeList implements .style() using the same syntax, omitting the "node" parameter, calling // dojo.style() on every element of the list. See: `dojo.query()` and `dojo/NodeList` // | dojo.query(".someClassName").style("visibility","hidden"); // | // or // | dojo.query("#baz > div").style({ // | opacity:0.75, // | fontSize:"13pt" // | }); var n = dom.byId(node), l = arguments.length, op = (name == "opacity"); name = _floatAliases[name] || name; if(l == 3){ return op ? _setOpacity(n, value) : n.style[name] = value; // Number } for(var x in name){ style.set(node, x, name[x]); } return style.getComputedStyle(n); }; return style; }); }, 'dojo/dom-geometry':function(){ define(["./sniff", "./_base/window","./dom", "./dom-style"], function(has, win, dom, style){ // module: // dojo/dom-geometry // the result object var geom = { // summary: // This module defines the core dojo DOM geometry API. }; // Box functions will assume this model. // On IE/Opera, BORDER_BOX will be set if the primary document is in quirks mode. // Can be set to change behavior of box setters. // can be either: // "border-box" // "content-box" (default) geom.boxModel = "content-box"; // We punt per-node box mode testing completely. // If anybody cares, we can provide an additional (optional) unit // that overrides existing code to include per-node box sensitivity. // Opera documentation claims that Opera 9 uses border-box in BackCompat mode. // but experiments (Opera 9.10.8679 on Windows Vista) indicate that it actually continues to use content-box. // IIRC, earlier versions of Opera did in fact use border-box. // Opera guys, this is really confusing. Opera being broken in quirks mode is not our fault. if(has("ie") /*|| has("opera")*/){ // client code may have to adjust if compatMode varies across iframes geom.boxModel = document.compatMode == "BackCompat" ? "border-box" : "content-box"; } geom.getPadExtents = function getPadExtents(/*DomNode*/ node, /*Object*/ computedStyle){ // summary: // Returns object with special values specifically useful for node // fitting. // description: // Returns an object with `w`, `h`, `l`, `t` properties: // | l/t/r/b = left/top/right/bottom padding (respectively) // | w = the total of the left and right padding // | h = the total of the top and bottom padding // If 'node' has position, l/t forms the origin for child nodes. // The w/h are used for calculating boxes. // Normally application code will not need to invoke this // directly, and will use the ...box... functions instead. // node: DOMNode // computedStyle: Object? // This parameter accepts computed styles object. // If this parameter is omitted, the functions will call // dojo.getComputedStyle to get one. It is a better way, calling // dojo.computedStyle once, and then pass the reference to this // computedStyle parameter. Wherever possible, reuse the returned // object of dojo/dom-style.getComputedStyle(). node = dom.byId(node); var s = computedStyle || style.getComputedStyle(node), px = style.toPixelValue, l = px(node, s.paddingLeft), t = px(node, s.paddingTop), r = px(node, s.paddingRight), b = px(node, s.paddingBottom); return {l: l, t: t, r: r, b: b, w: l + r, h: t + b}; }; var none = "none"; geom.getBorderExtents = function getBorderExtents(/*DomNode*/ node, /*Object*/ computedStyle){ // summary: // returns an object with properties useful for noting the border // dimensions. // description: // - l/t/r/b = the sum of left/top/right/bottom border (respectively) // - w = the sum of the left and right border // - h = the sum of the top and bottom border // // The w/h are used for calculating boxes. // Normally application code will not need to invoke this // directly, and will use the ...box... functions instead. // node: DOMNode // computedStyle: Object? // This parameter accepts computed styles object. // If this parameter is omitted, the functions will call // dojo.getComputedStyle to get one. It is a better way, calling // dojo.computedStyle once, and then pass the reference to this // computedStyle parameter. Wherever possible, reuse the returned // object of dojo/dom-style.getComputedStyle(). node = dom.byId(node); var px = style.toPixelValue, s = computedStyle || style.getComputedStyle(node), l = s.borderLeftStyle != none ? px(node, s.borderLeftWidth) : 0, t = s.borderTopStyle != none ? px(node, s.borderTopWidth) : 0, r = s.borderRightStyle != none ? px(node, s.borderRightWidth) : 0, b = s.borderBottomStyle != none ? px(node, s.borderBottomWidth) : 0; return {l: l, t: t, r: r, b: b, w: l + r, h: t + b}; }; geom.getPadBorderExtents = function getPadBorderExtents(/*DomNode*/ node, /*Object*/ computedStyle){ // summary: // Returns object with properties useful for box fitting with // regards to padding. // description: // - l/t/r/b = the sum of left/top/right/bottom padding and left/top/right/bottom border (respectively) // - w = the sum of the left and right padding and border // - h = the sum of the top and bottom padding and border // // The w/h are used for calculating boxes. // Normally application code will not need to invoke this // directly, and will use the ...box... functions instead. // node: DOMNode // computedStyle: Object? // This parameter accepts computed styles object. // If this parameter is omitted, the functions will call // dojo.getComputedStyle to get one. It is a better way, calling // dojo.computedStyle once, and then pass the reference to this // computedStyle parameter. Wherever possible, reuse the returned // object of dojo/dom-style.getComputedStyle(). node = dom.byId(node); var s = computedStyle || style.getComputedStyle(node), p = geom.getPadExtents(node, s), b = geom.getBorderExtents(node, s); return { l: p.l + b.l, t: p.t + b.t, r: p.r + b.r, b: p.b + b.b, w: p.w + b.w, h: p.h + b.h }; }; geom.getMarginExtents = function getMarginExtents(node, computedStyle){ // summary: // returns object with properties useful for box fitting with // regards to box margins (i.e., the outer-box). // // - l/t = marginLeft, marginTop, respectively // - w = total width, margin inclusive // - h = total height, margin inclusive // // The w/h are used for calculating boxes. // Normally application code will not need to invoke this // directly, and will use the ...box... functions instead. // node: DOMNode // computedStyle: Object? // This parameter accepts computed styles object. // If this parameter is omitted, the functions will call // dojo.getComputedStyle to get one. It is a better way, calling // dojo.computedStyle once, and then pass the reference to this // computedStyle parameter. Wherever possible, reuse the returned // object of dojo/dom-style.getComputedStyle(). node = dom.byId(node); var s = computedStyle || style.getComputedStyle(node), px = style.toPixelValue, l = px(node, s.marginLeft), t = px(node, s.marginTop), r = px(node, s.marginRight), b = px(node, s.marginBottom); return {l: l, t: t, r: r, b: b, w: l + r, h: t + b}; }; // Box getters work in any box context because offsetWidth/clientWidth // are invariant wrt box context // // They do *not* work for display: inline objects that have padding styles // because the user agent ignores padding (it's bogus styling in any case) // // Be careful with IMGs because they are inline or block depending on // browser and browser mode. // Although it would be easier to read, there are not separate versions of // _getMarginBox for each browser because: // 1. the branching is not expensive // 2. factoring the shared code wastes cycles (function call overhead) // 3. duplicating the shared code wastes bytes geom.getMarginBox = function getMarginBox(/*DomNode*/ node, /*Object*/ computedStyle){ // summary: // returns an object that encodes the width, height, left and top // positions of the node's margin box. // node: DOMNode // computedStyle: Object? // This parameter accepts computed styles object. // If this parameter is omitted, the functions will call // dojo.getComputedStyle to get one. It is a better way, calling // dojo.computedStyle once, and then pass the reference to this // computedStyle parameter. Wherever possible, reuse the returned // object of dojo/dom-style.getComputedStyle(). node = dom.byId(node); var s = computedStyle || style.getComputedStyle(node), me = geom.getMarginExtents(node, s), l = node.offsetLeft - me.l, t = node.offsetTop - me.t, p = node.parentNode, px = style.toPixelValue, pcs; if(has("mozilla")){ // Mozilla: // If offsetParent has a computed overflow != visible, the offsetLeft is decreased // by the parent's border. // We don't want to compute the parent's style, so instead we examine node's // computed left/top which is more stable. var sl = parseFloat(s.left), st = parseFloat(s.top); if(!isNaN(sl) && !isNaN(st)){ l = sl; t = st; }else{ // If child's computed left/top are not parseable as a number (e.g. "auto"), we // have no choice but to examine the parent's computed style. if(p && p.style){ pcs = style.getComputedStyle(p); if(pcs.overflow != "visible"){ l += pcs.borderLeftStyle != none ? px(node, pcs.borderLeftWidth) : 0; t += pcs.borderTopStyle != none ? px(node, pcs.borderTopWidth) : 0; } } } }else if(has("opera") || (has("ie") == 8 && !has("quirks"))){ // On Opera and IE 8, offsetLeft/Top includes the parent's border if(p){ pcs = style.getComputedStyle(p); l -= pcs.borderLeftStyle != none ? px(node, pcs.borderLeftWidth) : 0; t -= pcs.borderTopStyle != none ? px(node, pcs.borderTopWidth) : 0; } } return {l: l, t: t, w: node.offsetWidth + me.w, h: node.offsetHeight + me.h}; }; geom.getContentBox = function getContentBox(node, computedStyle){ // summary: // Returns an object that encodes the width, height, left and top // positions of the node's content box, irrespective of the // current box model. // node: DOMNode // computedStyle: Object? // This parameter accepts computed styles object. // If this parameter is omitted, the functions will call // dojo.getComputedStyle to get one. It is a better way, calling // dojo.computedStyle once, and then pass the reference to this // computedStyle parameter. Wherever possible, reuse the returned // object of dojo/dom-style.getComputedStyle(). // clientWidth/Height are important since the automatically account for scrollbars // fallback to offsetWidth/Height for special cases (see #3378) node = dom.byId(node); var s = computedStyle || style.getComputedStyle(node), w = node.clientWidth, h, pe = geom.getPadExtents(node, s), be = geom.getBorderExtents(node, s); if(!w){ w = node.offsetWidth; h = node.offsetHeight; }else{ h = node.clientHeight; be.w = be.h = 0; } // On Opera, offsetLeft includes the parent's border if(has("opera")){ pe.l += be.l; pe.t += be.t; } return {l: pe.l, t: pe.t, w: w - pe.w - be.w, h: h - pe.h - be.h}; }; // Box setters depend on box context because interpretation of width/height styles // vary wrt box context. // // The value of boxModel is used to determine box context. // boxModel can be set directly to change behavior. // // Beware of display: inline objects that have padding styles // because the user agent ignores padding (it's a bogus setup anyway) // // Be careful with IMGs because they are inline or block depending on // browser and browser mode. // // Elements other than DIV may have special quirks, like built-in // margins or padding, or values not detectable via computedStyle. // In particular, margins on TABLE do not seems to appear // at all in computedStyle on Mozilla. function setBox(/*DomNode*/ node, /*Number?*/ l, /*Number?*/ t, /*Number?*/ w, /*Number?*/ h, /*String?*/ u){ // summary: // sets width/height/left/top in the current (native) box-model // dimensions. Uses the unit passed in u. // node: // DOM Node reference. Id string not supported for performance // reasons. // l: // left offset from parent. // t: // top offset from parent. // w: // width in current box model. // h: // width in current box model. // u: // unit measure to use for other measures. Defaults to "px". u = u || "px"; var s = node.style; if(!isNaN(l)){ s.left = l + u; } if(!isNaN(t)){ s.top = t + u; } if(w >= 0){ s.width = w + u; } if(h >= 0){ s.height = h + u; } } function isButtonTag(/*DomNode*/ node){ // summary: // True if the node is BUTTON or INPUT.type="button". return node.tagName.toLowerCase() == "button" || node.tagName.toLowerCase() == "input" && (node.getAttribute("type") || "").toLowerCase() == "button"; // boolean } function usesBorderBox(/*DomNode*/ node){ // summary: // True if the node uses border-box layout. // We could test the computed style of node to see if a particular box // has been specified, but there are details and we choose not to bother. // TABLE and BUTTON (and INPUT type=button) are always border-box by default. // If you have assigned a different box to either one via CSS then // box functions will break. return geom.boxModel == "border-box" || node.tagName.toLowerCase() == "table" || isButtonTag(node); // boolean } geom.setContentSize = function setContentSize(/*DomNode*/ node, /*Object*/ box, /*Object*/ computedStyle){ // summary: // Sets the size of the node's contents, irrespective of margins, // padding, or borders. // node: DOMNode // box: Object // hash with optional "w", and "h" properties for "width", and "height" // respectively. All specified properties should have numeric values in whole pixels. // computedStyle: Object? // This parameter accepts computed styles object. // If this parameter is omitted, the functions will call // dojo.getComputedStyle to get one. It is a better way, calling // dojo.computedStyle once, and then pass the reference to this // computedStyle parameter. Wherever possible, reuse the returned // object of dojo/dom-style.getComputedStyle(). node = dom.byId(node); var w = box.w, h = box.h; if(usesBorderBox(node)){ var pb = geom.getPadBorderExtents(node, computedStyle); if(w >= 0){ w += pb.w; } if(h >= 0){ h += pb.h; } } setBox(node, NaN, NaN, w, h); }; var nilExtents = {l: 0, t: 0, w: 0, h: 0}; geom.setMarginBox = function setMarginBox(/*DomNode*/ node, /*Object*/ box, /*Object*/ computedStyle){ // summary: // sets the size of the node's margin box and placement // (left/top), irrespective of box model. Think of it as a // passthrough to setBox that handles box-model vagaries for // you. // node: DOMNode // box: Object // hash with optional "l", "t", "w", and "h" properties for "left", "right", "width", and "height" // respectively. All specified properties should have numeric values in whole pixels. // computedStyle: Object? // This parameter accepts computed styles object. // If this parameter is omitted, the functions will call // dojo.getComputedStyle to get one. It is a better way, calling // dojo.computedStyle once, and then pass the reference to this // computedStyle parameter. Wherever possible, reuse the returned // object of dojo/dom-style.getComputedStyle(). node = dom.byId(node); var s = computedStyle || style.getComputedStyle(node), w = box.w, h = box.h, // Some elements have special padding, margin, and box-model settings. // To use box functions you may need to set padding, margin explicitly. // Controlling box-model is harder, in a pinch you might set dojo/dom-geometry.boxModel. pb = usesBorderBox(node) ? nilExtents : geom.getPadBorderExtents(node, s), mb = geom.getMarginExtents(node, s); if(has("webkit")){ // on Safari (3.1.2), button nodes with no explicit size have a default margin // setting an explicit size eliminates the margin. // We have to swizzle the width to get correct margin reading. if(isButtonTag(node)){ var ns = node.style; if(w >= 0 && !ns.width){ ns.width = "4px"; } if(h >= 0 && !ns.height){ ns.height = "4px"; } } } if(w >= 0){ w = Math.max(w - pb.w - mb.w, 0); } if(h >= 0){ h = Math.max(h - pb.h - mb.h, 0); } setBox(node, box.l, box.t, w, h); }; // ============================= // Positioning // ============================= geom.isBodyLtr = function isBodyLtr(/*Document?*/ doc){ // summary: // Returns true if the current language is left-to-right, and false otherwise. // doc: Document? // Optional document to query. If unspecified, use win.doc. // returns: Boolean doc = doc || win.doc; return (win.body(doc).dir || doc.documentElement.dir || "ltr").toLowerCase() == "ltr"; // Boolean }; geom.docScroll = function docScroll(/*Document?*/ doc){ // summary: // Returns an object with {node, x, y} with corresponding offsets. // doc: Document? // Optional document to query. If unspecified, use win.doc. // returns: Object doc = doc || win.doc; var node = win.doc.parentWindow || win.doc.defaultView; // use UI window, not dojo.global window. TODO: use dojo/window::get() except for circular dependency problem return "pageXOffset" in node ? {x: node.pageXOffset, y: node.pageYOffset } : (node = has("quirks") ? win.body(doc) : doc.documentElement) && {x: geom.fixIeBiDiScrollLeft(node.scrollLeft || 0, doc), y: node.scrollTop || 0 }; }; if(has("ie")){ geom.getIeDocumentElementOffset = function getIeDocumentElementOffset(/*Document?*/ doc){ // summary: // returns the offset in x and y from the document body to the // visual edge of the page for IE // doc: Document? // Optional document to query. If unspecified, use win.doc. // description: // The following values in IE contain an offset: // | event.clientX // | event.clientY // | node.getBoundingClientRect().left // | node.getBoundingClientRect().top // But other position related values do not contain this offset, // such as node.offsetLeft, node.offsetTop, node.style.left and // node.style.top. The offset is always (2, 2) in LTR direction. // When the body is in RTL direction, the offset counts the width // of left scroll bar's width. This function computes the actual // offset. //NOTE: assumes we're being called in an IE browser doc = doc || win.doc; var de = doc.documentElement; // only deal with HTML element here, position() handles body/quirks if(has("ie") < 8){ var r = de.getBoundingClientRect(), // works well for IE6+ l = r.left, t = r.top; if(has("ie") < 7){ l += de.clientLeft; // scrollbar size in strict/RTL, or, t += de.clientTop; // HTML border size in strict } return { x: l < 0 ? 0 : l, // FRAME element border size can lead to inaccurate negative values y: t < 0 ? 0 : t }; }else{ return { x: 0, y: 0 }; } }; } geom.fixIeBiDiScrollLeft = function fixIeBiDiScrollLeft(/*Integer*/ scrollLeft, /*Document?*/ doc){ // summary: // In RTL direction, scrollLeft should be a negative value, but IE // returns a positive one. All codes using documentElement.scrollLeft // must call this function to fix this error, otherwise the position // will offset to right when there is a horizontal scrollbar. // scrollLeft: Number // doc: Document? // Optional document to query. If unspecified, use win.doc. // returns: Number // In RTL direction, scrollLeft should be a negative value, but IE // returns a positive one. All codes using documentElement.scrollLeft // must call this function to fix this error, otherwise the position // will offset to right when there is a horizontal scrollbar. doc = doc || win.doc; var ie = has("ie"); if(ie && !geom.isBodyLtr(doc)){ var qk = has("quirks"), de = qk ? win.body(doc) : doc.documentElement, pwin = win.global; // TODO: use winUtils.get(doc) after resolving circular dependency b/w dom-geometry.js and dojo/window.js if(ie == 6 && !qk && pwin.frameElement && de.scrollHeight > de.clientHeight){ scrollLeft += de.clientLeft; // workaround ie6+strict+rtl+iframe+vertical-scrollbar bug where clientWidth is too small by clientLeft pixels } return (ie < 8 || qk) ? (scrollLeft + de.clientWidth - de.scrollWidth) : -scrollLeft; // Integer } return scrollLeft; // Integer }; geom.position = function(/*DomNode*/ node, /*Boolean?*/ includeScroll){ // summary: // Gets the position and size of the passed element relative to // the viewport (if includeScroll==false), or relative to the // document root (if includeScroll==true). // // description: // Returns an object of the form: // `{ x: 100, y: 300, w: 20, h: 15 }`. // If includeScroll==true, the x and y values will include any // document offsets that may affect the position relative to the // viewport. // Uses the border-box model (inclusive of border and padding but // not margin). Does not act as a setter. // node: DOMNode|String // includeScroll: Boolean? // returns: Object node = dom.byId(node); var db = win.body(node.ownerDocument), ret = node.getBoundingClientRect(); ret = {x: ret.left, y: ret.top, w: ret.right - ret.left, h: ret.bottom - ret.top}; if(has("ie")){ // On IE there's a 2px offset that we need to adjust for, see dojo.getIeDocumentElementOffset() var offset = geom.getIeDocumentElementOffset(node.ownerDocument); // fixes the position in IE, quirks mode ret.x -= offset.x + (has("quirks") ? db.clientLeft + db.offsetLeft : 0); ret.y -= offset.y + (has("quirks") ? db.clientTop + db.offsetTop : 0); } // account for document scrolling // if offsetParent is used, ret value already includes scroll position // so we may have to actually remove that value if !includeScroll if(includeScroll){ var scroll = geom.docScroll(node.ownerDocument); ret.x += scroll.x; ret.y += scroll.y; } return ret; // Object }; // random "private" functions wildly used throughout the toolkit geom.getMarginSize = function getMarginSize(/*DomNode*/ node, /*Object*/ computedStyle){ // summary: // returns an object that encodes the width and height of // the node's margin box // node: DOMNode|String // computedStyle: Object? // This parameter accepts computed styles object. // If this parameter is omitted, the functions will call // dojo.getComputedStyle to get one. It is a better way, calling // dojo.computedStyle once, and then pass the reference to this // computedStyle parameter. Wherever possible, reuse the returned // object of dojo/dom-style.getComputedStyle(). node = dom.byId(node); var me = geom.getMarginExtents(node, computedStyle || style.getComputedStyle(node)); var size = node.getBoundingClientRect(); return { w: (size.right - size.left) + me.w, h: (size.bottom - size.top) + me.h }; }; geom.normalizeEvent = function(event){ // summary: // Normalizes the geometry of a DOM event, normalizing the pageX, pageY, // offsetX, offsetY, layerX, and layerX properties // event: Object if(!("layerX" in event)){ event.layerX = event.offsetX; event.layerY = event.offsetY; } if(!has("dom-addeventlistener")){ // old IE version // FIXME: scroll position query is duped from dojo.html to // avoid dependency on that entire module. Now that HTML is in // Base, we should convert back to something similar there. var se = event.target; var doc = (se && se.ownerDocument) || document; // DO NOT replace the following to use dojo.body(), in IE, document.documentElement should be used // here rather than document.body var docBody = has("quirks") ? doc.body : doc.documentElement; var offset = geom.getIeDocumentElementOffset(doc); event.pageX = event.clientX + geom.fixIeBiDiScrollLeft(docBody.scrollLeft || 0, doc) - offset.x; event.pageY = event.clientY + (docBody.scrollTop || 0) - offset.y; } }; // TODO: evaluate separate getters/setters for position and sizes? return geom; }); }, 'dojo/dom-prop':function(){ define(["exports", "./_base/kernel", "./sniff", "./_base/lang", "./dom", "./dom-style", "./dom-construct", "./_base/connect"], function(exports, dojo, has, lang, dom, style, ctr, conn){ // module: // dojo/dom-prop // summary: // This module defines the core dojo DOM properties API. // Indirectly depends on dojo.empty() and dojo.toDom(). // TODOC: summary not showing up in output, see https://github.com/csnover/js-doc-parse/issues/42 // ============================= // Element properties Functions // ============================= // helper to connect events var _evtHdlrMap = {}, _ctr = 0, _attrId = dojo._scopeName + "attrid"; exports.names = { // properties renamed to avoid clashes with reserved words "class": "className", "for": "htmlFor", // properties written as camelCase tabindex: "tabIndex", readonly: "readOnly", colspan: "colSpan", frameborder: "frameBorder", rowspan: "rowSpan", valuetype: "valueType" }; exports.get = function getProp(/*DOMNode|String*/ node, /*String*/ name){ // summary: // Gets a property on an HTML element. // description: // Handles normalized getting of properties on DOM nodes. // // node: DOMNode|String // id or reference to the element to get the property on // name: String // the name of the property to get. // returns: // the value of the requested property or its default value // // example: // | // get the current value of the "foo" property on a node // | dojo.getProp(dojo.byId("nodeId"), "foo"); // | // or we can just pass the id: // | dojo.getProp("nodeId", "foo"); node = dom.byId(node); var lc = name.toLowerCase(), propName = exports.names[lc] || name; return node[propName]; // Anything }; exports.set = function setProp(/*DOMNode|String*/ node, /*String|Object*/ name, /*String?*/ value){ // summary: // Sets a property on an HTML element. // description: // Handles normalized setting of properties on DOM nodes. // // When passing functions as values, note that they will not be // directly assigned to slots on the node, but rather the default // behavior will be removed and the new behavior will be added // using `dojo.connect()`, meaning that event handler properties // will be normalized and that some caveats with regards to // non-standard behaviors for onsubmit apply. Namely that you // should cancel form submission using `dojo.stopEvent()` on the // passed event object instead of returning a boolean value from // the handler itself. // node: DOMNode|String // id or reference to the element to set the property on // name: String|Object // the name of the property to set, or a hash object to set // multiple properties at once. // value: String? // The value to set for the property // returns: // the DOM node // // example: // | // use prop() to set the tab index // | dojo.setProp("nodeId", "tabIndex", 3); // | // // example: // Set multiple values at once, including event handlers: // | dojo.setProp("formId", { // | "foo": "bar", // | "tabIndex": -1, // | "method": "POST", // | "onsubmit": function(e){ // | // stop submitting the form. Note that the IE behavior // | // of returning true or false will have no effect here // | // since our handler is connect()ed to the built-in // | // onsubmit behavior and so we need to use // | // dojo.stopEvent() to ensure that the submission // | // doesn't proceed. // | dojo.stopEvent(e); // | // | // submit the form with Ajax // | dojo.xhrPost({ form: "formId" }); // | } // | }); // // example: // Style is s special case: Only set with an object hash of styles // | dojo.setProp("someNode",{ // | id:"bar", // | style:{ // | width:"200px", height:"100px", color:"#000" // | } // | }); // // example: // Again, only set style as an object hash of styles: // | var obj = { color:"#fff", backgroundColor:"#000" }; // | dojo.setProp("someNode", "style", obj); // | // | // though shorter to use `dojo.style()` in this case: // | dojo.style("someNode", obj); node = dom.byId(node); var l = arguments.length; if(l == 2 && typeof name != "string"){ // inline'd type check // the object form of setter: the 2nd argument is a dictionary for(var x in name){ exports.set(node, x, name[x]); } return node; // DomNode } var lc = name.toLowerCase(), propName = exports.names[lc] || name; if(propName == "style" && typeof value != "string"){ // inline'd type check // special case: setting a style style.set(node, value); return node; // DomNode } if(propName == "innerHTML"){ // special case: assigning HTML // the hash lists elements with read-only innerHTML on IE if(has("ie") && node.tagName.toLowerCase() in {col: 1, colgroup: 1, table: 1, tbody: 1, tfoot: 1, thead: 1, tr: 1, title: 1}){ ctr.empty(node); node.appendChild(ctr.toDom(value, node.ownerDocument)); }else{ node[propName] = value; } return node; // DomNode } if(lang.isFunction(value)){ // special case: assigning an event handler // clobber if we can var attrId = node[_attrId]; if(!attrId){ attrId = _ctr++; node[_attrId] = attrId; } if(!_evtHdlrMap[attrId]){ _evtHdlrMap[attrId] = {}; } var h = _evtHdlrMap[attrId][propName]; if(h){ //h.remove(); conn.disconnect(h); }else{ try{ delete node[propName]; }catch(e){} } // ensure that event objects are normalized, etc. if(value){ //_evtHdlrMap[attrId][propName] = on(node, propName, value); _evtHdlrMap[attrId][propName] = conn.connect(node, propName, value); }else{ node[propName] = null; } return node; // DomNode } node[propName] = value; return node; // DomNode }; }); }, 'dojo/when':function(){ define([ "./Deferred", "./promise/Promise" ], function(Deferred, Promise){ "use strict"; // module: // dojo/when return function when(valueOrPromise, callback, errback, progback){ // summary: // Transparently applies callbacks to values and/or promises. // description: // Accepts promises but also transparently handles non-promises. If no // callbacks are provided returns a promise, regardless of the initial // value. Foreign promises are converted. // // If callbacks are provided and the initial value is not a promise, // the callback is executed immediately with no error handling. Returns // a promise if the initial value is a promise, or the result of the // callback otherwise. // valueOrPromise: // Either a regular value or an object with a `then()` method that // follows the Promises/A specification. // callback: Function? // Callback to be invoked when the promise is resolved, or a non-promise // is received. // errback: Function? // Callback to be invoked when the promise is rejected. // progback: Function? // Callback to be invoked when the promise emits a progress update. // returns: dojo/promise/Promise // Promise, or if a callback is provided, the result of the callback. var receivedPromise = valueOrPromise && typeof valueOrPromise.then === "function"; var nativePromise = receivedPromise && valueOrPromise instanceof Promise; if(!receivedPromise){ if(callback){ return callback(valueOrPromise); }else{ return new Deferred().resolve(valueOrPromise); } }else if(!nativePromise){ var deferred = new Deferred(valueOrPromise.cancel); valueOrPromise.then(deferred.resolve, deferred.reject, deferred.progress); valueOrPromise = deferred.promise; } if(callback || errback || progback){ return valueOrPromise.then(callback, errback, progback); } return valueOrPromise; }; }); }, 'dojo/dom-attr':function(){ define(["exports", "./sniff", "./_base/lang", "./dom", "./dom-style", "./dom-prop"], function(exports, has, lang, dom, style, prop){ // module: // dojo/dom-attr // summary: // This module defines the core dojo DOM attributes API. // TODOC: summary not showing up in output see https://github.com/csnover/js-doc-parse/issues/42 // ============================= // Element attribute Functions // ============================= // This module will be obsolete soon. Use dojo/prop instead. // dojo.attr() should conform to http://www.w3.org/TR/DOM-Level-2-Core/ // attribute-related functions (to be obsolete soon) var forcePropNames = { innerHTML: 1, className: 1, htmlFor: has("ie"), value: 1 }, attrNames = { // original attribute names classname: "class", htmlfor: "for", // for IE tabindex: "tabIndex", readonly: "readOnly" }; function _hasAttr(node, name){ var attr = node.getAttributeNode && node.getAttributeNode(name); return attr && attr.specified; // Boolean } // There is a difference in the presence of certain properties and their default values // between browsers. For example, on IE "disabled" is present on all elements, // but it is value is "false"; "tabIndex" of
                                returns 0 by default on IE, yet other browsers // can return -1. exports.has = function hasAttr(/*DOMNode|String*/ node, /*String*/ name){ // summary: // Returns true if the requested attribute is specified on the // given element, and false otherwise. // node: DOMNode|String // id or reference to the element to check // name: String // the name of the attribute // returns: Boolean // true if the requested attribute is specified on the // given element, and false otherwise var lc = name.toLowerCase(); return forcePropNames[prop.names[lc] || name] || _hasAttr(dom.byId(node), attrNames[lc] || name); // Boolean }; exports.get = function getAttr(/*DOMNode|String*/ node, /*String*/ name){ // summary: // Gets an attribute on an HTML element. // description: // Handles normalized getting of attributes on DOM Nodes. // node: DOMNode|String // id or reference to the element to get the attribute on // name: String // the name of the attribute to get. // returns: // the value of the requested attribute or null if that attribute does not have a specified or // default value; // // example: // | // get the current value of the "foo" attribute on a node // | dojo.getAttr(dojo.byId("nodeId"), "foo"); // | // or we can just pass the id: // | dojo.getAttr("nodeId", "foo"); node = dom.byId(node); var lc = name.toLowerCase(), propName = prop.names[lc] || name, forceProp = forcePropNames[propName], value = node[propName]; // should we access this attribute via a property or via getAttribute()? if(forceProp && typeof value != "undefined"){ // node's property return value; // Anything } if(propName != "href" && (typeof value == "boolean" || lang.isFunction(value))){ // node's property return value; // Anything } // node's attribute // we need _hasAttr() here to guard against IE returning a default value var attrName = attrNames[lc] || name; return _hasAttr(node, attrName) ? node.getAttribute(attrName) : null; // Anything }; exports.set = function setAttr(/*DOMNode|String*/ node, /*String|Object*/ name, /*String?*/ value){ // summary: // Sets an attribute on an HTML element. // description: // Handles normalized setting of attributes on DOM Nodes. // // When passing functions as values, note that they will not be // directly assigned to slots on the node, but rather the default // behavior will be removed and the new behavior will be added // using `dojo.connect()`, meaning that event handler properties // will be normalized and that some caveats with regards to // non-standard behaviors for onsubmit apply. Namely that you // should cancel form submission using `dojo.stopEvent()` on the // passed event object instead of returning a boolean value from // the handler itself. // node: DOMNode|String // id or reference to the element to set the attribute on // name: String|Object // the name of the attribute to set, or a hash of key-value pairs to set. // value: String? // the value to set for the attribute, if the name is a string. // returns: // the DOM node // // example: // | // use attr() to set the tab index // | dojo.setAttr("nodeId", "tabIndex", 3); // // example: // Set multiple values at once, including event handlers: // | dojo.setAttr("formId", { // | "foo": "bar", // | "tabIndex": -1, // | "method": "POST", // | "onsubmit": function(e){ // | // stop submitting the form. Note that the IE behavior // | // of returning true or false will have no effect here // | // since our handler is connect()ed to the built-in // | // onsubmit behavior and so we need to use // | // dojo.stopEvent() to ensure that the submission // | // doesn't proceed. // | dojo.stopEvent(e); // | // | // submit the form with Ajax // | dojo.xhrPost({ form: "formId" }); // | } // | }); // // example: // Style is s special case: Only set with an object hash of styles // | dojo.setAttr("someNode",{ // | id:"bar", // | style:{ // | width:"200px", height:"100px", color:"#000" // | } // | }); // // example: // Again, only set style as an object hash of styles: // | var obj = { color:"#fff", backgroundColor:"#000" }; // | dojo.setAttr("someNode", "style", obj); // | // | // though shorter to use `dojo.style()` in this case: // | dojo.setStyle("someNode", obj); node = dom.byId(node); if(arguments.length == 2){ // inline'd type check // the object form of setter: the 2nd argument is a dictionary for(var x in name){ exports.set(node, x, name[x]); } return node; // DomNode } var lc = name.toLowerCase(), propName = prop.names[lc] || name, forceProp = forcePropNames[propName]; if(propName == "style" && typeof value != "string"){ // inline'd type check // special case: setting a style style.set(node, value); return node; // DomNode } if(forceProp || typeof value == "boolean" || lang.isFunction(value)){ return prop.set(node, name, value); } // node's attribute node.setAttribute(attrNames[lc] || name, value); return node; // DomNode }; exports.remove = function removeAttr(/*DOMNode|String*/ node, /*String*/ name){ // summary: // Removes an attribute from an HTML element. // node: DOMNode|String // id or reference to the element to remove the attribute from // name: String // the name of the attribute to remove dom.byId(node).removeAttribute(attrNames[name.toLowerCase()] || name); }; exports.getNodeProp = function getNodeProp(/*DomNode|String*/ node, /*String*/ name){ // summary: // Returns an effective value of a property or an attribute. // node: DOMNode|String // id or reference to the element to remove the attribute from // name: String // the name of the attribute // returns: // the value of the attribute node = dom.byId(node); var lc = name.toLowerCase(), propName = prop.names[lc] || name; if((propName in node) && propName != "href"){ // node's property return node[propName]; // Anything } // node's attribute var attrName = attrNames[lc] || name; return _hasAttr(node, attrName) ? node.getAttribute(attrName) : null; // Anything }; }); }, 'dojo/dom-construct':function(){ define(["exports", "./_base/kernel", "./sniff", "./_base/window", "./dom", "./dom-attr", "./on"], function(exports, dojo, has, win, dom, attr, on){ // module: // dojo/dom-construct // summary: // This module defines the core dojo DOM construction API. // TODOC: summary not showing up in output, see https://github.com/csnover/js-doc-parse/issues/42 // support stuff for toDom() var tagWrap = { option: ["select"], tbody: ["table"], thead: ["table"], tfoot: ["table"], tr: ["table", "tbody"], td: ["table", "tbody", "tr"], th: ["table", "thead", "tr"], legend: ["fieldset"], caption: ["table"], colgroup: ["table"], col: ["table", "colgroup"], li: ["ul"] }, reTag = /<\s*([\w\:]+)/, masterNode = {}, masterNum = 0, masterName = "__" + dojo._scopeName + "ToDomId"; // generate start/end tag strings to use // for the injection for each special tag wrap case. for(var param in tagWrap){ if(tagWrap.hasOwnProperty(param)){ var tw = tagWrap[param]; tw.pre = param == "option" ? '` to create: submit, reset or button. */ type: 'button', /** * @cfg {String} clickEvent * The DOM event that will fire the handler of the button. This can be any valid event name (dblclick, contextmenu). */ clickEvent: 'click', /** * @cfg {Boolean} preventDefault * True to prevent the default action when the {@link #clickEvent} is processed. */ preventDefault: true, /** * @cfg {Boolean} handleMouseEvents * False to disable visual cues on mouseover, mouseout and mousedown. */ handleMouseEvents: true, /** * @cfg {String} tooltipType * The type of tooltip to use. Either 'qtip' for QuickTips or 'title' for title attribute. */ tooltipType: 'qtip', /** * @cfg {String} [baseCls='x-btn'] * The base CSS class to add to all buttons. */ baseCls: Ext.baseCSSPrefix + 'btn', /** * @cfg {String} pressedCls * The CSS class to add to a button when it is in the pressed state. */ pressedCls: 'pressed', /** * @cfg {String} overCls * The CSS class to add to a button when it is in the over (hovered) state. */ overCls: 'over', /** * @cfg {String} focusCls * The CSS class to add to a button when it is in the focussed state. */ focusCls: 'focus', /** * @cfg {String} menuActiveCls * The CSS class to add to a button when it's menu is active. */ menuActiveCls: 'menu-active', /** * @cfg {String} href * The URL to open when the button is clicked. Specifying this config causes the Button to be * rendered with an anchor (An `` element) as its active element, referencing the specified URL. * * This is better than specifying a click handler of * * function() { window.location = "http://www.sencha.com" } * * because the UI will provide meaningful hints to the user as to what to expect upon clicking * the button, and will also allow the user to open in a new tab or window, bookmark or drag the URL, or directly save * the URL stream to disk. * * See also the {@link #hrefTarget} config. */ /** * @cfg {String} [hrefTarget="_blank"] * The target attribute to use for the underlying anchor. Only used if the {@link #href} * property is specified. */ hrefTarget: '_blank', border: true, /** * @cfg {Object} baseParams * An object literal of parameters to pass to the url when the {@link #href} property is specified. */ /** * @cfg {Object} params * An object literal of parameters to pass to the url when the {@link #href} property is specified. Any params * override {@link #baseParams}. New params can be set using the {@link #setParams} method. */ childEls: [ 'btnEl', 'btnWrap', 'btnInnerEl', 'btnIconEl' ], renderTpl: [ ' class="{splitCls}">', '', ' tabIndex="{tabIndex}"', ' disabled="disabled"', ' role="link">', '', '{text}', '', ' style="background-image:url({iconUrl})">', '', '', '', '', '', '', '', '' ], /** * @cfg {String} scale * The size of the Button. Three values are allowed: * * - 'small' - Results in the button element being 16px high. * - 'medium' - Results in the button element being 24px high. * - 'large' - Results in the button element being 32px high. */ scale: 'small', /** * @private * An array of allowed scales. */ allowedScales: ['small', 'medium', 'large'], /** * @cfg {Object} scope * The scope (**this** reference) in which the `{@link #handler}` and `{@link #toggleHandler}` is executed. * Defaults to this Button. */ /** * @cfg {String} iconAlign * The side of the Button box to render the icon. Four values are allowed: * * - 'top' * - 'right' * - 'bottom' * - 'left' */ iconAlign: 'left', /** * @cfg {String} arrowAlign * The side of the Button box to render the arrow if the button has an associated {@link #cfg-menu}. Two * values are allowed: * * - 'right' * - 'bottom' */ arrowAlign: 'right', /** * @cfg {String} arrowCls * The className used for the inner arrow element if the button has a menu. */ arrowCls: 'arrow', /** * @property {Ext.Template} template * A {@link Ext.Template Template} used to create the Button's DOM structure. * * Instances, or subclasses which need a different DOM structure may provide a different template layout in * conjunction with an implementation of {@link #getTemplateArgs}. */ /** * @cfg {String} cls * A CSS class string to apply to the button's main element. */ /** * @property {Ext.menu.Menu} menu * The {@link Ext.menu.Menu Menu} object associated with this Button when configured with the {@link #cfg-menu} config * option. */ maskOnDisable: false, /** * @private * @property persistentPadding * The padding spuriously added to a <button> element which must be accounted for in the margins of the innerEl. * This is calculated at first render time by creating a hidden button and measuring its insides. */ persistentPadding: undefined, shrinkWrap: 3, frame: true, // inherit docs initComponent: function() { var me = this; me.callParent(arguments); me.addEvents( /** * @event click * Fires when this button is clicked, before the configured {@link #handler} is invoked. Execution of the * {@link #handler} may be vetoed by returning false to this event. * @param {Ext.button.Button} this * @param {Event} e The click event */ 'click', /** * @event toggle * Fires when the 'pressed' state of this button changes (only if enableToggle = true) * @param {Ext.button.Button} this * @param {Boolean} pressed */ 'toggle', /** * @event mouseover * Fires when the mouse hovers over the button * @param {Ext.button.Button} this * @param {Event} e The event object */ 'mouseover', /** * @event mouseout * Fires when the mouse exits the button * @param {Ext.button.Button} this * @param {Event} e The event object */ 'mouseout', /** * @event menushow * If this button has a menu, this event fires when it is shown * @param {Ext.button.Button} this * @param {Ext.menu.Menu} menu */ 'menushow', /** * @event menuhide * If this button has a menu, this event fires when it is hidden * @param {Ext.button.Button} this * @param {Ext.menu.Menu} menu */ 'menuhide', /** * @event menutriggerover * If this button has a menu, this event fires when the mouse enters the menu triggering element * @param {Ext.button.Button} this * @param {Ext.menu.Menu} menu * @param {Event} e */ 'menutriggerover', /** * @event menutriggerout * If this button has a menu, this event fires when the mouse leaves the menu triggering element * @param {Ext.button.Button} this * @param {Ext.menu.Menu} menu * @param {Event} e */ 'menutriggerout' ); if (me.menu) { // Flag that we'll have a splitCls me.split = true; // retrieve menu by id or instantiate instance if needed me.menu = Ext.menu.Manager.get(me.menu); me.menu.ownerButton = me; } // Accept url as a synonym for href if (me.url) { me.href = me.url; } // preventDefault defaults to false for links if (me.href && !me.hasOwnProperty('preventDefault')) { me.preventDefault = false; } if (Ext.isString(me.toggleGroup) && me.toggleGroup !== '') { me.enableToggle = true; } if (me.html && !me.text) { me.text = me.html; delete me.html; } }, // inherit docs getActionEl: function() { return this.btnEl; }, // inherit docs getFocusEl: function() { return this.useElForFocus ? this.el : this.btnEl; }, // Buttons add the focus class to the *outermost element*, not the focusEl! onFocus: function(e) { var me = this; // Set this flag, so that when AbstractComponent's onFocus gets the focusEl to add the focusCls // to, it will get the encapsulating element - that's what the CSS rules for Button need right now me.useElForFocus = true; me.callParent(arguments); me.useElForFocus = false; }, // See comments in onFocus onBlur : function(e) { this.useElForFocus = true; this.callParent(arguments); this.useElForFocus = false; }, // See comments in onFocus onDisable: function(){ this.useElForFocus = true; this.callParent(arguments); this.useElForFocus = false; }, // private setComponentCls: function() { var me = this, cls = me.getComponentCls(); if (!Ext.isEmpty(me.oldCls)) { me.removeClsWithUI(me.oldCls); me.removeClsWithUI(me.pressedCls); } me.oldCls = cls; me.addClsWithUI(cls); }, getComponentCls: function() { var me = this, cls = []; // Check whether the button has an icon or not, and if it has an icon, what is the alignment if (me.iconCls || me.icon) { if (me.text) { cls.push('icon-text-' + me.iconAlign); } else { cls.push('icon'); } } else if (me.text) { cls.push('noicon'); } if (me.pressed) { cls.push(me.pressedCls); } return cls; }, beforeRender: function () { var me = this; me.callParent(); // Add all needed classes to the protoElement. me.oldCls = me.getComponentCls(); me.addClsWithUI(me.oldCls); // Apply the renderData to the template args Ext.applyIf(me.renderData, me.getTemplateArgs()); if (me.scale) { me.setScale(me.scale); } }, // private onRender: function() { var me = this, addOnclick, btn, btnListeners; me.doc = Ext.getDoc(); me.callParent(arguments); // If it is a split button + has a toolip for the arrow if (me.split && me.arrowTooltip) { me.arrowEl.dom.setAttribute(me.getTipAttr(), me.arrowTooltip); } // Set btn as a local variable for easy access btn = me.el; if (me.tooltip) { me.setTooltip(me.tooltip, true); } // Add the mouse events to the button if (me.handleMouseEvents) { btnListeners = { scope: me, mouseover: me.onMouseOver, mouseout: me.onMouseOut, mousedown: me.onMouseDown }; if (me.split) { btnListeners.mousemove = me.onMouseMove; } } else { btnListeners = { scope: me }; } // Check if the button has a menu if (me.menu) { me.mon(me.menu, { scope: me, show: me.onMenuShow, hide: me.onMenuHide }); me.keyMap = new Ext.util.KeyMap({ target: me.el, key: Ext.EventObject.DOWN, handler: me.onDownKey, scope: me }); } // Check if it is a repeat button if (me.repeat) { me.mon(new Ext.util.ClickRepeater(btn, Ext.isObject(me.repeat) ? me.repeat: {}), 'click', me.onRepeatClick, me); } else { // If the activation event already has a handler, make a note to add the handler later if (btnListeners[me.clickEvent]) { addOnclick = true; } else { btnListeners[me.clickEvent] = me.onClick; } } // Add whatever button listeners we need me.mon(btn, btnListeners); // If the listeners object had an entry for our clickEvent, add a listener now if (addOnclick) { me.mon(btn, me.clickEvent, me.onClick, me); } // Register the button in the toggle manager Ext.ButtonToggleManager.register(me); }, /** * This method returns an object which provides substitution parameters for the {@link #renderTpl XTemplate} used to * create this Button's DOM structure. * * Instances or subclasses which use a different Template to create a different DOM structure may need to provide * their own implementation of this method. * * @return {Object} Substitution data for a Template. The default implementation which provides data for the default * {@link #template} returns an Object containing the following properties: * @return {String} return.type The `