API: Always select rc_user from database (regardless of rcprop=user)
[lhc/web/wiklou.git] / resources / src / mediawiki.rcfilters / ui / MenuSelectWidget.js
1 var FilterMenuHeaderWidget = require( './FilterMenuHeaderWidget.js' ),
2 HighlightPopupWidget = require( './HighlightPopupWidget.js' ),
3 FilterMenuSectionOptionWidget = require( './FilterMenuSectionOptionWidget.js' ),
4 FilterMenuOptionWidget = require( './FilterMenuOptionWidget.js' ),
5 MenuSelectWidget;
6
7 /**
8 * A floating menu widget for the filter list
9 *
10 * @class mw.rcfilters.ui.MenuSelectWidget
11 * @extends OO.ui.MenuSelectWidget
12 *
13 * @constructor
14 * @param {mw.rcfilters.Controller} controller Controller
15 * @param {mw.rcfilters.dm.FiltersViewModel} model View model
16 * @param {Object} [config] Configuration object
17 * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
18 * @cfg {Object[]} [footers] An array of objects defining the footers for
19 * this menu, with a definition whether they appear per specific views.
20 * The expected structure is:
21 * [
22 * {
23 * name: {string} A unique name for the footer object
24 * $element: {jQuery} A jQuery object for the content of the footer
25 * views: {string[]} Optional. An array stating which views this footer is
26 * active on. Use null or omit to display this on all views.
27 * }
28 * ]
29 */
30 MenuSelectWidget = function MwRcfiltersUiMenuSelectWidget( controller, model, config ) {
31 var header;
32
33 config = config || {};
34
35 this.controller = controller;
36 this.model = model;
37 this.currentView = '';
38 this.views = {};
39 this.userSelecting = false;
40
41 this.menuInitialized = false;
42 this.$overlay = config.$overlay || this.$element;
43 this.$body = $( '<div>' ).addClass( 'mw-rcfilters-ui-menuSelectWidget-body' );
44 this.footers = [];
45
46 // Parent
47 MenuSelectWidget.parent.call( this, $.extend( config, {
48 $autoCloseIgnore: this.$overlay,
49 width: 650,
50 // Our filtering is done through the model
51 filterFromInput: false
52 } ) );
53 this.setGroupElement(
54 $( '<div>' )
55 .addClass( 'mw-rcfilters-ui-menuSelectWidget-group' )
56 );
57 this.setClippableElement( this.$body );
58 this.setClippableContainer( this.$element );
59
60 header = new FilterMenuHeaderWidget(
61 this.controller,
62 this.model,
63 {
64 $overlay: this.$overlay
65 }
66 );
67
68 this.noResults = new OO.ui.LabelWidget( {
69 label: mw.msg( 'rcfilters-filterlist-noresults' ),
70 classes: [ 'mw-rcfilters-ui-menuSelectWidget-noresults' ]
71 } );
72
73 // Events
74 this.model.connect( this, {
75 initialize: 'onModelInitialize',
76 searchChange: 'onModelSearchChange'
77 } );
78
79 // Initialization
80 this.$element
81 .addClass( 'mw-rcfilters-ui-menuSelectWidget' )
82 .append( header.$element )
83 .append(
84 this.$body
85 .append( this.$group, this.noResults.$element )
86 );
87
88 // Append all footers; we will control their visibility
89 // based on view
90 config.footers = config.footers || [];
91 config.footers.forEach( function ( footerData ) {
92 var isSticky = footerData.sticky === undefined ? true : !!footerData.sticky,
93 adjustedData = {
94 // Wrap the element with our own footer wrapper
95 $element: $( '<div>' )
96 .addClass( 'mw-rcfilters-ui-menuSelectWidget-footer' )
97 .addClass( 'mw-rcfilters-ui-menuSelectWidget-footer-' + footerData.name )
98 .append( footerData.$element ),
99 views: footerData.views
100 };
101
102 if ( !footerData.disabled ) {
103 this.footers.push( adjustedData );
104
105 if ( isSticky ) {
106 this.$element.append( adjustedData.$element );
107 } else {
108 this.$body.append( adjustedData.$element );
109 }
110 }
111 }.bind( this ) );
112
113 // Switch to the correct view
114 this.updateView();
115 };
116
117 /* Initialize */
118
119 OO.inheritClass( MenuSelectWidget, OO.ui.MenuSelectWidget );
120
121 /* Events */
122
123 /* Methods */
124 MenuSelectWidget.prototype.onModelSearchChange = function () {
125 this.updateView();
126 };
127
128 /**
129 * @inheritdoc
130 */
131 MenuSelectWidget.prototype.toggle = function ( show ) {
132 this.lazyMenuCreation();
133 MenuSelectWidget.parent.prototype.toggle.call( this, show );
134 // Always open this menu downwards. FilterTagMultiselectWidget scrolls it into view.
135 this.setVerticalPosition( 'below' );
136 };
137
138 /**
139 * lazy creation of the menu
140 */
141 MenuSelectWidget.prototype.lazyMenuCreation = function () {
142 var widget = this,
143 items = [],
144 viewGroupCount = {},
145 groups = this.model.getFilterGroups();
146
147 if ( this.menuInitialized ) {
148 return;
149 }
150
151 this.menuInitialized = true;
152
153 // Create shared popup for highlight buttons
154 this.highlightPopup = new HighlightPopupWidget( this.controller );
155 this.$overlay.append( this.highlightPopup.$element );
156
157 // Count groups per view
158 // eslint-disable-next-line no-jquery/no-each-util
159 $.each( groups, function ( groupName, groupModel ) {
160 if ( !groupModel.isHidden() ) {
161 viewGroupCount[ groupModel.getView() ] = viewGroupCount[ groupModel.getView() ] || 0;
162 viewGroupCount[ groupModel.getView() ]++;
163 }
164 } );
165
166 // eslint-disable-next-line no-jquery/no-each-util
167 $.each( groups, function ( groupName, groupModel ) {
168 var currentItems = [],
169 view = groupModel.getView();
170
171 if ( !groupModel.isHidden() ) {
172 if ( viewGroupCount[ view ] > 1 ) {
173 // Only add a section header if there is more than
174 // one group
175 currentItems.push(
176 // Group section
177 new FilterMenuSectionOptionWidget(
178 widget.controller,
179 groupModel,
180 {
181 $overlay: widget.$overlay
182 }
183 )
184 );
185 }
186
187 // Add items
188 widget.model.getGroupFilters( groupName ).forEach( function ( filterItem ) {
189 currentItems.push(
190 new FilterMenuOptionWidget(
191 widget.controller,
192 widget.model,
193 widget.model.getInvertModel(),
194 filterItem,
195 widget.highlightPopup,
196 {
197 $overlay: widget.$overlay
198 }
199 )
200 );
201 } );
202
203 // Cache the items per view, so we can switch between them
204 // without rebuilding the widgets each time
205 widget.views[ view ] = widget.views[ view ] || [];
206 widget.views[ view ] = widget.views[ view ].concat( currentItems );
207 items = items.concat( currentItems );
208 }
209 } );
210
211 this.addItems( items );
212 this.updateView();
213 };
214
215 /**
216 * Respond to model initialize event. Populate the menu from the model
217 */
218 MenuSelectWidget.prototype.onModelInitialize = function () {
219 this.menuInitialized = false;
220 // Set timeout for the menu to lazy build.
221 setTimeout( this.lazyMenuCreation.bind( this ) );
222 };
223
224 /**
225 * Update view
226 */
227 MenuSelectWidget.prototype.updateView = function () {
228 var viewName = this.model.getCurrentView();
229
230 if ( this.views[ viewName ] && this.currentView !== viewName ) {
231 this.updateFooterVisibility( viewName );
232
233 this.$element
234 .data( 'view', viewName )
235 .removeClass( 'mw-rcfilters-ui-menuSelectWidget-view-' + this.currentView )
236 .addClass( 'mw-rcfilters-ui-menuSelectWidget-view-' + viewName );
237
238 this.currentView = viewName;
239 this.scrollToTop();
240 }
241
242 this.postProcessItems();
243 this.clip();
244 };
245
246 /**
247 * Go over the available footers and decide which should be visible
248 * for this view
249 *
250 * @param {string} [currentView] Current view
251 */
252 MenuSelectWidget.prototype.updateFooterVisibility = function ( currentView ) {
253 currentView = currentView || this.model.getCurrentView();
254
255 this.footers.forEach( function ( data ) {
256 data.$element.toggle(
257 // This footer should only be shown if it is configured
258 // for all views or for this specific view
259 !data.views || data.views.length === 0 || data.views.indexOf( currentView ) > -1
260 );
261 } );
262 };
263
264 /**
265 * Post-process items after the visibility changed. Make sure
266 * that we always have an item selected, and that the no-results
267 * widget appears if the menu is empty.
268 */
269 MenuSelectWidget.prototype.postProcessItems = function () {
270 var i,
271 itemWasSelected = false,
272 items = this.getItems();
273
274 // If we are not already selecting an item, always make sure
275 // that the top item is selected
276 if ( !this.userSelecting ) {
277 // Select the first item in the list
278 for ( i = 0; i < items.length; i++ ) {
279 if (
280 !( items[ i ] instanceof OO.ui.MenuSectionOptionWidget ) &&
281 items[ i ].isVisible()
282 ) {
283 itemWasSelected = true;
284 this.selectItem( items[ i ] );
285 break;
286 }
287 }
288
289 if ( !itemWasSelected ) {
290 this.selectItem( null );
291 }
292 }
293
294 this.noResults.toggle( !this.getItems().some( function ( item ) {
295 return item.isVisible();
296 } ) );
297 };
298
299 /**
300 * Get the option widget that matches the model given
301 *
302 * @param {mw.rcfilters.dm.ItemModel} model Item model
303 * @return {mw.rcfilters.ui.ItemMenuOptionWidget} Option widget
304 */
305 MenuSelectWidget.prototype.getItemFromModel = function ( model ) {
306 this.lazyMenuCreation();
307 return this.views[ model.getGroupModel().getView() ].filter( function ( item ) {
308 return item.getName() === model.getName();
309 } )[ 0 ];
310 };
311
312 /**
313 * @inheritdoc
314 */
315 MenuSelectWidget.prototype.onDocumentKeyDown = function ( e ) {
316 var nextItem,
317 currentItem = this.findHighlightedItem() || this.findSelectedItem();
318
319 // Call parent
320 MenuSelectWidget.parent.prototype.onDocumentKeyDown.call( this, e );
321
322 // We want to select the item on arrow movement
323 // rather than just highlight it, like the menu
324 // does by default
325 if ( !this.isDisabled() && this.isVisible() ) {
326 switch ( e.keyCode ) {
327 case OO.ui.Keys.UP:
328 case OO.ui.Keys.LEFT:
329 // Get the next item
330 nextItem = this.findRelativeSelectableItem( currentItem, -1 );
331 break;
332 case OO.ui.Keys.DOWN:
333 case OO.ui.Keys.RIGHT:
334 // Get the next item
335 nextItem = this.findRelativeSelectableItem( currentItem, 1 );
336 break;
337 }
338
339 nextItem = nextItem && nextItem.constructor.static.selectable ?
340 nextItem : null;
341
342 // Select the next item
343 this.selectItem( nextItem );
344 }
345 };
346
347 /**
348 * Scroll to the top of the menu
349 */
350 MenuSelectWidget.prototype.scrollToTop = function () {
351 this.$body.scrollTop( 0 );
352 };
353
354 /**
355 * Set whether the user is currently selecting an item.
356 * This is important when the user selects an item that is in between
357 * different views, and makes sure we do not re-select a different
358 * item (like the item on top) when this is happening.
359 *
360 * @param {boolean} isSelecting User is selecting
361 */
362 MenuSelectWidget.prototype.setUserSelecting = function ( isSelecting ) {
363 this.userSelecting = !!isSelecting;
364 };
365
366 module.exports = MenuSelectWidget;