Merge "API: Add deprecation warnings for Iae0e2ce3"
[lhc/web/wiklou.git] / resources / src / mediawiki.rcfilters / dm / mw.rcfilters.dm.FilterGroup.js
1 ( function ( mw ) {
2 /**
3 * View model for a filter group
4 *
5 * @mixins OO.EventEmitter
6 * @mixins OO.EmitterList
7 *
8 * @constructor
9 * @param {string} name Group name
10 * @param {Object} [config] Configuration options
11 * @cfg {string} [type='send_unselected_if_any'] Group type
12 * @cfg {string} [view='default'] Name of the display group this group
13 * is a part of.
14 * @cfg {string} [title] Group title
15 * @cfg {boolean} [hidden] This group is hidden from the regular menu views
16 * @cfg {boolean} [allowArbitrary] Allows for an arbitrary value to be added to the
17 * group from the URL, even if it wasn't initially set up.
18 * @cfg {string} [separator='|'] Value separator for 'string_options' groups
19 * @cfg {boolean} [active] Group is active
20 * @cfg {boolean} [fullCoverage] This filters in this group collectively cover all results
21 * @cfg {Object} [conflicts] Defines the conflicts for this filter group
22 * @cfg {string|Object} [labelPrefixKey] An i18n key defining the prefix label for this
23 * group. If the prefix has 'invert' state, the parameter is expected to be an object
24 * with 'default' and 'inverted' as keys.
25 * @cfg {Object} [whatsThis] Defines the messages that should appear for the 'what's this' popup
26 * @cfg {string} [whatsThis.header] The header of the whatsThis popup message
27 * @cfg {string} [whatsThis.body] The body of the whatsThis popup message
28 * @cfg {string} [whatsThis.url] The url for the link in the whatsThis popup message
29 * @cfg {string} [whatsThis.linkMessage] The text for the link in the whatsThis popup message
30 */
31 mw.rcfilters.dm.FilterGroup = function MwRcfiltersDmFilterGroup( name, config ) {
32 config = config || {};
33
34 // Mixin constructor
35 OO.EventEmitter.call( this );
36 OO.EmitterList.call( this );
37
38 this.name = name;
39 this.type = config.type || 'send_unselected_if_any';
40 this.view = config.view || 'default';
41 this.title = config.title || name;
42 this.hidden = !!config.hidden;
43 this.allowArbitrary = !!config.allowArbitrary;
44 this.separator = config.separator || '|';
45 this.labelPrefixKey = config.labelPrefixKey;
46
47 this.currSelected = null;
48 this.active = !!config.active;
49 this.fullCoverage = !!config.fullCoverage;
50
51 this.whatsThis = config.whatsThis || {};
52
53 this.conflicts = config.conflicts || {};
54 this.defaultParams = {};
55
56 this.aggregate( { update: 'filterItemUpdate' } );
57 this.connect( this, { filterItemUpdate: 'onFilterItemUpdate' } );
58 };
59
60 /* Initialization */
61 OO.initClass( mw.rcfilters.dm.FilterGroup );
62 OO.mixinClass( mw.rcfilters.dm.FilterGroup, OO.EventEmitter );
63 OO.mixinClass( mw.rcfilters.dm.FilterGroup, OO.EmitterList );
64
65 /* Events */
66
67 /**
68 * @event update
69 *
70 * Group state has been updated
71 */
72
73 /* Methods */
74
75 /**
76 * Initialize the group and create its filter items
77 *
78 * @param {Object} filterDefinition Filter definition for this group
79 * @param {string|Object} [groupDefault] Definition of the group default
80 */
81 mw.rcfilters.dm.FilterGroup.prototype.initializeFilters = function ( filterDefinition, groupDefault ) {
82 var defaultParam,
83 supersetMap = {},
84 model = this,
85 items = [];
86
87 filterDefinition.forEach( function ( filter ) {
88 // Instantiate an item
89 var subsetNames = [],
90 filterItem = new mw.rcfilters.dm.FilterItem( filter.name, model, {
91 group: model.getName(),
92 label: filter.label || filter.name,
93 description: filter.description || '',
94 labelPrefixKey: model.labelPrefixKey,
95 cssClass: filter.cssClass,
96 identifiers: filter.identifiers
97 } );
98
99 if ( filter.subset ) {
100 filter.subset = filter.subset.map( function ( el ) {
101 return el.filter;
102 } );
103
104 subsetNames = [];
105
106 filter.subset.forEach( function ( subsetFilterName ) { // eslint-disable-line no-loop-func
107 // Subsets (unlike conflicts) are always inside the same group
108 // We can re-map the names of the filters we are getting from
109 // the subsets with the group prefix
110 var subsetName = model.getPrefixedName( subsetFilterName );
111 // For convenience, we should store each filter's "supersets" -- these are
112 // the filters that have that item in their subset list. This will just
113 // make it easier to go through whether the item has any other items
114 // that affect it (and are selected) at any given time
115 supersetMap[ subsetName ] = supersetMap[ subsetName ] || [];
116 mw.rcfilters.utils.addArrayElementsUnique(
117 supersetMap[ subsetName ],
118 filterItem.getName()
119 );
120
121 // Translate subset param name to add the group name, so we
122 // get consistent naming. We know that subsets are only within
123 // the same group
124 subsetNames.push( subsetName );
125 } );
126
127 // Set translated subset
128 filterItem.setSubset( subsetNames );
129 }
130
131 items.push( filterItem );
132
133 // Store default parameter state; in this case, default is defined per filter
134 if ( model.getType() === 'send_unselected_if_any' ) {
135 // Store the default parameter state
136 // For this group type, parameter values are direct
137 // We need to convert from a boolean to a string ('1' and '0')
138 model.defaultParams[ filter.name ] = String( Number( !!filter.default ) );
139 }
140 } );
141
142 // Add items
143 this.addItems( items );
144
145 // Now that we have all items, we can apply the superset map
146 this.getItems().forEach( function ( filterItem ) {
147 filterItem.setSuperset( supersetMap[ filterItem.getName() ] );
148 } );
149
150 // Store default parameter state; in this case, default is defined per the
151 // entire group, given by groupDefault method parameter
152 if ( this.getType() === 'string_options' ) {
153 // Store the default parameter group state
154 // For this group, the parameter is group name and value is the names
155 // of selected items
156 this.defaultParams[ this.getName() ] = mw.rcfilters.utils.normalizeParamOptions(
157 // Current values
158 groupDefault ?
159 groupDefault.split( this.getSeparator() ) :
160 [],
161 // Legal values
162 this.getItems().map( function ( item ) {
163 return item.getParamName();
164 } )
165 ).join( this.getSeparator() );
166 } else if ( this.getType() === 'single_option' ) {
167 defaultParam = groupDefault !== undefined ?
168 groupDefault : this.getItems()[ 0 ].getParamName();
169
170 // For this group, the parameter is the group name,
171 // and a single item can be selected: default or first item
172 this.defaultParams[ this.getName() ] = defaultParam;
173
174 // Single option means there must be a single option
175 // selected, so we have to either select the default
176 // or select the first option
177 this.selectItemByParamName( defaultParam );
178 }
179 };
180
181 /**
182 * Respond to filterItem update event
183 *
184 * @param {mw.rcfilters.dm.FilterItem} item Updated filter item
185 * @fires update
186 */
187 mw.rcfilters.dm.FilterGroup.prototype.onFilterItemUpdate = function ( item ) {
188 // Update state
189 var changed = false,
190 active = this.areAnySelected();
191
192 if (
193 item.isSelected() &&
194 this.getType() === 'single_option' &&
195 this.currSelected &&
196 this.currSelected !== item
197 ) {
198 this.currSelected.toggleSelected( false );
199 }
200
201 // For 'single_option' groups, check if we just unselected all
202 // items. This should never be the result. If we did unselect
203 // all (like resetting all filters to false) then this group
204 // must choose its default item or the first item in the group
205 if (
206 this.getType() === 'single_option' &&
207 !this.getItems().some( function ( filterItem ) {
208 return filterItem.isSelected();
209 } )
210 ) {
211 // Single option means there must be a single option
212 // selected, so we have to either select the default
213 // or select the first option
214 this.currSelected = this.getItemByParamName( this.defaultParams[ this.getName() ] );
215 this.currSelected.toggleSelected( true );
216 changed = true;
217 }
218
219 if (
220 changed ||
221 this.active !== active ||
222 this.currSelected !== item
223 ) {
224 this.active = active;
225 this.currSelected = item;
226
227 this.emit( 'update' );
228 }
229 };
230
231 /**
232 * Get group active state
233 *
234 * @return {boolean} Active state
235 */
236 mw.rcfilters.dm.FilterGroup.prototype.isActive = function () {
237 return this.active;
238 };
239
240 /**
241 * Get group hidden state
242 *
243 * @return {boolean} Hidden state
244 */
245 mw.rcfilters.dm.FilterGroup.prototype.isHidden = function () {
246 return this.hidden;
247 };
248
249 /**
250 * Get group allow arbitrary state
251 *
252 * @return {boolean} Group allows an arbitrary value from the URL
253 */
254 mw.rcfilters.dm.FilterGroup.prototype.isAllowArbitrary = function () {
255 return this.allowArbitrary;
256 };
257
258 /**
259 * Get group name
260 *
261 * @return {string} Group name
262 */
263 mw.rcfilters.dm.FilterGroup.prototype.getName = function () {
264 return this.name;
265 };
266
267 /**
268 * Get the default param state of this group
269 *
270 * @return {Object} Default param state
271 */
272 mw.rcfilters.dm.FilterGroup.prototype.getDefaultParams = function () {
273 return this.defaultParams;
274 };
275
276 /**
277 * This is for a single_option and string_options group types
278 * it returns the value of the default
279 *
280 * @return {string} Value of the default
281 */
282 mw.rcfilters.dm.FilterGroup.prototype.getDefaulParamValue = function () {
283 return this.defaultParams[ this.getName() ];
284 };
285 /**
286 * Get the messags defining the 'whats this' popup for this group
287 *
288 * @return {Object} What's this messages
289 */
290 mw.rcfilters.dm.FilterGroup.prototype.getWhatsThis = function () {
291 return this.whatsThis;
292 };
293
294 /**
295 * Check whether this group has a 'what's this' message
296 *
297 * @return {boolean} This group has a what's this message
298 */
299 mw.rcfilters.dm.FilterGroup.prototype.hasWhatsThis = function () {
300 return !!this.whatsThis.body;
301 };
302
303 /**
304 * Get the conflicts associated with the entire group.
305 * Conflict object is set up by filter name keys and conflict
306 * definition. For example:
307 * [
308 * {
309 * filterName: {
310 * filter: filterName,
311 * group: group1
312 * }
313 * },
314 * {
315 * filterName2: {
316 * filter: filterName2,
317 * group: group2
318 * }
319 * }
320 * ]
321 * @return {Object} Conflict definition
322 */
323 mw.rcfilters.dm.FilterGroup.prototype.getConflicts = function () {
324 return this.conflicts;
325 };
326
327 /**
328 * Set conflicts for this group. See #getConflicts for the expected
329 * structure of the definition.
330 *
331 * @param {Object} conflicts Conflicts for this group
332 */
333 mw.rcfilters.dm.FilterGroup.prototype.setConflicts = function ( conflicts ) {
334 this.conflicts = conflicts;
335 };
336
337 /**
338 * Set conflicts for each filter item in the group based on the
339 * given conflict map
340 *
341 * @param {Object} conflicts Object representing the conflict map,
342 * keyed by the item name, where its value is an object for all its conflicts
343 */
344 mw.rcfilters.dm.FilterGroup.prototype.setFilterConflicts = function ( conflicts ) {
345 this.getItems().forEach( function ( filterItem ) {
346 if ( conflicts[ filterItem.getName() ] ) {
347 filterItem.setConflicts( conflicts[ filterItem.getName() ] );
348 }
349 } );
350 };
351
352 /**
353 * Check whether this item has a potential conflict with the given item
354 *
355 * This checks whether the given item is in the list of conflicts of
356 * the current item, but makes no judgment about whether the conflict
357 * is currently at play (either one of the items may not be selected)
358 *
359 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item
360 * @return {boolean} This item has a conflict with the given item
361 */
362 mw.rcfilters.dm.FilterGroup.prototype.existsInConflicts = function ( filterItem ) {
363 return Object.prototype.hasOwnProperty.call( this.getConflicts(), filterItem.getName() );
364 };
365
366 /**
367 * Check whether there are any items selected
368 *
369 * @return {boolean} Any items in the group are selected
370 */
371 mw.rcfilters.dm.FilterGroup.prototype.areAnySelected = function () {
372 return this.getItems().some( function ( filterItem ) {
373 return filterItem.isSelected();
374 } );
375 };
376
377 /**
378 * Check whether all items selected
379 *
380 * @return {boolean} All items are selected
381 */
382 mw.rcfilters.dm.FilterGroup.prototype.areAllSelected = function () {
383 var selected = [],
384 unselected = [];
385
386 this.getItems().forEach( function ( filterItem ) {
387 if ( filterItem.isSelected() ) {
388 selected.push( filterItem );
389 } else {
390 unselected.push( filterItem );
391 }
392 } );
393
394 if ( unselected.length === 0 ) {
395 return true;
396 }
397
398 // check if every unselected is a subset of a selected
399 return unselected.every( function ( unselectedFilterItem ) {
400 return selected.some( function ( selectedFilterItem ) {
401 return selectedFilterItem.existsInSubset( unselectedFilterItem.getName() );
402 } );
403 } );
404 };
405
406 /**
407 * Get all selected items in this group
408 *
409 * @param {mw.rcfilters.dm.FilterItem} [excludeItem] Item to exclude from the list
410 * @return {mw.rcfilters.dm.FilterItem[]} Selected items
411 */
412 mw.rcfilters.dm.FilterGroup.prototype.getSelectedItems = function ( excludeItem ) {
413 var excludeName = ( excludeItem && excludeItem.getName() ) || '';
414
415 return this.getItems().filter( function ( item ) {
416 return item.getName() !== excludeName && item.isSelected();
417 } );
418 };
419
420 /**
421 * Check whether all selected items are in conflict with the given item
422 *
423 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
424 * @return {boolean} All selected items are in conflict with this item
425 */
426 mw.rcfilters.dm.FilterGroup.prototype.areAllSelectedInConflictWith = function ( filterItem ) {
427 var selectedItems = this.getSelectedItems( filterItem );
428
429 return selectedItems.length > 0 &&
430 (
431 // The group as a whole is in conflict with this item
432 this.existsInConflicts( filterItem ) ||
433 // All selected items are in conflict individually
434 selectedItems.every( function ( selectedFilter ) {
435 return selectedFilter.existsInConflicts( filterItem );
436 } )
437 );
438 };
439
440 /**
441 * Check whether any of the selected items are in conflict with the given item
442 *
443 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item to test
444 * @return {boolean} Any of the selected items are in conflict with this item
445 */
446 mw.rcfilters.dm.FilterGroup.prototype.areAnySelectedInConflictWith = function ( filterItem ) {
447 var selectedItems = this.getSelectedItems( filterItem );
448
449 return selectedItems.length > 0 && (
450 // The group as a whole is in conflict with this item
451 this.existsInConflicts( filterItem ) ||
452 // Any selected items are in conflict individually
453 selectedItems.some( function ( selectedFilter ) {
454 return selectedFilter.existsInConflicts( filterItem );
455 } )
456 );
457 };
458
459 /**
460 * Get the parameter representation from this group
461 *
462 * @param {Object} [filterRepresentation] An object defining the state
463 * of the filters in this group, keyed by their name and current selected
464 * state value.
465 * @return {Object} Parameter representation
466 */
467 mw.rcfilters.dm.FilterGroup.prototype.getParamRepresentation = function ( filterRepresentation ) {
468 var values,
469 areAnySelected = false,
470 buildFromCurrentState = !filterRepresentation,
471 result = {},
472 model = this,
473 filterParamNames = {},
474 getSelectedParameter = function ( filters ) {
475 var item,
476 selected = [];
477
478 // Find if any are selected
479 $.each( filters, function ( name, value ) {
480 if ( value ) {
481 selected.push( name );
482 }
483 } );
484
485 item = model.getItemByName( selected[ 0 ] );
486 return ( item && item.getParamName() ) || '';
487 };
488
489 filterRepresentation = filterRepresentation || {};
490
491 // Create or complete the filterRepresentation definition
492 this.getItems().forEach( function ( item ) {
493 // Map filter names to their parameter names
494 filterParamNames[ item.getName() ] = item.getParamName();
495
496 if ( buildFromCurrentState ) {
497 // This means we have not been given a filter representation
498 // so we are building one based on current state
499 filterRepresentation[ item.getName() ] = item.isSelected();
500 } else if ( !filterRepresentation[ item.getName() ] ) {
501 // We are given a filter representation, but we have to make
502 // sure that we fill in the missing filters if there are any
503 // we will assume they are all falsey
504 filterRepresentation[ item.getName() ] = false;
505 }
506
507 if ( filterRepresentation[ item.getName() ] ) {
508 areAnySelected = true;
509 }
510 } );
511
512 // Build result
513 if ( this.getType() === 'send_unselected_if_any' ) {
514 // First, check if any of the items are selected at all.
515 // If none is selected, we're treating it as if they are
516 // all false
517
518 // Go over the items and define the correct values
519 $.each( filterRepresentation, function ( name, value ) {
520 // We must store all parameter values as strings '0' or '1'
521 result[ filterParamNames[ name ] ] = areAnySelected ?
522 String( Number( !value ) ) :
523 '0';
524 } );
525 } else if ( this.getType() === 'string_options' ) {
526 values = [];
527
528 $.each( filterRepresentation, function ( name, value ) {
529 // Collect values
530 if ( value ) {
531 values.push( filterParamNames[ name ] );
532 }
533 } );
534
535 result[ this.getName() ] = ( values.length === Object.keys( filterRepresentation ).length ) ?
536 'all' : values.join( this.getSeparator() );
537 } else if ( this.getType() === 'single_option' ) {
538 result[ this.getName() ] = getSelectedParameter( filterRepresentation );
539 }
540
541 return result;
542 };
543
544 /**
545 * Get the filter representation this group would provide
546 * based on given parameter states.
547 *
548 * @param {Object|string} [paramRepresentation] An object defining a parameter
549 * state to translate the filter state from. If not given, an object
550 * representing all filters as falsey is returned; same as if the parameter
551 * given were an empty object, or had some of the filters missing.
552 * @return {Object} Filter representation
553 */
554 mw.rcfilters.dm.FilterGroup.prototype.getFilterRepresentation = function ( paramRepresentation ) {
555 var areAnySelected, paramValues, defaultValue, item,
556 oneWasSelected = false,
557 model = this,
558 paramToFilterMap = {},
559 result = {};
560
561 if ( this.getType() === 'send_unselected_if_any' ) {
562 paramRepresentation = paramRepresentation || {};
563 // Expand param representation to include all filters in the group
564 this.getItems().forEach( function ( filterItem ) {
565 var paramName = filterItem.getParamName();
566
567 paramRepresentation[ paramName ] = paramRepresentation[ paramName ] || '0';
568 paramToFilterMap[ paramName ] = filterItem;
569
570 if ( Number( paramRepresentation[ filterItem.getParamName() ] ) ) {
571 areAnySelected = true;
572 }
573 } );
574
575 $.each( paramRepresentation, function ( paramName, paramValue ) {
576 var filterItem = paramToFilterMap[ paramName ];
577
578 // Flip the definition between the parameter
579 // state and the filter state
580 // This is what the 'toggleSelected' value of the filter is
581 result[ filterItem.getName() ] = areAnySelected ?
582 !Number( paramValue ) :
583 // Otherwise, there are no selected items in the
584 // group, which means the state is false
585 false;
586 } );
587 } else if ( this.getType() === 'string_options' ) {
588 paramRepresentation = paramRepresentation || '';
589
590 // Normalize the given parameter values
591 paramValues = mw.rcfilters.utils.normalizeParamOptions(
592 // Given
593 paramRepresentation.split(
594 this.getSeparator()
595 ),
596 // Allowed values
597 this.getItems().map( function ( filterItem ) {
598 return filterItem.getParamName();
599 } )
600 );
601 // Translate the parameter values into a filter selection state
602 this.getItems().forEach( function ( filterItem ) {
603 // All true (either because all values are written or the term 'all' is written)
604 // is the same as all filters set to true
605 result[ filterItem.getName() ] = (
606 // If it is the word 'all'
607 paramValues.length === 1 && paramValues[ 0 ] === 'all' ||
608 // All values are written
609 paramValues.length === model.getItemCount()
610 ) ?
611 true :
612 // Otherwise, the filter is selected only if it appears in the parameter values
613 paramValues.indexOf( filterItem.getParamName() ) > -1;
614 } );
615 } else if ( this.getType() === 'single_option' ) {
616 // There is parameter that fits a single filter and if not, get the default
617 this.getItems().forEach( function ( filterItem ) {
618 result[ filterItem.getName() ] = filterItem.getParamName() === paramRepresentation;
619 oneWasSelected = oneWasSelected || filterItem.getParamName() === paramRepresentation;
620 } );
621 }
622
623 // Go over result and make sure all filters are represented.
624 // If any filters are missing, they will get a falsey value
625 this.getItems().forEach( function ( filterItem ) {
626 result[ filterItem.getName() ] = !!result[ filterItem.getName() ];
627 oneWasSelected = oneWasSelected || !!result[ filterItem.getName() ];
628 } );
629
630 // Make sure that at least one option is selected in
631 // single_option groups, no matter what path was taken
632 // If none was selected by the given definition, then
633 // we need to select the one in the base state -- either
634 // the default given, or the first item
635 if (
636 this.getType() === 'single_option' &&
637 !oneWasSelected
638 ) {
639 defaultValue = this.getDefaultParams();
640 item = this.getItemByParamName( defaultValue[ this.getName() ] );
641 result[ item.getName() ] = true;
642 }
643
644 return result;
645 };
646
647 /**
648 * Get item by its filter name
649 *
650 * @param {string} filterName Filter name
651 * @return {mw.rcfilters.dm.FilterItem} Filter item
652 */
653 mw.rcfilters.dm.FilterGroup.prototype.getItemByName = function ( filterName ) {
654 return this.getItems().filter( function ( item ) {
655 return item.getName() === filterName;
656 } )[ 0 ];
657 };
658
659 /**
660 * Select an item by its parameter name
661 *
662 * @param {string} paramName Filter parameter name
663 */
664 mw.rcfilters.dm.FilterGroup.prototype.selectItemByParamName = function ( paramName ) {
665 this.getItems().forEach( function ( item ) {
666 item.toggleSelected( item.getParamName() === paramName );
667 } );
668 };
669
670 /**
671 * Get item by its parameter name
672 *
673 * @param {string} paramName Parameter name
674 * @return {mw.rcfilters.dm.FilterItem} Filter item
675 */
676 mw.rcfilters.dm.FilterGroup.prototype.getItemByParamName = function ( paramName ) {
677 return this.getItems().filter( function ( item ) {
678 return item.getParamName() === paramName;
679 } )[ 0 ];
680 };
681
682 /**
683 * Get group type
684 *
685 * @return {string} Group type
686 */
687 mw.rcfilters.dm.FilterGroup.prototype.getType = function () {
688 return this.type;
689 };
690
691 /**
692 * Get display group
693 *
694 * @return {string} Display group
695 */
696 mw.rcfilters.dm.FilterGroup.prototype.getView = function () {
697 return this.view;
698 };
699
700 /**
701 * Get the prefix used for the filter names inside this group.
702 *
703 * @param {string} [name] Filter name to prefix
704 * @return {string} Group prefix
705 */
706 mw.rcfilters.dm.FilterGroup.prototype.getNamePrefix = function () {
707 return this.getName() + '__';
708 };
709
710 /**
711 * Get a filter name with the prefix used for the filter names inside this group.
712 *
713 * @param {string} name Filter name to prefix
714 * @return {string} Group prefix
715 */
716 mw.rcfilters.dm.FilterGroup.prototype.getPrefixedName = function ( name ) {
717 return this.getNamePrefix() + name;
718 };
719
720 /**
721 * Get group's title
722 *
723 * @return {string} Title
724 */
725 mw.rcfilters.dm.FilterGroup.prototype.getTitle = function () {
726 return this.title;
727 };
728
729 /**
730 * Get group's values separator
731 *
732 * @return {string} Values separator
733 */
734 mw.rcfilters.dm.FilterGroup.prototype.getSeparator = function () {
735 return this.separator;
736 };
737
738 /**
739 * Check whether the group is defined as full coverage
740 *
741 * @return {boolean} Group is full coverage
742 */
743 mw.rcfilters.dm.FilterGroup.prototype.isFullCoverage = function () {
744 return this.fullCoverage;
745 };
746 }( mediaWiki ) );