/*! * 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 } ); } ); };