Fetch more revisions as the user moves back and forward

This changes the previous behaviour of fetching always up to
500 most recent revisions.

Now the extensions fetches N revisions including the newer
revision selected to diff as the most recent revision.
N is number of revisions that would fit in the current
window when rendered as bars.
When user is close to either "end" of the slider, extensions
fetches another batch of up to N older or newer revisions,
as long as user does not reach the oldest or the newest
revision of the page.

Among others, this removes the limitations of the previous
approach: showing only 500 revisions, and failing to show
anything when any of selected revisions was older than
500 recent revisions.

This change also simplifies usage of Api class.

Bug: T135005
Change-Id: Ib3f4a6ac57ff17008f9d8784c4716bd294443096
This commit is contained in:
Leszek Manicki 2016-06-27 16:00:13 +02:00
parent c7190cf97d
commit dc838bc87d
12 changed files with 623 additions and 242 deletions

View file

@ -46,7 +46,6 @@
"messages": [
"revisionslider-show-help",
"revisionslider-show-help-tooltip",
"revisionslider-loading-out-of-range",
"revisionslider-loading-failed"
],
"position": "top"

View file

@ -16,7 +16,6 @@
"revisionslider-minoredit": "This is a minor edit",
"revisionslider-loading-placeholder": "The RevisionSlider is loading...",
"revisionslider-loading-failed": "The RevisionSlider failed to load.",
"revisionslider-loading-out-of-range": "The RevisionSlider failed to load as the requested revisions are not in the top 500 versions of the page.",
"revisionslider-arrow-tooltip-newer": "See newer revisions",
"revisionslider-arrow-tooltip-older": "See older revisions",
"revisionslider-show-help": "?",

View file

@ -18,7 +18,6 @@
"revisionslider-minoredit": "Text labeling a minor edit.",
"revisionslider-loading-placeholder": "Message shown while the RevisionSlider is still loading on a diff page. Once loaded the message is removed.",
"revisionslider-loading-failed": "Message shown if the RevisionSlider fails to initially load.",
"revisionslider-loading-out-of-range": "Message shown if the RevisionSlider fails to initially load due to revisions being requested that are not in the most recent 500 revisions.",
"revisionslider-arrow-tooltip-newer": "Text shown after hovering the button scrolling to newer revisions.",
"revisionslider-arrow-tooltip-older": "Text shown after hovering the button scrolling to older revisions.",
"revisionslider-show-help": "A symbol shown in the \"Show help\" button.",

View file

@ -10,51 +10,140 @@
$.extend( Api.prototype, {
url: '',
/**
* Fetches a batch of revision data, including a gender setting for users who edited the revision
*
* @param {string} pageName
* @param {Object} options - Options containing callbacks for `success` and `error` as well as
* optional fields for: `dir (defaults to `older`), `limit` (defaults to 500), `startId`, `endId`,
* `knownUserGenders`
*/
fetchRevisionData: function ( pageName, options ) {
var self = this;
this.fetchRevisions( pageName, options )
.done( function ( data ) {
var revs = data.query.pages[ 0 ].revisions,
/*jshint -W024 */
revContinue = data.continue,
/*jshint +W024 */
genderData = typeof options.knownUserGenders !== 'undefined' ? options.knownUserGenders : {},
userNames;
if ( !revs ) {
return;
}
userNames = self.getUserNames( revs, genderData );
self.fetchUserGenderData( userNames )
.done( function ( data ) {
var users = typeof data !== 'undefined' ? data.query.users : [];
if ( users.length > 0 ) {
$.extend( genderData, self.getUserGenderData( users, genderData ) );
}
revs.forEach( function ( rev ) {
if ( typeof rev.user !== 'undefined' && typeof genderData[ rev.user ] !== 'undefined' ) {
rev.userGender = genderData[ rev.user ];
}
} );
options.success( { revisions: revs, 'continue': revContinue } );
} )
.fail( options.error );
} )
.fail( options.error );
},
/**
* Fetches up to 500 revisions at a time
*
* @param {Object} options - Options containing callbacks for `success` and `error` as well as fields for
* `pageName` and `startId`
* @param {string} pageName
* @param {Object} options object containing optional options, fields: `dir` (defaults to `older`),
* `limit` (defaults to 500), `startId`, `endId`
* @return {jQuery}
*/
fetchRevisions: function ( options ) {
$.ajax( {
url: this.url,
data: {
fetchRevisions: function ( pageName, options ) {
var dir = options.dir !== undefined ? options.dir : 'older',
data = {
action: 'query',
prop: 'revisions',
format: 'json',
rvprop: 'ids|timestamp|user|comment|parsedcomment|size|flags',
titles: options.pageName,
titles: pageName,
formatversion: 2,
rvstartid: options.startId,
'continue': '',
rvlimit: 500
},
success: options.success,
error: options.error
rvlimit: 500,
rvdir: dir
};
if ( options.startId !== undefined ) {
data.rvstartid = options.startId;
}
if ( options.endId !== undefined ) {
data.rvendid = options.endId;
}
if ( options.limit !== undefined && options.limit <= 500 ) {
data.rvlimit = options.limit;
}
return $.ajax( {
url: this.url,
data: data
} );
},
/**
* Fetches gender data for up to 500 user names
*
* @param {Object} options - Options containing callbacks for `success` and `error` as well as list
* of user names in `users`
* @param {string[]} users
* @return {jQuery}
*/
fetchUserGenderData: function ( options ) {
$.ajax( {
fetchUserGenderData: function ( users ) {
if ( users.length === 0 ) {
return $.Deferred().resolve();
}
return $.ajax( {
url: this.url,
data: {
action: 'query',
list: 'users',
format: 'json',
usprop: 'gender',
ususers: options.users.join( '|' ),
ususers: users.join( '|' ),
uslimit: 500
},
success: options.success,
error: options.error
}
} );
},
/**
* @param {Array} revs
* @param {Object} knownUserGenders
* @return {string[]}
*/
getUserNames: function ( revs, knownUserGenders ) {
var allUsers = revs.map( function ( rev ) {
return typeof rev.user !== 'undefined' ? rev.user : '';
} );
return allUsers.filter( function ( value, index, array ) {
return value !== '' && typeof knownUserGenders[ value ] === 'undefined' && array.indexOf( value ) === index;
} );
},
/**
* @param {Array} data
* @return {Object}
*/
getUserGenderData: function ( data ) {
var genderData = {},
usersWithGender = data.filter( function ( item ) {
return typeof item.gender !== 'undefined' && item.gender !== 'unknown';
} );
usersWithGender.forEach( function ( item ) {
genderData[ item.name ] = item.gender;
} );
return genderData;
}
} );

View file

@ -19,6 +19,9 @@
}
if ( typeof data.user !== 'undefined' ) {
this.user = data.user;
if ( typeof data.userGender !== 'undefined' ) {
this.userGender = data.userGender;
}
}
};
@ -137,13 +140,6 @@
return this.user;
},
/**
* @param {string} gender
*/
setUserGender: function ( gender ) {
this.userGender = gender;
},
/**
* @return {string}
*/

View file

@ -64,34 +64,74 @@
return this.revisions.length;
},
/**
* @return {string[]}
*/
getUserNames: function () {
var allUsers = this.revisions.map( function ( revision ) {
return revision.getUser();
} );
return allUsers.filter( function ( value, index, array ) {
return value !== '' && array.indexOf( value ) === index;
} );
},
/**
* @param {Object} userGenderData
*/
setUserGenders: function ( userGenderData ) {
this.revisions.forEach( function ( revision ) {
if ( revision.getUser() !== '' && typeof userGenderData[ revision.getUser() ] !== 'undefined' ) {
revision.setUserGender( userGenderData[ revision.getUser() ] );
}
} );
},
/**
* @return {RevisionListView}
*/
getView: function () {
return this.view;
},
getUserGenders: function () {
var userGenders = {};
this.revisions.forEach( function ( revision ) {
if ( revision.getUser() ) {
userGenders[ revision.getUser() ] = revision.getUserGender();
}
} );
return userGenders;
},
/**
* Adds revisions to the end of the list.
*
* @param {Revision[]} revs
*/
push: function ( revs ) {
var i, rev;
for ( i = 0; i < revs.length; i++ ) {
rev = revs[ i ];
rev.setRelativeSize(
i > 0 ?
rev.getSize() - revs[ i - 1 ].getSize() :
rev.getSize() - this.revisions[ this.revisions.length - 1 ].getSize()
);
this.revisions.push( rev );
}
},
/**
* Adds revisions to the beginning of the list.
*
* @param {Revision[]} revs
* @param {number} sizeBefore optional size of the revision preceding the first of revs, defaults to 0
*/
unshift: function ( revs, sizeBefore ) {
var originalFirstRev = this.revisions[ 0 ],
i, rev;
sizeBefore = sizeBefore || 0;
originalFirstRev.setRelativeSize( originalFirstRev.getSize() - revs[ revs.length - 1 ].getSize() );
for ( i = revs.length - 1; i >= 0; i-- ) {
rev = revs[ i ];
rev.setRelativeSize( i > 0 ? rev.getSize() - revs[ i - 1 ].getSize() : rev.getSize() - sizeBefore );
this.revisions.unshift( rev );
}
},
/**
* Returns a subset of the list.
*
* @param {number} begin
* @param {number} end
* @return {RevisionList}
*/
slice: function ( begin, end ) {
var slicedList = new mw.libs.revisionSlider.RevisionList( [] );
slicedList.view = new mw.libs.revisionSlider.RevisionListView( slicedList );
slicedList.revisions = this.revisions.slice( begin, end );
return slicedList;
}
} );
@ -99,7 +139,7 @@
mw.libs.revisionSlider.RevisionList = RevisionList;
/**
* Transforms an array of revision data returned by MediaWiki API into
* Transforms an array of revision data returned by MediaWiki API (including user gender information) into
* an array of Revision objects
*
* @param {Array} revs

View file

@ -25,9 +25,10 @@
/**
* @param {number} revisionTickWidth
* @param {number} positionOffset
* @return {jQuery}
*/
render: function ( revisionTickWidth ) {
render: function ( revisionTickWidth, positionOffset ) {
var $html = $( '<div>' ).addClass( 'mw-revslider-revisions' ),
revs = this.revisionList.getRevisions(),
maxChangeSizeLogged = Math.log( this.revisionList.getBiggestChangeSize() ),
@ -41,6 +42,8 @@
self.hideTooltip( $( this ) );
};
positionOffset = positionOffset || 0;
for ( i = 0; i < revs.length; i++ ) {
diffSize = revs[ i ].getRelativeSize();
relativeChangeSize = diffSize !== 0 ? Math.ceil( 65.0 * Math.log( Math.abs( diffSize ) ) / maxChangeSizeLogged ) + 5 : 0;
@ -60,7 +63,7 @@
.append( $( '<div>' )
.addClass( 'mw-revslider-revision' )
.attr( 'data-revid', revs[ i ].getId() )
.attr( 'data-pos', i + 1 )
.attr( 'data-pos', positionOffset + i + 1 )
.css( {
height: relativeChangeSize + 'px',
width: revisionTickWidth + 'px',
@ -79,6 +82,23 @@
return $html;
},
/**
* @param {jQuery} $renderedList
*/
adjustRevisionSizes: function ( $renderedList ) {
var revs = this.revisionList.getRevisions(),
maxChangeSizeLogged = Math.log( this.revisionList.getBiggestChangeSize() ),
i, diffSize, relativeChangeSize;
for ( i = 0; i < revs.length; i++ ) {
diffSize = revs[ i ].getRelativeSize();
relativeChangeSize = diffSize !== 0 ? Math.ceil( 65.0 * Math.log( Math.abs( diffSize ) ) / maxChangeSizeLogged ) + 5 : 0;
$renderedList.find( '.mw-revslider-revision[data-pos="' + ( i + 1 ) + '"]' ).css( {
height: relativeChangeSize + 'px',
top: diffSize > 0 ? '-' + relativeChangeSize + 'px' : 0
} );
}
},
/**
* Hides the current tooltip immediately
*/

View file

@ -51,6 +51,16 @@
*/
rtlScrollLeftType: 'default',
/**
* @type {boolean}
*/
noMoreNewerRevisions: false,
/**
* @type {boolean}
*/
noMoreOlderRevisions: false,
render: function ( $container ) {
var containerWidth = this.calculateSliderContainerWidth(),
pointerContainerPosition = 55,
@ -197,7 +207,7 @@
$( '.mw-revslider-revision-wrapper' ).removeClass( 'mw-revslider-pointer-cursor' );
},
drag: function ( event, ui ) {
var newestVisibleRevisionLeftPos = containerWidth - self.revisionWidth;
var newestVisibleRevisionLeftPos = $( '.mw-revslider-revisions-container' ).width() - self.revisionWidth;
ui.position.left = Math.min( ui.position.left, newestVisibleRevisionLeftPos );
if ( $( this ).css( 'direction' ) === 'ltr' ) {
self.resetPointerColorsBasedOnValues(
@ -213,31 +223,7 @@
}
} );
$slider.find( '.mw-revslider-revision-wrapper' ).click( function ( e ) {
var $revWrap = $( this ),
$clickedRev = $revWrap.find( '.mw-revslider-revision' ),
hasClickedTop = e.pageY - $revWrap.offset().top < $revWrap.height() / 2,
pOld = self.getOldRevPointer(),
pNew = self.getNewRevPointer();
if ( hasClickedTop ) {
self.refreshRevisions(
self.getRevElementAtPosition( $revisions, pOld.getPosition() ).data( 'revid' ),
$clickedRev.data( 'revid' )
);
pNew.setPosition( $clickedRev.data( 'pos' ) );
} else {
self.refreshRevisions(
$clickedRev.data( 'revid' ),
self.getRevElementAtPosition( $revisions, pNew.getPosition() ).data( 'revid' )
);
pOld.setPosition( $clickedRev.data( 'pos' ) );
}
self.resetPointerColorsBasedOnValues( self.pointerOlder.getPosition(), self.pointerNewer.getPosition() );
self.resetRevisionStylesBasedOnPointerPosition( $revisions );
self.alignPointers();
} );
$slider.find( '.mw-revslider-revision-wrapper' ).on( 'click', null, { view: self, revisionsDom: $revisions }, this.revisionWrapperClickHandler );
this.slider.setRevisionsPerWindow( $slider.find( '.mw-revslider-revisions-container' ).width() / this.revisionWidth );
@ -252,6 +238,34 @@
this.diffPage.initOnPopState( this );
},
revisionWrapperClickHandler: function ( e ) {
var $revWrap = $( this ),
view = e.data.view,
$revisions = e.data.revisionsDom,
$clickedRev = $revWrap.find( '.mw-revslider-revision' ),
hasClickedTop = e.pageY - $revWrap.offset().top < $revWrap.height() / 2,
pOld = view.getOldRevPointer(),
pNew = view.getNewRevPointer();
if ( hasClickedTop ) {
view.refreshRevisions(
view.getRevElementAtPosition( $revisions, pOld.getPosition() ).data( 'revid' ),
$clickedRev.data( 'revid' )
);
pNew.setPosition( parseInt( $clickedRev.attr( 'data-pos' ), 10 ) );
} else {
view.refreshRevisions(
$clickedRev.data( 'revid' ),
view.getRevElementAtPosition( $revisions, pNew.getPosition() ).data( 'revid' )
);
pOld.setPosition( parseInt( $clickedRev.attr( 'data-pos' ), 10 ) ) ;
}
view.resetPointerColorsBasedOnValues( view.pointerOlder.getPosition(), view.pointerNewer.getPosition() );
view.resetRevisionStylesBasedOnPointerPosition( $revisions );
view.alignPointers();
},
/**
* Returns the pointer that points to the older revision
*
@ -319,11 +333,15 @@
* @param {jQuery} $newRevElement
*/
initializePointers: function ( $oldRevElement, $newRevElement ) {
if ( $oldRevElement.length === 0 || $newRevElement.length === 0 ) {
if ( $oldRevElement.length === 0 && $newRevElement.length === 0 ) {
// Note: this is currently caught in init.js
throw 'RS-rev-out-of-range';
throw 'RS-revs-not-specified';
}
if ( $oldRevElement.length !== 0 ) {
this.pointerOlder.setPosition( $oldRevElement.data( 'pos' ) );
} else {
this.pointerOlder.setPosition( -1 );
}
this.pointerOlder.setPosition( $oldRevElement.data( 'pos' ) );
this.pointerNewer.setPosition( $newRevElement.data( 'pos' ) );
this.resetPointerStylesBasedOnPosition();
},
@ -376,20 +394,14 @@
}
},
/**
* Determines how many revisions fit onto the screen at once depending on the browser window width
*
* @return {number}
*/
calculateRevisionsPerWindow: function () {
return Math.floor( ( $( '#mw-content-text' ).width() - this.containerMargin ) / this.revisionWidth );
},
/**
* @return {number}
*/
calculateSliderContainerWidth: function () {
return Math.min( this.slider.getRevisions().getLength(), this.calculateRevisionsPerWindow() ) * this.revisionWidth;
return Math.min(
this.slider.getRevisions().getLength(),
mw.libs.revisionSlider.calculateRevisionsPerWindow( this.containerMargin, this.revisionWidth )
) * this.revisionWidth;
},
slide: function ( direction, duration ) {
@ -424,6 +436,13 @@
function () {
self.pointerOlder.getView().getElement().draggable( 'enable' );
self.pointerNewer.getView().getElement().draggable( 'enable' );
if ( self.slider.isAtStart() && !self.noMoreOlderRevisions ) {
self.addOlderRevisionsIfNeeded( $( '.mw-revslider-revision-slider' ) );
}
if ( self.slider.isAtEnd() && !self.noMoreNewerRevisions ) {
self.addNewerRevisionsIfNeeded( $( '.mw-revslider-revision-slider' ) );
}
}
);
@ -496,9 +515,236 @@
*/
whichPointer: function ( $e ) {
return $e.attr( 'id' ) === 'mw-revslider-pointer-older' ? this.pointerOlder : this.pointerNewer;
},
/**
* @param {jQuery} $slider
*/
addNewerRevisionsIfNeeded: function ( $slider ) {
var api = new mw.libs.revisionSlider.Api( mw.util.wikiScript( 'api' ) ),
self = this,
revisions = this.slider.getRevisions().getRevisions(),
revisionCount = mw.libs.revisionSlider.calculateRevisionsPerWindow( this.containerMargin, this.revisionWidth ),
revs;
if ( this.noMoreNewerRevisions || !this.slider.isAtEnd() ) {
return;
}
api.fetchRevisionData( mw.config.get( 'wgPageName' ), {
startId: revisions[ revisions.length - 1 ].getId(),
dir: 'newer',
limit: revisionCount + 1,
knownUserGenders: this.slider.getRevisions().getUserGenders(),
success: function ( data ) {
revs = data.revisions.slice( 1 );
if ( revs.length === 0 ) {
self.noMoreNewerRevisions = true;
return;
}
self.addRevisionsAtEnd( $slider, revs );
/*jshint -W024 */
if ( data.continue === undefined ) {
self.noMoreNewerRevisions = true;
}
/*jshint +W024 */
}
} );
},
/**
* @param {jQuery} $slider
*/
addOlderRevisionsIfNeeded: function ( $slider ) {
var api = new mw.libs.revisionSlider.Api( mw.util.wikiScript( 'api' ) ),
self = this,
revisions = this.slider.getRevisions().getRevisions(),
revisionCount = mw.libs.revisionSlider.calculateRevisionsPerWindow( this.containerMargin, this.revisionWidth ),
revs,
precedingRevisionSize = 0;
if ( this.noMoreOlderRevisions || !this.slider.isAtStart() ) {
return;
}
api.fetchRevisionData( mw.config.get( 'wgPageName' ), {
startId: revisions[ 0 ].getId(),
dir: 'older',
// fetch an extra revision if there are more older revision than the current "window",
// this makes it possible to correctly set a size of the bar related to the oldest revision to add
limit: revisionCount + 2,
knownUserGenders: this.slider.getRevisions().getUserGenders(),
success: function ( data ) {
revs = data.revisions.slice( 1 ).reverse();
if ( revs.length === 0 ) {
self.noMoreOlderRevisions = true;
return;
}
if ( revs.length === revisionCount + 1 ) {
precedingRevisionSize = revs[ 0 ].size;
revs = revs.slice( 1 );
}
self.addRevisionsAtStart( $slider, revs, precedingRevisionSize );
/*jshint -W024 */
if ( data.continue === undefined ) {
self.noMoreOlderRevisions = true;
}
/*jshint +W024 */
}
} );
},
/**
* @param {jQuery} $slider
* @param {Array} revs
*/
addRevisionsAtEnd: function ( $slider, revs ) {
var revPositionOffset = this.slider.getRevisions().getLength(),
$revisions = $slider.find( '.mw-revslider-revisions-container .mw-revslider-revisions' ),
revisionsToRender,
$addedRevisions;
this.slider.getRevisions().push( mw.libs.revisionSlider.makeRevisions( revs ) );
// Pushed revisions have their relative sizes set correctly with regard to the last previously
// loaded revision. This should be taken into account when rendering newly loaded revisions (tooltip)
revisionsToRender = this.slider.getRevisions().slice( revPositionOffset );
$addedRevisions = new mw.libs.revisionSlider.RevisionListView( revisionsToRender ).render( this.revisionWidth, revPositionOffset );
this.addClickHandlerToRevisions( $addedRevisions, $revisions, this.revisionWrapperClickHandler );
$addedRevisions.find( '.mw-revslider-revision-wrapper' ).each( function () {
$revisions.append( $( this ) );
} );
if ( this.shouldExpandSlider( $slider ) ) {
this.expandSlider( $slider );
}
this.slider.getRevisions().getView().adjustRevisionSizes( $slider );
if ( !this.slider.isAtEnd() ) {
$slider.find( '.mw-revslider-arrow-forwards' ).removeClass( 'mw-revslider-arrow-disabled' ).addClass( 'mw-revslider-arrow-enabled' );
}
},
/**
* @param {jQuery} $slider
* @param {Array} revs
* @param {number} precedingRevisionSize optional size of the revision preceding the first of revs,
* used to correctly determine first revision's relative size
*/
addRevisionsAtStart: function ( $slider, revs, precedingRevisionSize ) {
var self = this,
$revisions = $slider.find( '.mw-revslider-revisions-container .mw-revslider-revisions' ),
$revisionContainer = $slider.find( '.mw-revslider-revisions-container' ),
revisionsToRender,
$addedRevisions,
pOld, pNew,
revisionStyleResetRequired = false,
$oldRevElement,
scrollLeft;
this.slider.getRevisions().unshift( mw.libs.revisionSlider.makeRevisions( revs ), precedingRevisionSize );
$slider.find( '.mw-revslider-revision' ).each( function () {
$( this ).attr( 'data-pos', parseInt( $( this ).attr( 'data-pos' ), 10 ) + revs.length );
} );
// Pushed (unshifted) revisions have their relative sizes set correctly with regard to the last previously
// loaded revision. This should be taken into account when rendering newly loaded revisions (tooltip)
revisionsToRender = this.slider.getRevisions().slice( 0, revs.length );
$addedRevisions = new mw.libs.revisionSlider.RevisionListView( revisionsToRender ).render( this.revisionWidth );
pOld = this.getOldRevPointer();
pNew = this.getNewRevPointer();
if ( pOld.getPosition() !== -1 ) {
pOld.setPosition( pOld.getPosition() + revisionsToRender.getLength() );
} else {
// Special case: old revision has been previously not loaded, need to initialize correct position
$oldRevElement = this.getOldRevElement( $addedRevisions );
if ( $oldRevElement.length !== 0 ) {
pOld.setPosition( $oldRevElement.data( 'pos' ) );
revisionStyleResetRequired = true;
}
}
pNew.setPosition( pNew.getPosition() + revisionsToRender.getLength() );
this.addClickHandlerToRevisions( $addedRevisions, $revisions, this.revisionWrapperClickHandler );
$( $addedRevisions.find( '.mw-revslider-revision-wrapper' ).get().reverse() ).each( function () { // TODO: this is horrible
$revisions.prepend( $( this ) );
} );
if ( revisionStyleResetRequired ) {
this.resetRevisionStylesBasedOnPointerPosition( $slider );
}
this.slider.setFirstVisibleRevisionIndex( this.slider.getFirstVisibleRevisionIndex() + revisionsToRender.getLength() );
scrollLeft = this.slider.getFirstVisibleRevisionIndex() * this.revisionWidth;
$revisionContainer.scrollLeft( scrollLeft );
if ( this.$element.css( 'direction' ) === 'rtl' ) {
$revisionContainer.scrollLeft( self.getRtlScrollLeft( $revisionContainer, scrollLeft ) );
}
this.slider.getRevisions().getView().adjustRevisionSizes( $slider );
$slider.find( '.mw-revslider-arrow-backwards' ).removeClass( 'mw-revslider-arrow-disabled' ).addClass( 'mw-revslider-arrow-enabled' );
},
/**
* @param {jQuery} $revisions
* @param {jQuery} $allRevisions
* @param {Function} clickHandler
*/
addClickHandlerToRevisions: function ( $revisions, $allRevisions, clickHandler ) {
var self = this;
$revisions.find( '.mw-revslider-revision-wrapper' ).on(
'click',
null,
{ view: self, revisionsDom: $allRevisions },
clickHandler
);
},
/**
* @param {jQuery} $slider
*/
shouldExpandSlider: function ( $slider ) {
var sliderWidth = parseInt( $slider.css( 'width' ), 10 ),
maxAvailableWidth = this.calculateSliderContainerWidth() + this.containerMargin;
return !this.noMoreNewerRevisions && sliderWidth < maxAvailableWidth;
},
/**
* @param {jQuery} $slider
*/
expandSlider: function ( $slider ) {
var containerWidth = this.calculateSliderContainerWidth();
$slider.css( { width: ( containerWidth + this.containerMargin ) + 'px' } );
$slider.find( '.mw-revslider-revisions-container' ).css( { width: containerWidth + 'px' } );
$slider.find( '.mw-revslider-pointer-container' ).css( { width: containerWidth + this.revisionWidth - 1 + 'px' } );
if ( $slider.css( 'direction' ) === 'rtl' ) {
this.alignPointers( 0 );
}
this.slider.setRevisionsPerWindow( $slider.find( '.mw-revslider-revisions-container' ).width() / this.revisionWidth );
}
} );
mw.libs.revisionSlider = mw.libs.revisionSlider || {};
mw.libs.revisionSlider.SliderView = SliderView;
mw.libs.revisionSlider.calculateRevisionsPerWindow = function ( containerMargin, revisionWidth ) {
return Math.floor( ( $( '#mw-content-text' ).width() - containerMargin ) / revisionWidth );
};
}( mediaWiki, jQuery ) );

View file

@ -1,27 +1,12 @@
( function ( mw, $ ) {
var api = new mw.libs.revisionSlider.Api( mw.util.wikiScript( 'api' ) );
/**
* @param {Array} data
* @return {Object}
*/
function getUserGenderData( data ) {
var genderData = {},
usersWithGender = data.filter( function ( item ) {
return typeof item.gender !== 'undefined' && item.gender !== 'unknown';
} );
usersWithGender.forEach( function ( item ) {
genderData[ item.name ] = item.gender;
} );
return genderData;
}
mw.track( 'counter.MediaWiki.RevisionSlider.event.init' );
mw.libs.revisionSlider.userOffset = mw.user.options.values.timecorrection ? mw.user.options.values.timecorrection.split( '|' )[ 1 ] : mw.config.values.extRevisionSliderTimeOffset;
api.fetchRevisions( {
pageName: mw.config.get( 'wgPageName' ),
startId: mw.config.get( 'wgCurRevisionId' ),
api.fetchRevisionData( mw.config.get( 'wgPageName' ), {
startId: mw.config.values.extRevisionSliderNewRev,
limit: mw.libs.revisionSlider.calculateRevisionsPerWindow( 120, 16 ),
success: function ( data ) {
var revs,
@ -30,69 +15,42 @@
slider;
try {
revs = data.query.pages[ 0 ].revisions;
if ( !revs ) {
return;
}
revs = data.revisions;
revs.reverse();
revisionList = new mw.libs.revisionSlider.RevisionList( mw.libs.revisionSlider.makeRevisions( revs ) );
api.fetchUserGenderData( {
users: revisionList.getUserNames(),
success: function ( data ) {
var users = data.query.users;
$container = $( '#mw-revslider-container' );
slider = new mw.libs.revisionSlider.Slider( revisionList );
slider.getView().render( $container );
if ( users ) {
revisionList.setUserGenders( getUserGenderData( users ) );
}
$container = $( '#mw-revslider-container' );
slider = new mw.libs.revisionSlider.Slider( revisionList );
slider.getView().render( $container );
if ( !mw.user.options.get( 'userjs-revslider-hidehelp' ) ) {
mw.libs.revisionSlider.HelpDialog.show();
( new mw.Api() ).saveOption( 'userjs-revslider-hidehelp', true );
}
$container.append(
$( '<button>' )
.click( function () {
mw.libs.revisionSlider.HelpDialog.show();
} )
.text( mw.message( 'revisionslider-show-help' ).text() )
.addClass( 'mw-revslider-show-help' )
.tipsy( {
gravity: $( 'body' ).hasClass( 'ltr' ) ? 'se' : 'sw',
offset: 15,
title: function () {
return mw.msg( 'revisionslider-show-help-tooltip' );
}
} )
);
$( '#mw-revslider-placeholder' ).remove();
},
error: function ( err ) {
$( '#mw-revslider-placeholder' )
.text( mw.message( 'revisionslider-loading-failed' ).text() );
console.log( err );
mw.track( 'counter.MediaWiki.RevisionSlider.error.init.genders' );
}
} );
} catch ( err ) {
if ( err === 'RS-rev-out-of-range' ) {
$( '#mw-revslider-placeholder' )
.text( mw.message( 'revisionslider-loading-out-of-range' ).text() );
console.log( err );
mw.track( 'counter.MediaWiki.RevisionSlider.error.outOfRange' );
} else {
$( '#mw-revslider-placeholder' )
.text( mw.message( 'revisionslider-loading-failed' ).text() );
console.log( err );
mw.track( 'counter.MediaWiki.RevisionSlider.error.init' );
if ( !mw.user.options.get( 'userjs-revslider-hidehelp' ) ) {
mw.libs.revisionSlider.HelpDialog.show();
( new mw.Api() ).saveOption( 'userjs-revslider-hidehelp', true );
}
$container.append(
$( '<button>' )
.click( function () {
mw.libs.revisionSlider.HelpDialog.show();
} )
.text( mw.message( 'revisionslider-show-help' ).text() )
.addClass( 'mw-revslider-show-help' )
.tipsy( {
gravity: $( 'body' ).hasClass( 'ltr' ) ? 'se' : 'sw',
offset: 15,
title: function () {
return mw.msg( 'revisionslider-show-help-tooltip' );
}
} )
);
$( '#mw-revslider-placeholder' ).remove();
} catch ( err ) {
$( '#mw-revslider-placeholder' )
.text( mw.message( 'revisionslider-loading-failed' ).text() );
console.log( err );
mw.track( 'counter.MediaWiki.RevisionSlider.error.init' );
}
},
@ -103,4 +61,5 @@
mw.track( 'counter.MediaWiki.RevisionSlider.error.init' );
}
} );
}( mediaWiki, jQuery ) );

View file

@ -9,7 +9,8 @@
comment: 'hello',
parsedcomment: '<b>hello</b>',
timestamp: '2016-04-26T10:27:14Z', // 10:27, 26 Apr 2016
user: 'meh'
user: 'meh',
userGender: 'female'
},
rev = new Revision( data );
@ -19,7 +20,7 @@
assert.equal( rev.getComment(), data.comment );
assert.equal( rev.getParsedComment(), data.parsedcomment );
assert.equal( rev.getUser(), data.user );
assert.equal( rev.getUserGender(), '' );
assert.equal( rev.getUserGender(), 'female' );
assert.equal( rev.isMinor(), false );
if ( mw.config.get( 'wgUserLanguage' ) === 'en' ) {
@ -106,15 +107,5 @@
assert.notOk( rev.hasEmptyComment() );
} );
QUnit.test( 'setUserGender adjusts a gender', function ( assert ) {
var rev = new Revision( { user: 'Foo' } );
assert.equal( rev.getUserGender(), '' );
rev.setUserGender( 'female' );
assert.equal( rev.getUserGender(), 'female' );
} );
} )( mediaWiki );

View file

@ -27,66 +27,129 @@
assert.equal( revs.getRevisions()[ 2 ].getRelativeSize(), -8 );
} );
QUnit.test( 'getUserNames returns a list of unique names', function ( assert ) {
QUnit.test( 'getUserGenders', function ( assert ) {
var revs = new RevisionList( [
new Revision( { revid: 1, user: 'User1' } ),
new Revision( { revid: 1, user: 'User1', userGender: 'female' } ),
new Revision( { revid: 2, user: 'User2' } ),
new Revision( { revid: 3, user: 'User1' } )
new Revision( { revid: 3, user: 'User3', userGender: 'male' } )
] );
assert.deepEqual( revs.getUserGenders(), { User1: 'female', User2: '', User3: 'male' } );
} );
QUnit.test( 'Push appends revisions to the end of the list', function ( assert ) {
var list = new RevisionList( [
new Revision( { revid: 1, size: 5 } ),
new Revision( { revid: 2, size: 21 } ),
new Revision( { revid: 3, size: 13 } )
] ),
userNames = revs.getUserNames();
revisions;
list.push( [
new Revision( { revid: 6, size: 19 } ),
new Revision( { revid: 8, size: 25 } )
] );
assert.deepEqual( userNames, [ 'User1', 'User2' ] );
revisions = list.getRevisions();
assert.equal( list.getLength(), 5 );
assert.equal( revisions[ 0 ].getId(), 1 );
assert.equal( revisions[ 0 ].getRelativeSize(), 5 );
assert.equal( revisions[ 1 ].getId(), 2 );
assert.equal( revisions[ 1 ].getRelativeSize(), 16 );
assert.equal( revisions[ 2 ].getId(), 3 );
assert.equal( revisions[ 2 ].getRelativeSize(), -8 );
assert.equal( revisions[ 3 ].getId(), 6 );
assert.equal( revisions[ 3 ].getRelativeSize(), 6 );
assert.equal( revisions[ 4 ].getId(), 8 );
assert.equal( revisions[ 4 ].getRelativeSize(), 6 );
} );
QUnit.test( 'getUserNames skips revisions without user specified', function ( assert ) {
var revs = new RevisionList( [
new Revision( { revid: 1, user: 'User1' } ),
new Revision( { revid: 2 } )
QUnit.test( 'Unshift prepends revisions to the beginning of the list', function ( assert ) {
var list = new RevisionList( [
new Revision( { revid: 5, size: 5 } ),
new Revision( { revid: 6, size: 21 } ),
new Revision( { revid: 7, size: 13 } )
] ),
userNames = revs.getUserNames();
revisions;
list.unshift( [
new Revision( { revid: 2, size: 19 } ),
new Revision( { revid: 4, size: 25 } )
] );
assert.deepEqual( userNames, [ 'User1' ] );
revisions = list.getRevisions();
assert.equal( list.getLength(), 5 );
assert.equal( revisions[ 0 ].getId(), 2 );
assert.equal( revisions[ 0 ].getRelativeSize(), 19 );
assert.equal( revisions[ 1 ].getId(), 4 );
assert.equal( revisions[ 1 ].getRelativeSize(), 6 );
assert.equal( revisions[ 2 ].getId(), 5 );
assert.equal( revisions[ 2 ].getRelativeSize(), -20 );
assert.equal( revisions[ 3 ].getId(), 6 );
assert.equal( revisions[ 3 ].getRelativeSize(), 16 );
assert.equal( revisions[ 4 ].getId(), 7 );
assert.equal( revisions[ 4 ].getRelativeSize(), -8 );
} );
QUnit.test( 'setUserGenders adjusts revision data', function ( assert ) {
var revs = new RevisionList( [
new Revision( { revid: 1, user: 'User1' } ),
new Revision( { revid: 2, user: 'User2' } ),
new Revision( { revid: 3, user: 'User3' } )
QUnit.test( 'Unshift considers the size of the preceding revision if specified', function ( assert ) {
var list = new RevisionList( [
new Revision( { revid: 5, size: 5 } ),
new Revision( { revid: 6, size: 21 } ),
new Revision( { revid: 7, size: 13 } )
] ),
genders = { User1: 'female', User2: 'male', User3: 'unknown' };
revisions;
list.unshift(
[
new Revision( { revid: 2, size: 19 } ),
new Revision( { revid: 4, size: 25 } )
],
12
);
assert.equal( revs.getRevisions()[ 0 ].getUserGender(), '' );
assert.equal( revs.getRevisions()[ 1 ].getUserGender(), '' );
assert.equal( revs.getRevisions()[ 2 ].getUserGender(), '' );
revs.setUserGenders( genders );
assert.equal( revs.getRevisions()[ 0 ].getUserGender(), 'female' );
assert.equal( revs.getRevisions()[ 1 ].getUserGender(), 'male' );
assert.equal( revs.getRevisions()[ 2 ].getUserGender(), 'unknown' );
revisions = list.getRevisions();
assert.equal( list.getLength(), 5 );
assert.equal( revisions[ 0 ].getId(), 2 );
assert.equal( revisions[ 0 ].getRelativeSize(), 7 );
} );
QUnit.test( 'setUserGenders no gender for a user', function ( assert ) {
var revs = new RevisionList( [
new Revision( { revid: 1, user: 'User1' } ),
new Revision( { revid: 2, user: 'User2' } )
QUnit.test( 'Slice returns a subset of the list', function ( assert ) {
var list = new RevisionList( [
new Revision( { revid: 1, size: 5 } ),
new Revision( { revid: 2, size: 21 } ),
new Revision( { revid: 3, size: 13 } ),
new Revision( { revid: 6, size: 19 } ),
new Revision( { revid: 8, size: 25 } )
] ),
slicedList = list.slice( 1, 3 ),
revisions = slicedList.getRevisions();
assert.equal( slicedList.getLength(), 2 );
assert.equal( revisions[ 0 ].getId(), 2 );
assert.equal( revisions[ 0 ].getRelativeSize(), 16 );
assert.equal( revisions[ 1 ].getId(), 3 );
assert.equal( revisions[ 1 ].getRelativeSize(), -8 );
} );
QUnit.test( 'Slice returns a subset of the list, end param omitted', function ( assert ) {
var list = new RevisionList( [
new Revision( { revid: 1, size: 5 } ),
new Revision( { revid: 2, size: 21 } ),
new Revision( { revid: 3, size: 13 } ),
new Revision( { revid: 6, size: 19 } ),
new Revision( { revid: 8, size: 25 } )
] ),
genders = { User1: 'female' };
slicedList = list.slice( 1 ),
revisions = slicedList.getRevisions();
assert.equal( revs.getRevisions()[ 0 ].getUserGender(), '' );
assert.equal( revs.getRevisions()[ 1 ].getUserGender(), '' );
revs.setUserGenders( genders );
assert.equal( revs.getRevisions()[ 0 ].getUserGender(), 'female' );
assert.equal( revs.getRevisions()[ 1 ].getUserGender(), '' );
assert.equal( slicedList.getLength(), 4 );
assert.equal( revisions[ 0 ].getId(), 2 );
assert.equal( revisions[ 1 ].getId(), 3 );
assert.equal( revisions[ 2 ].getId(), 6 );
assert.equal( revisions[ 3 ].getId(), 8 );
} );
QUnit.test( 'makeRevisions converts revision data into list of Revision objects', function ( assert ) {
var revs = [
{ revid: 1, size: 5 },
{ revid: 2, size: 21 },
{ revid: 1, size: 5, userGender: 'female' },
{ revid: 2, size: 21, userGender: 'unknown' },
{ revid: 3, size: 13 }
],
revisions = makeRevisions( revs );

View file

@ -40,26 +40,6 @@
assert.equal( $revisionNew.attr( 'data-revid' ), 37 );
} );
QUnit.test( 'render throws an exception when selected revision not in available range', function ( assert ) {
var $container = $( '<div>' ),
view = new SliderView( new Slider( new RevisionList( [
new Revision( { revid: 3, size: 21, comment: 'Comment2', user: 'User2' } ),
new Revision( { revid: 37, size: 13, comment: 'Comment3', user: 'User3' } )
] ) ) );
mw.config.values.extRevisionSliderOldRev = 1;
mw.config.values.extRevisionSliderNewRev = 37;
assert.throws(
function () {
view.render( $container );
},
function ( e ) {
return e === 'RS-rev-out-of-range';
}
);
} );
QUnit.test( 'render throws an exception when no selected revisions provided', function ( assert ) {
var $container = $( '<div>' ),
view = new SliderView( new Slider( new RevisionList( [