this.defaultParams = {};
this.defaultFiltersEmpty = null;
this.highlightEnabled = false;
+ this.invertedNamespaces = false;
this.parameterMap = {};
+ this.views = {};
+ this.currentView = 'default';
+
// Events
this.aggregate( { update: 'filterItemUpdate' } );
this.connect( this, { filterItemUpdate: [ 'emit', 'itemUpdate' ] } );
* Filter list is initialized
*/
+ /**
+ * @event update
+ *
+ * Model has been updated
+ */
+
/**
* @event itemUpdate
* @param {mw.rcfilters.dm.FilterItem} item Filter item updated
* Highlight feature has been toggled enabled or disabled
*/
+ /**
+ * @event invertChange
+ * @param {boolean} isInverted Namespace selected is inverted
+ *
+ * Namespace selection is inverted or straight forward
+ */
+
/* Methods */
/**
* the definition given by an object
*
* @param {Array} filters Filter group definition
+ * @param {Object} [namespaces] Namespace definition
+ * @param {Object[]} [tags] Tag array definition
*/
- mw.rcfilters.dm.FiltersViewModel.prototype.initializeFilters = function ( filters ) {
+ mw.rcfilters.dm.FiltersViewModel.prototype.initializeFilters = function ( filters, namespaces, tags ) {
var filterItem, filterConflictResult, groupConflictResult,
model = this,
items = [],
+ namespaceDefinition = [],
groupConflictMap = {},
filterConflictMap = {},
/*!
// Reset
this.clearItems();
this.groups = {};
+ this.views = {};
+ // Filters
+ this.views.default = { name: 'default', label: mw.msg( 'rcfilters-filterlist-title' ) };
filters.forEach( function ( data ) {
var i,
group = data.name;
if ( !model.groups[ group ] ) {
model.groups[ group ] = new mw.rcfilters.dm.FilterGroup( group, {
type: data.type,
- title: mw.msg( data.title ),
+ title: data.title ? mw.msg( data.title ) : group,
separator: data.separator,
fullCoverage: !!data.fullCoverage,
whatsThis: {
}
} );
}
+
+ // Filters are given to us with msg-keys, we need
+ // to translate those before we hand them off
+ for ( i = 0; i < data.filters.length; i++ ) {
+ data.filters[ i ].label = data.filters[ i ].label ? mw.msg( data.filters[ i ].label ) : data.filters[ i ].name;
+ data.filters[ i ].description = data.filters[ i ].description ? mw.msg( data.filters[ i ].description ) : '';
+ }
+
model.groups[ group ].initializeFilters( data.filters, data.default );
items = items.concat( model.groups[ group ].getItems() );
}
} );
+ namespaces = namespaces || {};
+ if (
+ mw.config.get( 'wgStructuredChangeFiltersEnableExperimentalViews' ) &&
+ !$.isEmptyObject( namespaces )
+ ) {
+ // Namespaces group
+ this.views.namespaces = { name: 'namespaces', label: mw.msg( 'namespaces' ), trigger: ':' };
+ $.each( namespaces, function ( namespaceID, label ) {
+ // Build and clean up the definition
+ namespaceDefinition.push( {
+ name: namespaceID,
+ label: label || mw.msg( 'blanknamespace' ),
+ description: '',
+ identifiers: [
+ ( namespaceID < 0 || namespaceID % 2 === 0 ) ?
+ 'subject' : 'talk'
+ ],
+ cssClass: 'mw-changeslist-ns-' + namespaceID
+ } );
+ } );
+
+ // Add the group
+ model.groups.namespace = new mw.rcfilters.dm.FilterGroup(
+ 'namespace', // Parameter name is singular
+ {
+ type: 'string_options',
+ view: 'namespaces',
+ title: 'namespaces', // Message key
+ separator: ';',
+ labelPrefixKey: { 'default': 'rcfilters-tag-prefix-namespace', inverted: 'rcfilters-tag-prefix-namespace-inverted' },
+ fullCoverage: true
+ }
+ );
+ // Add namespace items to group
+ model.groups.namespace.initializeFilters( namespaceDefinition );
+ items = items.concat( model.groups.namespace.getItems() );
+ }
+
+ tags = tags || [];
+ if (
+ mw.config.get( 'wgStructuredChangeFiltersEnableExperimentalViews' ) &&
+ tags.length > 0
+ ) {
+ // Define view
+ this.views.tags = { name: 'tags', label: mw.msg( 'rcfilters-view-tags' ), trigger: '#' };
+
+ // Add the group
+ model.groups.tagfilter = new mw.rcfilters.dm.FilterGroup(
+ 'tagfilter',
+ {
+ type: 'string_options',
+ view: 'tags',
+ title: 'rcfilters-view-tags', // Message key
+ labelPrefixKey: 'rcfilters-tag-prefix-tags',
+ separator: '|',
+ fullCoverage: false
+ }
+ );
+
+ // Add tag items to group
+ model.groups.tagfilter.initializeFilters( tags );
+
+ // Add item references to the model, for lookup
+ items = items.concat( model.groups.tagfilter.getItems() );
+ }
+
// Add item references to the model, for lookup
this.addItems( items );
}
} );
+ this.currentView = 'default';
+
// Finish initialization
this.emit( 'initialize' );
};
return this.groups;
};
+ /**
+ * Get the object that defines groups that match a certain view by their name.
+ *
+ * @param {string} [view] Requested view. If not given, uses current view
+ * @return {Object} Filter groups matching a display group
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getFilterGroupsByView = function ( view ) {
+ var result = {};
+
+ view = view || this.getCurrentView();
+
+ $.each( this.groups, function ( groupName, groupModel ) {
+ if ( groupModel.getView() === view ) {
+ result[ groupName ] = groupModel;
+ }
+ } );
+
+ return result;
+ };
+
+ /**
+ * Get an array of filters matching the given display group.
+ *
+ * @param {string} [view] Requested view. If not given, uses current view
+ * @return {mw.rcfilters.dm.FilterItem} Filter items matching the group
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getFiltersByView = function ( view ) {
+ var groups,
+ result = [];
+
+ view = view || this.getCurrentView();
+
+ groups = this.getFilterGroupsByView( view );
+
+ $.each( groups, function ( groupName, groupModel ) {
+ result = result.concat( groupModel.getItems() );
+ } );
+
+ return result;
+ };
+
+ /**
+ * Get the trigger for the requested view.
+ *
+ * @param {string} view View name
+ * @return {string} View trigger, if exists
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getViewTrigger = function ( view ) {
+ return ( this.views[ view ] && this.views[ view ].trigger ) || '';
+ };
/**
* Get the value of a specific parameter
*
// Get default filter state
$.each( this.groups, function ( name, model ) {
- result = $.extend( true, {}, result, model.getDefaultParams() );
+ $.extend( true, result, model.getDefaultParams() );
} );
- // Get default highlight state
- result = $.extend( true, {}, result, this.getHighlightParameters() );
-
return result;
};
* arranged by their group names
*/
mw.rcfilters.dm.FiltersViewModel.prototype.findMatches = function ( query, returnFlat ) {
- var i,
+ var i, searchIsEmpty,
groupTitle,
result = {},
flatResult = [],
- items = this.getItems();
+ view = this.getViewByTrigger( query.substr( 0, 1 ) ),
+ items = this.getFiltersByView( view );
- // Normalize so we can search strings regardless of case
+ // Normalize so we can search strings regardless of case and view
query = query.toLowerCase();
+ if ( view !== 'default' ) {
+ query = query.substr( 1 );
+ }
+
+ // Check if the search if actually empty; this can be a problem when
+ // we use prefixes to denote different views
+ searchIsEmpty = query.length === 0;
// item label starting with the query string
for ( i = 0; i < items.length; i++ ) {
- if ( items[ i ].getLabel().toLowerCase().indexOf( query ) === 0 ) {
+ if (
+ searchIsEmpty ||
+ items[ i ].getLabel().toLowerCase().indexOf( query ) === 0 ||
+ (
+ // For tags, we want the parameter name to be included in the search
+ view === 'tags' &&
+ items[ i ].getParamName().toLowerCase().indexOf( query ) > -1
+ )
+ ) {
result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
result[ items[ i ].getGroupName() ].push( items[ i ] );
flatResult.push( items[ i ] );
for ( i = 0; i < items.length; i++ ) {
groupTitle = items[ i ].getGroupModel().getTitle();
if (
+ searchIsEmpty ||
items[ i ].getLabel().toLowerCase().indexOf( query ) > -1 ||
items[ i ].getDescription().toLowerCase().indexOf( query ) > -1 ||
- groupTitle.toLowerCase().indexOf( query ) > -1
+ groupTitle.toLowerCase().indexOf( query ) > -1 ||
+ (
+ // For tags, we want the parameter name to be included in the search
+ view === 'tags' &&
+ items[ i ].getParamName().toLowerCase().indexOf( query ) > -1
+ )
) {
result[ items[ i ].getGroupName() ] = result[ items[ i ].getGroupName() ] || [];
result[ items[ i ].getGroupName() ].push( items[ i ] );
} );
};
+ /**
+ * Switch the current view
+ *
+ * @param {string} view View name
+ * @fires update
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.switchView = function ( view ) {
+ if ( this.views[ view ] && this.currentView !== view ) {
+ this.currentView = view;
+ this.emit( 'update' );
+ }
+ };
+
+ /**
+ * Get the current view
+ *
+ * @return {string} Current view
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getCurrentView = function () {
+ return this.currentView;
+ };
+
+ /**
+ * Get the label for the current view
+ *
+ * @return {string} Label for the current view
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getCurrentViewLabel = function () {
+ return this.views[ this.getCurrentView() ].label;
+ };
+
+ /**
+ * Get an array of all available view names
+ *
+ * @return {string} Available view names
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getAvailableViews = function () {
+ return Object.keys( this.views );
+ };
+
+ /**
+ * Get the view that fits the given trigger
+ *
+ * @param {string} trigger Trigger
+ * @return {string} Name of view
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getViewByTrigger = function ( trigger ) {
+ var result = 'default';
+
+ $.each( this.views, function ( name, data ) {
+ if ( data.trigger === trigger ) {
+ result = name;
+ }
+ } );
+
+ return result;
+ };
+
/**
* Toggle the highlight feature on and off.
* Propagate the change to filter items.
return !!this.highlightEnabled;
};
+ /**
+ * Toggle the inverted namespaces property on and off.
+ * Propagate the change to namespace filter items.
+ *
+ * @param {boolean} enable Inverted property is enabled
+ * @fires invertChange
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.toggleInvertedNamespaces = function ( enable ) {
+ enable = enable === undefined ? !this.invertedNamespaces : enable;
+
+ if ( this.invertedNamespaces !== enable ) {
+ this.invertedNamespaces = enable;
+
+ this.getFiltersByView( 'namespaces' ).forEach( function ( filterItem ) {
+ filterItem.toggleInverted( this.invertedNamespaces );
+ }.bind( this ) );
+
+ this.emit( 'invertChange', this.invertedNamespaces );
+ }
+ };
+
+ /**
+ * Check if the namespaces selection is set to be inverted
+ * @return {boolean}
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.areNamespacesInverted = function () {
+ return !!this.invertedNamespaces;
+ };
+
/**
* Set highlight color for a specific filter item
*
filterItem.clearHighlightColor();
} );
};
+
+ /**
+ * Return a version of the given string that is without any
+ * view triggers.
+ *
+ * @param {string} str Given string
+ * @return {string} Result
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.removeViewTriggers = function ( str ) {
+ if ( this.getViewByTrigger( str.substr( 0, 1 ) ) !== 'default' ) {
+ str = str.substr( 1 );
+ }
+
+ return str;
+ };
}( mediaWiki, jQuery ) );