Merge "filerepo: clean up remote description cache keys"
[lhc/web/wiklou.git] / resources / src / mediawiki.rcfilters / ui / mw.rcfilters.ui.ChangesListWrapperWidget.js
1 ( function ( mw ) {
2 /**
3 * List of changes
4 *
5 * @extends OO.ui.Widget
6 *
7 * @constructor
8 * @param {mw.rcfilters.dm.FiltersViewModel} filtersViewModel View model
9 * @param {mw.rcfilters.dm.ChangesListViewModel} changesListViewModel View model
10 * @param {mw.rcfilters.Controller} controller
11 * @param {jQuery} $changesListRoot Root element of the changes list to attach to
12 * @param {Object} [config] Configuration object
13 */
14 mw.rcfilters.ui.ChangesListWrapperWidget = function MwRcfiltersUiChangesListWrapperWidget(
15 filtersViewModel,
16 changesListViewModel,
17 controller,
18 $changesListRoot,
19 config
20 ) {
21 config = $.extend( {}, config, {
22 $element: $changesListRoot
23 } );
24
25 // Parent
26 mw.rcfilters.ui.ChangesListWrapperWidget.parent.call( this, config );
27
28 this.filtersViewModel = filtersViewModel;
29 this.changesListViewModel = changesListViewModel;
30 this.controller = controller;
31 this.highlightClasses = null;
32 this.filtersModelInitialized = false;
33
34 // Events
35 this.filtersViewModel.connect( this, {
36 itemUpdate: 'onItemUpdate',
37 highlightChange: 'onHighlightChange',
38 initialize: 'onFiltersModelInitialize'
39 } );
40 this.changesListViewModel.connect( this, {
41 invalidate: 'onModelInvalidate',
42 update: 'onModelUpdate'
43 } );
44
45 this.$element
46 .addClass( 'mw-rcfilters-ui-changesListWrapperWidget' )
47 // We handle our own display/hide of the empty results message
48 // We keep the timeout class here and remove it later, since at this
49 // stage it is still needed to identify that the timeout occurred.
50 .removeClass( 'mw-changeslist-empty' );
51 };
52
53 /* Initialization */
54
55 OO.inheritClass( mw.rcfilters.ui.ChangesListWrapperWidget, OO.ui.Widget );
56
57 /**
58 * Respond to filters model initialize event
59 */
60 mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onFiltersModelInitialize = function () {
61 this.filtersModelInitialized = true;
62 };
63
64 /**
65 * Get all available highlight classes
66 *
67 * @return {string[]} An array of available highlight class names
68 */
69 mw.rcfilters.ui.ChangesListWrapperWidget.prototype.getHighlightClasses = function () {
70 if ( !this.highlightClasses || !this.highlightClasses.length ) {
71 this.highlightClasses = this.filtersViewModel.getItemsSupportingHighlights()
72 .map( function ( filterItem ) {
73 return filterItem.getCssClass();
74 } );
75 }
76
77 return this.highlightClasses;
78 };
79
80 /**
81 * Respond to the highlight feature being toggled on and off
82 *
83 * @param {boolean} highlightEnabled
84 */
85 mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onHighlightChange = function ( highlightEnabled ) {
86 if ( highlightEnabled ) {
87 this.applyHighlight();
88 } else {
89 this.clearHighlight();
90 }
91 };
92
93 /**
94 * Respond to a filter item model update
95 */
96 mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onItemUpdate = function () {
97 if ( this.filtersModelInitialized && this.filtersViewModel.isHighlightEnabled() ) {
98 this.clearHighlight();
99 this.applyHighlight();
100 }
101 };
102
103 /**
104 * Respond to changes list model invalidate
105 */
106 mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onModelInvalidate = function () {
107 $( 'body' ).addClass( 'mw-rcfilters-ui-loading' );
108 };
109
110 /**
111 * Respond to changes list model update
112 *
113 * @param {jQuery|string} $changesListContent The content of the updated changes list
114 * @param {jQuery} $fieldset The content of the updated fieldset
115 * @param {string} noResultsDetails Type of no result error
116 * @param {boolean} isInitialDOM Whether $changesListContent is the existing (already attached) DOM
117 * @param {boolean} from Timestamp of the new changes
118 */
119 mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onModelUpdate = function (
120 $changesListContent, $fieldset, noResultsDetails, isInitialDOM, from
121 ) {
122 var conflictItem,
123 $message = $( '<div>' )
124 .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-results' ),
125 isEmpty = $changesListContent === 'NO_RESULTS',
126 // For enhanced mode, we have to load these modules, which are
127 // not loaded for the 'regular' mode in the backend
128 loaderPromise = mw.user.options.get( 'usenewrc' ) ?
129 mw.loader.using( [ 'mediawiki.special.changeslist.enhanced', 'mediawiki.icon' ] ) :
130 $.Deferred().resolve(),
131 widget = this;
132
133 this.$element.toggleClass( 'mw-changeslist', !isEmpty );
134 if ( isEmpty ) {
135 this.$element.empty();
136
137 if ( this.filtersViewModel.hasConflict() ) {
138 conflictItem = this.filtersViewModel.getFirstConflictedItem();
139
140 $message
141 .append(
142 $( '<div>' )
143 .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-results-conflict' )
144 .text( mw.message( 'rcfilters-noresults-conflict' ).text() ),
145 $( '<div>' )
146 .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-results-message' )
147 .text( mw.message( conflictItem.getCurrentConflictResultMessage() ).text() )
148 );
149 } else {
150 $message
151 .append(
152 $( '<div>' )
153 .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-results-noresult' )
154 .text( mw.msg( this.getMsgKeyForNoResults( noResultsDetails ) ) )
155 );
156
157 // remove all classes matching mw-changeslist-*
158 this.$element.removeClass( function ( elementIndex, allClasses ) {
159 return allClasses
160 .split( ' ' )
161 .filter( function ( className ) {
162 return className.indexOf( 'mw-changeslist-' ) === 0;
163 } )
164 .join( ' ' );
165 } );
166 }
167
168 this.$element.append( $message );
169 } else {
170 if ( !isInitialDOM ) {
171 this.$element.empty().append( $changesListContent );
172
173 if ( from ) {
174 this.emphasizeNewChanges( from );
175 }
176 }
177
178 // Apply highlight
179 this.applyHighlight();
180
181 }
182
183 this.$element.prepend( $( '<div>' ).addClass( 'mw-changeslist-overlay' ) );
184
185 loaderPromise.done( function () {
186 if ( !isInitialDOM && !isEmpty ) {
187 // Make sure enhanced RC re-initializes correctly
188 mw.hook( 'wikipage.content' ).fire( widget.$element );
189 }
190
191 $( 'body' ).removeClass( 'mw-rcfilters-ui-loading' );
192 } );
193 };
194
195 /** Toggles overlay class on changes list
196 *
197 * @param {boolean} isVisible True if overlay should be visible
198 */
199 mw.rcfilters.ui.ChangesListWrapperWidget.prototype.toggleOverlay = function ( isVisible ) {
200 this.$element.toggleClass( 'mw-rcfilters-ui-changesListWrapperWidget--overlaid', isVisible );
201 };
202
203 /**
204 * Map a reason for having no results to its message key
205 *
206 * @param {string} reason One of the NO_RESULTS_* "constant" that represent
207 * a reason for having no results
208 * @return {string} Key for the message that explains why there is no results in this case
209 */
210 mw.rcfilters.ui.ChangesListWrapperWidget.prototype.getMsgKeyForNoResults = function ( reason ) {
211 var reasonMsgKeyMap = {
212 NO_RESULTS_NORMAL: 'recentchanges-noresult',
213 NO_RESULTS_TIMEOUT: 'recentchanges-timeout',
214 NO_RESULTS_NETWORK_ERROR: 'recentchanges-network',
215 NO_RESULTS_NO_TARGET_PAGE: 'recentchanges-notargetpage',
216 NO_RESULTS_INVALID_TARGET_PAGE: 'allpagesbadtitle'
217 };
218 return reasonMsgKeyMap[ reason ];
219 };
220
221 /**
222 * Emphasize the elements (or groups) newer than the 'from' parameter
223 * @param {string} from Anything newer than this is considered 'new'
224 */
225 mw.rcfilters.ui.ChangesListWrapperWidget.prototype.emphasizeNewChanges = function ( from ) {
226 var $firstNew,
227 $indicator,
228 $newChanges = $( [] ),
229 selector = this.inEnhancedMode() ?
230 'table.mw-enhanced-rc[data-mw-ts]' :
231 'li[data-mw-ts]',
232 set = this.$element.find( selector ),
233 length = set.length;
234
235 set.each( function ( index ) {
236 var $this = $( this ),
237 ts = $this.data( 'mw-ts' );
238
239 if ( ts >= from ) {
240 $newChanges = $newChanges.add( $this );
241 $firstNew = $this;
242
243 // guards against putting the marker after the last element
244 if ( index === ( length - 1 ) ) {
245 $firstNew = null;
246 }
247 }
248 } );
249
250 if ( $firstNew ) {
251 $indicator = $( '<div>' )
252 .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-previousChangesIndicator' );
253
254 $firstNew.after( $indicator );
255 }
256
257 $newChanges
258 .hide()
259 .fadeIn( 1000 );
260 };
261
262 /**
263 * In enhanced mode, we need to check whether the grouped results all have the
264 * same active highlights in order to see whether the "parent" of the group should
265 * be grey or highlighted normally.
266 *
267 * This is called every time highlights are applied.
268 */
269 mw.rcfilters.ui.ChangesListWrapperWidget.prototype.updateEnhancedParentHighlight = function () {
270 var activeHighlightClasses,
271 $enhancedTopPageCell = this.$element.find( 'table.mw-enhanced-rc.mw-collapsible' );
272
273 activeHighlightClasses = this.filtersViewModel.getCurrentlyUsedHighlightColors().map( function ( color ) {
274 return 'mw-rcfilters-highlight-color-' + color;
275 } );
276
277 // Go over top pages and their children, and figure out if all sub-pages have the
278 // same highlights between themselves. If they do, the parent should be highlighted
279 // with all colors. If classes are different, the parent should receive a grey
280 // background
281 $enhancedTopPageCell.each( function () {
282 var firstChildClasses, $rowsWithDifferentHighlights,
283 $table = $( this );
284
285 // Collect the relevant classes from the first nested child
286 firstChildClasses = activeHighlightClasses.filter( function ( className ) {
287 return $table.find( 'tr:nth-child(2)' ).hasClass( className );
288 } );
289 // Filter the non-head rows and see if they all have the same classes
290 // to the first row
291 $rowsWithDifferentHighlights = $table.find( 'tr:not(:first-child)' ).filter( function () {
292 var classesInThisRow,
293 $this = $( this );
294
295 classesInThisRow = activeHighlightClasses.filter( function ( className ) {
296 return $this.hasClass( className );
297 } );
298
299 return !OO.compare( firstChildClasses, classesInThisRow );
300 } );
301
302 // If classes are different, tag the row for using grey color
303 $table.find( 'tr:first-child' )
304 .toggleClass( 'mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey', $rowsWithDifferentHighlights.length > 0 );
305 } );
306 };
307
308 /**
309 * @return {boolean} Whether the changes are grouped by page
310 */
311 mw.rcfilters.ui.ChangesListWrapperWidget.prototype.inEnhancedMode = function () {
312 var uri = new mw.Uri();
313 return ( uri.query.enhanced !== undefined && Number( uri.query.enhanced ) ) ||
314 ( uri.query.enhanced === undefined && Number( mw.user.options.get( 'usenewrc' ) ) );
315 };
316
317 /**
318 * Apply color classes based on filters highlight configuration
319 */
320 mw.rcfilters.ui.ChangesListWrapperWidget.prototype.applyHighlight = function () {
321 if ( !this.filtersViewModel.isHighlightEnabled() ) {
322 return;
323 }
324
325 this.filtersViewModel.getHighlightedItems().forEach( function ( filterItem ) {
326 var $elements = this.$element.find( '.' + filterItem.getCssClass() );
327
328 // Add highlight class to all highlighted list items
329 $elements
330 .addClass(
331 'mw-rcfilters-highlighted ' +
332 'mw-rcfilters-highlight-color-' + filterItem.getHighlightColor()
333 );
334
335 // Track the filters for each item in .data( 'highlightedFilters' )
336 $elements.each( function () {
337 var filters = $( this ).data( 'highlightedFilters' );
338 if ( !filters ) {
339 filters = [];
340 $( this ).data( 'highlightedFilters', filters );
341 }
342 if ( filters.indexOf( filterItem.getLabel() ) === -1 ) {
343 filters.push( filterItem.getLabel() );
344 }
345 } );
346 }.bind( this ) );
347 // Apply a title to each highlighted item, with a list of filters
348 this.$element.find( '.mw-rcfilters-highlighted' ).each( function () {
349 var filters = $( this ).data( 'highlightedFilters' );
350
351 if ( filters && filters.length ) {
352 $( this ).attr( 'title', mw.msg(
353 'rcfilters-highlighted-filters-list',
354 filters.join( mw.msg( 'comma-separator' ) )
355 ) );
356 }
357
358 } );
359 if ( this.inEnhancedMode() ) {
360 this.updateEnhancedParentHighlight();
361 }
362
363 // Turn on highlights
364 this.$element.addClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlighted' );
365 };
366
367 /**
368 * Remove all color classes
369 */
370 mw.rcfilters.ui.ChangesListWrapperWidget.prototype.clearHighlight = function () {
371 // Remove highlight classes
372 mw.rcfilters.HighlightColors.forEach( function ( color ) {
373 this.$element
374 .find( '.mw-rcfilters-highlight-color-' + color )
375 .removeClass( 'mw-rcfilters-highlight-color-' + color );
376 }.bind( this ) );
377
378 this.$element.find( '.mw-rcfilters-highlighted' )
379 .removeAttr( 'title' )
380 .removeData( 'highlightedFilters' )
381 .removeClass( 'mw-rcfilters-highlighted' );
382
383 // Remove grey from enhanced rows
384 this.$element.find( '.mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey' )
385 .removeClass( 'mw-rcfilters-ui-changesListWrapperWidget-enhanced-grey' );
386
387 // Turn off highlights
388 this.$element.removeClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlighted' );
389 };
390 }( mediaWiki ) );