]> git.sur5r.net Git - i3/i3.github.io/blob - js/jquery.ba-hashchange.js
update doc for py3status 3.0 (#24)
[i3/i3.github.io] / js / jquery.ba-hashchange.js
1 /*!
2  * jQuery hashchange event - v1.3 - 7/21/2010
3  * http://benalman.com/projects/jquery-hashchange-plugin/
4  * 
5  * Copyright (c) 2010 "Cowboy" Ben Alman
6  * Dual licensed under the MIT and GPL licenses.
7  * http://benalman.com/about/license/
8  */
9
10 // Script: jQuery hashchange event
11 //
12 // *Version: 1.3, Last updated: 7/21/2010*
13 // 
14 // Project Home - http://benalman.com/projects/jquery-hashchange-plugin/
15 // GitHub       - http://github.com/cowboy/jquery-hashchange/
16 // Source       - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.js
17 // (Minified)   - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.min.js (0.8kb gzipped)
18 // 
19 // About: License
20 // 
21 // Copyright (c) 2010 "Cowboy" Ben Alman,
22 // Dual licensed under the MIT and GPL licenses.
23 // http://benalman.com/about/license/
24 // 
25 // About: Examples
26 // 
27 // These working examples, complete with fully commented code, illustrate a few
28 // ways in which this plugin can be used.
29 // 
30 // hashchange event - http://benalman.com/code/projects/jquery-hashchange/examples/hashchange/
31 // document.domain - http://benalman.com/code/projects/jquery-hashchange/examples/document_domain/
32 // 
33 // About: Support and Testing
34 // 
35 // Information about what version or versions of jQuery this plugin has been
36 // tested with, what browsers it has been tested in, and where the unit tests
37 // reside (so you can test it yourself).
38 // 
39 // jQuery Versions - 1.2.6, 1.3.2, 1.4.1, 1.4.2
40 // Browsers Tested - Internet Explorer 6-8, Firefox 2-4, Chrome 5-6, Safari 3.2-5,
41 //                   Opera 9.6-10.60, iPhone 3.1, Android 1.6-2.2, BlackBerry 4.6-5.
42 // Unit Tests      - http://benalman.com/code/projects/jquery-hashchange/unit/
43 // 
44 // About: Known issues
45 // 
46 // While this jQuery hashchange event implementation is quite stable and
47 // robust, there are a few unfortunate browser bugs surrounding expected
48 // hashchange event-based behaviors, independent of any JavaScript
49 // window.onhashchange abstraction. See the following examples for more
50 // information:
51 // 
52 // Chrome: Back Button - http://benalman.com/code/projects/jquery-hashchange/examples/bug-chrome-back-button/
53 // Firefox: Remote XMLHttpRequest - http://benalman.com/code/projects/jquery-hashchange/examples/bug-firefox-remote-xhr/
54 // WebKit: Back Button in an Iframe - http://benalman.com/code/projects/jquery-hashchange/examples/bug-webkit-hash-iframe/
55 // Safari: Back Button from a different domain - http://benalman.com/code/projects/jquery-hashchange/examples/bug-safari-back-from-diff-domain/
56 // 
57 // Also note that should a browser natively support the window.onhashchange 
58 // event, but not report that it does, the fallback polling loop will be used.
59 // 
60 // About: Release History
61 // 
62 // 1.3   - (7/21/2010) Reorganized IE6/7 Iframe code to make it more
63 //         "removable" for mobile-only development. Added IE6/7 document.title
64 //         support. Attempted to make Iframe as hidden as possible by using
65 //         techniques from http://www.paciellogroup.com/blog/?p=604. Added 
66 //         support for the "shortcut" format $(window).hashchange( fn ) and
67 //         $(window).hashchange() like jQuery provides for built-in events.
68 //         Renamed jQuery.hashchangeDelay to <jQuery.fn.hashchange.delay> and
69 //         lowered its default value to 50. Added <jQuery.fn.hashchange.domain>
70 //         and <jQuery.fn.hashchange.src> properties plus document-domain.html
71 //         file to address access denied issues when setting document.domain in
72 //         IE6/7.
73 // 1.2   - (2/11/2010) Fixed a bug where coming back to a page using this plugin
74 //         from a page on another domain would cause an error in Safari 4. Also,
75 //         IE6/7 Iframe is now inserted after the body (this actually works),
76 //         which prevents the page from scrolling when the event is first bound.
77 //         Event can also now be bound before DOM ready, but it won't be usable
78 //         before then in IE6/7.
79 // 1.1   - (1/21/2010) Incorporated document.documentMode test to fix IE8 bug
80 //         where browser version is incorrectly reported as 8.0, despite
81 //         inclusion of the X-UA-Compatible IE=EmulateIE7 meta tag.
82 // 1.0   - (1/9/2010) Initial Release. Broke out the jQuery BBQ event.special
83 //         window.onhashchange functionality into a separate plugin for users
84 //         who want just the basic event & back button support, without all the
85 //         extra awesomeness that BBQ provides. This plugin will be included as
86 //         part of jQuery BBQ, but also be available separately.
87
88 (function($,window,undefined){
89   '$:nomunge'; // Used by YUI compressor.
90   
91   // Reused string.
92   var str_hashchange = 'hashchange',
93     
94     // Method / object references.
95     doc = document,
96     fake_onhashchange,
97     special = $.event.special,
98     
99     // Does the browser support window.onhashchange? Note that IE8 running in
100     // IE7 compatibility mode reports true for 'onhashchange' in window, even
101     // though the event isn't supported, so also test document.documentMode.
102     doc_mode = doc.documentMode,
103     supports_onhashchange = 'on' + str_hashchange in window && ( doc_mode === undefined || doc_mode > 7 );
104   
105   // Get location.hash (or what you'd expect location.hash to be) sans any
106   // leading #. Thanks for making this necessary, Firefox!
107   function get_fragment( url ) {
108     url = url || location.href;
109     return '#' + url.replace( /^[^#]*#?(.*)$/, '$1' );
110   };
111   
112   // Method: jQuery.fn.hashchange
113   // 
114   // Bind a handler to the window.onhashchange event or trigger all bound
115   // window.onhashchange event handlers. This behavior is consistent with
116   // jQuery's built-in event handlers.
117   // 
118   // Usage:
119   // 
120   // > jQuery(window).hashchange( [ handler ] );
121   // 
122   // Arguments:
123   // 
124   //  handler - (Function) Optional handler to be bound to the hashchange
125   //    event. This is a "shortcut" for the more verbose form:
126   //    jQuery(window).bind( 'hashchange', handler ). If handler is omitted,
127   //    all bound window.onhashchange event handlers will be triggered. This
128   //    is a shortcut for the more verbose
129   //    jQuery(window).trigger( 'hashchange' ). These forms are described in
130   //    the <hashchange event> section.
131   // 
132   // Returns:
133   // 
134   //  (jQuery) The initial jQuery collection of elements.
135   
136   // Allow the "shortcut" format $(elem).hashchange( fn ) for binding and
137   // $(elem).hashchange() for triggering, like jQuery does for built-in events.
138   $.fn[ str_hashchange ] = function( fn ) {
139     return fn ? this.bind( str_hashchange, fn ) : this.trigger( str_hashchange );
140   };
141   
142   // Property: jQuery.fn.hashchange.delay
143   // 
144   // The numeric interval (in milliseconds) at which the <hashchange event>
145   // polling loop executes. Defaults to 50.
146   
147   // Property: jQuery.fn.hashchange.domain
148   // 
149   // If you're setting document.domain in your JavaScript, and you want hash
150   // history to work in IE6/7, not only must this property be set, but you must
151   // also set document.domain BEFORE jQuery is loaded into the page. This
152   // property is only applicable if you are supporting IE6/7 (or IE8 operating
153   // in "IE7 compatibility" mode).
154   // 
155   // In addition, the <jQuery.fn.hashchange.src> property must be set to the
156   // path of the included "document-domain.html" file, which can be renamed or
157   // modified if necessary (note that the document.domain specified must be the
158   // same in both your main JavaScript as well as in this file).
159   // 
160   // Usage:
161   // 
162   // jQuery.fn.hashchange.domain = document.domain;
163   
164   // Property: jQuery.fn.hashchange.src
165   // 
166   // If, for some reason, you need to specify an Iframe src file (for example,
167   // when setting document.domain as in <jQuery.fn.hashchange.domain>), you can
168   // do so using this property. Note that when using this property, history
169   // won't be recorded in IE6/7 until the Iframe src file loads. This property
170   // is only applicable if you are supporting IE6/7 (or IE8 operating in "IE7
171   // compatibility" mode).
172   // 
173   // Usage:
174   // 
175   // jQuery.fn.hashchange.src = 'path/to/file.html';
176   
177   $.fn[ str_hashchange ].delay = 50;
178   /*
179   $.fn[ str_hashchange ].domain = null;
180   $.fn[ str_hashchange ].src = null;
181   */
182   
183   // Event: hashchange event
184   // 
185   // Fired when location.hash changes. In browsers that support it, the native
186   // HTML5 window.onhashchange event is used, otherwise a polling loop is
187   // initialized, running every <jQuery.fn.hashchange.delay> milliseconds to
188   // see if the hash has changed. In IE6/7 (and IE8 operating in "IE7
189   // compatibility" mode), a hidden Iframe is created to allow the back button
190   // and hash-based history to work.
191   // 
192   // Usage as described in <jQuery.fn.hashchange>:
193   // 
194   // > // Bind an event handler.
195   // > jQuery(window).hashchange( function(e) {
196   // >   var hash = location.hash;
197   // >   ...
198   // > });
199   // > 
200   // > // Manually trigger the event handler.
201   // > jQuery(window).hashchange();
202   // 
203   // A more verbose usage that allows for event namespacing:
204   // 
205   // > // Bind an event handler.
206   // > jQuery(window).bind( 'hashchange', function(e) {
207   // >   var hash = location.hash;
208   // >   ...
209   // > });
210   // > 
211   // > // Manually trigger the event handler.
212   // > jQuery(window).trigger( 'hashchange' );
213   // 
214   // Additional Notes:
215   // 
216   // * The polling loop and Iframe are not created until at least one handler
217   //   is actually bound to the 'hashchange' event.
218   // * If you need the bound handler(s) to execute immediately, in cases where
219   //   a location.hash exists on page load, via bookmark or page refresh for
220   //   example, use jQuery(window).hashchange() or the more verbose 
221   //   jQuery(window).trigger( 'hashchange' ).
222   // * The event can be bound before DOM ready, but since it won't be usable
223   //   before then in IE6/7 (due to the necessary Iframe), recommended usage is
224   //   to bind it inside a DOM ready handler.
225   
226   // Override existing $.event.special.hashchange methods (allowing this plugin
227   // to be defined after jQuery BBQ in BBQ's source code).
228   special[ str_hashchange ] = $.extend( special[ str_hashchange ], {
229     
230     // Called only when the first 'hashchange' event is bound to window.
231     setup: function() {
232       // If window.onhashchange is supported natively, there's nothing to do..
233       if ( supports_onhashchange ) { return false; }
234       
235       // Otherwise, we need to create our own. And we don't want to call this
236       // until the user binds to the event, just in case they never do, since it
237       // will create a polling loop and possibly even a hidden Iframe.
238       $( fake_onhashchange.start );
239     },
240     
241     // Called only when the last 'hashchange' event is unbound from window.
242     teardown: function() {
243       // If window.onhashchange is supported natively, there's nothing to do..
244       if ( supports_onhashchange ) { return false; }
245       
246       // Otherwise, we need to stop ours (if possible).
247       $( fake_onhashchange.stop );
248     }
249     
250   });
251   
252   // fake_onhashchange does all the work of triggering the window.onhashchange
253   // event for browsers that don't natively support it, including creating a
254   // polling loop to watch for hash changes and in IE 6/7 creating a hidden
255   // Iframe to enable back and forward.
256   fake_onhashchange = (function(){
257     var self = {},
258       timeout_id,
259       
260       // Remember the initial hash so it doesn't get triggered immediately.
261       last_hash = get_fragment(),
262       
263       fn_retval = function(val){ return val; },
264       history_set = fn_retval,
265       history_get = fn_retval;
266     
267     // Start the polling loop.
268     self.start = function() {
269       timeout_id || poll();
270     };
271     
272     // Stop the polling loop.
273     self.stop = function() {
274       timeout_id && clearTimeout( timeout_id );
275       timeout_id = undefined;
276     };
277     
278     // This polling loop checks every $.fn.hashchange.delay milliseconds to see
279     // if location.hash has changed, and triggers the 'hashchange' event on
280     // window when necessary.
281     function poll() {
282       var hash = get_fragment(),
283         history_hash = history_get( last_hash );
284       
285       if ( hash !== last_hash ) {
286         history_set( last_hash = hash, history_hash );
287         
288         $(window).trigger( str_hashchange );
289         
290       } else if ( history_hash !== last_hash ) {
291         location.href = location.href.replace( /#.*/, '' ) + history_hash;
292       }
293       
294       timeout_id = setTimeout( poll, $.fn[ str_hashchange ].delay );
295     };
296     
297     // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
298     // vvvvvvvvvvvvvvvvvvv REMOVE IF NOT SUPPORTING IE6/7/8 vvvvvvvvvvvvvvvvvvv
299     // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
300     $.browser.msie && !supports_onhashchange && (function(){
301       // Not only do IE6/7 need the "magical" Iframe treatment, but so does IE8
302       // when running in "IE7 compatibility" mode.
303       
304       var iframe,
305         iframe_src;
306       
307       // When the event is bound and polling starts in IE 6/7, create a hidden
308       // Iframe for history handling.
309       self.start = function(){
310         if ( !iframe ) {
311           iframe_src = $.fn[ str_hashchange ].src;
312           iframe_src = iframe_src && iframe_src + get_fragment();
313           
314           // Create hidden Iframe. Attempt to make Iframe as hidden as possible
315           // by using techniques from http://www.paciellogroup.com/blog/?p=604.
316           iframe = $('<iframe tabindex="-1" title="empty"/>').hide()
317             
318             // When Iframe has completely loaded, initialize the history and
319             // start polling.
320             .one( 'load', function(){
321               iframe_src || history_set( get_fragment() );
322               poll();
323             })
324             
325             // Load Iframe src if specified, otherwise nothing.
326             .attr( 'src', iframe_src || 'javascript:0' )
327             
328             // Append Iframe after the end of the body to prevent unnecessary
329             // initial page scrolling (yes, this works).
330             .insertAfter( 'body' )[0].contentWindow;
331           
332           // Whenever `document.title` changes, update the Iframe's title to
333           // prettify the back/next history menu entries. Since IE sometimes
334           // errors with "Unspecified error" the very first time this is set
335           // (yes, very useful) wrap this with a try/catch block.
336           doc.onpropertychange = function(){
337             try {
338               if ( event.propertyName === 'title' ) {
339                 iframe.document.title = doc.title;
340               }
341             } catch(e) {}
342           };
343           
344         }
345       };
346       
347       // Override the "stop" method since an IE6/7 Iframe was created. Even
348       // if there are no longer any bound event handlers, the polling loop
349       // is still necessary for back/next to work at all!
350       self.stop = fn_retval;
351       
352       // Get history by looking at the hidden Iframe's location.hash.
353       history_get = function() {
354         return get_fragment( iframe.location.href );
355       };
356       
357       // Set a new history item by opening and then closing the Iframe
358       // document, *then* setting its location.hash. If document.domain has
359       // been set, update that as well.
360       history_set = function( hash, history_hash ) {
361         var iframe_doc = iframe.document,
362           domain = $.fn[ str_hashchange ].domain;
363         
364         if ( hash !== history_hash ) {
365           // Update Iframe with any initial `document.title` that might be set.
366           iframe_doc.title = doc.title;
367           
368           // Opening the Iframe's document after it has been closed is what
369           // actually adds a history entry.
370           iframe_doc.open();
371           
372           // Set document.domain for the Iframe document as well, if necessary.
373           domain && iframe_doc.write( '<script>document.domain="' + domain + '"</script>' );
374           
375           iframe_doc.close();
376           
377           // Update the Iframe's hash, for great justice.
378           iframe.location.hash = hash;
379         }
380       };
381       
382     })();
383     // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
384     // ^^^^^^^^^^^^^^^^^^^ REMOVE IF NOT SUPPORTING IE6/7/8 ^^^^^^^^^^^^^^^^^^^
385     // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
386     
387     return self;
388   })();
389   
390 })(jQuery,this);