mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-12-27 13:33:00 +00:00
f923081e3f
Change-Id: I4df49b1ea202dcb7bbb82cb99d26f0dec17133bc
230 lines
6.6 KiB
JavaScript
230 lines
6.6 KiB
JavaScript
/*!
|
|
* VisualEditor UserInterface MWEditSummaryWidget class.
|
|
*
|
|
* @copyright 2011-2018 VisualEditor Team and others; see http://ve.mit-license.org
|
|
*/
|
|
|
|
/**
|
|
* Multi line text input for edit summary, with auto completion based on
|
|
* the user's previous edit summaries.
|
|
*
|
|
* @class
|
|
* @extends OO.ui.MultilineTextInputWidget
|
|
* @mixins OO.ui.mixin.LookupElement
|
|
*
|
|
* @constructor
|
|
* @param {Object} [config] Configuration options
|
|
* @cfg {number} [limit=6] Number of suggestions to show
|
|
*/
|
|
ve.ui.MWEditSummaryWidget = function VeUiMWEditSummaryWidget( config ) {
|
|
config = config || {};
|
|
|
|
// Parent method
|
|
ve.ui.MWEditSummaryWidget.super.call( this, ve.extendObject( {
|
|
autosize: true,
|
|
maxRows: 15,
|
|
inputFilter: function ( value ) {
|
|
// Prevent the user from inputting newlines (this kicks in on paste, etc.)
|
|
return value.replace( /\r?\n/g, ' ' );
|
|
}
|
|
}, config ) );
|
|
|
|
// Mixin method
|
|
OO.ui.mixin.LookupElement.call( this, ve.extendObject( {
|
|
showPendingRequest: false,
|
|
showSuggestionsOnFocus: false,
|
|
allowSuggestionsWhenEmpty: false,
|
|
highlightFirst: false
|
|
}, config ) );
|
|
|
|
this.limit = config.limit || 6;
|
|
};
|
|
|
|
/* Inheritance */
|
|
|
|
OO.inheritClass( ve.ui.MWEditSummaryWidget, OO.ui.MultilineTextInputWidget );
|
|
|
|
OO.mixinClass( ve.ui.MWEditSummaryWidget, OO.ui.mixin.LookupElement );
|
|
|
|
/* Static properties */
|
|
|
|
ve.ui.MWEditSummaryWidget.static.summarySplitter = /^(\/\*.*?\*\/\s*)?([^]*)$/;
|
|
|
|
/* Static methods */
|
|
|
|
/**
|
|
* Split a summary into the section and the actual summary
|
|
*
|
|
* @param {string} summary
|
|
* @return {Object} Object with section and comment string properties
|
|
*/
|
|
ve.ui.MWEditSummaryWidget.static.splitSummary = function ( summary ) {
|
|
var result = summary.match( this.summarySplitter );
|
|
return {
|
|
section: result[ 1 ] || '',
|
|
comment: result[ 2 ]
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Filter a list of edit summaries to a specific query string
|
|
*
|
|
* @param {string[]} summaries Edit summaries
|
|
* @param {string} query User query
|
|
* @return {string[]} Filtered edit summaries
|
|
*/
|
|
ve.ui.MWEditSummaryWidget.static.getMatchingSummaries = function ( summaries, query ) {
|
|
var summaryPrefixMatches = [], wordPrefixMatches = [], otherMatches = [],
|
|
lowerQuery = query.toLowerCase();
|
|
|
|
if ( !query.trim() ) {
|
|
// Show no results for empty query
|
|
return [];
|
|
}
|
|
|
|
summaries.forEach( function ( summary ) {
|
|
var lowerSummary = summary.toLowerCase(),
|
|
index = lowerSummary.indexOf( lowerQuery );
|
|
if ( index === 0 ) {
|
|
// Exclude exact matches
|
|
if ( lowerQuery !== summary ) {
|
|
summaryPrefixMatches.push( summary );
|
|
}
|
|
} else if ( index !== -1 ) {
|
|
if ( /^\s/.test( lowerSummary.charAt( index - 1 ) ) ) {
|
|
// Character before match is whitespace
|
|
wordPrefixMatches.push( summary );
|
|
} else {
|
|
otherMatches.push( summary );
|
|
}
|
|
}
|
|
} );
|
|
return summaryPrefixMatches.concat( wordPrefixMatches, otherMatches );
|
|
};
|
|
|
|
/* Methods */
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
ve.ui.MWEditSummaryWidget.prototype.adjustSize = function () {
|
|
// To autosize, the widget will render another element beneath the input
|
|
// with the same text for measuring. This extra element could cause scrollbars
|
|
// to appear, changing the available width, so if scrollbars are intially
|
|
// hidden, force them to stay hidden during the adjustment.
|
|
// TODO: Consider upstreaming this?
|
|
var scrollContainer = this.getClosestScrollableElementContainer();
|
|
var hasScrollbar = scrollContainer.offsetWidth > scrollContainer.scrollWidth;
|
|
var overflowY;
|
|
if ( !hasScrollbar ) {
|
|
overflowY = scrollContainer.style.overflowY;
|
|
scrollContainer.style.overflowY = 'hidden';
|
|
}
|
|
|
|
// Parent method
|
|
ve.ui.MWEditSummaryWidget.super.prototype.adjustSize.apply( this, arguments );
|
|
|
|
if ( !hasScrollbar ) {
|
|
scrollContainer.style.overflowY = overflowY;
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
ve.ui.MWEditSummaryWidget.prototype.onKeyPress = function ( e ) {
|
|
if ( e.which === OO.ui.Keys.ENTER ) {
|
|
e.preventDefault();
|
|
}
|
|
// Grand-parent method
|
|
// Multi-line only fires 'enter' on ctrl+enter, but this should
|
|
// fire on plain enter as it behaves like a single line input.
|
|
OO.ui.TextInputWidget.prototype.onKeyPress.call( this, e );
|
|
};
|
|
|
|
/**
|
|
* Get recent edit summaries for the logged in user
|
|
*
|
|
* @return {jQuery.Promise} Promise which resolves with a list of summaries
|
|
*/
|
|
ve.ui.MWEditSummaryWidget.prototype.getSummaries = function () {
|
|
var splitSummary = this.constructor.static.splitSummary.bind( this.constructor.static );
|
|
if ( !this.getSummariesPromise ) {
|
|
if ( mw.user.isAnon() ) {
|
|
this.getSummariesPromise = ve.createDeferred().resolve( [] ).promise();
|
|
} else {
|
|
// Allow this for temp users as well. The isAnon() check above is just to avoid autocompleting
|
|
// with someone else's summaries.
|
|
this.getSummariesPromise = ve.init.target.getLocalApi().get( {
|
|
action: 'query',
|
|
list: 'usercontribs',
|
|
ucuser: mw.user.getName(),
|
|
ucprop: 'comment',
|
|
uclimit: 500
|
|
} ).then( function ( response ) {
|
|
var usedComments = {},
|
|
changes = ve.getProp( response, 'query', 'usercontribs' ) || [];
|
|
|
|
return changes
|
|
// Filter out changes without comment (e.g. due to RevisionDelete)
|
|
.filter( function ( change ) {
|
|
return Object.prototype.hasOwnProperty.call( change, 'comment' );
|
|
} )
|
|
// Remove section /* headings */
|
|
.map( function ( change ) {
|
|
return splitSummary( change.comment ).comment.trim();
|
|
} )
|
|
// Filter out duplicates and empty comments
|
|
.filter( function ( comment ) {
|
|
if ( !comment || Object.prototype.hasOwnProperty.call( usedComments, comment ) ) {
|
|
return false;
|
|
}
|
|
usedComments[ comment ] = true;
|
|
return true;
|
|
} )
|
|
.sort();
|
|
} );
|
|
}
|
|
}
|
|
return this.getSummariesPromise;
|
|
};
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
ve.ui.MWEditSummaryWidget.prototype.getLookupRequest = function () {
|
|
var query = this.constructor.static.splitSummary( this.value ),
|
|
limit = this.limit,
|
|
widget = this;
|
|
|
|
return this.getSummaries().then( function ( allSummaries ) {
|
|
var matchingSummaries = widget.constructor.static.getMatchingSummaries( allSummaries, query.comment );
|
|
if ( matchingSummaries.length > limit ) {
|
|
// Quick in-place truncate
|
|
matchingSummaries.length = limit;
|
|
}
|
|
return { summaries: matchingSummaries, section: query.section };
|
|
} ).promise( { abort: function () {} } ); // don't abort, the actual request will be the same anyway
|
|
};
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
ve.ui.MWEditSummaryWidget.prototype.getLookupCacheDataFromResponse = function ( response ) {
|
|
return response;
|
|
};
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
ve.ui.MWEditSummaryWidget.prototype.getLookupMenuOptionsFromData = function ( data ) {
|
|
return data.summaries.map( function ( item ) {
|
|
return new OO.ui.MenuOptionWidget( {
|
|
label: item,
|
|
data: data.section + item
|
|
} );
|
|
} );
|
|
};
|