2 * MediaWiki Widgets - UsersMultiselectWidget class.
4 * @copyright 2017 MediaWiki Widgets Team and others; see AUTHORS.txt
5 * @license The MIT License (MIT); see LICENSE.txt
10 * @classdesc Input list of users in a single line.
12 * If used inside HTML form the results will be sent as the list of
13 * newline-separated usernames.
15 * This can be configured to accept IP addresses and/or ranges as well as
19 * @extends OO.ui.MenuTagMultiselectWidget
22 * @description Create an instance of `mw.widgets.UsersMultiselectWidget`.
23 * @param {Object} [config] Configuration options
24 * @param {mw.Api} [config.api] Instance of mw.Api (or subclass thereof) to use for queries
25 * @param {number} [config.limit=10] Number of results to show in autocomplete menu
26 * @param {string} [config.name] Name of input to submit results (when used in HTML forms)
27 * @param {boolean} [config.ipAllowed=false] Show IP addresses in autocomplete menu
28 * If false, single IP addresses are not allowed, even if IP ranges are allowed.
29 * @param {boolean} [config.ipRangeAllowed=false] Show IP ranges in autocomplete menu
30 * @param {Object} [config.ipRangeLimits] Maximum allowed IP ranges (defaults match HTMLUserTextField.php)
31 * @param {number} [config.ipRangeLimits.IPv4 = 16] Maximum allowed IPv4 range
32 * @param {number} [config.ipRangeLimits.IPv6 = 32] Maximum allowed IPv6 range
33 * @param {boolean} [config.excludenamed] Whether to exclude named users or not
34 * @param {boolean} [config.excludetemp] Whether to exclude temporary users or not
36 mw.widgets.UsersMultiselectWidget = function MwWidgetsUsersMultiselectWidget( config ) {
37 // Config initialization
38 config = Object.assign( {
41 ipRangeAllowed: false,
51 mw.widgets.UsersMultiselectWidget.super.call( this, Object.assign( {}, config, {} ) );
54 OO.ui.mixin.PendingElement.call( this, Object.assign( {}, config, { $pending: this.$handle } ) );
57 this.limit = config.limit;
58 this.ipAllowed = config.ipAllowed;
59 this.ipRangeAllowed = config.ipRangeAllowed;
60 this.ipRangeLimits = config.ipRangeLimits;
61 this.excludeNamed = config.excludeNamed;
62 this.excludeTemp = config.excludeTemp;
64 if ( 'name' in config ) {
65 // Use this instead of <input type="hidden">, because hidden inputs do not have separate
66 // 'value' and 'defaultValue' properties. The script on Special:Preferences
67 // (mw.special.preferences.confirmClose) checks this property to see if a field was changed.
68 this.$hiddenInput = $( '<textarea>' )
69 .addClass( 'oo-ui-element-hidden' )
70 .attr( 'name', config.name )
71 .appendTo( this.$element );
72 // Update with preset values
73 this.updateHiddenInput();
74 // Set the default value (it might be different from just being empty)
75 this.$hiddenInput.prop( 'defaultValue', this.getSelectedUsernames().join( '\n' ) );
79 // When list of selected usernames changes, update hidden input
81 change: 'updateHiddenInput'
85 this.api = config.api || new mw.Api();
90 OO.inheritClass( mw.widgets.UsersMultiselectWidget, OO.ui.MenuTagMultiselectWidget );
91 OO.mixinClass( mw.widgets.UsersMultiselectWidget, OO.ui.mixin.PendingElement );
96 * Get currently selected usernames.
98 * @return {string[]} usernames
100 mw.widgets.UsersMultiselectWidget.prototype.getSelectedUsernames = function () {
101 return this.getValue();
105 * Update autocomplete menu with items.
109 mw.widgets.UsersMultiselectWidget.prototype.updateMenuItems = function () {
110 const inputValue = this.input.getValue();
112 if ( inputValue === this.inputValue ) {
113 // Do not restart api query if nothing has changed in the input
116 this.inputValue = inputValue;
119 this.api.abort(); // Abort all unfinished api requests
121 if ( inputValue.length > 0 ) {
124 let isValidIp, isValidRange;
125 if ( this.ipAllowed || this.ipRangeAllowed ) {
126 isValidIp = mw.util.isIPAddress( inputValue, false );
127 isValidRange = !isValidIp &&
128 mw.util.isIPAddress( inputValue, true ) &&
129 this.validateIpRange( inputValue );
132 if ( this.ipAllowed && isValidIp || this.ipRangeAllowed && isValidRange ) {
133 this.menu.clearItems();
134 this.menu.addItems( [
135 new OO.ui.MenuOptionWidget( {
140 this.menu.toggle( true );
146 auprefix: inputValue,
148 auexcludenamed: this.excludeNamed,
149 auexcludetemp: this.excludeTemp
150 } ).done( ( response ) => {
151 let suggestions = response.query.allusers;
153 const selected = this.getSelectedUsernames();
155 // Remove usernames, which are already selected from suggestions
156 suggestions = suggestions.map( ( user ) => {
157 if ( selected.indexOf( user.name ) === -1 ) {
158 return new OO.ui.MenuOptionWidget( {
165 } ).filter( ( item ) => item !== undefined );
167 // Remove all items from menu add fill it with new
168 this.menu.clearItems();
169 this.menu.addItems( suggestions );
171 if ( suggestions.length ) {
172 // Enable Narrator focus on menu item, see T250762.
173 this.menu.$focusOwner.attr( 'aria-activedescendant', suggestions[ 0 ].$element.attr( 'id' ) );
176 // Make the menu visible; it might not be if it was previously empty
177 this.menu.toggle( true );
180 } ).fail( this.popPending.bind( this ) );
184 this.menu.clearItems();
190 * @param {string} ipRange Valid IPv4 or IPv6 range
191 * @return {boolean} The IP range is within the size limit
193 mw.widgets.UsersMultiselectWidget.prototype.validateIpRange = function ( ipRange ) {
194 ipRange = ipRange.split( '/' );
196 return mw.util.isIPv4Address( ipRange[ 0 ] ) && +ipRange[ 1 ] >= this.ipRangeLimits.IPv4 ||
197 mw.util.isIPv6Address( ipRange[ 0 ] ) && +ipRange[ 1 ] >= this.ipRangeLimits.IPv6;
200 mw.widgets.UsersMultiselectWidget.prototype.onInputChange = function () {
201 mw.widgets.UsersMultiselectWidget.super.prototype.onInputChange.apply( this, arguments );
203 this.updateMenuItems();
207 * If used inside HTML form, then update hiddenInput with list of
208 * newline-separated usernames.
212 mw.widgets.UsersMultiselectWidget.prototype.updateHiddenInput = function () {
213 if ( '$hiddenInput' in this ) {
214 this.$hiddenInput.val( this.getSelectedUsernames().join( '\n' ) );
215 // Trigger a 'change' event as if a user edited the text
216 // (it is not triggered when changing the value from JS code).
217 this.$hiddenInput.trigger( 'change' );
222 * We have an empty menu when the input is empty, override the implementation from
223 * MenuTagMultiselectWidget to avoid error and make tags editable.
225 * Only editable when the input is empty.
227 mw.widgets.UsersMultiselectWidget.prototype.onTagSelect = function () {
228 if ( this.hasInput && !this.input.getValue() ) {
229 OO.ui.TagMultiselectWidget.prototype.onTagSelect.apply( this, arguments );