2019-12-10 16:18:24 +00:00
|
|
|
/*!
|
|
|
|
* VisualEditor UserInterface MWUsernameCompletionAction class.
|
|
|
|
*
|
|
|
|
* @copyright 2011-2019 VisualEditor Team and others; see http://ve.mit-license.org
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* MWUsernameCompletionAction action.
|
|
|
|
*
|
|
|
|
* Controls autocompletion of usernames
|
|
|
|
*
|
|
|
|
* @class
|
|
|
|
* @extends ve.ui.CompletionAction
|
|
|
|
* @constructor
|
|
|
|
* @param {ve.ui.Surface} surface Surface to act on
|
|
|
|
*/
|
|
|
|
function MWUsernameCompletionAction( surface ) {
|
2020-05-06 17:52:23 +00:00
|
|
|
var action = this;
|
|
|
|
|
2019-12-10 16:18:24 +00:00
|
|
|
// Parent constructor
|
|
|
|
MWUsernameCompletionAction.super.call( this, surface );
|
|
|
|
|
2020-06-11 17:27:30 +00:00
|
|
|
this.api = new mw.Api( { parameters: { formatversion: 2 } } );
|
2019-12-10 16:18:24 +00:00
|
|
|
this.searchedPrefixes = {};
|
2020-05-06 17:52:23 +00:00
|
|
|
this.localUsers = [];
|
|
|
|
this.ipUsers = [];
|
|
|
|
this.surface.authors.forEach( function ( user ) {
|
|
|
|
if ( mw.util.isIPAddress( user ) ) {
|
|
|
|
action.ipUsers.push( user );
|
|
|
|
} else {
|
|
|
|
action.localUsers.push( user );
|
|
|
|
}
|
|
|
|
} );
|
2019-12-10 16:18:24 +00:00
|
|
|
this.remoteUsers = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Inheritance */
|
|
|
|
|
|
|
|
OO.inheritClass( MWUsernameCompletionAction, ve.ui.CompletionAction );
|
|
|
|
|
|
|
|
/* Static Properties */
|
|
|
|
|
|
|
|
MWUsernameCompletionAction.static.name = 'mwUsernameCompletion';
|
|
|
|
|
2020-06-11 14:56:17 +00:00
|
|
|
MWUsernameCompletionAction.static.methods = OO.copy( MWUsernameCompletionAction.static.methods );
|
|
|
|
MWUsernameCompletionAction.static.methods.push( 'insertAndOpen' );
|
|
|
|
|
2019-12-10 16:18:24 +00:00
|
|
|
/* Methods */
|
|
|
|
|
2020-06-11 14:56:17 +00:00
|
|
|
MWUsernameCompletionAction.prototype.insertAndOpen = function () {
|
2020-06-23 15:23:13 +00:00
|
|
|
// This is opening a window in a slightly weird way, so the normal logging
|
|
|
|
// doesn't catch it. This assumes that the only way to get here is from
|
|
|
|
// the tool. If we add other paths, we'd need to change the logging.
|
|
|
|
ve.track(
|
|
|
|
'activity.' + this.constructor.static.name,
|
|
|
|
{ action: 'window-open-from-tool' }
|
|
|
|
);
|
2020-06-11 14:56:17 +00:00
|
|
|
this.surface.getModel().getFragment().insertContent( '@' ).collapseToEnd().select();
|
2020-06-22 11:22:08 +00:00
|
|
|
return this.open();
|
2020-06-11 14:56:17 +00:00
|
|
|
};
|
|
|
|
|
2019-12-10 16:18:24 +00:00
|
|
|
MWUsernameCompletionAction.prototype.getSuggestions = function ( input ) {
|
2020-07-02 14:12:30 +00:00
|
|
|
var apiPromise,
|
|
|
|
capitalizedInput = input.length > 0 && input[ 0 ].toUpperCase() + input.slice( 1 ),
|
2019-12-10 16:18:24 +00:00
|
|
|
action = this;
|
|
|
|
|
|
|
|
this.api.abort(); // Abort all unfinished API requests
|
|
|
|
if ( capitalizedInput && !this.searchedPrefixes[ capitalizedInput ] ) {
|
2020-07-02 14:12:30 +00:00
|
|
|
apiPromise = this.api.get( {
|
2019-12-10 16:18:24 +00:00
|
|
|
action: 'query',
|
|
|
|
list: 'allusers',
|
|
|
|
// Prefix of list=allusers is case sensitive, and users are stored in the DB capitalized, so:
|
|
|
|
auprefix: capitalizedInput,
|
|
|
|
aulimit: this.limit
|
2020-07-02 14:12:30 +00:00
|
|
|
} ).then( function ( response ) {
|
2019-12-10 16:18:24 +00:00
|
|
|
var suggestions = response.query.allusers.map( function ( user ) {
|
|
|
|
return user.name;
|
|
|
|
} ).filter( function ( username ) {
|
2020-05-06 17:52:23 +00:00
|
|
|
// API doesn't return IPs
|
|
|
|
return action.localUsers.indexOf( username ) === -1 &&
|
2019-12-10 16:18:24 +00:00
|
|
|
action.remoteUsers.indexOf( username ) === -1;
|
|
|
|
} );
|
|
|
|
|
|
|
|
action.remoteUsers.push.apply( action.remoteUsers, suggestions );
|
|
|
|
action.remoteUsers.sort();
|
|
|
|
|
|
|
|
action.searchedPrefixes[ capitalizedInput ] = true;
|
|
|
|
} );
|
2020-07-02 14:12:30 +00:00
|
|
|
} else {
|
|
|
|
apiPromise = ve.createDeferred().resolve().promise();
|
2019-12-10 16:18:24 +00:00
|
|
|
}
|
|
|
|
|
2020-07-02 14:12:30 +00:00
|
|
|
return apiPromise.then( function () {
|
2019-12-10 16:18:24 +00:00
|
|
|
// By concatenating on-thread authors and remote-fetched authors, both
|
|
|
|
// sorted alphabetically, we'll get our suggestion popup sorted so all
|
|
|
|
// on-thread matches come first.
|
2020-07-02 14:12:30 +00:00
|
|
|
return action.filterSuggestionsForInput(
|
|
|
|
action.localUsers
|
2020-05-11 18:35:48 +00:00
|
|
|
// Show no remote users if no input provided
|
2020-07-02 14:12:30 +00:00
|
|
|
.concat( capitalizedInput ? action.remoteUsers : [] ),
|
2020-05-06 18:15:19 +00:00
|
|
|
// TODO: Consider showing IP users
|
2020-05-12 11:51:20 +00:00
|
|
|
// * Change link to Special:Contributions/<ip> (localised)
|
2020-05-06 18:15:19 +00:00
|
|
|
// * Let users know that mentioning an IP will not create a notification?
|
|
|
|
// .concat( this.ipUsers )
|
2020-05-06 17:52:23 +00:00
|
|
|
input
|
2020-07-02 14:12:30 +00:00
|
|
|
);
|
|
|
|
} );
|
2019-12-10 16:18:24 +00:00
|
|
|
};
|
|
|
|
|
2020-07-08 17:20:23 +00:00
|
|
|
MWUsernameCompletionAction.prototype.getHeaderLabel = function ( input, suggestions ) {
|
|
|
|
var $query;
|
|
|
|
if ( suggestions === undefined ) {
|
|
|
|
$query = $( '<span>' ).text( input );
|
|
|
|
return mw.message( 'discussiontools-replywidget-mention-tool-header', $query ).parseDom();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2019-12-10 16:18:24 +00:00
|
|
|
MWUsernameCompletionAction.prototype.insertCompletion = function ( word, range ) {
|
2020-05-20 11:28:31 +00:00
|
|
|
var fragment,
|
|
|
|
// TODO: Allow output customisation (T250332)
|
|
|
|
prefix = '@',
|
2020-06-26 16:23:07 +00:00
|
|
|
title = mw.Title.newFromText( word, mw.config.get( 'wgNamespaceIds' ).user );
|
|
|
|
|
2019-12-10 16:18:24 +00:00
|
|
|
if ( this.surface.getMode() === 'source' ) {
|
|
|
|
// TODO: this should be configurable per-wiki so that e.g. custom templates can be used
|
2020-05-20 11:28:31 +00:00
|
|
|
word = prefix + '[[' + title.getPrefixedText() + '|' + word + ']]';
|
2019-12-10 16:18:24 +00:00
|
|
|
return MWUsernameCompletionAction.super.prototype.insertCompletion.call( this, word, range );
|
|
|
|
}
|
2020-06-26 16:23:07 +00:00
|
|
|
|
|
|
|
fragment = this.surface.getModel().getLinearFragment( range );
|
|
|
|
fragment.removeContent().insertContent( [
|
|
|
|
{ type: 'mwPing', attributes: { user: word } },
|
|
|
|
{ type: '/mwPing' }
|
|
|
|
] );
|
2020-05-20 11:28:31 +00:00
|
|
|
|
|
|
|
fragment.collapseToStart().insertContent( '@' );
|
|
|
|
|
|
|
|
return fragment;
|
2019-12-10 16:18:24 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
MWUsernameCompletionAction.prototype.shouldAbandon = function ( input ) {
|
|
|
|
// TODO: need to consider whether pending loads from server are happening here
|
|
|
|
return MWUsernameCompletionAction.super.prototype.shouldAbandon.apply( this, arguments ) && input.split( /\s+/ ).length > 2;
|
|
|
|
};
|
|
|
|
|
|
|
|
/* Registration */
|
|
|
|
|
|
|
|
ve.ui.actionFactory.register( MWUsernameCompletionAction );
|
|
|
|
|
|
|
|
ve.ui.commandRegistry.register(
|
|
|
|
new ve.ui.Command(
|
2020-06-11 14:56:17 +00:00
|
|
|
'openMWUsernameCompletions', MWUsernameCompletionAction.static.name, 'open',
|
|
|
|
{ supportedSelections: [ 'linear' ] }
|
|
|
|
)
|
|
|
|
);
|
|
|
|
ve.ui.commandRegistry.register(
|
|
|
|
new ve.ui.Command(
|
|
|
|
'insertAndOpenMWUsernameCompletions', MWUsernameCompletionAction.static.name, 'insertAndOpen',
|
2019-12-10 16:18:24 +00:00
|
|
|
{ supportedSelections: [ 'linear' ] }
|
|
|
|
)
|
|
|
|
);
|
|
|
|
ve.ui.sequenceRegistry.register(
|
2020-06-26 16:19:10 +00:00
|
|
|
new ve.ui.Sequence( 'autocompleteMWUsernames', 'openMWUsernameCompletions', '@', 0, false, false, true, true )
|
2019-12-10 16:18:24 +00:00
|
|
|
);
|
|
|
|
ve.ui.wikitextSequenceRegistry.register(
|
2020-06-26 16:19:10 +00:00
|
|
|
new ve.ui.Sequence( 'autocompleteMWUsernamesWikitext', 'openMWUsernameCompletions', '@', 0, false, false, true, true )
|
2019-12-10 16:18:24 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
module.exports = MWUsernameCompletionAction;
|