"recentchanges-submit": "Show",
"rcfilters-activefilters": "Active filters",
"rcfilters-advancedfilters": "Advanced filters",
+ "rcfilters-limit-title": "Changes to show",
+ "rcfilters-limit-shownum": "Show last $1 changes",
+ "rcfilters-days-title": "Recent days",
+ "rcfilters-hours-title": "Recent hours",
+ "rcfilters-days-show-days": "$1 {{PLURAL:$1|day|days}}",
+ "rcfilters-days-show-hours": "$1 {{PLURAL:$1|hour|hours}}",
"rcfilters-quickfilters": "Saved filters",
"rcfilters-quickfilters-placeholder-title": "No links saved yet",
"rcfilters-quickfilters-placeholder-description": "To save your filter settings and reuse them later, click the bookmark icon in the Active Filter area, below.",
"recentchanges-legend-plusminus": "{{optional}}\nA plus/minus sign with a number for the legend.",
"recentchanges-submit": "Label for submit button in [[Special:RecentChanges]]\n{{Identical|Show}}",
"rcfilters-activefilters": "Title for the filters selection showing the active filters.",
+ "rcfilters-limit-title": "Title for the options to change the number of results shown.",
+ "rcfilters-days-title": "Title for the options to change the number of days for the results shown.",
+ "rcfilters-hours-title": "Title for the options to change the number of hours for the results shown.",
+ "rcfilters-limit-shownum": "Title for the button that opens the operation to control how many results are shown. \n\nParameters: $1 - Number of results shown",
+ "rcfilters-days-show-days": "Title for the button that opens the operation to control the day range for the results. \n\nParameters: $1 - Number of days shown",
+ "rcfilters-days-show-hours": "Title for the button that opens the operation to control the hour range for the results. \n\nParameters: $1 - Number of hours shown",
"rcfilters-advancedfilters": "Title for the buttons allowing the user to switch to the various advanced filters views.",
"rcfilters-quickfilters": "Label for the button that opens the saved filter settings menu in [[Special:RecentChanges]]",
"rcfilters-quickfilters-placeholder-title": "Title for the text shown in the quick filters menu on [[Special:RecentChanges]] if the user has not saved any quick filters.",
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterMenuHeaderWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.MenuSelectWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ViewSwitchWidget.js',
+ 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ValuePickerWidget.js',
+ 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesLimitPopupWidget.js',
+ 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesLimitButtonWidget.js',
+ 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.DateButtonWidget.js',
+ 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.DatePopupWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesListWrapperWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SavedLinksListWidget.js',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterMenuHeaderWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.MenuSelectWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ViewSwitchWidget.less',
+ 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ValuePickerWidget.less',
+ 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.DatePopupWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterWrapperWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ChangesListWrapperWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.HighlightColorPickerWidget.less',
'messages' => [
'rcfilters-activefilters',
'rcfilters-advancedfilters',
+ 'rcfilters-limit-title',
+ 'rcfilters-limit-shownum',
+ 'rcfilters-days-title',
+ 'rcfilters-hours-title',
+ 'rcfilters-days-show-days',
+ 'rcfilters-days-show-hours',
'rcfilters-quickfilters',
'rcfilters-quickfilters-placeholder-title',
'rcfilters-quickfilters-placeholder-description',
* is a part of.
* @cfg {string} [title] Group title
* @cfg {boolean} [hidden] This group is hidden from the regular menu views
+ * @cfg {boolean} [allowArbitrary] Allows for an arbitrary value to be added to the
+ * group from the URL, even if it wasn't initially set up.
* @cfg {string} [separator='|'] Value separator for 'string_options' groups
* @cfg {boolean} [active] Group is active
* @cfg {boolean} [fullCoverage] This filters in this group collectively cover all results
this.view = config.view || 'default';
this.title = config.title || name;
this.hidden = !!config.hidden;
+ this.allowArbitrary = !!config.allowArbitrary;
this.separator = config.separator || '|';
this.labelPrefixKey = config.labelPrefixKey;
+ this.currSelected = null;
this.active = !!config.active;
this.fullCoverage = !!config.fullCoverage;
* @param {string|Object} [groupDefault] Definition of the group default
*/
mw.rcfilters.dm.FilterGroup.prototype.initializeFilters = function ( filterDefinition, groupDefault ) {
- var supersetMap = {},
+ var defaultParam,
+ supersetMap = {},
model = this,
items = [];
} )
).join( this.getSeparator() );
} else if ( this.getType() === 'single_option' ) {
+ defaultParam = groupDefault !== undefined ?
+ groupDefault : this.getItems()[ 0 ].getParamName();
+
// For this group, the parameter is the group name,
- // and a single item can be selected, or none at all
- // The item also must be recognized or none is set as
- // default
- model.defaultParams[ this.getName() ] = this.getItemByParamName( groupDefault ) ? groupDefault : '';
+ // and a single item can be selected: default or first item
+ this.defaultParams[ this.getName() ] = defaultParam;
+
+ // Single option means there must be a single option
+ // selected, so we have to either select the default
+ // or select the first option
+ this.selectItemByParamName( defaultParam );
}
};
*/
mw.rcfilters.dm.FilterGroup.prototype.onFilterItemUpdate = function ( item ) {
// Update state
- var active = this.areAnySelected(),
- itemName = item && item.getName();
-
- if ( item.isSelected() && this.getType() === 'single_option' ) {
- // Change the selection to only be the newly selected item
- this.getItems().forEach( function ( filterItem ) {
- if ( filterItem.getName() !== itemName ) {
- filterItem.toggleSelected( false );
- }
- } );
+ var active = this.areAnySelected();
+
+ if (
+ item.isSelected() &&
+ this.getType() === 'single_option' &&
+ this.currSelected &&
+ this.currSelected !== item
+ ) {
+ this.currSelected.toggleSelected( false );
}
- if ( this.active !== active ) {
+ if (
+ this.active !== active ||
+ this.currSelected !== item
+ ) {
this.active = active;
+ this.currSelected = item;
+
this.emit( 'update' );
}
};
return this.hidden;
};
+ /**
+ * Get group allow arbitrary state
+ *
+ * @return {boolean} Group allows an arbitrary value from the URL
+ */
+ mw.rcfilters.dm.FilterGroup.prototype.isAllowArbitrary = function () {
+ return this.allowArbitrary;
+ };
+
/**
* Get group name
*
return this.defaultParams;
};
+ /**
+ * This is for a single_option and string_options group types
+ * it returns the value of the default
+ *
+ * @return {string} Value of the default
+ */
+ mw.rcfilters.dm.FilterGroup.prototype.getDefaulParamValue = function () {
+ return this.defaultParams[ this.getName() ];
+ };
/**
* Get the messags defining the 'whats this' popup for this group
*
* @return {Object} Filter representation
*/
mw.rcfilters.dm.FilterGroup.prototype.getFilterRepresentation = function ( paramRepresentation ) {
- var areAnySelected, paramValues,
+ var areAnySelected, paramValues, defaultValue, item,
+ oneWasSelected = false,
model = this,
paramToFilterMap = {},
result = {};
paramValues.indexOf( filterItem.getParamName() ) > -1;
} );
} else if ( this.getType() === 'single_option' ) {
- // There is parameter that fits a single filter, or none at all
+ // There is parameter that fits a single filter and if not, get the default
this.getItems().forEach( function ( filterItem ) {
result[ filterItem.getName() ] = filterItem.getParamName() === paramRepresentation;
+ oneWasSelected = oneWasSelected || filterItem.getParamName() === paramRepresentation;
} );
}
// If any filters are missing, they will get a falsey value
this.getItems().forEach( function ( filterItem ) {
result[ filterItem.getName() ] = !!result[ filterItem.getName() ];
+ oneWasSelected = oneWasSelected || !!result[ filterItem.getName() ];
} );
+ // Make sure that at least one option is selected in
+ // single_option groups, no matter what path was taken
+ // If none was selected by the given definition, then
+ // we need to select the one in the base state -- either
+ // the default given, or the first item
+ if (
+ this.getType() === 'single_option' &&
+ !oneWasSelected
+ ) {
+ defaultValue = this.getDefaultParams();
+ item = this.getItemByParamName( defaultValue[ this.getName() ] );
+ result[ item.getName() ] = true;
+ }
+
return result;
};
} )[ 0 ];
};
+ /**
+ * Select an item by its parameter name
+ *
+ * @param {string} paramName Filter parameter name
+ */
+ mw.rcfilters.dm.FilterGroup.prototype.selectItemByParamName = function ( paramName ) {
+ this.getItems().forEach( function ( item ) {
+ item.toggleSelected( item.getParamName() === paramName );
+ } );
+ };
+
/**
* Get item by its parameter name
*
views = {},
items = [],
uri = new mw.Uri(),
- $changesList = $( '.mw-changeslist' ).first().contents();
+ $changesList = $( '.mw-changeslist' ).first().contents(),
+ createFilterDataFromNumber = function ( num, convertedNumForLabel ) {
+ return {
+ name: String( num ),
+ label: mw.language.convertNumber( convertedNumForLabel )
+ };
+ };
// Prepare views
if ( namespaceStructure ) {
};
}
+ // Add parameter range operations
+ views.range = {
+ groups: [
+ {
+ name: 'limit',
+ type: 'single_option',
+ title: '', // Because it's a hidden group, this title actually appears nowhere
+ hidden: true,
+ allowArbitrary: true,
+ validate: $.isNumeric,
+ sortFunc: function ( a, b ) { return Number( a.name ) - Number( b.name ); },
+ 'default': '50',
+ filters: [ 50, 100, 250, 500 ].map( function ( num ) {
+ return createFilterDataFromNumber( num, num );
+ } )
+ },
+ {
+ name: 'days',
+ type: 'single_option',
+ title: '', // Because it's a hidden group, this title actually appears nowhere
+ hidden: true,
+ allowArbitrary: true,
+ validate: $.isNumeric,
+ sortFunc: function ( a, b ) { return Number( a.name ) - Number( b.name ); },
+ 'default': '7',
+ filters: [
+ // Hours (1, 2, 6, 12)
+ 0.04166, 0.0833, 0.25, 0.5,
+ // Days
+ 1, 3, 7, 14, 30
+ ].map( function ( num ) {
+ return createFilterDataFromNumber(
+ num,
+ // Convert fractions of days to number of hours for the labels
+ num < 1 ? Math.round( num * 24 ) : num
+ );
+ } )
+ }
+ ]
+ };
+
+ // Before we do anything, we need to see if we require another item in the
+ // groups that have 'AllowArbitrary'. For the moment, those are only single_option
+ // groups; if we ever expand it, this might need further generalization:
+ $.each( views, function ( viewName, viewData ) {
+ viewData.groups.forEach( function ( groupData ) {
+ // This is only true for single_option and string_options
+ // We assume these are the only groups that will allow for
+ // arbitrary, since it doesn't make any sense for the other
+ // groups.
+ var uriValue = uri.query[ groupData.name ];
+
+ if (
+ // If the group allows for arbitrary data
+ groupData.allowArbitrary &&
+ // and it is single_option (or string_options, but we
+ // don't have cases of those yet, nor do we plan to)
+ groupData.type === 'single_option' &&
+ // and if there is a valid value in the URI already
+ uri.query[ groupData.name ] !== undefined &&
+ // and, if there is a validate method and it passes on
+ // the data
+ ( !groupData.validate || groupData.validate( uri.query[ groupData.name ] ) ) &&
+ // but if that value isn't already in the definition
+ groupData.filters
+ .map( function ( filterData ) {
+ return filterData.name;
+ } )
+ .indexOf( uri.query[ groupData.name ] ) === -1
+ ) {
+ // Add the filter information
+ if ( groupData.name === 'days' ) {
+ // Specific fix for hours/days which go by the same param
+ groupData.filters.push( createFilterDataFromNumber(
+ uriValue,
+ // In this case we don't want to round because it can be arbitrary
+ // weird numbers but we want to round to 2 decimal digits
+ Number( uriValue ) < 1 ?
+ ( Number( uriValue ) * 24 ).toFixed( 2 ) :
+ Number( uriValue )
+ ) );
+ } else {
+ groupData.filters.push( createFilterDataFromNumber( uriValue, uriValue ) );
+ }
+
+ // If there's a sort function set up, re-sort the values
+ if ( groupData.sortFunc ) {
+ groupData.filters.sort( groupData.sortFunc );
+ }
+ }
+ } );
+ } );
+
// Initialize the model
this.filtersModel.initializeFilters( filterStructure, views );
// wiki default.
// Any subsequent change of the URL through the RCFilters
// system will receive 'urlversion=2'
- var base = this.getVersion( uriQuery ) === 2 ?
- {} :
- this.filtersModel.getDefaultParams();
+ var hiddenParamDefaults = {},
+ base = this.getVersion( uriQuery ) === 2 ?
+ {} :
+ this.filtersModel.getDefaultParams();
+
+ // Go over the model and get all hidden parameters' defaults
+ // These defaults should be applied regardless of the urlversion
+ // but be overridden by the URL params if they exist
+ $.each( this.filtersModel.getFilterGroups(), function ( groupName, groupModel ) {
+ if ( groupModel.isHidden() ) {
+ $.extend( true, hiddenParamDefaults, groupModel.getDefaultParams() );
+ }
+ } );
return this.minimizeQuery(
- $.extend( true, {}, base, uriQuery, { urlversion: '2' } )
+ $.extend( true, {}, hiddenParamDefaults, base, uriQuery, { urlversion: '2' } )
);
};
--- /dev/null
+.mw-rcfilters-ui-datePopupWidget {
+ &-days {
+ margin-top: 1em;
+ }
+}
&-bottom {
margin-top: 1em;
+
+ .mw-rcfilters-ui-changesLimitButtonWidget,
+ .mw-rcfilters-ui-dateButtonWidget {
+ display: inline-block;
+
+ &:not( :first-child ) {
+ margin-left: 0.5em;
+ }
+ }
}
}
.mw-rcfilters-ui-liveUpdateButtonWidget {
+ margin-left: 1em;
+
&.oo-ui-toggleWidget-on {
position: relative;
overflow: hidden;
--- /dev/null
+.mw-rcfilters-ui-valuePickerWidget {
+ &-title {
+ display: block;
+ font-weight: bold;
+ margin-bottom: 0.5em;
+ }
+}
--- /dev/null
+( function ( mw ) {
+ /**
+ * Widget defining the button controlling the popup for the number of results
+ *
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller Controller
+ * @param {mw.rcfilters.dm.FiltersViewModel} model View model
+ * @param {Object} [config] Configuration object
+ * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
+ */
+ mw.rcfilters.ui.ChangesLimitButtonWidget = function MwRcfiltersUiChangesLimitWidget( controller, model, config ) {
+ config = config || {};
+
+ // Parent
+ mw.rcfilters.ui.ChangesLimitButtonWidget.parent.call( this, config );
+
+ this.controller = controller;
+ this.model = model;
+
+ this.$overlay = config.$overlay || this.$element;
+
+ this.button = null;
+ this.limitGroupModel = null;
+
+ this.model.connect( this, {
+ initialize: 'onModelInitialize'
+ } );
+
+ this.$element
+ .addClass( 'mw-rcfilters-ui-changesLimitButtonWidget' );
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.rcfilters.ui.ChangesLimitButtonWidget, OO.ui.Widget );
+
+ /**
+ * Respond to model initialize event
+ */
+ mw.rcfilters.ui.ChangesLimitButtonWidget.prototype.onModelInitialize = function () {
+ var changesLimitPopupWidget, selectedItem, currentValue;
+
+ this.limitGroupModel = this.model.getGroup( 'limit' );
+
+ // HACK: We need the model to be ready before we populate the button
+ // and the widget, because we require the filter items for the
+ // limit and their events. This addition is only done after the
+ // model is initialized.
+ // Note: This will be fixed soon!
+ if ( this.limitGroupModel ) {
+ changesLimitPopupWidget = new mw.rcfilters.ui.ChangesLimitPopupWidget(
+ this.limitGroupModel
+ );
+
+ selectedItem = this.limitGroupModel.getSelectedItems()[ 0 ];
+ currentValue = ( selectedItem && selectedItem.getLabel() ) ||
+ mw.language.convertNumber( this.limitGroupModel.getDefaultParamValue() );
+
+ this.button = new OO.ui.PopupButtonWidget( {
+ indicator: 'down',
+ label: mw.msg( 'rcfilters-limit-shownum', currentValue ),
+ $overlay: this.$overlay,
+ popup: {
+ width: 300,
+ padded: true,
+ anchor: false,
+ align: 'backwards',
+ $autoCloseIgnore: this.$overlay,
+ $content: changesLimitPopupWidget.$element
+ }
+ } );
+
+ // Events
+ this.limitGroupModel.connect( this, { update: 'onLimitGroupModelUpdate' } );
+ changesLimitPopupWidget.connect( this, { limit: 'onPopupLimit' } );
+
+ this.$element.append( this.button.$element );
+ }
+ };
+
+ /**
+ * Respond to popup limit change event
+ *
+ * @param {string} filterName Chosen filter name
+ */
+ mw.rcfilters.ui.ChangesLimitButtonWidget.prototype.onPopupLimit = function ( filterName ) {
+ this.controller.toggleFilterSelect( filterName, true );
+ };
+
+ /**
+ * Respond to limit choose event
+ *
+ * @param {string} filterName Filter name
+ */
+ mw.rcfilters.ui.ChangesLimitButtonWidget.prototype.onLimitGroupModelUpdate = function () {
+ var item = this.limitGroupModel.getSelectedItems()[ 0 ],
+ label = item && item.getLabel();
+
+ // Update the label
+ if ( label ) {
+ this.button.setLabel( mw.msg( 'rcfilters-limit-shownum', label ) );
+ }
+ };
+
+}( mediaWiki ) );
--- /dev/null
+( function ( mw ) {
+ /**
+ * Widget defining the popup to choose number of results
+ *
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.dm.FilterGroup} model Group model for 'limit'
+ * @param {Object} [config] Configuration object
+ */
+ mw.rcfilters.ui.ChangesLimitPopupWidget = function MwRcfiltersUiChangesLimitPopupWidget( model, config ) {
+ config = config || {};
+
+ // Parent
+ mw.rcfilters.ui.ChangesLimitPopupWidget.parent.call( this, config );
+
+ this.model = model;
+
+ this.valuePicker = new mw.rcfilters.ui.ValuePickerWidget(
+ this.model,
+ {
+ label: mw.msg( 'rcfilters-limit-title' )
+ }
+ );
+
+ // Events
+ this.valuePicker.connect( this, { choose: [ 'emit', 'limit' ] } );
+
+ // Initialize
+ this.$element
+ .addClass( 'mw-rcfilters-ui-changesLimitPopupWidget' )
+ .append( this.valuePicker.$element );
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.rcfilters.ui.ChangesLimitPopupWidget, OO.ui.Widget );
+
+ /* Events */
+
+ /**
+ * @event limit
+ * @param {string} name Item name
+ *
+ * A limit item was chosen
+ */
+}( mediaWiki ) );
--- /dev/null
+( function ( mw ) {
+ /**
+ * Widget defining the button controlling the popup for the date range for the results
+ *
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller Controller
+ * @param {mw.rcfilters.dm.FiltersViewModel} model View model
+ * @param {Object} [config] Configuration object
+ * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
+ */
+ mw.rcfilters.ui.DateButtonWidget = function MwRcfiltersUiDateButtonWidget( controller, model, config ) {
+ config = config || {};
+
+ // Parent
+ mw.rcfilters.ui.ChangesLimitButtonWidget.parent.call( this, config );
+
+ this.controller = controller;
+ this.model = model;
+
+ this.$overlay = config.$overlay || this.$element;
+
+ this.button = null;
+ this.daysGroupModel = null;
+
+ this.model.connect( this, {
+ initialize: 'onModelInitialize'
+ } );
+
+ this.$element
+ .addClass( 'mw-rcfilters-ui-dateButtonWidget' );
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.rcfilters.ui.DateButtonWidget, OO.ui.Widget );
+
+ /**
+ * Respond to model initialize event
+ */
+ mw.rcfilters.ui.DateButtonWidget.prototype.onModelInitialize = function () {
+ var datePopupWidget;
+
+ this.daysGroupModel = this.model.getGroup( 'days' );
+
+ // HACK: We need the model to be ready before we populate the button
+ // and the widget, because we require the filter items for the
+ // limit and their events. This addition is only done after the
+ // model is initialized.
+ // Note: This will be fixed soon!
+ if ( this.daysGroupModel ) {
+ datePopupWidget = new mw.rcfilters.ui.DatePopupWidget(
+ this.daysGroupModel
+ );
+
+ this.button = new OO.ui.PopupButtonWidget( {
+ indicator: 'down',
+ icon: 'calendar',
+ $overlay: this.$overlay,
+ popup: {
+ width: 300,
+ padded: true,
+ anchor: false,
+ align: 'backwards',
+ $autoCloseIgnore: this.$overlay,
+ $content: datePopupWidget.$element
+ }
+ } );
+ this.updateButtonLabel();
+
+ // Events
+ this.daysGroupModel.connect( this, { update: 'onDaysGroupModelUpdate' } );
+ datePopupWidget.connect( this, { days: 'onPopupDays' } );
+
+ this.$element.append( this.button.$element );
+ }
+ };
+
+ /**
+ * Respond to popup limit change event
+ *
+ * @param {string} filterName Chosen filter name
+ */
+ mw.rcfilters.ui.DateButtonWidget.prototype.onPopupDays = function ( filterName ) {
+ this.controller.toggleFilterSelect( filterName, true );
+ };
+
+ /**
+ * Respond to limit choose event
+ *
+ * @param {string} filterName Filter name
+ */
+ mw.rcfilters.ui.DateButtonWidget.prototype.onDaysGroupModelUpdate = function () {
+ this.updateButtonLabel();
+ };
+
+ /**
+ * Update the button label
+ */
+ mw.rcfilters.ui.DateButtonWidget.prototype.updateButtonLabel = function () {
+ var item = this.daysGroupModel.getSelectedItems()[ 0 ];
+
+ // Update the label
+ if ( item ) {
+ this.button.setLabel(
+ mw.msg(
+ Number( item.getParamName() ) < 1 ?
+ 'rcfilters-days-show-hours' : 'rcfilters-days-show-days',
+ item.getLabel()
+ )
+ );
+ }
+ };
+}( mediaWiki ) );
--- /dev/null
+( function ( mw ) {
+ /**
+ * Widget defining the popup to choose date for the results
+ *
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {mw.rcfilters.dm.FilterGroup} model Group model for 'days'
+ * @param {Object} [config] Configuration object
+ */
+ mw.rcfilters.ui.DatePopupWidget = function MwRcfiltersUiDatePopupWidget( model, config ) {
+ config = config || {};
+
+ // Parent
+ mw.rcfilters.ui.ChangesLimitPopupWidget.parent.call( this, config );
+
+ this.model = model;
+
+ this.hoursValuePicker = new mw.rcfilters.ui.ValuePickerWidget(
+ this.model,
+ {
+ classes: [ 'mw-rcfilters-ui-datePopupWidget-hours' ],
+ label: mw.msg( 'rcfilters-hours-title' ),
+ itemFilter: function ( itemModel ) { return Number( itemModel.getParamName() ) < 1; }
+ }
+ );
+ this.daysValuePicker = new mw.rcfilters.ui.ValuePickerWidget(
+ this.model,
+ {
+ classes: [ 'mw-rcfilters-ui-datePopupWidget-days' ],
+ label: mw.msg( 'rcfilters-days-title' ),
+ itemFilter: function ( itemModel ) { return Number( itemModel.getParamName() ) >= 1; }
+ }
+ );
+
+ // Events
+ this.hoursValuePicker.connect( this, { choose: [ 'emit', 'days' ] } );
+ this.daysValuePicker.connect( this, { choose: [ 'emit', 'days' ] } );
+
+ // Initialize
+ this.$element
+ .addClass( 'mw-rcfilters-ui-datePopupWidget' )
+ .append(
+ this.hoursValuePicker.$element,
+ this.daysValuePicker.$element
+ );
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.rcfilters.ui.DatePopupWidget, OO.ui.Widget );
+
+ /* Events */
+
+ /**
+ * @event days
+ * @param {string} name Item name
+ *
+ * A days item was chosen
+ */
+}( mediaWiki ) );
* @param {mw.rcfilters.dm.FilterItem} item Filter item model
*/
mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onModelItemUpdate = function ( item ) {
+ if ( item.getGroupModel().isHidden() ) {
+ return;
+ }
+
if (
item.isSelected() ||
(
this.controller
);
+ this.numChangesWidget = new mw.rcfilters.ui.ChangesLimitButtonWidget(
+ this.controller,
+ this.model,
+ {
+ $overlay: this.$overlay
+ }
+ );
+
+ this.dateWidget = new mw.rcfilters.ui.DateButtonWidget(
+ this.controller,
+ this.model,
+ {
+ $overlay: this.$overlay
+ }
+ );
+
// Initialize
this.$element
.addClass( 'mw-rcfilters-ui-filterWrapperWidget' );
}
$bottom = $( '<div>' )
- .addClass( 'mw-rcfilters-ui-filterWrapperWidget-bottom' );
+ .addClass( 'mw-rcfilters-ui-filterWrapperWidget-bottom' )
+ .append(
+ this.numChangesWidget.$element,
+ this.dateWidget.$element
+ );
if ( mw.config.get( 'wgStructuredChangeFiltersEnableLiveUpdate' ) ) {
$bottom.append( this.liveUpdateButton.$element );
this.$element.find( '.mw-tagfilter-label' ).closest( 'tr' ).detach();
}
+ // Hide limit and days
+ this.$element.find( '.rclinks' ).detach();
+
if ( !this.$element.find( '.mw-recentchanges-table tr' ).length ) {
+ this.$element.find( '.mw-recentchanges-table' ).detach();
this.$element.find( 'hr' ).detach();
}
+ if ( !this.$element.find( '.rcshowhide' ).contents().length ) {
+ this.$element.find( '.rcshowhide' ).detach();
+ // If we're hiding rcshowhide, the '<br>'s are around it,
+ // there's no need for them either.
+ this.$element.find( 'br' ).detach();
+ }
};
}( mediaWiki ) );
--- /dev/null
+( function ( mw ) {
+ /**
+ * Widget defining the behavior used to choose from a set of values
+ * in a single_value group
+ *
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.LabelElement
+ *
+ * @constructor
+ * @param {mw.rcfilters.dm.FilterGroup} model Group model
+ * @param {Object} [config] Configuration object
+ * @cfg {Function} [itemFilter] A filter function for the items from the
+ * model. If not given, all items will be included. The function must
+ * handle item models and return a boolean whether the item is included
+ * or not. Example: function ( itemModel ) { return itemModel.isSelected(); }
+ */
+ mw.rcfilters.ui.ValuePickerWidget = function MwRcfiltersUiValuePickerWidget( model, config ) {
+ config = config || {};
+
+ // Parent
+ mw.rcfilters.ui.ValuePickerWidget.parent.call( this, config );
+ // Mixin constructors
+ OO.ui.mixin.LabelElement.call( this, config );
+
+ this.model = model;
+ this.itemFilter = config.itemFilter || function () { return true; };
+
+ // Build the selection from the item models
+ this.selectWidget = new OO.ui.ButtonSelectWidget();
+ this.initializeSelectWidget();
+
+ // Events
+ this.model.connect( this, { update: 'onModelUpdate' } );
+ this.selectWidget.connect( this, { choose: 'onSelectWidgetChoose' } );
+
+ // Initialize
+ this.$element
+ .addClass( 'mw-rcfilters-ui-valuePickerWidget' )
+ .append(
+ this.$label
+ .addClass( 'mw-rcfilters-ui-valuePickerWidget-title' ),
+ this.selectWidget.$element
+ );
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.rcfilters.ui.ValuePickerWidget, OO.ui.Widget );
+ OO.mixinClass( mw.rcfilters.ui.ValuePickerWidget, OO.ui.mixin.LabelElement );
+
+ /* Events */
+
+ /**
+ * @event choose
+ * @param {string} name Item name
+ *
+ * An item has been chosen
+ */
+
+ /* Methods */
+
+ /**
+ * Respond to model update event
+ */
+ mw.rcfilters.ui.ValuePickerWidget.prototype.onModelUpdate = function () {
+ this.selectCurrentModelItem();
+ };
+
+ /**
+ * Respond to select widget choose event
+ *
+ * @param {OO.ui.ButtonOptionWidget} chosenItem Chosen item
+ * @fires choose
+ */
+ mw.rcfilters.ui.ValuePickerWidget.prototype.onSelectWidgetChoose = function ( chosenItem ) {
+ this.emit( 'choose', chosenItem.getData() );
+ };
+
+ /**
+ * Initialize the select widget
+ */
+ mw.rcfilters.ui.ValuePickerWidget.prototype.initializeSelectWidget = function () {
+ var items = this.model.getItems()
+ .filter( this.itemFilter )
+ .map( function ( filterItem ) {
+ return new OO.ui.ButtonOptionWidget( {
+ data: filterItem.getName(),
+ label: filterItem.getLabel()
+ } );
+ } );
+
+ this.selectWidget.clearItems();
+ this.selectWidget.addItems( items );
+
+ this.selectCurrentModelItem();
+ };
+
+ /**
+ * Select the current item that corresponds with the model item
+ * that is currently selected
+ */
+ mw.rcfilters.ui.ValuePickerWidget.prototype.selectCurrentModelItem = function () {
+ var selectedItem = this.model.getSelectedItems()[ 0 ];
+
+ if ( selectedItem ) {
+ this.selectWidget.selectItemByData( selectedItem.getName() );
+ }
+ };
+}( mediaWiki ) );
}, {
name: 'group4',
type: 'single_option',
- default: 'option1',
+ default: 'option2',
filters: [
{ name: 'option1', label: 'group4option1-label', description: 'group4option1-desc' },
{ name: 'option2', label: 'group4option2-label', description: 'group4option2-desc' },
{ name: 'option3', label: 'group4option3-label', description: 'group4option3-desc' }
]
+ }, {
+ name: 'group5',
+ type: 'single_option',
+ filters: [
+ { name: 'option1', label: 'group5option1-label', description: 'group5option1-desc' },
+ { name: 'option2', label: 'group5option2-label', description: 'group5option2-desc' },
+ { name: 'option3', label: 'group5option3-label', description: 'group5option3-desc' }
+ ]
} ],
viewsDefinition = {
namespaces: {
filter5: '1',
filter6: '0',
group3: 'filter8',
- group4: 'option1',
+ group4: 'option2',
+ group5: 'option1',
namespace: ''
},
baseParamRepresentation = {
filter5: '0',
filter6: '0',
group3: '',
- group4: '',
+ group4: 'option2',
+ group5: 'option1',
namespace: ''
},
baseFilterRepresentation = {
group3__filter7: false,
group3__filter8: false,
group3__filter9: false,
+ // The 'single_value' type of group can't have empty value; it's either
+ // the default given or the first item that will get the truthy value
group4__option1: false,
- group4__option2: false,
+ group4__option2: true, // Default
group4__option3: false,
+ group5__option1: true, // No default set, first item is default value
+ group5__option2: false,
+ group5__option3: false,
namespace__0: false,
namespace__1: false,
namespace__2: false,
group3__filter8: { selected: false, conflicted: false, included: false },
group3__filter9: { selected: false, conflicted: false, included: false },
group4__option1: { selected: false, conflicted: false, included: false },
- group4__option2: { selected: false, conflicted: false, included: false },
+ group4__option2: { selected: true, conflicted: false, included: false },
group4__option3: { selected: false, conflicted: false, included: false },
+ group5__option1: { selected: true, conflicted: false, included: false },
+ group5__option2: { selected: false, conflicted: false, included: false },
+ group5__option3: { selected: false, conflicted: false, included: false },
namespace__0: { selected: false, conflicted: false, included: false },
namespace__1: { selected: false, conflicted: false, included: false },
namespace__2: { selected: false, conflicted: false, included: false },
assert.deepEqual(
model.getFiltersFromParameters( {} ),
baseFilterRepresentation,
- 'Empty parameter query results in an object representing all filters set to false'
+ 'Empty parameter query results in an object representing all filters set to their base state'
);
assert.deepEqual(
assert.deepEqual(
model.getSelectedState(),
$.extend( {}, baseFilterRepresentation, {
- group4__option1: true
+ group4__option1: true,
+ group4__option2: false
} ),
'A \'single_option\' parameter reflects a single selected value.'
);