mediawiki.special.preferences: Support Back/Forward navigation
authorJoan Creus <joan.creus.c@gmail.com>
Sat, 14 Apr 2012 06:45:06 +0000 (08:45 +0200)
committerTimo Tijhof <ttijhof@wikimedia.org>
Fri, 29 Jun 2012 02:05:49 +0000 (04:05 +0200)
The current preferences tabbed dialog already uses hashes to identify
different tab sections, but using the browser's Back or Forward
button had no effect.

Added an event listener to the browser 'hashchange' event, so
that when the hash changes (as a result of using Back/Forward), the
display switched to the desired section.

If on the 'hashchange' event the hash is empty, it opens up the first
tab (just like the default when loading plain Special:Preferences
without a hash).

If the hash does not correspond to any section, nothing happens (this
can happen when the user follows an old hash-link to an tab that no
longer exists, in that case we need not do anything as the first tab
is visible by default). Another thing that can cause this is when the
page is already opened and some other component is using the hashes
for something. So we don't want to force a switch to the first tab
either.

Removed duplication of code by introducing function switchPrefTab().

Tested:
- Loading to a hash value
- Switching tabs
- Pressing Back/Forward
in:
* Chrome 19: OK
* Safari 5.1: OK
* Firefox 12: OK
* Opera 11.1: OK
* Opera 11.5: OK
* Opera 11.6: OK

Change-Id: Ic51c3954884680496f76814eed3fe65c9d0daacc

resources/mediawiki.special/mediawiki.special.preferences.js

index e9df83c..0804825 100644 (file)
@@ -1,7 +1,7 @@
 /*
  * JavaScript for Special:Preferences
  */
-jQuery( document ).ready( function( $ ) {
+jQuery( document ).ready( function ( $ ) {
 $( '#prefsubmit' ).attr( 'id', 'prefcontrol' );
 var $preftoc = $('<ul id="preftoc"></ul>');
 var $preferences = $( '#preferences' )
@@ -15,8 +15,35 @@ var $fieldsets = $preferences.children( 'fieldset' )
 var $legends = $fieldsets.children( 'legend' )
        .addClass( 'mainLegend' );
 
+/**
+ * It uses document.getElementById for security reasons (html injections in
+ * jQuery()).
+ *
+ * @param String name: the name of a tab without the prefix ("mw-prefsection-")
+ * @param String mode: [optional] A hash will be set according to the current
+ * open section. Set mode 'noHash' to surpress this.
+ */
+function switchPrefTab( name, mode ) {
+       var $tab, scrollTop;
+       // Handle hash manually to prevent jumping,
+       // therefore save and restore scrollTop to prevent jumping.
+       scrollTop = $( window ).scrollTop();
+       if ( mode !== 'noHash' ) {
+               window.location.hash = '#mw-prefsection-' + name;
+       }
+       $( window ).scrollTop( scrollTop );
+
+       $preftoc.find( 'li' ).removeClass( 'selected' );
+       $tab = $( document.getElementById( 'preftab-' + name ) );
+       if ( $tab.length ) {
+               $tab.parent().addClass( 'selected' );
+               $preferences.children( 'fieldset' ).hide();
+               $( document.getElementById( 'mw-prefsection-' + name ) ).show();
+       }
+}
+
 // Populate the prefToc
-$legends.each( function( i, legend ) {
+$legends.each( function ( i, legend ) {
        var $legend = $(legend);
        if ( i === 0 ) {
                $legend.parent().show();
@@ -30,18 +57,6 @@ $legends.each( function( i, legend ) {
                text : $legend.text(),
                id : ident.replace( 'mw-prefsection', 'preftab' ),
                href : '#' + ident
-       }).click( function( e ) {
-               e.preventDefault();
-               // Handle hash manually to prevent jumping
-               // Therefore save and restore scrollTop to prevent jumping
-               var scrollTop = $(window).scrollTop();
-               window.location.hash = $(this).attr('href');
-               $(window).scrollTop(scrollTop);
-
-               $preftoc.find( 'li' ).removeClass( 'selected' );
-               $(this).parent().addClass( 'selected' );
-               $( '#preferences > fieldset' ).hide();
-               $( '#' + ident ).show();
        });
        $li.append( $a );
        $preftoc.append( $li );
@@ -50,11 +65,34 @@ $legends.each( function( i, legend ) {
 // If we've reloaded the page or followed an open-in-new-window,
 // make the selected tab visible.
 var hash = window.location.hash;
-if( hash.match( /^#mw-prefsection-[\w-]+/ ) ) {
-       var $tab = $( hash.replace( 'mw-prefsection', 'preftab' ) );
-       $tab.click();
+if ( hash.match( /^#mw-prefsection-[\w-]+/ ) ) {
+       switchPrefTab( hash.replace( '#mw-prefsection-' , '' ) );
 }
 
+// In browsers that support the onhashchange event we will not bind click
+// handlers and instead let the browser do the default behavior (clicking the
+// <a href="#.."> will naturally set the hash, handled by onhashchange.
+// But other things that change the hash will also be catched (e.g. using
+// the Back and Forward browser navigation).
+if ( 'onhashchange' in window ) {
+       $(window).on( 'hashchange' , function () {
+               var hash = window.location.hash;
+               if ( hash.match( /^#mw-prefsection-[\w-]+/ ) ) {
+                       switchPrefTab( hash.replace( '#mw-prefsection-', '' ) );
+               } else if ( hash === '' ) {
+                       switchPrefTab( 'personal', 'noHash' );
+               }
+       });
+// In older browsers we'll bind a click handler as fallback.
+// We must not have onhashchange *and* the click handlers, other wise
+// the click handler calls switchPrefTab() which sets the hash value,
+// which triggers onhashcange and calls switchPrefTab() again.
+} else {
+       $preftoc.on( 'click', 'li a', function ( e ) {
+               switchPrefTab( $( this ).attr( 'href' ).replace( '#mw-prefsection-', '' ) );
+               e.preventDefault();
+       });
+}
 
 /**
 * Timezone functions.
@@ -68,7 +106,7 @@ var $localtimeHolder = $( '#wpLocalTime' );
 var servertime = parseInt( $( 'input[name=wpServerTime]' ).val(), 10 );
 var minuteDiff = 0;
 
-var minutesToHours = function( min ) {
+var minutesToHours = function ( min ) {
        var tzHour = Math.floor( Math.abs( min ) / 60 );
        var tzMin = Math.abs( min ) % 60;
        var tzString = ( ( min >= 0 ) ? '' : '-' ) + ( ( tzHour < 10 ) ? '0' : '' ) + tzHour +
@@ -76,7 +114,7 @@ var minutesToHours = function( min ) {
        return tzString;
 };
 
-var hoursToMinutes = function( hour ) {
+var hoursToMinutes = function ( hour ) {
        var arr = hour.split( ':' );
        arr[0] = parseInt( arr[0], 10 );
 
@@ -99,7 +137,7 @@ var hoursToMinutes = function( hour ) {
        }
 };
 
-var updateTimezoneSelection = function() {
+var updateTimezoneSelection = function () {
        var type = $tzSelect.val();
        if ( type == 'guess' ) {
                // Get browser timezone & fill it in
@@ -130,8 +168,8 @@ var updateTimezoneSelection = function() {
 };
 
 if ( $tzSelect.length && $tzTextbox.length ) {
-       $tzSelect.change( function() { updateTimezoneSelection(); } );
-       $tzTextbox.blur( function() { updateTimezoneSelection(); } );
+       $tzSelect.change( function () { updateTimezoneSelection(); } );
+       $tzTextbox.blur( function () { updateTimezoneSelection(); } );
        updateTimezoneSelection();
 }
 } );