RCFilters: Add 'views' concept and a namespace view to RCFilters
[lhc/web/wiklou.git] / resources / src / mediawiki.rcfilters / ui / mw.rcfilters.ui.MenuSelectWidget.js
1 ( function ( mw ) {
2 /**
3 * A floating menu widget for the filter list
4 *
5 * @extends OO.ui.MenuSelectWidget
6 *
7 * @constructor
8 * @param {mw.rcfilters.Controller} controller Controller
9 * @param {mw.rcfilters.dm.FiltersViewModel} model View model
10 * @param {Object} [config] Configuration object
11 * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
12 * @cfg {jQuery} [$footer] An optional footer for the menu
13 */
14 mw.rcfilters.ui.MenuSelectWidget = function MwRcfiltersUiMenuSelectWidget( controller, model, config ) {
15 var header;
16
17 config = config || {};
18
19 this.controller = controller;
20 this.model = model;
21 this.currentView = '';
22 this.views = {};
23
24 this.inputValue = '';
25 this.$overlay = config.$overlay || this.$element;
26 this.$footer = config.$footer;
27 this.$body = $( '<div>' )
28 .addClass( 'mw-rcfilters-ui-menuSelectWidget-body' );
29
30 // Parent
31 mw.rcfilters.ui.MenuSelectWidget.parent.call( this, $.extend( {
32 $autoCloseIgnore: this.$overlay,
33 width: 650
34 }, config ) );
35 this.setGroupElement(
36 $( '<div>' )
37 .addClass( 'mw-rcfilters-ui-menuSelectWidget-group' )
38 );
39 this.setClippableElement( this.$body );
40 this.setClippableContainer( this.$element );
41
42 header = new mw.rcfilters.ui.FilterMenuHeaderWidget(
43 this.controller,
44 this.model,
45 {
46 $overlay: this.$overlay
47 }
48 );
49
50 this.noResults = new OO.ui.LabelWidget( {
51 label: mw.msg( 'rcfilters-filterlist-noresults' ),
52 classes: [ 'mw-rcfilters-ui-menuSelectWidget-noresults' ]
53 } );
54
55 // Events
56 this.model.connect( this, {
57 update: 'onModelUpdate',
58 initialize: 'onModelInitialize'
59 } );
60
61 // Initialization
62 this.$element
63 .addClass( 'mw-rcfilters-ui-menuSelectWidget' )
64 .append( header.$element )
65 .append(
66 this.$body
67 .append( this.$group, this.noResults.$element )
68 );
69
70 if ( this.$footer ) {
71 this.$element.append(
72 this.$footer
73 .addClass( 'mw-rcfilters-ui-menuSelectWidget-footer' )
74 );
75 }
76 this.switchView( this.model.getCurrentView() );
77 };
78
79 /* Initialize */
80
81 OO.inheritClass( mw.rcfilters.ui.MenuSelectWidget, OO.ui.MenuSelectWidget );
82
83 /* Events */
84
85 /**
86 * @event itemVisibilityChange
87 *
88 * Item visibility has changed
89 */
90
91 /* Methods */
92
93 /**
94 * Respond to model update event
95 */
96 mw.rcfilters.ui.MenuSelectWidget.prototype.onModelUpdate = function () {
97 // Change view
98 this.switchView( this.model.getCurrentView() );
99 };
100
101 /**
102 * Respond to model initialize event. Populate the menu from the model
103 */
104 mw.rcfilters.ui.MenuSelectWidget.prototype.onModelInitialize = function () {
105 var widget = this,
106 viewGroupCount = {},
107 groups = this.model.getFilterGroups();
108
109 // Reset
110 this.clearItems();
111
112 // Count groups per view
113 $.each( groups, function ( groupName, groupModel ) {
114 viewGroupCount[ groupModel.getView() ] = viewGroupCount[ groupModel.getView() ] || 0;
115 viewGroupCount[ groupModel.getView() ]++;
116 } );
117
118 $.each( groups, function ( groupName, groupModel ) {
119 var currentItems = [],
120 view = groupModel.getView();
121
122 if ( viewGroupCount[ view ] > 1 ) {
123 // Only add a section header if there is more than
124 // one group
125 currentItems.push(
126 // Group section
127 new mw.rcfilters.ui.FilterMenuSectionOptionWidget(
128 widget.controller,
129 groupModel,
130 {
131 $overlay: widget.$overlay
132 }
133 )
134 );
135 }
136
137 // Add items
138 widget.model.getGroupFilters( groupName ).forEach( function ( filterItem ) {
139 currentItems.push(
140 new mw.rcfilters.ui.FilterMenuOptionWidget(
141 widget.controller,
142 filterItem,
143 {
144 $overlay: widget.$overlay
145 }
146 )
147 );
148 } );
149
150 // Cache the items per view, so we can switch between them
151 // without rebuilding the widgets each time
152 widget.views[ view ] = widget.views[ view ] || [];
153 widget.views[ view ] = widget.views[ view ].concat( currentItems );
154 } );
155
156 this.switchView( this.model.getCurrentView() );
157 };
158
159 /**
160 * Switch view
161 *
162 * @param {string} [viewName] View name. If not given, default is used.
163 */
164 mw.rcfilters.ui.MenuSelectWidget.prototype.switchView = function ( viewName ) {
165 viewName = viewName || 'default';
166
167 if ( this.views[ viewName ] && this.currentView !== viewName ) {
168 this.clearItems();
169 this.addItems( this.views[ viewName ] );
170
171 this.$element
172 .data( 'view', viewName )
173 .removeClass( 'mw-rcfilters-ui-menuSelectWidget-view-' + this.currentView )
174 .addClass( 'mw-rcfilters-ui-menuSelectWidget-view-' + viewName );
175
176 this.currentView = viewName;
177 }
178 };
179
180 /**
181 * @fires itemVisibilityChange
182 * @inheritdoc
183 */
184 mw.rcfilters.ui.MenuSelectWidget.prototype.updateItemVisibility = function () {
185 var i,
186 itemWasSelected = false,
187 inputVal = this.$input.val(),
188 items = this.getItems();
189
190 // Since the method hides/shows items, we don't want to
191 // call it unless the input actually changed
192 if ( this.inputValue !== inputVal ) {
193 // Parent method
194 mw.rcfilters.ui.MenuSelectWidget.parent.prototype.updateItemVisibility.call( this );
195
196 // Select the first item in the list
197 for ( i = 0; i < items.length; i++ ) {
198 if (
199 !( items[ i ] instanceof OO.ui.MenuSectionOptionWidget ) &&
200 items[ i ].isVisible()
201 ) {
202 itemWasSelected = true;
203 this.selectItem( items[ i ] );
204 break;
205 }
206 }
207
208 if ( !itemWasSelected ) {
209 this.selectItem( null );
210 }
211
212 // Cache value
213 this.inputValue = inputVal;
214
215 this.emit( 'itemVisibilityChange' );
216 }
217 };
218
219 /**
220 * Get the option widget that matches the model given
221 *
222 * @param {mw.rcfilters.dm.ItemModel} model Item model
223 * @return {mw.rcfilters.ui.ItemMenuOptionWidget} Option widget
224 */
225 mw.rcfilters.ui.MenuSelectWidget.prototype.getItemFromModel = function ( model ) {
226 return this.views[ model.getGroupModel().getView() ].filter( function ( item ) {
227 return item.getName() === model.getName();
228 } )[ 0 ];
229 };
230
231 /**
232 * Override the item matcher to use the model's match process
233 *
234 * @inheritdoc
235 */
236 mw.rcfilters.ui.MenuSelectWidget.prototype.getItemMatcher = function ( s ) {
237 var results = this.model.findMatches( s, true );
238
239 return function ( item ) {
240 return results.indexOf( item.getModel() ) > -1;
241 };
242 };
243
244 /**
245 * @inheritdoc
246 */
247 mw.rcfilters.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
248 var nextItem,
249 currentItem = this.getHighlightedItem() || this.getSelectedItem();
250
251 // Call parent
252 mw.rcfilters.ui.MenuSelectWidget.parent.prototype.onKeyDown.call( this, e );
253
254 // We want to select the item on arrow movement
255 // rather than just highlight it, like the menu
256 // does by default
257 if ( !this.isDisabled() && this.isVisible() ) {
258 switch ( e.keyCode ) {
259 case OO.ui.Keys.UP:
260 case OO.ui.Keys.LEFT:
261 // Get the next item
262 nextItem = this.getRelativeSelectableItem( currentItem, -1 );
263 break;
264 case OO.ui.Keys.DOWN:
265 case OO.ui.Keys.RIGHT:
266 // Get the next item
267 nextItem = this.getRelativeSelectableItem( currentItem, 1 );
268 break;
269 }
270
271 nextItem = nextItem && nextItem.constructor.static.selectable ?
272 nextItem : null;
273
274 // Select the next item
275 this.selectItem( nextItem );
276 }
277 };
278
279 /**
280 * Scroll to the top of the menu
281 */
282 mw.rcfilters.ui.MenuSelectWidget.prototype.scrollToTop = function () {
283 this.$body.scrollTop( 0 );
284 };
285 }( mediaWiki ) );