Add namespace names for Atikamekw (atj)
[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 {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
12 * @param {Object} config Configuration object
13 * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
14 */
15 mw.rcfilters.ui.FilterTagMultiselectWidget = function MwRcfiltersUiFilterTagMultiselectWidget( controller, model, savedQueriesModel, config ) {
16 var rcFiltersRow,
17 areSavedQueriesEnabled = mw.config.get( 'wgStructuredChangeFiltersEnableSaving' ),
18 title = new OO.ui.LabelWidget( {
19 label: mw.msg( 'rcfilters-activefilters' ),
20 classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-content-title' ]
21 } ),
22 $contentWrapper = $( '<div>' )
23 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper' );
24
25 config = config || {};
26
27 this.controller = controller;
28 this.model = model;
29 this.queriesModel = savedQueriesModel;
30 this.$overlay = config.$overlay || this.$element;
31 this.matchingQuery = null;
32 this.areSavedQueriesEnabled = areSavedQueriesEnabled;
33
34 // Parent
35 mw.rcfilters.ui.FilterTagMultiselectWidget.parent.call( this, $.extend( true, {
36 label: mw.msg( 'rcfilters-filterlist-title' ),
37 placeholder: mw.msg( 'rcfilters-empty-filter' ),
38 inputPosition: 'outline',
39 allowArbitrary: false,
40 allowDisplayInvalidTags: false,
41 allowReordering: false,
42 $overlay: this.$overlay,
43 menu: {
44 hideWhenOutOfView: false,
45 hideOnChoose: false,
46 width: 650,
47 $footer: $( '<div>' )
48 .append(
49 new OO.ui.ButtonWidget( {
50 framed: false,
51 icon: 'feedback',
52 flags: [ 'progressive' ],
53 label: mw.msg( 'rcfilters-filterlist-feedbacklink' ),
54 href: 'https://www.mediawiki.org/wiki/Help_talk:New_filters_for_edit_review'
55 } ).$element
56 )
57 },
58 input: {
59 icon: 'search',
60 placeholder: mw.msg( 'rcfilters-search-placeholder' )
61 }
62 }, config ) );
63
64 this.savedQueryTitle = new OO.ui.LabelWidget( {
65 label: '',
66 classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-content-savedQueryTitle' ]
67 } );
68
69 this.resetButton = new OO.ui.ButtonWidget( {
70 framed: false,
71 classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-resetButton' ]
72 } );
73
74 if ( areSavedQueriesEnabled ) {
75 this.saveQueryButton = new mw.rcfilters.ui.SaveFiltersPopupButtonWidget(
76 this.controller,
77 this.queriesModel
78 );
79
80 this.saveQueryButton.$element.on( 'mousedown', function ( e ) { e.stopPropagation(); } );
81
82 this.saveQueryButton.connect( this, {
83 click: 'onSaveQueryButtonClick',
84 saveCurrent: 'setSavedQueryVisibility'
85 } );
86 }
87
88 this.emptyFilterMessage = new OO.ui.LabelWidget( {
89 label: mw.msg( 'rcfilters-empty-filter' ),
90 classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-emptyFilters' ]
91 } );
92 this.$content.append( this.emptyFilterMessage.$element );
93
94 // Events
95 this.resetButton.connect( this, { click: 'onResetButtonClick' } );
96 // Stop propagation for mousedown, so that the widget doesn't
97 // trigger the focus on the input and scrolls up when we click the reset button
98 this.resetButton.$element.on( 'mousedown', function ( e ) { e.stopPropagation(); } );
99 this.model.connect( this, {
100 initialize: 'onModelInitialize',
101 update: 'onModelUpdate',
102 itemUpdate: 'onModelItemUpdate',
103 highlightChange: 'onModelHighlightChange'
104 } );
105 this.input.connect( this, { change: 'onInputChange' } );
106 this.queriesModel.connect( this, { itemUpdate: 'onSavedQueriesItemUpdate' } );
107
108 // The filter list and button should appear side by side regardless of how
109 // wide the button is; the button also changes its width depending
110 // on language and its state, so the safest way to present both side
111 // by side is with a table layout
112 rcFiltersRow = $( '<div>' )
113 .addClass( 'mw-rcfilters-ui-row' )
114 .append(
115 this.$content
116 .addClass( 'mw-rcfilters-ui-cell' )
117 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-filters' )
118 );
119
120 if ( areSavedQueriesEnabled ) {
121 rcFiltersRow.append(
122 $( '<div>' )
123 .addClass( 'mw-rcfilters-ui-cell' )
124 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-save' )
125 .append( this.saveQueryButton.$element )
126 );
127 }
128
129 rcFiltersRow.append(
130 $( '<div>' )
131 .addClass( 'mw-rcfilters-ui-cell' )
132 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-cell-reset' )
133 .append( this.resetButton.$element )
134 );
135
136 // Build the content
137 $contentWrapper.append(
138 title.$element,
139 this.savedQueryTitle.$element,
140 $( '<div>' )
141 .addClass( 'mw-rcfilters-ui-table' )
142 .append(
143 rcFiltersRow
144 )
145 );
146
147 // Initialize
148 this.$handle.append( $contentWrapper );
149 this.emptyFilterMessage.toggle( this.isEmpty() );
150 this.savedQueryTitle.toggle( false );
151
152 this.$element
153 .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget' );
154
155 this.reevaluateResetRestoreState();
156 };
157
158 /* Initialization */
159
160 OO.inheritClass( mw.rcfilters.ui.FilterTagMultiselectWidget, OO.ui.MenuTagMultiselectWidget );
161
162 /* Methods */
163
164 /**
165 * Respond to input change event
166 *
167 * @param {string} value Value of the input
168 */
169 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onInputChange = function ( value ) {
170 var view = 'default';
171
172 if ( value.indexOf( this.model.getViewTrigger( 'namespaces' ) ) === 0 ) {
173 view = 'namespaces';
174 }
175
176 this.controller.switchView( view );
177 };
178 /**
179 * Respond to query button click
180 */
181 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onSaveQueryButtonClick = function () {
182 this.getMenu().toggle( false );
183 };
184
185 /**
186 * Respond to save query item change. Mainly this is done to update the label in case
187 * a query item has been edited
188 *
189 * @param {mw.rcfilters.dm.SavedQueryItemModel} item Saved query item
190 */
191 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onSavedQueriesItemUpdate = function ( item ) {
192 if ( this.matchingQuery === item ) {
193 // This means we just edited the item that is currently matched
194 this.savedQueryTitle.setLabel( item.getLabel() );
195 }
196 };
197
198 /**
199 * Respond to menu toggle
200 *
201 * @param {boolean} isVisible Menu is visible
202 */
203 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onMenuToggle = function ( isVisible ) {
204 // Parent
205 mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onMenuToggle.call( this );
206
207 if ( isVisible ) {
208 mw.hook( 'RcFilters.popup.open' ).fire();
209
210 if ( !this.getMenu().getSelectedItem() ) {
211 // If there are no selected items, scroll menu to top
212 // This has to be in a setTimeout so the menu has time
213 // to be positioned and fixed
214 setTimeout( function () { this.getMenu().scrollToTop(); }.bind( this ), 0 );
215 }
216 } else {
217 // Clear selection
218 this.selectTag( null );
219
220 // Clear input if the only thing in the input is the prefix
221 if (
222 this.input.getValue() === this.model.getViewTrigger( this.model.getCurrentView() )
223 ) {
224 // Clear the input
225 this.input.setValue( '' );
226 }
227 }
228 };
229
230 /**
231 * @inheritdoc
232 */
233 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onInputFocus = function () {
234 // Parent
235 mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onInputFocus.call( this );
236
237 // Scroll to top
238 this.scrollToTop( this.$element );
239 };
240
241 /**
242 * @inheritdoc
243 */
244 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.doInputEscape = function () {
245 // Parent
246 mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.doInputEscape.call( this );
247
248 // Blur the input
249 this.input.$input.blur();
250 };
251
252 /**
253 * @inheridoc
254 */
255 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onChangeTags = function () {
256 // Parent method
257 mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onChangeTags.call( this );
258
259 this.emptyFilterMessage.toggle( this.isEmpty() );
260 };
261
262 /**
263 * Respond to model initialize event
264 */
265 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelInitialize = function () {
266 this.setSavedQueryVisibility();
267 };
268
269 /**
270 * Respond to model update event
271 */
272 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelUpdate = function () {
273 this.updateElementsForView();
274 };
275
276 /**
277 * Update the elements in the widget to the current view
278 */
279 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.updateElementsForView = function () {
280 var view = this.model.getCurrentView(),
281 inputValue = this.input.getValue(),
282 newInputValue = inputValue;
283
284 switch ( view ) {
285 case 'namespaces':
286 if ( inputValue.indexOf( this.model.getViewTrigger( 'namespaces' ) ) !== 0 ) {
287 // Add the prefix to the input
288 newInputValue = this.model.getViewTrigger( 'namespaces' ) + inputValue;
289 }
290 break;
291 default:
292 case 'default':
293 if ( inputValue.indexOf( this.model.getViewTrigger( 'namespaces' ) ) === 0 ) {
294 // Remove the prefix
295 newInputValue = inputValue.substr( 1 );
296 }
297 break;
298 }
299
300 // Update input
301 this.input.setValue( newInputValue );
302 };
303
304 /**
305 * Set the visibility of the saved query button
306 */
307 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.setSavedQueryVisibility = function () {
308 if ( this.areSavedQueriesEnabled ) {
309 this.matchingQuery = this.controller.findQueryMatchingCurrentState();
310
311 this.savedQueryTitle.setLabel(
312 this.matchingQuery ? this.matchingQuery.getLabel() : ''
313 );
314 this.savedQueryTitle.toggle( !!this.matchingQuery );
315 this.saveQueryButton.toggle(
316 !this.isEmpty() &&
317 !this.matchingQuery
318 );
319 }
320 };
321
322 /**
323 * Respond to model itemUpdate event
324 *
325 * @param {mw.rcfilters.dm.FilterItem} item Filter item model
326 */
327 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelItemUpdate = function ( item ) {
328 if (
329 item.isSelected() ||
330 (
331 this.model.isHighlightEnabled() &&
332 item.isHighlightSupported() &&
333 item.getHighlightColor()
334 )
335 ) {
336 this.addTag( item.getName(), item.getLabel() );
337 } else {
338 this.removeTagByData( item.getName() );
339 }
340
341 this.setSavedQueryVisibility();
342
343 // Re-evaluate reset state
344 this.reevaluateResetRestoreState();
345 };
346
347 /**
348 * @inheritdoc
349 */
350 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.isAllowedData = function ( data ) {
351 return (
352 this.model.getItemByName( data ) &&
353 !this.isDuplicateData( data )
354 );
355 };
356
357 /**
358 * @inheritdoc
359 */
360 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onMenuChoose = function ( item ) {
361 this.controller.toggleFilterSelect( item.model.getName() );
362
363 // Select the tag if it exists, or reset selection otherwise
364 this.selectTag( this.getItemFromData( item.model.getName() ) );
365
366 this.focus();
367 };
368
369 /**
370 * Respond to highlightChange event
371 *
372 * @param {boolean} isHighlightEnabled Highlight is enabled
373 */
374 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelHighlightChange = function ( isHighlightEnabled ) {
375 var highlightedItems = this.model.getHighlightedItems();
376
377 if ( isHighlightEnabled ) {
378 // Add capsule widgets
379 highlightedItems.forEach( function ( filterItem ) {
380 this.addTag( filterItem.getName(), filterItem.getLabel() );
381 }.bind( this ) );
382 } else {
383 // Remove capsule widgets if they're not selected
384 highlightedItems.forEach( function ( filterItem ) {
385 if ( !filterItem.isSelected() ) {
386 this.removeTagByData( filterItem.getName() );
387 }
388 }.bind( this ) );
389 }
390 };
391
392 /**
393 * @inheritdoc
394 */
395 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onTagSelect = function ( tagItem ) {
396 var widget = this,
397 menuOption = this.menu.getItemFromModel( tagItem.getModel() ),
398 oldInputValue = this.input.getValue();
399
400 // Reset input
401 this.input.setValue( '' );
402
403 // Switch view
404 this.controller.switchView( tagItem.getView() );
405
406 // Parent method
407 mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onTagSelect.call( this, tagItem );
408
409 this.menu.selectItem( menuOption );
410 this.selectTag( tagItem );
411
412 // Scroll to the item
413 if ( oldInputValue ) {
414 // We're binding a 'once' to the itemVisibilityChange event
415 // so this happens when the menu is ready after the items
416 // are visible again, in case this is done right after the
417 // user filtered the results
418 this.getMenu().once(
419 'itemVisibilityChange',
420 function () { widget.scrollToTop( menuOption.$element ); }
421 );
422 } else {
423 this.scrollToTop( menuOption.$element );
424 }
425 };
426
427 /**
428 * Select a tag by reference. This is what OO.ui.SelectWidget is doing.
429 * If no items are given, reset selection from all.
430 *
431 * @param {mw.rcfilters.ui.FilterTagItemWidget} [item] Tag to select,
432 * omit to deselect all
433 */
434 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.selectTag = function ( item ) {
435 var i, len, selected;
436
437 for ( i = 0, len = this.items.length; i < len; i++ ) {
438 selected = this.items[ i ] === item;
439 if ( this.items[ i ].isSelected() !== selected ) {
440 this.items[ i ].toggleSelected( selected );
441 }
442 }
443 };
444 /**
445 * @inheritdoc
446 */
447 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onTagRemove = function ( tagItem ) {
448 // Parent method
449 mw.rcfilters.ui.FilterTagMultiselectWidget.parent.prototype.onTagRemove.call( this, tagItem );
450
451 this.controller.clearFilter( tagItem.getName() );
452
453 tagItem.destroy();
454 };
455
456 /**
457 * Respond to click event on the reset button
458 */
459 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onResetButtonClick = function () {
460 if ( this.model.areCurrentFiltersEmpty() ) {
461 // Reset to default filters
462 this.controller.resetToDefaults();
463 } else {
464 // Reset to have no filters
465 this.controller.emptyFilters();
466 }
467 };
468
469 /**
470 * Reevaluate the restore state for the widget between setting to defaults and clearing all filters
471 */
472 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.reevaluateResetRestoreState = function () {
473 var defaultsAreEmpty = this.model.areDefaultFiltersEmpty(),
474 currFiltersAreEmpty = this.model.areCurrentFiltersEmpty(),
475 hideResetButton = currFiltersAreEmpty && defaultsAreEmpty;
476
477 this.resetButton.setIcon(
478 currFiltersAreEmpty ? 'history' : 'trash'
479 );
480
481 this.resetButton.setLabel(
482 currFiltersAreEmpty ? mw.msg( 'rcfilters-restore-default-filters' ) : ''
483 );
484 this.resetButton.setTitle(
485 currFiltersAreEmpty ? null : mw.msg( 'rcfilters-clear-all-filters' )
486 );
487
488 this.resetButton.toggle( !hideResetButton );
489 this.emptyFilterMessage.toggle( currFiltersAreEmpty );
490 };
491
492 /**
493 * @inheritdoc
494 */
495 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.createMenuWidget = function ( menuConfig ) {
496 return new mw.rcfilters.ui.MenuSelectWidget(
497 this.controller,
498 this.model,
499 $.extend( {
500 filterFromInput: true
501 }, menuConfig )
502 );
503 };
504
505 /**
506 * @inheritdoc
507 */
508 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.createTagItemWidget = function ( data ) {
509 var filterItem = this.model.getItemByName( data );
510
511 if ( filterItem ) {
512 return new mw.rcfilters.ui.FilterTagItemWidget(
513 this.controller,
514 filterItem,
515 {
516 $overlay: this.$overlay
517 }
518 );
519 }
520 };
521
522 /**
523 * Scroll the element to top within its container
524 *
525 * @private
526 * @param {jQuery} $element Element to position
527 * @param {number} [marginFromTop] When scrolling the entire widget to the top, leave this
528 * much space (in pixels) above the widget.
529 */
530 mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.scrollToTop = function ( $element, marginFromTop ) {
531 var container = OO.ui.Element.static.getClosestScrollableContainer( $element[ 0 ], 'y' ),
532 pos = OO.ui.Element.static.getRelativePosition( $element, $( container ) ),
533 containerScrollTop = $( container ).is( 'body, html' ) ? 0 : $( container ).scrollTop();
534
535 // Scroll to item
536 $( container ).animate( {
537 scrollTop: containerScrollTop + pos.top - ( marginFromTop || 0 )
538 } );
539 };
540 }( mediaWiki ) );