mediawiki-extensions-Revisi.../modules/ext.RevisionSlider.RevisionListView.js
WMDE-Fisch a96b079e8f Unify highlighting CSS classes
Change-Id: Iceb094075d1fb1bd4aca53c120dbc3c6ccd39eb6
2019-03-25 17:30:47 +01:00

693 lines
20 KiB
JavaScript

( function () {
/**
* @param {RevisionList} revisionList
* @param {string} [dir]
* @constructor
*/
var RevisionListView = function ( revisionList, dir ) {
this.revisionList = revisionList;
this.dir = dir;
};
$.extend( RevisionListView.prototype, {
/**
* @type {RevisionList}
*/
revisionList: null,
/**
* @type {number}
*/
revisionWidth: 16,
/**
* @type {number}
*/
minRevisionHeight: 5,
/**
* @type {number}
*/
maxRevisionHeight: 66,
/**
* @type {number}
*/
tooltipTimeout: -1,
/**
* @type {boolean}
*/
allowHover: true,
/**
* @type {string}
*/
dir: null,
/**
* @type {string}
*/
selectedUser: '',
/**
* @type {string}
*/
selectedTag: '',
/**
* @type {jQuery}
*/
html: null,
/**
* @param {number} revisionTickWidth
* @param {number} positionOffset
* @return {jQuery}
*/
render: function ( revisionTickWidth, positionOffset ) {
var revs = this.revisionList.getRevisions(),
maxChangeSizeLogged = Math.log( this.revisionList.getBiggestChangeSize() ),
self = this,
i, diffSize, relativeChangeSize,
setHovered = function ( event ) {
if ( self.allowHover ) {
self.setRevisionHovered( $( this ), event );
}
},
unsetHovered = function () {
self.unsetRevisionHovered( $( this ) );
};
positionOffset = positionOffset || 0;
this.revisionWidth = revisionTickWidth;
this.$html = $( '<div>' ).addClass( 'mw-revslider-revisions' );
for ( i = 0; i < revs.length; i++ ) {
diffSize = revs[ i ].getRelativeSize();
relativeChangeSize = this.calcRelativeChangeSize( diffSize, maxChangeSizeLogged );
this.$html
.append( $( '<div>' )
.addClass( 'mw-revslider-revision-wrapper' )
.width( this.revisionWidth )
.append( $( '<div>' )
.addClass( 'mw-revslider-revision' )
.attr( 'data-revid', revs[ i ].getId() )
.attr( 'data-pos', positionOffset + i + 1 )
.css( {
height: relativeChangeSize + 'px',
width: this.revisionWidth + 'px',
top: diffSize > 0 ? '-' + relativeChangeSize + 'px' : 0
} )
.addClass( diffSize > 0 ? 'mw-revslider-revision-up' : 'mw-revslider-revision-down' )
.append( $( '<div>' ).addClass( 'mw-revslider-revision-border-box' ) )
)
.append( $( '<div>' )
.addClass( 'mw-revslider-revision-wrapper-up' )
.width( this.revisionWidth )
.append(
$( '<div>' )
.addClass( 'mw-revslider-pointer mw-revslider-pointer-ghost' )
)
)
.append( $( '<div>' )
.addClass( 'mw-revslider-revision-wrapper-down' )
.width( this.revisionWidth )
.append(
$( '<div>' )
.addClass( 'mw-revslider-pointer mw-revslider-pointer-ghost' )
)
)
.on( 'mouseenter', setHovered )
.on( 'mouseleave', unsetHovered )
);
}
this.keepTooltipsOnHover();
this.closeTooltipsOnClick();
return this.$html;
},
enableHover: function () {
this.allowHover = true;
},
disableHover: function () {
this.allowHover = false;
this.unsetAllHovered();
},
/**
* @param {jQuery} $revisionWrapper
* @param {Event} event
*/
setRevisionHovered: function ( $revisionWrapper, event ) {
var hasMovedTop = event.pageY - $revisionWrapper.offset().top < $revisionWrapper.height() / 2,
isOlderTop = $revisionWrapper.hasClass( 'mw-revslider-revision-older' ) && hasMovedTop,
isNewerBottom = $revisionWrapper.hasClass( 'mw-revslider-revision-newer' ) && !hasMovedTop,
$neighborRevisionWrapper = $revisionWrapper;
this.showTooltip( $revisionWrapper );
if ( isOlderTop ) {
$neighborRevisionWrapper = $revisionWrapper.prev();
} else if ( isNewerBottom ) {
$neighborRevisionWrapper = $revisionWrapper.next();
}
if ( $neighborRevisionWrapper.length === 0 ) {
return;
}
if ( hasMovedTop ) {
this.setRevisionGhost( $revisionWrapper.find( '.mw-revslider-revision-wrapper-up' ) );
if ( isOlderTop ) {
this.setRevisionGhost( $neighborRevisionWrapper.find( '.mw-revslider-revision-wrapper-down' ) );
}
} else {
this.setRevisionGhost( $revisionWrapper.find( '.mw-revslider-revision-wrapper-down' ) );
if ( isNewerBottom ) {
this.setRevisionGhost( $neighborRevisionWrapper.find( '.mw-revslider-revision-wrapper-up' ) );
}
}
},
/**
* @param {jQuery} $revisionWrapper
* @return {number}
*/
getRevisionWrapperPos: function ( $revisionWrapper ) {
return +$revisionWrapper.find( '.mw-revslider-revision' ).attr( 'data-pos' );
},
/**
* @param {jQuery} $revisionWrapper
*/
setRevisionGhost: function ( $revisionWrapper ) {
$revisionWrapper.addClass( 'mw-revslider-revision-hovered' );
},
/**
* @param {jQuery} $revisionWrapper
*/
unsetRevisionHovered: function ( $revisionWrapper ) {
this.unsetRevisionGhosts( $revisionWrapper );
this.hideTooltip( $revisionWrapper );
},
unsetAllHovered: function () {
$( '.mw-revslider-revision-wrapper-up, .mw-revslider-revision-wrapper-down' )
.removeClass( 'mw-revslider-revision-hovered' );
},
/**
* @param {jQuery} $revisionWrapper
*/
unsetRevisionGhosts: function ( $revisionWrapper ) {
$revisionWrapper.children().removeClass( 'mw-revslider-revision-hovered' );
$revisionWrapper.prev().children().removeClass( 'mw-revslider-revision-hovered' );
$revisionWrapper.next().children().removeClass( 'mw-revslider-revision-hovered' );
},
/**
* @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 = this.calcRelativeChangeSize( diffSize, maxChangeSizeLogged );
$renderedList.find( '.mw-revslider-revision[data-pos="' + ( i + 1 ) + '"]' ).css( {
height: relativeChangeSize + 'px',
top: diffSize > 0 ? '-' + relativeChangeSize + 'px' : 0
} );
}
},
calcRelativeChangeSize: function ( diffSize, maxChangeSizeLogged ) {
if ( diffSize === 0 ) {
return 0;
}
return Math.ceil(
( this.maxRevisionHeight - this.minRevisionHeight ) *
Math.log( Math.abs( diffSize ) ) / maxChangeSizeLogged ) +
this.minRevisionHeight;
},
/**
* Hides the current tooltip immediately
*/
hideCurrentTooltip: function () {
window.clearTimeout( this.tooltipTimeout );
$( '.mw-revslider-revision-wrapper-hovered' )
.removeClass( 'mw-revslider-revision-wrapper-hovered' );
$( '.mw-revslider-revision-tooltip' ).remove();
},
/**
* Hides the tooltip after 500ms
*
* @param {jQuery} $revisionWrapper
*/
hideTooltip: function ( $revisionWrapper ) {
this.tooltipTimeout = window.setTimeout( function () {
$revisionWrapper.removeClass( 'mw-revslider-revision-wrapper-hovered' );
$( '.mw-revslider-revision-tooltip' ).remove();
}, 500 );
},
/**
* Hides the previous tooltip and shows the new one
*
* @param {jQuery} $revisionWrapper
*/
showTooltip: function ( $revisionWrapper ) {
var pos = +$revisionWrapper.find( '.mw-revslider-revision' ).attr( 'data-pos' ),
revId = +$revisionWrapper.find( '.mw-revslider-revision' ).attr( 'data-revid' ),
revision = this.getRevisionWithId( revId ),
tooltip;
if ( revision === null ) {
return;
}
this.hideCurrentTooltip();
tooltip = this.makeTooltip( revision, $revisionWrapper );
tooltip.$element.addClass( 'mw-revslider-revision-tooltip-' + pos );
$( 'body' ).append( tooltip.$element );
tooltip.toggle( true );
$revisionWrapper.addClass( 'mw-revslider-revision-wrapper-hovered' );
},
/**
* @param {number} revId
* @return {Revision|null}
*/
getRevisionWithId: function ( revId ) {
var matchedRevision = null;
this.revisionList.revisions.forEach( function ( revision ) {
if ( revision.getId() === revId ) {
matchedRevision = revision;
}
} );
return matchedRevision;
},
/**
* Sets event handlers on tooltips so they do not disappear when hovering over them
*/
keepTooltipsOnHover: function () {
var self = this;
$( document )
.on( 'mouseenter', '.mw-revslider-revision-tooltip', function () {
window.clearTimeout( self.tooltipTimeout );
} )
.on( 'mouseleave', '.mw-revslider-revision-tooltip', function () {
self.hideTooltip( $( '.mw-revslider-revision-wrapper-hovered' ) );
} );
},
/**
* Sets an event handler to close tooltips when clicking somewhere outside
*/
closeTooltipsOnClick: function () {
var self = this;
$( document )
.on( 'click', function ( event ) {
if ( $( event.target ).closest( '.mw-revslider-revision-tooltip' ).length === 0 &&
$( event.target ).closest( '.mw-revslider-revisions-container' ).length === 0 ) {
self.hideCurrentTooltip();
}
} );
},
/**
* Generates the HTML for a tooltip that appears on hover above each revision on the slider
*
* @param {Revision} revision
* @param {jQuery} $revisionWrapper
* @return {OO.ui.PopupWidget}
*/
makeTooltip: function ( revision, $revisionWrapper ) {
var $tooltip = $( '<div>' )
.append(
$( '<p>' ).append(
$( '<strong>' ).text( mw.msg( 'revisionslider-label-date' ) + mw.msg( 'colon-separator' ) ),
$( '<a>' ).attr( 'href', mw.util.getUrl( null, { oldid: revision.id } ) )
.text( revision.getFormattedDate() )
),
this.makeUserLine( revision.getUser(), revision.getUserGender() ),
this.makeCommentLine( revision ),
this.makePageSizeLine( revision.getSize() ),
this.makeChangeSizeLine( revision.getRelativeSize() ),
revision.isMinor() ? $( '<p>' ).text( mw.message( 'revisionslider-minoredit' ).text() ) : '',
this.makeTagsLine( revision )
);
return new OO.ui.PopupWidget( {
$content: $tooltip,
$floatableContainer: $revisionWrapper,
padded: true,
classes: [ 'mw-revslider-tooltip', 'mw-revslider-revision-tooltip' ]
} );
},
/**
* Generates a link to user page or to contributions page for IP addresses
*
* @param {string} user
* @return {string}
*/
getUserPage: function ( user ) {
return ( mw.util.isIPAddress( user, false ) ? 'Special:Contributions/' : 'User:' ) + this.stripInvalidCharacters( user );
},
/**
* Generates the HTML for the user label
*
* @param {string} userString
* @param {string} userGender
* @return {string|jQuery}
*/
makeUserLine: function ( userString, userGender ) {
var self = this,
$userLine,
$userBubble,
$tagLine,
$tagBubble;
if ( !userString ) {
return '';
}
if ( !userGender ) {
userGender = 'unknown';
}
$userLine = $( '<p>' ).addClass( 'mw-revslider-highlightable-row' ).append(
$( '<strong>' ).text( mw.msg( 'revisionslider-label-username', userGender ) + mw.msg( 'colon-separator' ) ),
$( '<bdi>' ).append(
$( '<a>' ).addClass( 'mw-userlink' ).attr( 'href', mw.util.getUrl( this.getUserPage( userString ) ) ).text( this.stripInvalidCharacters( userString ) )
),
$userBubble = $( '<div>' ).addClass( 'mw-revslider-bubble' )
.on( 'click', function () {
$tagLine = $userLine.siblings( 'div' ).children( 'div.mw-revslider-highlightable-row' );
$tagLine.removeClass( 'mw-revslider-highlight' );
$tagBubble = $tagLine.children( 'div.mw-revslider-bubble' );
$tagBubble.removeClass( 'mw-revslider-highlite-bubble' );
if ( self.selectedUser !== userString ) {
$( '.mw-revslider-highlightable-row' ).addClass( 'mw-revslider-highlight' );
$( this ).addClass( 'mw-revslider-highlite-bubble' );
self.highlightSameUserRevisions( userString, 'addClass' );
self.selectedUser = userString;
self.selectedTag = '';
} else {
$( '.mw-revslider-highlightable-row' ).addClass( 'mw-revslider-highlight' );
$( this ).addClass( 'mw-revslider-highlite-bubble' );
self.highlightSameUserRevisions( userString, 'removeClass' );
self.selectedUser = '';
}
} )
.on( {
mouseenter: function () {
if ( self.selectedUser !== userString ) {
$( '.mw-revslider-highlightable-row' ).addClass( 'mw-revslider-highlight' );
$( this ).addClass( 'mw-revslider-highlite-bubble' );
self.highlightSameUserRevisions( userString, 'addClass' );
}
},
mouseleave: function () {
if ( self.selectedUser !== userString ) {
$( '.mw-revslider-highlightable-row' ).removeClass( 'mw-revslider-highlight' );
$( this ).removeClass( 'mw-revslider-highlite-bubble' );
self.highlightSameUserRevisions( userString, 'removeClass' );
if ( self.selectedTag ) {
self.highlightSameTagRevisions( self.selectedTag, 'addClass' );
}
}
}
} )
);
if ( self.selectedUser === userString ) {
self.selectedTag = '';
$userLine.addClass( 'mw-revslider-highlight' );
$userBubble.addClass( 'mw-revslider-highlite-bubble' );
}
return $userLine;
},
/**
* Highlights revisions of the sameUser
* @param {string} userString
* @param {string} event
*/
highlightSameUserRevisions: function ( userString, event ) {
var i,
revs = this.revisionList.getRevisions();
$( '.mw-revslider-revision-wrapper' ).removeClass( 'mw-revslider-revision-highlight' );
for ( i = 0; i < revs.length; i++ ) {
if ( userString === revs[ i ].getUser() ) {
if ( event === 'addClass' ) {
$( '[data-revid~="' + revs[ i ].id + '"]' ).parent().addClass( 'mw-revslider-revision-highlight' );
} else if ( event === 'removeClass' ) {
$( '[data-revid~="' + revs[ i ].id + '"]' ).parent().removeClass( 'mw-revslider-revision-highlight' );
}
}
}
},
/**
* @param {string} s
* @return {string}
*/
stripInvalidCharacters: function ( s ) {
return s.replace( /[<>&]/g, '' );
},
/**
* Generates the HTML for the comment label
*
* @param {Revision} rev
* @return {string|jQuery}
*/
makeCommentLine: function ( rev ) {
if ( rev.hasEmptyComment() ) {
return '';
}
return $( '<p>' ).append(
$( '<strong>' ).text( mw.msg( 'revisionslider-label-comment' ) + mw.msg( 'colon-separator' ) ),
$( '<em>' ).append(
$( '<bdi>' ).append(
rev.getParsedComment()
)
)
);
},
/**
* Generates the HTML for the tags label
*
* @param {Revision} rev
* @return {string|jQuery}
*/
makeTagsLine: function ( rev ) {
var self = this,
tags, $tagLines, i, $tagLine, $tagBubble,
updateTagLineHighlighting = function ( event ) {
self.setTagFilter( $( this ), event );
};
if ( rev.hasNoTags() ) {
return '';
}
tags = rev.getTags();
$tagLines = $( '<div>' );
for ( i = 0; i < tags.length; i++ ) {
$tagLine = $( '<div>' ).addClass( 'mw-revslider-highlightable-row' ).append(
tags[ i ],
$tagBubble = $( '<div>' ).addClass( 'mw-revslider-bubble' )
.on( 'click mouseenter mouseleave', updateTagLineHighlighting ),
'<br>'
);
if ( self.selectedTag === tags[ i ] ) {
self.selectedUser = '';
$tagLine.addClass( 'mw-revslider-highlight' );
$tagLine.find( $tagBubble ).addClass( 'mw-revslider-highlite-bubble' );
}
$tagLine.attr( 'data-tag-name', tags[ i ] );
$tagLines.append( $tagLine );
}
return $tagLines;
},
/**
* Set tag filter for revisions
* @param {jQuery} $tagBubble
* @param {Event} event
*/
setTagFilter: function ( $tagBubble, event ) {
var self = this,
$tagLine = $tagBubble.parent(),
tagName = $tagLine.data( 'tag-name' ),
$userLine, $userBubble;
if ( event.type === 'mouseenter' ) {
if ( self.selectedTag !== tagName ) {
$tagLine.addClass( 'mw-revslider-highlight' );
$tagBubble.addClass( 'mw-revslider-highlite-bubble' );
self.highlightSameTagRevisions( tagName, 'addClass' );
}
}
if ( event.type === 'mouseleave' ) {
if ( self.selectedTag !== tagName ) {
$tagLine.removeClass( 'mw-revslider-highlight' );
$tagBubble.removeClass( 'mw-revslider-highlite-bubble' );
self.highlightSameTagRevisions( tagName, 'removeClass' );
if ( self.selectedTag ) {
self.highlightSameTagRevisions( self.selectedTag, 'addClass' );
}
if ( self.selectedUser ) {
self.highlightSameUserRevisions( self.selectedUser, 'addClass' );
}
}
}
if ( event.type === 'click' ) {
$userLine = $tagLine.parent().siblings( 'p.mw-revslider-highlightable-row' );
$userBubble = $userLine.children( 'div.mw-revslider-bubble' );
$userLine.removeClass( 'mw-revslider-highlight' );
$userBubble.removeClass( 'mw-revslider-highlite-bubble' );
$tagLine.siblings().removeClass( 'mw-revslider-highlight' );
$tagLine.siblings().children().removeClass( 'mw-revslider-highlite-bubble' );
$tagLine.addClass( 'mw-revslider-highlight' );
$tagBubble.addClass( 'mw-revslider-highlite-bubble' );
if ( self.selectedTag !== tagName ) {
self.highlightSameTagRevisions( tagName, 'addClass' );
self.selectedTag = tagName;
self.selectedUser = '';
} else {
self.highlightSameTagRevisions( tagName, 'removeClass' );
self.selectedTag = '';
}
}
},
/**
* Highlights same tag revisions
* @param {string} tagName
* @param {string} event
*/
highlightSameTagRevisions: function ( tagName, event ) {
var i, j, revTags,
revs = this.revisionList.getRevisions();
$( '.mw-revslider-revision-wrapper' ).removeClass( 'mw-revslider-revision-highlight' );
for ( i = 0; i < revs.length; i++ ) {
revTags = revs[ i ].getTags();
for ( j = 0; j < revTags.length; j++ ) {
if ( tagName === revTags[ j ] ) {
if ( event === 'addClass' ) {
$( '[data-revid~="' + revs[ i ].id + '"]' ).parent().addClass( 'mw-revslider-revision-highlight' );
} else if ( event === 'removeClass' ) {
$( '[data-revid~="' + revs[ i ].id + '"]' ).parent().removeClass( 'mw-revslider-revision-highlight' );
}
}
}
}
},
/**
* Generates the HTML for the page size label
*
* @param {number} size
* @return {jQuery}
*/
makePageSizeLine: function ( size ) {
return $( '<p>' ).append(
$( '<strong>' ).text( mw.msg( 'revisionslider-label-page-size' ) + mw.msg( 'colon-separator' ) ),
mw.msg( 'revisionslider-page-size', mw.language.convertNumber( size ), size )
);
},
/**
* Generates the HTML for the change size label
*
* @param {number} relativeSize
* @return {jQuery}
*/
makeChangeSizeLine: function ( relativeSize ) {
var changeSizeClass = 'mw-revslider-change-none',
leadingSign = '',
$changeNumber;
if ( relativeSize > 0 ) {
changeSizeClass = 'mw-revslider-change-positive';
leadingSign = '+';
} else if ( relativeSize < 0 ) {
changeSizeClass = 'mw-revslider-change-negative';
}
$changeNumber = $( '<span>' )
.addClass( changeSizeClass )
.attr( {
dir: 'ltr' // Make sure that minus/plus is on the left
} )
.text( leadingSign + mw.language.convertNumber( relativeSize ) );
return $( '<p>' ).append(
$( '<strong>' ).text( mw.msg( 'revisionslider-label-change-size' ) + mw.msg( 'colon-separator' ) ),
mw.message( 'revisionslider-change-size', $changeNumber, relativeSize, Math.abs( relativeSize ) ).parse()
);
},
/**
* Set direction for the view
*
* @param {string} dir
*/
setDir: function ( dir ) {
this.dir = dir;
},
/**
* @return {jQuery}
*/
getElement: function () {
return this.$html;
}
} );
mw.libs.revisionSlider = mw.libs.revisionSlider || {};
mw.libs.revisionSlider.RevisionListView = RevisionListView;
}() );