Merge "includes/api: Replace implicitly-Bugzilla bug numbers with Phab ones"
[lhc/web/wiklou.git] / resources / src / mediawiki.rcfilters / ui / mw.rcfilters.ui.FilterCapsuleMultiselectWidget.js
1 ( function ( mw, $ ) {
2 /**
3 * Filter-specific CapsuleMultiselectWidget
4 *
5 * @class
6 * @extends OO.ui.CapsuleMultiselectWidget
7 *
8 * @constructor
9 * @param {mw.rcfilters.Controller} controller RCFilters controller
10 * @param {mw.rcfilters.dm.FiltersViewModel} model RCFilters view model
11 * @param {OO.ui.InputWidget} filterInput A filter input that focuses the capsule widget
12 * @param {Object} config Configuration object
13 * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
14 * @cfg {number} [topScrollOffset=10] When scrolling the entire widget to the top, leave this
15 * much space (in pixels) above the widget.
16 */
17 mw.rcfilters.ui.FilterCapsuleMultiselectWidget = function MwRcfiltersUiFilterCapsuleMultiselectWidget( controller, model, filterInput, config ) {
18 var title = new OO.ui.LabelWidget( {
19 label: mw.msg( 'rcfilters-activefilters' ),
20 classes: [ 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-wrapper-content-title' ]
21 } ),
22 $contentWrapper = $( '<div>' )
23 .addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-wrapper' );
24
25 this.$overlay = config.$overlay || this.$element;
26
27 // Parent
28 mw.rcfilters.ui.FilterCapsuleMultiselectWidget.parent.call( this, $.extend( true, {
29 popup: { $autoCloseIgnore: filterInput.$element.add( this.$overlay ) }
30 }, config ) );
31
32 this.controller = controller;
33 this.model = model;
34 this.filterInput = filterInput;
35
36 this.topScrollOffset = config.topScrollOffset || 10;
37
38 this.resetButton = new OO.ui.ButtonWidget( {
39 icon: 'trash',
40 framed: false,
41 title: mw.msg( 'rcfilters-clear-all-filters' ),
42 classes: [ 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-resetButton' ]
43 } );
44
45 this.emptyFilterMessage = new OO.ui.LabelWidget( {
46 label: mw.msg( 'rcfilters-empty-filter' ),
47 classes: [ 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-emptyFilters' ]
48 } );
49 this.$content.append( this.emptyFilterMessage.$element );
50
51 // Events
52 this.resetButton.connect( this, { click: 'onResetButtonClick' } );
53 this.model.connect( this, {
54 itemUpdate: 'onModelItemUpdate',
55 highlightChange: 'onModelHighlightChange'
56 } );
57 this.popup.connect( this, { toggle: 'onPopupToggle' } );
58
59 // Add the filterInput as trigger
60 this.filterInput.$input
61 .on( 'focus', this.focus.bind( this ) );
62
63 // Build the content
64 $contentWrapper.append(
65 title.$element,
66 $( '<div>' )
67 .addClass( 'mw-rcfilters-ui-table' )
68 .append(
69 // The filter list and button should appear side by side regardless of how
70 // wide the button is; the button also changes its width depending
71 // on language and its state, so the safest way to present both side
72 // by side is with a table layout
73 $( '<div>' )
74 .addClass( 'mw-rcfilters-ui-row' )
75 .append(
76 this.$content
77 .addClass( 'mw-rcfilters-ui-cell' )
78 .addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-cell-filters' ),
79 $( '<div>' )
80 .addClass( 'mw-rcfilters-ui-cell' )
81 .addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget-cell-reset' )
82 .append( this.resetButton.$element )
83 )
84 )
85 );
86
87 // Initialize
88 this.$handle.append( $contentWrapper );
89
90 this.$element
91 .addClass( 'mw-rcfilters-ui-filterCapsuleMultiselectWidget' );
92
93 this.reevaluateResetRestoreState();
94 };
95
96 /* Initialization */
97
98 OO.inheritClass( mw.rcfilters.ui.FilterCapsuleMultiselectWidget, OO.ui.CapsuleMultiselectWidget );
99
100 /* Events */
101
102 /**
103 * @event remove
104 * @param {string[]} filters Array of names of removed filters
105 *
106 * Filters were removed
107 */
108
109 /* Methods */
110
111 /**
112 * Respond to model itemUpdate event
113 *
114 * @param {mw.rcfilters.dm.FilterItem} item Filter item model
115 */
116 mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onModelItemUpdate = function ( item ) {
117 if (
118 item.isSelected() ||
119 (
120 this.model.isHighlightEnabled() &&
121 item.isHighlightSupported() &&
122 item.getHighlightColor()
123 )
124 ) {
125 this.addItemByName( item.getName() );
126 } else {
127 this.removeItemByName( item.getName() );
128 }
129
130 // Re-evaluate reset state
131 this.reevaluateResetRestoreState();
132 };
133
134 /**
135 * Respond to highlightChange event
136 *
137 * @param {boolean} isHighlightEnabled Highlight is enabled
138 */
139 mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onModelHighlightChange = function ( isHighlightEnabled ) {
140 var highlightedItems = this.model.getHighlightedItems();
141
142 if ( isHighlightEnabled ) {
143 // Add capsule widgets
144 highlightedItems.forEach( function ( filterItem ) {
145 this.addItemByName( filterItem.getName() );
146 }.bind( this ) );
147 } else {
148 // Remove capsule widgets if they're not selected
149 highlightedItems.forEach( function ( filterItem ) {
150 if ( !filterItem.isSelected() ) {
151 this.removeItemByName( filterItem.getName() );
152 }
153 }.bind( this ) );
154 }
155 };
156
157 /**
158 * Respond to click event on the reset button
159 */
160 mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onResetButtonClick = function () {
161 if ( this.model.areCurrentFiltersEmpty() ) {
162 // Reset to default filters
163 this.controller.resetToDefaults();
164 } else {
165 // Reset to have no filters
166 this.controller.emptyFilters();
167 }
168 };
169
170 /**
171 * Respond to popup toggle event
172 *
173 * @param {boolean} isVisible Popup is visible
174 */
175 mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onPopupToggle = function ( isVisible ) {
176 if ( isVisible ) {
177 this.scrollToTop();
178 }
179 };
180
181 /**
182 * Scroll the capsule to the top of the screen
183 */
184 mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.scrollToTop = function () {
185 var container = OO.ui.Element.static.getClosestScrollableContainer( this.$element[ 0 ], 'y' );
186
187 $( container ).animate( {
188 scrollTop: this.$element.offset().top - this.topScrollOffset
189 } );
190 };
191
192 /**
193 * Reevaluate the restore state for the widget between setting to defaults and clearing all filters
194 */
195 mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.reevaluateResetRestoreState = function () {
196 var defaultsAreEmpty = this.model.areDefaultFiltersEmpty(),
197 currFiltersAreEmpty = this.model.areCurrentFiltersEmpty(),
198 hideResetButton = currFiltersAreEmpty && defaultsAreEmpty;
199
200 this.resetButton.setIcon(
201 currFiltersAreEmpty ? 'history' : 'trash'
202 );
203
204 this.resetButton.setLabel(
205 currFiltersAreEmpty ? mw.msg( 'rcfilters-restore-default-filters' ) : ''
206 );
207
208 this.resetButton.toggle( !hideResetButton );
209 this.emptyFilterMessage.toggle( currFiltersAreEmpty );
210 };
211
212 /**
213 * @inheritdoc
214 */
215 mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.createItemWidget = function ( data ) {
216 var item = this.model.getItemByName( data );
217
218 if ( !item ) {
219 return;
220 }
221
222 return new mw.rcfilters.ui.CapsuleItemWidget(
223 this.controller,
224 item,
225 { $overlay: this.$overlay }
226 );
227 };
228
229 /**
230 * Add items by their filter name
231 *
232 * @param {string} name Filter name
233 */
234 mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.addItemByName = function ( name ) {
235 var item = this.model.getItemByName( name );
236
237 if ( !item ) {
238 return;
239 }
240
241 // Check that the item isn't already added
242 if ( !this.getItemFromData( name ) ) {
243 this.addItems( [ this.createItemWidget( name ) ] );
244 }
245 };
246
247 /**
248 * Remove items by their filter name
249 *
250 * @param {string} name Filter name
251 */
252 mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.removeItemByName = function ( name ) {
253 this.removeItemsFromData( [ name ] );
254 };
255
256 /**
257 * @inheritdoc
258 */
259 mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.focus = function () {
260 // Override this method; we don't want to focus on the popup, and we
261 // don't want to bind the size to the handle.
262 if ( !this.isDisabled() ) {
263 this.popup.toggle( true );
264 this.filterInput.$input.get( 0 ).focus();
265 }
266 return this;
267 };
268
269 /**
270 * @inheritdoc
271 */
272 mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onFocusForPopup = function () {
273 // HACK can be removed once I21b8cff4048 is merged in oojs-ui
274 this.focus();
275 };
276
277 /**
278 * @inheritdoc
279 */
280 mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onKeyDown = function () {};
281
282 /**
283 * @inheritdoc
284 */
285 mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onPopupFocusOut = function () {};
286
287 /**
288 * @inheritdoc
289 */
290 mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.clearInput = function () {
291 if ( this.filterInput ) {
292 this.filterInput.setValue( '' );
293 }
294 this.menu.toggle( false );
295 this.menu.selectItem();
296 this.menu.highlightItem();
297 };
298
299 /**
300 * @inheritdoc
301 */
302 mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.removeItems = function ( items ) {
303 // Parent call
304 mw.rcfilters.ui.FilterCapsuleMultiselectWidget.parent.prototype.removeItems.call( this, items );
305
306 // Destroy the item widget when it is removed
307 // This is done because we re-add items by recreating them, rather than hiding them
308 // and items include popups, that will just continue to be created and appended
309 // unnecessarily.
310 items.forEach( function ( widget ) {
311 widget.destroy();
312 } );
313 };
314
315 /**
316 * Override 'editItem' since it tries to use $input which does
317 * not exist when a popup is available.
318 */
319 mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.editItem = function () {};
320 }( mediaWiki, jQuery ) );