/*!
- * Change multi-page image navigation so that the current page display can be changed
- * without a page reload. Currently, the only image formats that can be multi-page images are
- * PDF and DjVu files
+ * Implement AJAX navigation for multi-page images so the user may browse without a full page reload.
*/
( function ( mw, $ ) {
+ var jqXhr, $multipageimage, $spinner,
+ cache = {}, cacheOrder = [];
- // Initialize ajax request variable
- var xhr;
-
- // Use jQuery's load function to specifically select and replace table.multipageimage's child
- // tr with the new page's table.multipageimage's tr element.
- // table.multipageimage always has only one row.
- function loadPage( page, hist ) {
- if ( xhr ) {
- // Abort previous requests to prevent backlog created by
- // repeatedly pressing back/forwards buttons
- xhr.abort();
+ /* Fetch the next page, caching up to 10 last-loaded pages.
+ * @param {string} url
+ * @return {jQuery.Promise}
+ */
+ function fetchPageData( url ) {
+ if ( jqXhr && jqXhr.abort ) {
+ // Prevent race conditions and piling up pending requests
+ jqXhr.abort();
}
+ jqXhr = undefined;
- var $multipageimage = $( 'table.multipageimage' ),
- $spinner;
+ // Try the cache
+ if ( cache[url] ) {
+ // Update access freshness
+ cacheOrder.splice( $.inArray( url, cacheOrder ), 1 );
+ cacheOrder.push( url );
+ return $.Deferred().resolve( cache[url] ).promise();
+ }
+
+ // @todo Don't fetch the entire page. Ideally we'd only fetch the content portion or the data
+ // (thumbnail urls) and update the interface manually.
+ jqXhr = $.ajax( url ).then( function ( data ) {
+ return $( data ).find( 'table.multipageimage' ).contents();
+ } );
+
+ // Handle cache updates
+ jqXhr.done( function ( $contents ) {
+ jqXhr = undefined;
- // Add a new spinner if one doesn't already exist
- if ( !$multipageimage.find( '.mw-spinner' ).length ) {
+ // Cache the newly loaded page
+ cache[url] = $contents;
+ cacheOrder.push( url );
+
+ // Remove the oldest entry if we're over the limit
+ if ( cacheOrder.length > 10 ) {
+ delete cache[ cacheOrder[0] ];
+ cacheOrder = cacheOrder.slice( 1 );
+ }
+ } );
+ return jqXhr.promise();
+ }
+
+ /* Fetch the next page and use jQuery to swap the table.multipageimage contents.
+ * @param {string} url
+ * @param {boolean} [hist=false] Whether this is a load triggered by history navigation (if
+ * true, this function won't push a new history state, for the browser did so already).
+ */
+ function switchPage( url, hist ) {
+ var $tr, promise;
+
+ // Start fetching data (might be cached)
+ promise = fetchPageData( url );
+
+ // Add a new spinner if one doesn't already exist and the data is not already ready
+ if ( !$spinner && promise.state() !== 'resolved' ) {
+ $tr = $multipageimage.find( 'tr' );
$spinner = $.createSpinner( {
size: 'large',
type: 'block'
} )
- // Set the spinner's dimensions equal to the table's dimensions so that
- // the current scroll position is not lost after the table is emptied prior to
- // its contents being updated
+ // Copy the old content dimensions equal so that the current scroll position is not
+ // lost between emptying the table is and receiving the new contents.
.css( {
- height: $multipageimage.find( 'tr' ).height(),
- width: $multipageimage.find( 'tr' ).width()
+ height: $tr.outerHeight(),
+ width: $tr.outerWidth()
} );
$multipageimage.empty().append( $spinner );
}
- xhr = $.ajax( {
- url: page,
- success: function ( data ) {
- // Load the page
- $multipageimage.empty().append( $( data ).find( 'table.multipageimage tr' ) );
- // Fire hook because the page's content has changed
- mw.hook( 'wikipage.content' ).fire( $multipageimage );
- // Set up the new page for pagination
- ajaxifyPageNavigation();
- // Add new page of image to history. To preserve the back-forwards chain in the browser,
- // if the user gets here via the back/forward button, don't update the history.
- if ( window.history && history.pushState && !hist ) {
- history.pushState( { url: page }, document.title, page );
- }
+ promise.done( function ( $contents ) {
+ $spinner = undefined;
+
+ // Replace table contents
+ $multipageimage.empty().append( $contents.clone() );
+
+ bindPageNavigation( $multipageimage );
+
+ // Fire hook because the page's content has changed
+ mw.hook( 'wikipage.content' ).fire( $multipageimage );
+
+ // Update browser history and address bar. But not if we came here from a history
+ // event, in which case the url is already updated by the browser.
+ if ( history.pushState && !hist ) {
+ history.pushState( { tag: 'mw-pagination' }, document.title, url );
}
} );
}
- function ajaxifyPageNavigation() {
- // Intercept the default action of the links in the thumbnail navigation
- $( '.multipageimagenavbox' ).one( 'click', 'a', function ( e ) {
- loadPage( this.href );
+ function bindPageNavigation( $container ) {
+ $container.find( '.multipageimagenavbox' ).one( 'click', 'a', function ( e ) {
+ var page, uri;
+
+ // Generate the same URL on client side as the one generated in ImagePage::openShowImage.
+ // We avoid using the URL in the link directly since it could have been manipulated (bug 66608)
+ page = Number( mw.util.getParamValue( 'page', this.href ) );
+ uri = new mw.Uri( mw.util.wikiScript() )
+ .extend( { title: mw.config.get( 'wgPageName' ), page: page } )
+ .toString();
+
+ switchPage( uri );
e.preventDefault();
} );
- // Prevent the submission of the page select form and instead call loadPage
- $( 'form[name="pageselector"]' ).one( 'change submit', function ( e ) {
- loadPage( this.action + '?' + $( this ).serialize() );
+ $container.find( 'form[name="pageselector"]' ).one( 'change submit', function ( e ) {
+ switchPage( this.action + '?' + $( this ).serialize() );
e.preventDefault();
} );
}
- $( document ).ready( function () {
- // The presence of table.multipageimage signifies that this file is a multi-page image
- if ( mw.config.get( 'wgNamespaceNumber' ) === 6 && $( 'table.multipageimage' ).length !== 0 ) {
- ajaxifyPageNavigation();
-
- // Set up history.pushState (if available), so that when the user browses to a new page of
- // the same file, the browser's history is updated. If the user clicks the back/forward button
- // in the midst of navigating a file's pages, load the page inline.
- if ( window.history && history.pushState && history.replaceState ) {
- history.replaceState( { url: window.location.href }, '' );
- $( window ).on( 'popstate', function ( e ) {
- var state = e.originalEvent.state;
- if ( state ) {
- loadPage( state.url, true );
- }
- } );
- }
+ $( function () {
+ if ( mw.config.get( 'wgNamespaceNumber' ) !== 6 ) {
+ return;
+ }
+ $multipageimage = $( 'table.multipageimage' );
+ if ( !$multipageimage.length ) {
+ return;
+ }
+
+ bindPageNavigation( $multipageimage );
+
+ // Update the url using the History API (if available)
+ if ( history.pushState && history.replaceState ) {
+ history.replaceState( { tag: 'mw-pagination' }, '' );
+ $( window ).on( 'popstate', function ( e ) {
+ var state = e.originalEvent.state;
+ if ( state && state.tag === 'mw-pagination' ) {
+ switchPage( location.href, true );
+ }
+ } );
}
} );
}( mediaWiki, jQuery ) );