Localisation updates from https://translatewiki.net.
[mediawiki.git] / resources / src / mediawiki.widgets / mw.widgets.UsersMultiselectWidget.js
blob72db6d96df6906114718072abcc7e0473354a34f
1 /*!
2  * MediaWiki Widgets - UsersMultiselectWidget class.
3  *
4  * @copyright 2017 MediaWiki Widgets Team and others; see AUTHORS.txt
5  * @license The MIT License (MIT); see LICENSE.txt
6  */
7 ( function () {
9         /**
10          * @classdesc Input list of users in a single line.
11          *
12          * If used inside HTML form the results will be sent as the list of
13          * newline-separated usernames.
14          *
15          * This can be configured to accept IP addresses and/or ranges as well as
16          * usernames.
17          *
18          * @class
19          * @extends OO.ui.MenuTagMultiselectWidget
20          *
21          * @constructor
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
35          */
36         mw.widgets.UsersMultiselectWidget = function MwWidgetsUsersMultiselectWidget( config ) {
37                 // Config initialization
38                 config = Object.assign( {
39                         limit: 10,
40                         ipAllowed: false,
41                         ipRangeAllowed: false,
42                         ipRangeLimits: {
43                                 IPv4: 16,
44                                 IPv6: 32
45                         },
46                         excludeNamed: false,
47                         excludeTemp: false
48                 }, config );
50                 // Parent constructor
51                 mw.widgets.UsersMultiselectWidget.super.call( this, Object.assign( {}, config, {} ) );
53                 // Mixin constructors
54                 OO.ui.mixin.PendingElement.call( this, Object.assign( {}, config, { $pending: this.$handle } ) );
56                 // Properties
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' ) );
76                 }
78                 // Events
79                 // When list of selected usernames changes, update hidden input
80                 this.connect( this, {
81                         change: 'updateHiddenInput'
82                 } );
84                 // API init
85                 this.api = config.api || new mw.Api();
86         };
88         /* Setup */
90         OO.inheritClass( mw.widgets.UsersMultiselectWidget, OO.ui.MenuTagMultiselectWidget );
91         OO.mixinClass( mw.widgets.UsersMultiselectWidget, OO.ui.mixin.PendingElement );
93         /* Methods */
95         /**
96          * Get currently selected usernames.
97          *
98          * @return {string[]} usernames
99          */
100         mw.widgets.UsersMultiselectWidget.prototype.getSelectedUsernames = function () {
101                 return this.getValue();
102         };
104         /**
105          * Update autocomplete menu with items.
106          *
107          * @private
108          */
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
114                         return;
115                 } else {
116                         this.inputValue = inputValue;
117                 }
119                 this.api.abort(); // Abort all unfinished api requests
121                 if ( inputValue.length > 0 ) {
122                         this.pushPending();
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 );
130                         }
132                         if ( this.ipAllowed && isValidIp || this.ipRangeAllowed && isValidRange ) {
133                                 this.menu.clearItems();
134                                 this.menu.addItems( [
135                                         new OO.ui.MenuOptionWidget( {
136                                                 data: inputValue,
137                                                 label: inputValue
138                                         } )
139                                 ] );
140                                 this.menu.toggle( true );
141                                 this.popPending();
142                         } else {
143                                 this.api.get( {
144                                         action: 'query',
145                                         list: 'allusers',
146                                         auprefix: inputValue,
147                                         aulimit: this.limit,
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( {
159                                                                 data: user.name,
160                                                                 label: user.name,
161                                                                 id: user.name
162                                                         } );
163                                                 }
164                                                 return undefined;
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' ) );
174                                         }
176                                         // Make the menu visible; it might not be if it was previously empty
177                                         this.menu.toggle( true );
179                                         this.popPending();
180                                 } ).fail( this.popPending.bind( this ) );
181                         }
183                 } else {
184                         this.menu.clearItems();
185                 }
186         };
188         /**
189          * @private
190          * @param {string} ipRange Valid IPv4 or IPv6 range
191          * @return {boolean} The IP range is within the size limit
192          */
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;
198         };
200         mw.widgets.UsersMultiselectWidget.prototype.onInputChange = function () {
201                 mw.widgets.UsersMultiselectWidget.super.prototype.onInputChange.apply( this, arguments );
203                 this.updateMenuItems();
204         };
206         /**
207          * If used inside HTML form, then update hiddenInput with list of
208          * newline-separated usernames.
209          *
210          * @private
211          */
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' );
218                 }
219         };
221         /**
222          * We have an empty menu when the input is empty, override the implementation from
223          * MenuTagMultiselectWidget to avoid error and make tags editable.
224          *
225          * Only editable when the input is empty.
226          */
227         mw.widgets.UsersMultiselectWidget.prototype.onTagSelect = function () {
228                 if ( this.hasInput && !this.input.getValue() ) {
229                         OO.ui.TagMultiselectWidget.prototype.onTagSelect.apply( this, arguments );
230                 }
231         };
233 }() );