Change delimiter for multiple namespaces and tags
[lhc/web/wiklou.git] / resources / src / mediawiki.rcfilters / ui / mw.rcfilters.ui.FilterTagMultiselectWidget.js
1 ( function ( mw ) {
2 /**
3 * List displaying all filter groups
4 *
5 * @extends OO.ui.MenuTagMultiselectWidget
6 * @mixins OO.ui.mixin.PendingElement
7 *
8 * @constructor
9 * @param {mw.rcfilters.Controller} controller Controller
10 * @param {mw.rcfilters.dm.FiltersViewModel} model View model
11 * @param {Object} config Configuration object
12 * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
13 */
14 mw.rcfilters.ui.FilterTagMultiselectWidget = function MwRcfiltersUiFilterTagMultiselectWidget( controller, model, config ) {
15 var title = new OO.ui.LabelWidget( {
16 label: mw.msg( 'rcfilters-activefilters' ),
17 classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-content-title' ]
18 } ),
19 $contentWrapper = $( '<div>' )
20 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper' );
21
22 config = config || {};
23
24 this.controller = controller;
25 this.model = model;
26 this.$overlay = config.$overlay || this.$element;
27
28 // Parent
29 mw.rcfilters.ui.FilterTagMultiselectWidget.parent.call( this, $.extend( true, {
30 label: mw.msg( 'rcfilters-filterlist-title' ),
31 placeholder: mw.msg( 'rcfilters-empty-filter' ),
32 inputPosition: 'outline',
33 allowArbitrary: false,
34 allowDisplayInvalidTags: false,
35 allowReordering: false,
36 $overlay: this.$overlay,
37 menu: {
38 hideWhenOutOfView: false,
39 hideOnChoose: false,
40 width: 650,
41 $footer: $( '<div>' )
42 .append(
43 new OO.ui.ButtonWidget( {
44 framed: false,
45 icon: 'feedback',
46 flags: [ 'progressive' ],
47 label: mw.msg( 'rcfilters-filterlist-feedbacklink' ),
48 href: 'https://www.mediawiki.org/wiki/Help_talk:New_filters_for_edit_review'
49 } ).$element
50 )
51 },
52 input: {
53 icon: 'search',
54 placeholder: mw.msg( 'rcfilters-search-placeholder' )
55 }
56 }, config ) );
57
58 this.resetButton = new OO.ui.ButtonWidget( {
59 framed: false,
60 classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-resetButton' ]
61 } );
62
63 this.emptyFilterMessage = new OO.ui.LabelWidget( {
64 label: mw.msg( 'rcfilters-empty-filter' ),
65 classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-emptyFilters' ]
66 } );
67 this.$content.append( this.emptyFilterMessage.$element );
68
69 // Events
70 this.resetButton.connect( this, { click: 'onResetButtonClick' } );
71 // Stop propagation for mousedown, so that the widget doesn't
72 // trigger the focus on the input and scrolls up when we click the reset button
73 this.resetButton.$element.on( 'mousedown', function ( e ) { e.stopPropagation(); } );
74 this.model.connect( this, {
75 initialize: 'onModelInitialize',
76 itemUpdate: 'onModelItemUpdate',
77 highlightChange: 'onModelHighlightChange'
78 } );
79
80 // Build the content
81 $contentWrapper.append(
82 title.$element,
83 $( '<div>' )
84 .addClass( 'mw-rcfilters-ui-table' )
85 .append(
86 // The filter list and button should appear side by side regardless of how
87 // wide the button is; the button also changes its width depending
88 // on language and its state, so the safest way to present both side
89 // by side is with a table layout
90 $( '<div>' )
91 .addClass( 'mw-rcfilters-ui-row' )
92 .append(
93 this.$content
94 .addClass( 'mw-rcfilters-ui-cell' )
95 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-filters' ),
96 $( '<div>' )
97 .addClass( 'mw-rcfilters-ui-cell' )
98 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-reset' )
99 .append( this.resetButton.$element )
100 )
101 )
102 );
103
104 // Initialize
105 this.$handle.append( $contentWrapper );
106 this.emptyFilterMessage.toggle( this.isEmpty() );
107
108 this.$element
109 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget' );
110
111 this.populateFromModel();
112 this.reevaluateResetRestoreState();
113 };
114
115 /* Initialization */
116
117 OO.inheritClass( mw.rcfilters.ui.FilterTagMultiselectWidget, OO.ui.MenuTagMultiselectWidget );
118
119 /* Methods */
120
121 /**
122 * Respond to menu toggle
123 *
124 * @param {boolean} isVisible Menu is visible
125 */
126 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onMenuToggle = function ( isVisible ) {
127 // Parent
128 mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onMenuToggle.call( this );
129
130 if ( isVisible ) {
131 mw.hook( 'RcFilters.popup.open' ).fire();
132
133 if ( !this.getMenu().getSelectedItem() ) {
134 // If there are no selected items, scroll menu to top
135 // This has to be in a setTimeout so the menu has time
136 // to be positioned and fixed
137 setTimeout( function () { this.getMenu().scrollToTop(); }.bind( this ), 0 );
138 }
139 } else {
140 // Clear selection
141 this.selectTag( null );
142 }
143 };
144
145 /**
146 * @inheritdoc
147 */
148 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onInputFocus = function () {
149 // Parent
150 mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onInputFocus.call( this );
151
152 // Scroll to top
153 this.scrollToTop( this.$element );
154 };
155
156 /**
157 * @inheridoc
158 */
159 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onChangeTags = function () {
160 // Parent method
161 mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onChangeTags.call( this );
162
163 this.emptyFilterMessage.toggle( this.isEmpty() );
164 };
165
166 /**
167 * Respond to model initialize event
168 */
169 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelInitialize = function () {
170 this.populateFromModel();
171 };
172
173 /**
174 * Respond to model itemUpdate event
175 *
176 * @param {mw.rcfilters.dm.FilterItem} item Filter item model
177 */
178 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelItemUpdate = function ( item ) {
179 if (
180 item.isSelected() ||
181 (
182 this.model.isHighlightEnabled() &&
183 item.isHighlightSupported() &&
184 item.getHighlightColor()
185 )
186 ) {
187 this.addTag( item.getName(), item.getLabel() );
188 } else {
189 this.removeTagByData( item.getName() );
190 }
191
192 // Re-evaluate reset state
193 this.reevaluateResetRestoreState();
194 };
195
196 /**
197 * @inheritdoc
198 */
199 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.isAllowedData = function ( data ) {
200 return (
201 this.menu.getItemFromData( data ) &&
202 !this.isDuplicateData( data )
203 );
204 };
205
206 /**
207 * @inheritdoc
208 */
209 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onMenuChoose = function ( item ) {
210 this.controller.toggleFilterSelect( item.model.getName() );
211
212 // Select the tag if it exists, or reset selection otherwise
213 this.selectTag( this.getItemFromData( item.model.getName() ) );
214
215 this.focus();
216 };
217
218 /**
219 * Respond to highlightChange event
220 *
221 * @param {boolean} isHighlightEnabled Highlight is enabled
222 */
223 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelHighlightChange = function ( isHighlightEnabled ) {
224 var highlightedItems = this.model.getHighlightedItems();
225
226 if ( isHighlightEnabled ) {
227 // Add capsule widgets
228 highlightedItems.forEach( function ( filterItem ) {
229 this.addTag( filterItem.getName(), filterItem.getLabel() );
230 }.bind( this ) );
231 } else {
232 // Remove capsule widgets if they're not selected
233 highlightedItems.forEach( function ( filterItem ) {
234 if ( !filterItem.isSelected() ) {
235 this.removeTagByData( filterItem.getName() );
236 }
237 }.bind( this ) );
238 }
239 };
240
241 /**
242 * @inheritdoc
243 */
244 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onTagSelect = function ( tagItem ) {
245 var widget = this,
246 menuOption = this.menu.getItemFromData( tagItem.getData() ),
247 oldInputValue = this.input.getValue();
248
249 // Reset input
250 this.input.setValue( '' );
251
252 // Parent method
253 mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onTagSelect.call( this, tagItem );
254
255 this.menu.selectItem( menuOption );
256 this.selectTag( tagItem );
257
258 // Scroll to the item
259 if ( oldInputValue ) {
260 // We're binding a 'once' to the itemVisibilityChange event
261 // so this happens when the menu is ready after the items
262 // are visible again, in case this is done right after the
263 // user filtered the results
264 this.getMenu().once(
265 'itemVisibilityChange',
266 function () { widget.scrollToTop( menuOption.$element ); }
267 );
268 } else {
269 this.scrollToTop( menuOption.$element );
270 }
271 };
272
273 /**
274 * Select a tag by reference. This is what OO.ui.SelectWidget is doing.
275 * If no items are given, reset selection from all.
276 *
277 * @param {mw.rcfilters.ui.FilterTagItemWidget} [item] Tag to select,
278 * omit to deselect all
279 */
280 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.selectTag = function ( item ) {
281 var i, len, selected;
282
283 for ( i = 0, len = this.items.length; i < len; i++ ) {
284 selected = this.items[ i ] === item;
285 if ( this.items[ i ].isSelected() !== selected ) {
286 this.items[ i ].toggleSelected( selected );
287 }
288 }
289 };
290 /**
291 * @inheritdoc
292 */
293 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onTagRemove = function ( tagItem ) {
294 // Parent method
295 mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onTagRemove.call( this, tagItem );
296
297 this.controller.clearFilter( tagItem.getName() );
298
299 tagItem.destroy();
300 };
301
302 /**
303 * Respond to click event on the reset button
304 */
305 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onResetButtonClick = function () {
306 if ( this.model.areCurrentFiltersEmpty() ) {
307 // Reset to default filters
308 this.controller.resetToDefaults();
309 } else {
310 // Reset to have no filters
311 this.controller.emptyFilters();
312 }
313 };
314
315 /**
316 * Reevaluate the restore state for the widget between setting to defaults and clearing all filters
317 */
318 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.reevaluateResetRestoreState = function () {
319 var defaultsAreEmpty = this.model.areDefaultFiltersEmpty(),
320 currFiltersAreEmpty = this.model.areCurrentFiltersEmpty(),
321 hideResetButton = currFiltersAreEmpty && defaultsAreEmpty;
322
323 this.resetButton.setIcon(
324 currFiltersAreEmpty ? 'history' : 'trash'
325 );
326
327 this.resetButton.setLabel(
328 currFiltersAreEmpty ? mw.msg( 'rcfilters-restore-default-filters' ) : ''
329 );
330 this.resetButton.setTitle(
331 currFiltersAreEmpty ? null : mw.msg( 'rcfilters-clear-all-filters' )
332 );
333
334 this.resetButton.toggle( !hideResetButton );
335 this.emptyFilterMessage.toggle( currFiltersAreEmpty );
336 };
337
338 /**
339 * @inheritdoc
340 */
341 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.createMenuWidget = function ( menuConfig ) {
342 return new mw.rcfilters.ui.FloatingMenuSelectWidget(
343 this.controller,
344 this.model,
345 $.extend( {
346 filterFromInput: true
347 }, menuConfig )
348 );
349 };
350
351 /**
352 * Populate the menu from the model
353 */
354 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.populateFromModel = function () {
355 var widget = this,
356 items = [];
357
358 // Reset
359 this.getMenu().clearItems();
360
361 $.each( this.model.getFilterGroups(), function ( groupName, groupModel ) {
362 items.push(
363 // Group section
364 new mw.rcfilters.ui.FilterMenuSectionOptionWidget(
365 widget.controller,
366 groupModel,
367 {
368 $overlay: widget.$overlay
369 }
370 )
371 );
372
373 // Add items
374 widget.model.getGroupFilters( groupName ).forEach( function ( filterItem ) {
375 items.push(
376 new mw.rcfilters.ui.FilterMenuOptionWidget(
377 widget.controller,
378 filterItem,
379 {
380 $overlay: widget.$overlay
381 }
382 )
383 );
384 } );
385 } );
386
387 // Add all items to the menu
388 this.getMenu().addItems( items );
389 };
390
391 /**
392 * @inheritdoc
393 */
394 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.createTagItemWidget = function ( data ) {
395 var filterItem = this.model.getItemByName( data );
396
397 if ( filterItem ) {
398 return new mw.rcfilters.ui.FilterTagItemWidget(
399 this.controller,
400 filterItem,
401 {
402 $overlay: this.$overlay
403 }
404 );
405 }
406 };
407
408 /**
409 * Scroll the element to top within its container
410 *
411 * @private
412 * @param {jQuery} $element Element to position
413 * @param {number} [marginFromTop] When scrolling the entire widget to the top, leave this
414 * much space (in pixels) above the widget.
415 */
416 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.scrollToTop = function ( $element, marginFromTop ) {
417 var container = OO.ui.Element.static.getClosestScrollableContainer( $element[ 0 ], 'y' ),
418 pos = OO.ui.Element.static.getRelativePosition( $element, $( container ) ),
419 containerScrollTop = $( container ).is( 'body, html' ) ? 0 : $( container ).scrollTop();
420
421 // Scroll to item
422 $( container ).animate( {
423 scrollTop: containerScrollTop + pos.top - ( marginFromTop || 0 )
424 } );
425 };
426 }( mediaWiki ) );