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