protected function getHTMLClasses( $rc, $watched ) {
$classes = [];
$logType = $rc->mAttribs['rc_log_type'];
+ $prefix = 'mw-changeslist-';
if ( $logType ) {
- $classes[] = Sanitizer::escapeClass( 'mw-changeslist-log-' . $logType );
+ $classes[] = Sanitizer::escapeClass( $prefix . 'log-' . $logType );
} else {
- $classes[] = Sanitizer::escapeClass( 'mw-changeslist-ns' .
+ $classes[] = Sanitizer::escapeClass( $prefix . 'ns' .
$rc->mAttribs['rc_namespace'] . '-' . $rc->mAttribs['rc_title'] );
}
// Indicate watched status on the line to allow for more
// comprehensive styling.
$classes[] = $watched && $rc->mAttribs['rc_timestamp'] >= $watched
- ? 'mw-changeslist-line-watched'
- : 'mw-changeslist-line-not-watched';
+ ? $prefix . 'line-watched'
+ : $prefix . 'line-not-watched';
+
+ $classes = array_merge( $classes, $this->getHTMLClassesForFilters( $rc ) );
+
+ return $classes;
+ }
+
+ protected function getHTMLClassesForFilters( $rc ) {
+ $classes = [];
+ $prefix = 'mw-changeslist-';
+
+ $classes[] = $prefix . ( $rc->getAttribute( 'rc_bot' ) ? 'bot' : 'human' );
+ $classes[] = $prefix . ( $rc->getAttribute( 'rc_user' ) ? 'liu' : 'anon' );
+ $classes[] = $prefix . ( $rc->getAttribute( 'rc_minor' ) ? 'minor' : 'major' );
+ $classes[] = $prefix .
+ ( $rc->getAttribute( 'rc_patrolled' ) ? 'patrolled' : 'unpatrolled' );
+ $classes[] = $prefix .
+ ( $this->getUser()->equals( $rc->getPerformer() ) ? 'self' : 'others' );
+ $classes[] = $prefix . 'src-' . str_replace( '.', '-', $rc->getAttribute( 'rc_source' ) );
+
+ $performer = $rc->getPerformer();
+ if ( $performer && $performer->isLoggedIn() ) {
+ $classes[] = $prefix . 'user-' . $performer->getExperienceLevel();
+ }
return $classes;
}
&& $block[0]->mAttribs['rc_timestamp'] >= $block[0]->watched
) {
$tableClasses[] = 'mw-changeslist-line-watched';
+ $tableClasses = array_merge( $tableClasses, $this->getHTMLClassesForFilters( $block[0] ) );
} else {
$tableClasses[] = 'mw-changeslist-line-not-watched';
}
protected function getLineData( array $block, RCCacheEntry $rcObj, array $queryParams = [] ) {
$RCShowChangedSize = $this->getConfig()->get( 'RCShowChangedSize' );
- $classes = [ 'mw-enhanced-rc' ];
$type = $rcObj->mAttribs['rc_type'];
$data = [];
$lineParams = [];
+ $classes = [ 'mw-enhanced-rc' ];
if ( $rcObj->watched
&& $rcObj->mAttribs['rc_timestamp'] >= $rcObj->watched
) {
- $classes = [ 'mw-enhanced-watched' ];
+ $classes[] = [ 'mw-enhanced-watched' ];
}
+ $classes = array_merge( $classes, $this->getHTMLClassesForFilters( $rcObj ) );
$separator = ' <span class="mw-changeslist-separator">. .</span> ';
// user_talk page; it's cleared one page view later in WikiPage::doViewUpdates().
}
+ /**
+ * Compute experienced level based on edit count and registration date.
+ *
+ * @return string 'newcomer', 'learner', or 'experienced'
+ */
+ public function getExperienceLevel() {
+ global $wgLearnerEdits,
+ $wgExperiencedUserEdits,
+ $wgLearnerMemberSince,
+ $wgExperiencedUserMemberSince;
+
+ if ( $this->isAnon() ) {
+ return false;
+ }
+
+ $editCount = $this->getEditCount();
+ $registration = $this->getRegistration();
+ $now = time();
+ $learnerRegistration = wfTimestamp( TS_MW, $now - $wgLearnerMemberSince * 86400 );
+ $experiencedRegistration = wfTimestamp( TS_MW, $now - $wgExperiencedUserMemberSince * 86400 );
+
+ if (
+ $editCount < $wgLearnerEdits ||
+ $registration > $learnerRegistration
+ ) {
+ return 'newcomer';
+ } elseif (
+ $editCount > $wgExperiencedUserEdits &&
+ $registration <= $experiencedRegistration
+ ) {
+ return 'experienced';
+ } else {
+ return 'learner';
+ }
+ }
+
/**
* Set a cookie on the user's client. Wrapper for
* WebResponse::setCookie
"rcfilters-invalid-filter": "Invalid filter",
"rcfilters-empty-filter": "No active filters. All contributions are shown.",
"rcfilters-filterlist-title": "Filters",
+ "rcfilters-highlightbutton-title": "Highlight results",
+ "rcfilters-highlightmenu-title": "Select a color",
"rcfilters-filterlist-noresults": "No filters found",
"rcfilters-filtergroup-registration": "User registration",
"rcfilters-filter-registered-label": "Registered",
"rcfilters-invalid-filter": "A label for an invalid filter.",
"rcfilters-empty-filter": "Placeholder for the filter list when no filters were chosen.",
"rcfilters-filterlist-title": "Title for the filters list.\n{{Identical|Filter}}",
+ "rcfilters-highlightbutton-title": "Title for the highlight button used to toggle the highlight feature on and off.",
+ "rcfilters-highlightmenu-title": "Title for the highlight menu used to select the highlight color for an individual filter.",
"rcfilters-filterlist-noresults": "Message showing no results found for searching a filter.",
"rcfilters-filtergroup-registration": "Title for the filter group for editor registration type.",
"rcfilters-filter-registered-label": "Label for the filter for showing edits made by logged-in users.\n{{Identical|Registered}}",
'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.FormWrapperWidget.js',
+ 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemHighlightButton.js',
+ 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.HighlightColorPickerWidget.js',
+ 'resources/src/mediawiki.rcfilters/mw.rcfilters.HighlightColors.js',
'resources/src/mediawiki.rcfilters/mw.rcfilters.init.js',
],
'styles' => [
+ 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.mixins.less',
+ 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.variables.less',
+ 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.Overlay.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.CapsuleItemWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FiltersListWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterWrapperWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterCapsuleMultiselectWidget.less',
+ 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ChangesListWrapperWidget.less',
+ 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.HighlightColorPickerWidget.less',
+ 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemHighlightButton.less',
],
'messages' => [
'rcfilters-activefilters',
'rcfilters-filter-categorization-description',
'rcfilters-filter-logactions-label',
'rcfilters-filter-logactions-description',
+ 'rcfilters-highlightbutton-title',
+ 'rcfilters-highlightmenu-title',
'recentchanges-noresult',
],
'dependencies' => [
'oojs-ui',
'mediawiki.rcfilters.filters.dm',
- 'oojs-ui.styles.icons-moderation'
+ 'oojs-ui.styles.icons-moderation',
+ 'oojs-ui.styles.icons-editing-core',
],
],
'mediawiki.special' => [
* @cfg {boolean} [selected] The item is selected
* @cfg {string[]} [subset] Defining the names of filters that are a subset of this filter
* @cfg {string[]} [conflictsWith] Defining the names of filters that conflict with this item
+ * @cfg {string} [cssClass] The class identifying the results that match this filter
*/
mw.rcfilters.dm.FilterItem = function MwRcfiltersDmFilterItem( name, groupModel, config ) {
config = config || {};
this.included = false;
this.conflicted = false;
this.fullyCovered = false;
+
+ // Highlight
+ this.cssClass = config.cssClass;
+ this.highlightColor = null;
+ this.highlightEnabled = false;
};
/* Initialization */
this.emit( 'update' );
}
};
+
+ /**
+ * Set the highlight color
+ *
+ * @param {string|null} highlightColor
+ */
+ mw.rcfilters.dm.FilterItem.prototype.setHighlightColor = function ( highlightColor ) {
+ if ( this.highlightColor !== highlightColor ) {
+ this.highlightColor = highlightColor;
+ this.emit( 'update' );
+ }
+ };
+
+ /**
+ * Clear the highlight color
+ */
+ mw.rcfilters.dm.FilterItem.prototype.clearHighlightColor = function () {
+ this.setHighlightColor( null );
+ };
+
+ /**
+ * Get the highlight color, or null if none is configured
+ *
+ * @return {string|null}
+ */
+ mw.rcfilters.dm.FilterItem.prototype.getHighlightColor = function () {
+ return this.highlightColor;
+ };
+
+ /**
+ * Get the CSS class that matches changes that fit this filter
+ * or null if none is configured
+ *
+ * @return {string|null}
+ */
+ mw.rcfilters.dm.FilterItem.prototype.getCssClass = function () {
+ return this.cssClass;
+ };
+
+ /**
+ * Toggle the highlight feature on and off for this filter.
+ * It only works if highlight is supported for this filter.
+ *
+ * @param {boolean} enable Highlight should be enabled
+ */
+ mw.rcfilters.dm.FilterItem.prototype.toggleHighlight = function ( enable ) {
+ enable = enable === undefined ? !this.highlightEnabled : enable;
+
+ if ( !this.isHighlightSupported() ) {
+ return;
+ }
+
+ if ( enable === this.highlightEnabled ) {
+ return;
+ }
+
+ this.highlightEnabled = enable;
+ this.emit( 'update' );
+ };
+
+ /**
+ * Check if the highlight feature is currently enabled for this filter
+ *
+ * @return {boolean}
+ */
+ mw.rcfilters.dm.FilterItem.prototype.isHighlightEnabled = function () {
+ return !!this.highlightEnabled;
+ };
+
+ /**
+ * Check if the highlight feature is supported for this filter
+ *
+ * @return {boolean}
+ */
+ mw.rcfilters.dm.FilterItem.prototype.isHighlightSupported = function () {
+ return !!this.getCssClass();
+ };
}( mediaWiki ) );
this.groups = {};
this.defaultParams = {};
this.defaultFiltersEmpty = null;
+ this.highlightEnabled = false;
// Events
this.aggregate( { update: 'filterItemUpdate' } );
* Filter item has changed
*/
+ /**
+ * @event highlightChange
+ * @param {boolean} Highlight feature is enabled
+ *
+ * Highlight feature has been toggled enabled or disabled
+ */
+
/* Methods */
/**
group: group,
label: data.filters[ i ].label,
description: data.filters[ i ].description,
- subset: data.filters[ i ].subset
+ subset: data.filters[ i ].subset,
+ cssClass: data.filters[ i ].class
} );
// For convenience, we should store each filter's "supersets" -- these are
return result;
};
+ /**
+ * Get the highlight parameters based on current filter configuration
+ *
+ * @return {object} Object where keys are "<filter name>_color" and values
+ * are the selected highlight colors.
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getHighlightParameters = function () {
+ var result = { highlight: this.isHighlightEnabled() };
+
+ this.getItems().forEach( function ( filterItem ) {
+ result[ filterItem.getName() + '_color' ] = filterItem.getHighlightColor();
+ } );
+ return result;
+ };
+
/**
* Sanitize value group of a string_option groups type
* Remove duplicates and make sure to only use valid
* @return {boolean} Current filters are all empty
*/
mw.rcfilters.dm.FiltersViewModel.prototype.areCurrentFiltersEmpty = function () {
- var currFilters = this.getSelectedState();
-
- return Object.keys( currFilters ).every( function ( filterName ) {
- return !currFilters[ filterName ];
+ var model = this;
+
+ // Check if there are either any selected items or any items
+ // that have highlight enabled
+ return !this.getItems().some( function ( filterItem ) {
+ return (
+ filterItem.isSelected() ||
+ ( model.isHighlightEnabled() && filterItem.getHighlightColor() )
+ );
} );
};
return result;
};
+ /**
+ * Get items that are highlighted
+ *
+ * @return {mw.rcfilters.dm.FilterItem[]} Highlighted items
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.getHighlightedItems = function () {
+ return this.getItems().filter( function ( filterItem ) {
+ return filterItem.isHighlightSupported() &&
+ filterItem.getHighlightColor();
+ } );
+ };
+
+ /**
+ * Toggle the highlight feature on and off.
+ * Propagate the change to filter items.
+ *
+ * @param {boolean} enable Highlight should be enabled
+ * @fires highlightChange
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.toggleHighlight = function ( enable ) {
+ enable = enable === undefined ? !this.highlightEnabled : enable;
+
+ if ( this.highlightEnabled !== enable ) {
+ this.highlightEnabled = enable;
+
+ this.getItems().forEach( function ( filterItem ) {
+ filterItem.toggleHighlight( this.highlightEnabled );
+ }.bind( this ) );
+
+ this.emit( 'highlightChange', this.highlightEnabled );
+ }
+ };
+
+ /**
+ * Check if the highlight feature is enabled
+ * @return {boolean}
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.isHighlightEnabled = function () {
+ return this.highlightEnabled;
+ };
+
+ /**
+ * Set highlight color for a specific filter item
+ *
+ * @param {string} filterName Name of the filter item
+ * @param {string} color Selected color
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.setHighlightColor = function ( filterName, color ) {
+ this.getItemByName( filterName ).setHighlightColor( color );
+ };
+
+ /**
+ * Clear highlight for a specific filter item
+ *
+ * @param {string} filterName Name of the filter item
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.clearHighlightColor = function ( filterName ) {
+ this.getItemByName( filterName ).clearHighlightColor();
+ };
+
+ /**
+ * Clear highlight for all filter items
+ */
+ mw.rcfilters.dm.FiltersViewModel.prototype.clearAllHighlightColors = function () {
+ this.getItems().forEach( function ( filterItem ) {
+ filterItem.clearHighlightColor();
+ } );
+ };
}( mediaWiki, jQuery ) );
)
);
+ // Initialize highlights
+ this.filtersModel.toggleHighlight( !!uri.query.highlight );
+ this.filtersModel.getItems().forEach( function ( filterItem ) {
+ var color = uri.query[ filterItem.getName() + '_color' ];
+ if ( !color ) {
+ return;
+ }
+
+ filterItem.setHighlightColor( color );
+ } );
+
// Check all filter interactions
this.filtersModel.reassessFilterInteractions();
};
*/
mw.rcfilters.Controller.prototype.emptyFilters = function () {
this.filtersModel.emptyAllFilters();
+ this.filtersModel.clearAllHighlightColors();
this.updateURL();
this.updateChangesList();
};
* @param {boolean} isSelected Filter selected state
*/
mw.rcfilters.Controller.prototype.updateFilter = function ( filterName, isSelected ) {
- var obj = {};
+ var obj = {},
+ filterItem = this.filtersModel.getItemByName( filterName );
- obj[ filterName ] = isSelected;
+ if ( filterItem.isSelected() !== isSelected ) {
+ obj[ filterName ] = isSelected;
+ this.filtersModel.updateFilters( obj );
- this.filtersModel.updateFilters( obj );
- this.updateURL();
- this.updateChangesList();
+ this.updateURL();
+ this.updateChangesList();
- // Check filter interactions
- this.filtersModel.reassessFilterInteractions( this.filtersModel.getItemByName( filterName ) );
+ // Check filter interactions
+ this.filtersModel.reassessFilterInteractions( this.filtersModel.getItemByName( filterName ) );
+ }
};
/**
* Update the URL of the page to reflect current filters
*/
mw.rcfilters.Controller.prototype.updateURL = function () {
- var uri = new mw.Uri();
+ var uri = this.getUpdatedUri();
+ window.history.pushState( { tag: 'rcfilters' }, document.title, uri.toString() );
+ };
+
+ /**
+ * Get an updated mw.Uri object based on the model state
+ *
+ * @return {mw.Uri} Updated Uri
+ */
+ mw.rcfilters.Controller.prototype.getUpdatedUri = function () {
+ var uri = new mw.Uri(),
+ highlightParams = this.filtersModel.getHighlightParameters();
// Add to existing queries in URL
// TODO: Clean up the list of filters; perhaps 'falsy' filters
// and see if current state of a specific filter is needed?
uri.extend( this.filtersModel.getParametersFromFilters() );
- // Update the URL itself
- window.history.pushState( { tag: 'rcfilters' }, document.title, uri.toString() );
+ // highlight params
+ Object.keys( highlightParams ).forEach( function ( paramName ) {
+ if ( highlightParams[ paramName ] ) {
+ uri.query[ paramName ] = highlightParams[ paramName ];
+ } else {
+ delete uri.query[ paramName ];
+ }
+ } );
+
+ return uri;
};
/**
* Fetch the list of changes from the server for the current filters
*
- * @returns {jQuery.Promise} Promise object that will resolve with the changes list
+ * @return {jQuery.Promise} Promise object that will resolve with the changes list
*/
mw.rcfilters.Controller.prototype.fetchChangesList = function () {
- var uri = new mw.Uri(),
+ var uri = this.getUpdatedUri(),
requestId = ++this.requestCounter,
latestRequest = function () {
return requestId === this.requestCounter;
}
}.bind( this ) );
};
+
+ /**
+ * Toggle the highlight feature on and off
+ */
+ mw.rcfilters.Controller.prototype.toggleHighlight = function () {
+ this.filtersModel.toggleHighlight();
+ this.updateURL();
+ };
+
+ /**
+ * Set the highlight color for a filter item
+ *
+ * @param {string} filterName Name of the filter item
+ * @param {string} color Selected color
+ */
+ mw.rcfilters.Controller.prototype.setHighlightColor = function ( filterName, color ) {
+ this.filtersModel.setHighlightColor( filterName, color );
+ this.updateURL();
+ };
+
+ /**
+ * Clear highlight for a filter item
+ *
+ * @param {string} filterName Name of the filter item
+ */
+ mw.rcfilters.Controller.prototype.clearHighlightColor = function ( filterName ) {
+ this.filtersModel.clearHighlightColor( filterName );
+ this.updateURL();
+ };
}( mediaWiki, jQuery ) );
--- /dev/null
+( function ( mw ) {
+ /**
+ * Supported highlight colors.
+ * Warning: These are also hardcoded in "styles/mw.rcfilters.variables.less"
+ *
+ * @type {string[]}
+ */
+ mw.rcfilters.HighlightColors = [ 'c1', 'c2', 'c3', 'c4', 'c5' ];
+}( mediaWiki ) );
// eslint-disable-next-line no-new
new mw.rcfilters.ui.ChangesListWrapperWidget(
- changesListModel, $( '.mw-changeslist, .mw-changeslist-empty' ) );
+ filtersModel, changesListModel, $( '.mw-changeslist, .mw-changeslist-empty' ) );
// eslint-disable-next-line no-new
new mw.rcfilters.ui.FormWrapperWidget(
{
name: 'hideliu',
label: mw.msg( 'rcfilters-filter-registered-label' ),
- description: mw.msg( 'rcfilters-filter-registered-description' )
+ description: mw.msg( 'rcfilters-filter-registered-description' ),
+ 'class': 'mw-changeslist-liu'
},
{
name: 'hideanons',
label: mw.msg( 'rcfilters-filter-unregistered-label' ),
- description: mw.msg( 'rcfilters-filter-unregistered-description' )
+ description: mw.msg( 'rcfilters-filter-unregistered-description' ),
+ 'class': 'mw-changeslist-anon'
}
]
},
name: 'newcomer',
label: mw.msg( 'rcfilters-filter-userExpLevel-newcomer-label' ),
description: mw.msg( 'rcfilters-filter-userExpLevel-newcomer-description' ),
- conflicts: [ 'hideanons' ]
+ conflicts: [ 'hideanons' ],
+ 'class': 'mw-changeslist-user-newcomer'
},
{
name: 'learner',
label: mw.msg( 'rcfilters-filter-userExpLevel-learner-label' ),
description: mw.msg( 'rcfilters-filter-userExpLevel-learner-description' ),
- conflicts: [ 'hideanons' ]
+ conflicts: [ 'hideanons' ],
+ 'class': 'mw-changeslist-user-learner'
},
{
name: 'experienced',
label: mw.msg( 'rcfilters-filter-userExpLevel-experienced-label' ),
description: mw.msg( 'rcfilters-filter-userExpLevel-experienced-description' ),
- conflicts: [ 'hideanons' ]
+ conflicts: [ 'hideanons' ],
+ 'class': 'mw-changeslist-user-experienced'
}
]
},
{
name: 'hidemyself',
label: mw.msg( 'rcfilters-filter-editsbyself-label' ),
- description: mw.msg( 'rcfilters-filter-editsbyself-description' )
+ description: mw.msg( 'rcfilters-filter-editsbyself-description' ),
+ 'class': 'mw-changeslist-self'
},
{
name: 'hidebyothers',
label: mw.msg( 'rcfilters-filter-editsbyother-label' ),
- description: mw.msg( 'rcfilters-filter-editsbyother-description' )
+ description: mw.msg( 'rcfilters-filter-editsbyother-description' ),
+ 'class': 'mw-changeslist-others'
}
]
},
name: 'hidebots',
label: mw.msg( 'rcfilters-filter-bots-label' ),
description: mw.msg( 'rcfilters-filter-bots-description' ),
- 'default': true
+ 'default': true,
+ 'class': 'mw-changeslist-bot'
},
{
name: 'hidehumans',
label: mw.msg( 'rcfilters-filter-humans-label' ),
description: mw.msg( 'rcfilters-filter-humans-description' ),
- 'default': false
+ 'default': false,
+ 'class': 'mw-changeslist-human'
}
]
},
{
name: 'hideminor',
label: mw.msg( 'rcfilters-filter-minor-label' ),
- description: mw.msg( 'rcfilters-filter-minor-description' )
+ description: mw.msg( 'rcfilters-filter-minor-description' ),
+ 'class': 'mw-changeslist-minor'
},
{
name: 'hidemajor',
label: mw.msg( 'rcfilters-filter-major-label' ),
- description: mw.msg( 'rcfilters-filter-major-description' )
+ description: mw.msg( 'rcfilters-filter-major-description' ),
+ 'class': 'mw-changeslist-major'
}
]
},
name: 'hidepageedits',
label: mw.msg( 'rcfilters-filter-pageedits-label' ),
description: mw.msg( 'rcfilters-filter-pageedits-description' ),
- 'default': false
+ 'default': false,
+ 'class': 'mw-changeslist-src-mw-edit'
+
},
{
name: 'hidenewpages',
label: mw.msg( 'rcfilters-filter-newpages-label' ),
description: mw.msg( 'rcfilters-filter-newpages-description' ),
- 'default': false
+ 'default': false,
+ 'class': 'mw-changeslist-src-mw-new'
},
{
name: 'hidecategorization',
label: mw.msg( 'rcfilters-filter-categorization-label' ),
description: mw.msg( 'rcfilters-filter-categorization-description' ),
- 'default': true
+ 'default': true,
+ 'class': 'mw-changeslist-src-mw-categorize'
},
{
name: 'hidelog',
label: mw.msg( 'rcfilters-filter-logactions-label' ),
description: mw.msg( 'rcfilters-filter-logactions-description' ),
- 'default': false
+ 'default': false,
+ 'class': 'mw-changeslist-src-mw-log'
}
]
}
--- /dev/null
+@import "mediawiki.mixins";
+@import "mw.rcfilters.variables";
+
+// This is a general mixin for a color circle
+.mw-rcfilters-mixin-circle( @color: white, @diameter: 2em, @padding: 0.5em, @border: false ) {
+ border-radius: 50%;
+ min-width: @diameter;
+ width: @diameter;
+ min-height: @diameter;
+ height: @diameter;
+ margin: @padding;
+ .box-sizing( border-box );
+
+ background-color: @color;
+
+ & when (@border = true) {
+ border: 1px solid #565656;
+ }
+}
+
+// This is the circle that appears next to the results
+// Its visibility is directly dependent on whether there is
+// a color class on its parent element
+.result-circle( @colorName: 'none' ) {
+ &-@{colorName} {
+ .mw-rcfilters-mixin-circle( ~"@{highlight-@{colorName}}", @result-circle-diameter, 0 );
+ display: none;
+
+ .mw-rcfilters-highlight-color-@{colorName} & {
+ display: inline-block;
+ }
+ }
+}
+
+// This mixin produces color mixes for two, three and four colors
+.highlight-color-mix( @color1, @color2, @color3: false, @color4: false ) {
+ @highlight-color-class-var: ~".mw-rcfilters-highlight-color-@{color1}.mw-rcfilters-highlight-color-@{color2}";
+
+ // The nature of these variables and them being inside
+ // a 'tint' and 'average' LESS functions is such where
+ // the parsing is failing if it is done inside those functions.
+ // Instead, we first construct their LESS variable names,
+ // and then we call them inside those functions by calling @@var
+ @c1var: ~"highlight-@{color1}";
+ @c2var: ~"highlight-@{color2}";
+
+ // Two colors
+ @{highlight-color-class-var} when ( @color3 = false ) and ( @color4 = false ) and not ( @color1 = false ), ( @color2 = false ) {
+ background-color: tint( average( @@c1var, @@c2var ), 50% );
+ }
+ // Three colors
+ @{highlight-color-class-var}.mw-rcfilters-highlight-color-@{color3} when ( @color4 = false ) and not ( @color3 = false ) {
+ @c3var: ~"highlight-@{color3}";
+ background-color: tint( mix( @@c1var, average( @@c2var, @@c3var ), 33% ), 30% );
+ }
+
+ // Four colors
+ @{highlight-color-class-var}.mw-rcfilters-highlight-color-@{color3}.mw-rcfilters-highlight-color-@{color4} when not ( @color4 = false ) {
+ @c3var: ~"highlight-@{color3}";
+ @c4var: ~"highlight-@{color4}";
+ background-color: tint( mix( @@c1var, mix( @@c2var, average( @@c3var, @@c4var ), 25% ), 25% ), 25% );
+ }
+}
+@import "mw.rcfilters.mixins";
+
.mw-rcfilters-ui-capsuleItemWidget {
&-popup {
padding: 1em;
margin-top: 1em;
}
+ .oo-ui-labelElement-label {
+ vertical-align: middle;
+ }
+
&-muted {
- opacity: 0.5;
+ // Muted state
+ // We want everything muted except the circle
+ background-color: rgba( 255, 255, 255, @muted-opacity );
+
+ .oo-ui-labelElement-label,
+ .oo-ui-buttonWidget {
+ opacity: @muted-opacity;
+ }
+ }
+
+ &-highlight {
+ display: none;
+ padding-right: 0.5em;
+
+ &-highlighted {
+ display: inline-block;
+
+ }
+
+ &[data-color="c1"] {
+ .mw-rcfilters-mixin-circle( @highlight-c1, 0.7em, ~"0 0.5em 0 0" );
+ }
+ &[data-color="c2"] {
+ .mw-rcfilters-mixin-circle( @highlight-c2, 0.7em, ~"0 0.5em 0 0" );
+ }
+ &[data-color="c3"] {
+ .mw-rcfilters-mixin-circle( @highlight-c3, 0.7em, ~"0 0.5em 0 0" );
+ }
+ &[data-color="c4"] {
+ .mw-rcfilters-mixin-circle( @highlight-c4, 0.7em, ~"0 0.5em 0 0" );
+ }
+ &[data-color="c5"] {
+ .mw-rcfilters-mixin-circle( @highlight-c5, 0.7em, ~"0 0.5em 0 0" );
+ }
}
}
--- /dev/null
+@import 'mw.rcfilters.mixins';
+
+.mw-rcfilters-ui-changesListWrapperWidget {
+ &-highlighted {
+ ul {
+ list-style: none;
+ // Each li's margin-left should be the width of the highlights
+ // element + the margin
+ margin-left: ~"calc( ( @{result-circle-diameter} + @{result-circle-margin} ) * 5 + @{result-circle-general-margin} )";
+ }
+ }
+
+ // Correction for Enhanced RC
+ // This is outside the scope of the 'highlights' wrapper
+ table.mw-enhanced-rc {
+ margin-left: ~"calc( ( @{result-circle-diameter} + @{result-circle-margin} ) * 5 + @{result-circle-general-margin} )";
+
+ td:last-child {
+ width: 100%;
+ }
+ }
+
+ &-highlights {
+ display: none;
+ padding: 0 @result-circle-general-margin 0 0;
+ text-align: right;
+ // The width is 5 circles times their diameter + individual margin
+ // and then plus the general margin
+ width: ~"calc( ( @{result-circle-diameter} + @{result-circle-margin} ) * 5 )";
+ // And we want to shift the entire block to the left of the li
+ position: absolute;
+ left: 0;
+
+ .mw-rcfilters-ui-changesListWrapperWidget-highlighted & {
+ display: inline-block;
+ }
+
+ div {
+ .box-sizing( border-box );
+ margin-right: @result-circle-margin;
+ vertical-align: middle;
+ // This is to make the dots appear at the center of the
+ // text itself; it's a horrendous hack and blame JamesF for it.
+ margin-top: -2px;
+ }
+
+ &-color {
+
+ &-none {
+ .mw-rcfilters-mixin-circle( @highlight-none, @result-circle-diameter, 0, true );
+ display: inline-block;
+
+ .mw-rcfilters-highlight-color-c1 &,
+ .mw-rcfilters-highlight-color-c2 &,
+ .mw-rcfilters-highlight-color-c3 &,
+ .mw-rcfilters-highlight-color-c4 &,
+ .mw-rcfilters-highlight-color-c5 & {
+ display: none;
+ }
+ }
+ .result-circle( c1 );
+ .result-circle( c2 );
+ .result-circle( c3 );
+ .result-circle( c4 );
+ .result-circle( c5 );
+ }
+ }
+
+ // One color
+ .mw-rcfilters-highlight-color-c1 {
+ background-color: tint( @highlight-c1, 70% );
+ }
+
+ .mw-rcfilters-highlight-color-c2 {
+ background-color: tint( @highlight-c2, 70% );
+ }
+
+ .mw-rcfilters-highlight-color-c3 {
+ background-color: tint( @highlight-c3, 70% );
+ }
+
+ .mw-rcfilters-highlight-color-c4 {
+ background-color: tint( @highlight-c4, 70% );
+ }
+
+ .mw-rcfilters-highlight-color-c5 {
+ background-color: tint( @highlight-c5, 70% );
+ }
+
+ // Two colors
+ .highlight-color-mix( c1, c2 );
+ .highlight-color-mix( c1, c3 );
+ .highlight-color-mix( c1, c4 );
+ .highlight-color-mix( c1, c5 );
+ .highlight-color-mix( c2, c3 );
+ .highlight-color-mix( c2, c4 );
+ .highlight-color-mix( c2, c5 );
+ .highlight-color-mix( c3, c4 );
+ .highlight-color-mix( c3, c5 );
+ .highlight-color-mix( c4, c5 );
+
+ // Three colors
+ .highlight-color-mix( c1, c2, c3 );
+ .highlight-color-mix( c1, c2, c5 );
+ .highlight-color-mix( c1, c2, c4 );
+ .highlight-color-mix( c1, c3, c4 );
+ .highlight-color-mix( c1, c3, c5 );
+ .highlight-color-mix( c1, c4, c5 );
+ .highlight-color-mix( c2, c3, c4 );
+ .highlight-color-mix( c2, c3, c5 );
+ .highlight-color-mix( c2, c4, c5 );
+ .highlight-color-mix( c3, c4, c5 );
+
+ // Four colors
+ .highlight-color-mix( c1, c2, c3, c4 );
+ .highlight-color-mix( c1, c2, c3, c5 );
+ .highlight-color-mix( c1, c2, c4, c5 );
+ .highlight-color-mix( c1, c3, c4, c5 );
+ .highlight-color-mix( c2, c3, c4, c5 );
+
+ // Five colors:
+ .mw-rcfilters-highlight-color-c1.mw-rcfilters-highlight-color-c2.mw-rcfilters-highlight-color-c3.mw-rcfilters-highlight-color-c4.mw-rcfilters-highlight-color-c5 {
+ background-color: tint( mix( @highlight-c1, mix( @highlight-c2, mix( @highlight-c3, average( @highlight-c4, @highlight-c5 ), 20% ), 20% ), 20% ), 15% );
+ }
+}
--- /dev/null
+@import "mw.rcfilters.mixins";
+
+.mw-rcfilters-ui-filterItemHighlightButton {
+
+ &-circle {
+ display: inline-block;
+ vertical-align: middle;
+ background-image: none;
+ margin-right: 0.2em;
+
+ &-color {
+ &-c1 {
+ // These values duplicate the sizing of the icon
+ // width/height 1.875em
+ .mw-rcfilters-mixin-circle( @highlight-c1, 1.875em, 0 );
+ }
+ &-c2 {
+ .mw-rcfilters-mixin-circle( @highlight-c2, 1.875em, 0 );
+ }
+ &-c3 {
+ .mw-rcfilters-mixin-circle( @highlight-c3, 1.875em, 0 );
+ }
+ &-c4 {
+ .mw-rcfilters-mixin-circle( @highlight-c4, 1.875em, 0 );
+ }
+ &-c5 {
+ .mw-rcfilters-mixin-circle( @highlight-c5, 1.875em, 0 );
+ }
+ }
+ }
+}
@import "mediawiki.mixins";
.mw-rcfilters-ui-filterItemWidget {
- padding-left: 0.5em;
+ padding: 0 0.5em;
.box-sizing( border-box );
- &-label {
- &-title {
- font-weight: bold;
- font-size: 1.2em;
- color: #222;
+ .mw-rcfilters-ui-table {
+ padding-top: 0.5em;
+ }
+
+ &-filterCheckbox {
+ &-label {
+ &-title {
+ font-weight: bold;
+ font-size: 1.2em;
+ color: #222;
+ }
+ &-desc {
+ color: #464a4f;
+ }
}
- &-desc {
- color: #464a4f;
+
+ .oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline {
+ // Override margin-top and -bottom rules from FieldLayout
+ margin: 0 !important;
}
- }
- .oo-ui-fieldLayout.oo-ui-fieldLayout-align-inline {
- // Override margin-top and -bottom rules from FieldLayout
- margin: 0 !important;
+ &-muted {
+ opacity: 0.5;
+ }
}
- &-muted {
- opacity: 0.5;
+ &-highlightButton {
+ width: 4em;
+ padding-left: 1em;
}
}
color: #54595d;
border-bottom: 1px solid #c8ccd1;
background: #f8f9fa;
+ overflow: hidden;
}
&-noresults {
// TODO: Unify colors with official design palette
color: #666;
}
+
+ &-hightlightButton {
+ float: right;
+ }
}
--- /dev/null
+@import "mw.rcfilters.mixins";
+
+.mw-rcfilters-ui-highlightColorPickerWidget {
+ &-label {
+ display: block;
+ font-weight: bold;
+ font-size: 1.2em;
+ }
+
+ &-buttonSelect {
+ &-color {
+ .oo-ui-iconElement-icon {
+ width: 2em;
+ height: 2em;
+ }
+
+ &-none {
+ .mw-rcfilters-mixin-circle( @highlight-none, 2em, 0.5em, true );
+
+ &.oo-ui-buttonOptionWidget.oo-ui-buttonElement-active,
+ &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-pressed,
+ &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-selected {
+ background-color: @highlight-none;
+ }
+ }
+ &-c1 {
+ .mw-rcfilters-mixin-circle( @highlight-c1 );
+
+ &.oo-ui-buttonOptionWidget.oo-ui-buttonElement-active,
+ &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-pressed,
+ &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-selected {
+ background-color: @highlight-c1;
+ }
+ }
+ &-c2 {
+ .mw-rcfilters-mixin-circle( @highlight-c2 );
+
+ &.oo-ui-buttonOptionWidget.oo-ui-buttonElement-active,
+ &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-pressed,
+ &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-selected {
+ background-color: @highlight-c2;
+ }
+ }
+ &-c3 {
+ .mw-rcfilters-mixin-circle( @highlight-c3 );
+
+ &.oo-ui-buttonOptionWidget.oo-ui-buttonElement-active,
+ &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-pressed,
+ &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-selected {
+ background-color: @highlight-c3;
+ }
+ }
+ &-c4 {
+ .mw-rcfilters-mixin-circle( @highlight-c4 );
+
+ &.oo-ui-buttonOptionWidget.oo-ui-buttonElement-active,
+ &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-pressed,
+ &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-selected {
+ background-color: @highlight-c4;
+ }
+ }
+ &-c5 {
+ .mw-rcfilters-mixin-circle( @highlight-c5 );
+
+ &.oo-ui-buttonOptionWidget.oo-ui-buttonElement-active,
+ &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-pressed,
+ &.oo-ui-buttonOptionWidget.oo-ui-optionWidget-selected {
+ background-color: @highlight-c5;
+ }
+ }
+ }
+ }
+}
--- /dev/null
+.mw-rcfilters-ui {
+ &-table {
+ display: table;
+ width: 100%;
+ }
+
+ &-row {
+ display: table-row;
+ }
+
+ &-cell {
+ display: table-cell;
+ vertical-align: top;
+ }
+}
+
--- /dev/null
+// Highlight color definitions
+@highlight-none: #fff;
+@highlight-c1: #36c;
+@highlight-c2: #00af89;
+@highlight-c3: #fc3;
+@highlight-c4: #ff6d22;
+@highlight-c5: #d33;
+
+// Muted state
+@muted-opacity: 0.5;
+
+// Result list circle indicators
+// Defined and used in mw.rcfilters.ui.ChangesListWrapperWidget.less
+@result-circle-margin: 0.1em;
+@result-circle-general-margin: 0.5em;
+// In these small sizes, 'em' appears
+// squished and inconsistent.
+// Pixels are better for this use case:
+@result-circle-diameter: 5px;
// Set initial text for the popup - the description
descLabelWidget.setLabel( this.model.getDescription() );
+ this.$highlight = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-capsuleItemWidget-highlight' );
+
// Events
this.model.connect( this, { update: 'onModelUpdate' } );
// Initialization
this.$overlay.append( this.popup.$element );
this.$element
+ .prepend( this.$highlight )
.attr( 'aria-haspopup', 'true' )
.addClass( 'mw-rcfilters-ui-capsuleItemWidget' )
.on( 'mouseover', this.onHover.bind( this, true ) )
.on( 'mouseout', this.onHover.bind( this, false ) );
this.setCurrentMuteState();
+ this.setHighlightColor();
};
OO.inheritClass( mw.rcfilters.ui.CapsuleItemWidget, OO.ui.CapsuleItemWidget );
*/
mw.rcfilters.ui.CapsuleItemWidget.prototype.onModelUpdate = function () {
this.setCurrentMuteState();
+
+ this.setHighlightColor();
+ };
+
+ mw.rcfilters.ui.CapsuleItemWidget.prototype.setHighlightColor = function () {
+ var selectedColor = this.model.isHighlightEnabled() ? this.model.getHighlightColor() : null;
+
+ this.$highlight
+ .attr( 'data-color', selectedColor )
+ .toggleClass(
+ 'mw-rcfilters-ui-capsuleItemWidget-highlight-highlighted',
+ !!selectedColor
+ );
};
/**
this.$element
.toggleClass(
'mw-rcfilters-ui-capsuleItemWidget-muted',
+ !this.model.isSelected() ||
this.model.isIncluded() ||
this.model.isConflicted() ||
this.model.isFullyCovered()
*/
mw.rcfilters.ui.CapsuleItemWidget.prototype.onCapsuleRemovedByUser = function () {
this.controller.updateFilter( this.model.getName(), false );
+ this.controller.clearHighlightColor( this.model.getName() );
};
/**
* @mixins OO.ui.mixin.PendingElement
*
* @constructor
- * @param {mw.rcfilters.dm.ChangesListViewModel} model View model
+ * @param {mw.rcfilters.dm.FiltersViewModel} filtersViewModel View model
+ * @param {mw.rcfilters.dm.ChangesListViewModel} changesListViewModel View model
* @param {jQuery} $changesListRoot Root element of the changes list to attach to
* @param {Object} config Configuration object
*/
- mw.rcfilters.ui.ChangesListWrapperWidget = function MwRcfiltersUiChangesListWrapperWidget( model, $changesListRoot, config ) {
- config = config || {};
+ mw.rcfilters.ui.ChangesListWrapperWidget = function MwRcfiltersUiChangesListWrapperWidget(
+ filtersViewModel,
+ changesListViewModel,
+ $changesListRoot,
+ config
+ ) {
+ config = $.extend( {}, config, {
+ $element: $changesListRoot
+ } );
// Parent
- mw.rcfilters.ui.ChangesListWrapperWidget.parent.call( this, $.extend( {}, config, {
- $element: $changesListRoot
- } ) );
+ mw.rcfilters.ui.ChangesListWrapperWidget.parent.call( this, config );
// Mixin constructors
OO.ui.mixin.PendingElement.call( this, config );
- this.model = model;
+ this.filtersViewModel = filtersViewModel;
+ this.changesListViewModel = changesListViewModel;
// Events
- this.model.connect( this, {
+ this.filtersViewModel.connect( this, {
+ itemUpdate: 'onItemUpdate',
+ highlightChange: 'onHighlightChange'
+ } );
+ this.changesListViewModel.connect( this, {
invalidate: 'onModelInvalidate',
update: 'onModelUpdate'
} );
+
+ this.$element.addClass( 'mw-rcfilters-ui-changesListWrapperWidget' );
+
+ // Set up highlight containers
+ this.setupHighlightContainers( this.$element );
};
/* Initialization */
OO.mixinClass( mw.rcfilters.ui.ChangesListWrapperWidget, OO.ui.mixin.PendingElement );
/**
- * Respond to model invalidate
+ * Respond to the highlight feature being toggled on and off
+ *
+ * @param {boolean} highlightEnabled
+ */
+ mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onHighlightChange = function ( highlightEnabled ) {
+ if ( highlightEnabled ) {
+ this.applyHighlight();
+ } else {
+ this.clearHighlight();
+ }
+ };
+
+ /**
+ * Respond to a filter item model update
+ */
+ mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onItemUpdate = function () {
+ if ( this.filtersViewModel.isHighlightEnabled() ) {
+ this.clearHighlight();
+ this.applyHighlight();
+ }
+ };
+
+ /**
+ * Respond to changes list model invalidate
*/
mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onModelInvalidate = function () {
this.pushPending();
};
/**
- * Respond to model update
+ * Respond to changes list model update
*
- * @param {jQuery|string} changesListContent The content of the updated changes list
+ * @param {jQuery|string} $changesListContent The content of the updated changes list
*/
- mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onModelUpdate = function ( changesListContent ) {
- var isEmpty = changesListContent === 'NO_RESULTS';
+ mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onModelUpdate = function ( $changesListContent ) {
+ var isEmpty = $changesListContent === 'NO_RESULTS';
+
this.$element.toggleClass( 'mw-changeslist', !isEmpty );
this.$element.toggleClass( 'mw-changeslist-empty', isEmpty );
- this.$element.empty().append(
- isEmpty ?
- document.createTextNode( mw.message( 'recentchanges-noresult' ).text() ) :
- changesListContent
- );
+ if ( isEmpty ) {
+ this.$changesListContent = null;
+ this.$element.empty().append(
+ document.createTextNode( mw.message( 'recentchanges-noresult' ).text() )
+ );
+ } else {
+ this.$changesListContent = $changesListContent;
+ this.$element.empty().append( this.$changesListContent );
+ // Set up highlight containers
+ this.setupHighlightContainers( this.$element );
+
+ // Apply highlight
+ this.applyHighlight();
+
+ // Make sure enhanced RC re-initializes correctly
+ mw.hook( 'wikipage.content' ).fire( this.$changesListContent );
+ }
this.popPending();
};
+
+ /**
+ * Set up the highlight containers with all color circle indicators.
+ *
+ * @param {jQuery|string} $content The content of the updated changes list
+ */
+ mw.rcfilters.ui.ChangesListWrapperWidget.prototype.setupHighlightContainers = function ( $content ) {
+ var $highlights = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlights' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlights-color-none' )
+ .prop( 'data-color', 'none' )
+ );
+
+ mw.rcfilters.HighlightColors.forEach( function ( color ) {
+ $highlights.append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlights-color-' + color )
+ .prop( 'data-color', color )
+ );
+ } );
+
+ if ( Number( mw.user.options.get( 'usenewrc' ) ) ) {
+ // Enhanced RC
+ $content.find( 'td.mw-enhanced-rc' )
+ .parent()
+ .prepend(
+ $( '<td>' )
+ .append( $highlights.clone() )
+ );
+ } else {
+ // Regular RC
+ $content.find( 'ul.special li' )
+ .prepend( $highlights.clone() );
+ }
+ };
+
+ /**
+ * Apply color classes based on filters highlight configuration
+ */
+ mw.rcfilters.ui.ChangesListWrapperWidget.prototype.applyHighlight = function () {
+ if ( !this.filtersViewModel.isHighlightEnabled() ) {
+ return;
+ }
+
+ this.filtersViewModel.getHighlightedItems().forEach( function ( filterItem ) {
+ // Add highlight class to all highlighted list items
+ this.$element.find( '.' + filterItem.getCssClass() )
+ .addClass( 'mw-rcfilters-highlight-color-' + filterItem.getHighlightColor() );
+ }.bind( this ) );
+
+ // Turn on highlights
+ this.$element.addClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlighted' );
+ };
+
+ /**
+ * Remove all color classes
+ */
+ mw.rcfilters.ui.ChangesListWrapperWidget.prototype.clearHighlight = function () {
+ // Remove highlight classes
+ mw.rcfilters.HighlightColors.forEach( function ( color ) {
+ this.$element.find( '.mw-rcfilters-highlight-color-' + color ).removeClass( 'mw-rcfilters-highlight-color-' + color );
+ }.bind( this ) );
+
+ // Turn off highlights
+ this.$element.removeClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlighted' );
+ };
}( mediaWiki ) );
* @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
*/
mw.rcfilters.ui.FilterCapsuleMultiselectWidget = function MwRcfiltersUiFilterCapsuleMultiselectWidget( controller, model, filterInput, config ) {
+ this.$overlay = config.$overlay || this.$element;
+
// Parent
- mw.rcfilters.ui.FilterCapsuleMultiselectWidget.parent.call( this, $.extend( {
- $autoCloseIgnore: filterInput.$element
+ mw.rcfilters.ui.FilterCapsuleMultiselectWidget.parent.call( this, $.extend( true, {
+ popup: { $autoCloseIgnore: filterInput.$element.add( this.$overlay ) }
}, config ) );
this.controller = controller;
this.model = model;
- this.$overlay = config.$overlay || this.$element;
this.filterInput = filterInput;
// Events
this.resetButton.connect( this, { click: 'onResetButtonClick' } );
- this.model.connect( this, { itemUpdate: 'onModelItemUpdate' } );
+ this.model.connect( this, {
+ itemUpdate: 'onModelItemUpdate',
+ highlightChange: 'onModelHighlightChange'
+ } );
// Add the filterInput as trigger
this.filterInput.$input
.on( 'focus', this.focus.bind( this ) );
* @param {mw.rcfilters.dm.FilterItem} item Filter item model
*/
mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onModelItemUpdate = function ( item ) {
- if ( item.isSelected() ) {
+ if (
+ item.isSelected() ||
+ (
+ this.model.isHighlightEnabled() &&
+ item.isHighlightSupported() &&
+ item.getHighlightColor()
+ )
+ ) {
this.addItemByName( item.getName() );
} else {
this.removeItemByName( item.getName() );
this.reevaluateResetRestoreState();
};
+ /**
+ * Respond to highlightChange event
+ *
+ * @param {boolean} isHighlightEnabled Highlight is enabled
+ */
+ mw.rcfilters.ui.FilterCapsuleMultiselectWidget.prototype.onModelHighlightChange = function ( isHighlightEnabled ) {
+ var highlightedItems = this.model.getHighlightedItems();
+
+ if ( isHighlightEnabled ) {
+ // Add capsule widgets
+ highlightedItems.forEach( function ( filterItem ) {
+ this.addItemByName( filterItem.getName() );
+ }.bind( this ) );
+ } else {
+ // Remove capsule widgets if they're not selected
+ highlightedItems.forEach( function ( filterItem ) {
+ if ( !filterItem.isSelected() ) {
+ this.removeItemByName( filterItem.getName() );
+ }
+ }.bind( this ) );
+ }
+ };
+
/**
* Respond to click event on the reset button
*/
$label: $( '<div>' )
.addClass( 'mw-rcfilters-ui-filterGroupWidget-title' )
} ) );
+ this.$overlay = config.$overlay || this.$element;
// Populate
this.populateFromModel();
filterItem,
{
label: filterItem.getLabel(),
- description: filterItem.getDescription()
+ description: filterItem.getDescription(),
+ $overlay: widget.$overlay
}
);
} )
--- /dev/null
+( function ( mw, $ ) {
+ /**
+ * A button to configure highlight for a filter item
+ *
+ * @extends OO.ui.PopupButtonWidget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller RCFilters controller
+ * @param {mw.rcfilters.dm.FilterItem} model Filter item model
+ * @param {Object} [config] Configuration object
+ */
+ mw.rcfilters.ui.FilterItemHighlightButton = function MwRcfiltersUiFilterItemHighlightButton( controller, model, config ) {
+ config = config || {};
+
+ this.colorPickerWidget = new mw.rcfilters.ui.HighlightColorPickerWidget( controller, model );
+
+ // Parent
+ mw.rcfilters.ui.FilterItemHighlightButton.parent.call( this, $.extend( {}, config, {
+ icon: 'edit',
+ indicator: 'down',
+ popup: {
+ anchor: false,
+ padded: true,
+ align: 'backwards',
+ width: 290,
+ $content: this.colorPickerWidget.$element
+ }
+ } ) );
+
+ this.controller = controller;
+ this.model = model;
+
+ // Event
+ this.model.connect( this, { update: 'onModelUpdate' } );
+ this.colorPickerWidget.connect( this, { chooseColor: 'onChooseColor' } );
+
+ this.$element
+ .addClass( 'mw-rcfilters-ui-filterItemHighlightButton' );
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.rcfilters.ui.FilterItemHighlightButton, OO.ui.PopupButtonWidget );
+
+ /* Methods */
+
+ /**
+ * Respond to item model update event
+ */
+ mw.rcfilters.ui.FilterItemHighlightButton.prototype.onModelUpdate = function () {
+ var currentColor = this.model.getHighlightColor(),
+ widget = this;
+
+ this.$icon.toggleClass(
+ 'mw-rcfilters-ui-filterItemHighlightButton-circle',
+ currentColor !== null
+ );
+
+ mw.rcfilters.HighlightColors.forEach( function ( c ) {
+ widget.$icon
+ .toggleClass(
+ 'mw-rcfilters-ui-filterItemHighlightButton-circle-color-' + c,
+ c === currentColor
+ );
+ } );
+ };
+
+ mw.rcfilters.ui.FilterItemHighlightButton.prototype.onChooseColor = function () {
+ this.popup.toggle( false );
+ };
+}( mediaWiki, jQuery ) );
);
}
+ this.highlightButton = new mw.rcfilters.ui.FilterItemHighlightButton(
+ this.controller,
+ this.model,
+ {
+ $overlay: config.$overlay || this.$element
+ }
+ );
+ this.highlightButton.toggle( this.model.isHighlightEnabled() );
+
layout = new OO.ui.FieldLayout( this.checkboxWidget, {
label: $label,
align: 'inline'
this.$element
.addClass( 'mw-rcfilters-ui-filterItemWidget' )
.append(
- layout.$element
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-table' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-row' )
+ .append(
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-filterItemWidget-filterCheckbox' )
+ .append( layout.$element ),
+ $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-cell mw-rcfilters-ui-filterItemWidget-highlightButton' )
+ .append( this.highlightButton.$element )
+ )
+ )
);
};
!this.model.isSelected()
)
);
+
+ this.highlightButton.toggle( this.model.isHighlightEnabled() );
};
+
/**
* Get the name of this filter
*
mw.rcfilters.ui.FilterItemWidget.prototype.getName = function () {
return this.model.getName();
};
-
}( mediaWiki, jQuery ) );
this.controller,
this.model,
{
- label: mw.msg( 'rcfilters-filterlist-title' )
+ label: mw.msg( 'rcfilters-filterlist-title' ),
+ $overlay: this.$overlay
}
);
this.controller = controller;
this.model = model;
+ this.$overlay = config.$overlay || this.$element;
+
+ this.highlightButton = new OO.ui.ButtonWidget( {
+ label: mw.message( 'rcfilters-highlightbutton-title' ).text(),
+ classes: [ 'mw-rcfilters-ui-filtersListWidget-hightlightButton' ]
+ } );
+
+ this.$label.append( this.highlightButton.$element );
this.noResultsLabel = new OO.ui.LabelWidget( {
label: mw.msg( 'rcfilters-filterlist-noresults' ),
} );
// Events
+ this.highlightButton.connect( this, { click: 'onHighlightButtonClick' } );
this.model.connect( this, {
- initialize: 'onModelInitialize'
+ initialize: 'onModelInitialize',
+ highlightChange: 'onHighlightChange'
} );
// Initialize
Object.keys( this.model.getFilterGroups() ).map( function ( groupName ) {
return new mw.rcfilters.ui.FilterGroupWidget(
widget.controller,
- widget.model.getGroup( groupName )
+ widget.model.getGroup( groupName ),
+ {
+ $overlay: widget.$overlay
+ }
);
} )
);
};
+ mw.rcfilters.ui.FiltersListWidget.prototype.onHighlightChange = function ( highlightEnabled ) {
+ this.highlightButton.setActive( highlightEnabled );
+ };
+
+ /**
+ * Respond to highlight button click
+ */
+ mw.rcfilters.ui.FiltersListWidget.prototype.onHighlightButtonClick = function () {
+ this.controller.toggleHighlight();
+ };
+
/**
* Switch between showing the 'no results' message for filtering results or the result list.
*
--- /dev/null
+( function ( mw, $ ) {
+ /**
+ * A widget representing a filter item highlight color picker
+ *
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.mixin.LabelElement
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller RCFilters controller
+ * @param {mw.rcfilters.dm.FilterItem} model Filter item model
+ * @param {Object} [config] Configuration object
+ */
+ mw.rcfilters.ui.HighlightColorPickerWidget = function MwRcfiltersUiHighlightColorPickerWidget( controller, model, config ) {
+ var colors = [ 'none' ].concat( mw.rcfilters.HighlightColors );
+ config = config || {};
+
+ // Parent
+ mw.rcfilters.ui.HighlightColorPickerWidget.parent.call( this, config );
+ // Mixin constructors
+ OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, {
+ label: mw.message( 'rcfilters-highlightmenu-title' ).text()
+ } ) );
+
+ this.controller = controller;
+ this.model = model;
+
+ this.currentSelection = '';
+ this.buttonSelect = new OO.ui.ButtonSelectWidget( {
+ items: colors.map( function ( color ) {
+ return new OO.ui.ButtonOptionWidget( {
+ icon: color === 'none' ? 'check' : null,
+ data: color,
+ classes: [
+ 'mw-rcfilters-ui-highlightColorPickerWidget-buttonSelect-color',
+ 'mw-rcfilters-ui-highlightColorPickerWidget-buttonSelect-color-' + color
+ ],
+ framed: false
+ } );
+ } ),
+ classes: 'mw-rcfilters-ui-highlightColorPickerWidget-buttonSelect'
+ } );
+ this.selectColor( 'none' );
+
+ // Event
+ this.model.connect( this, { update: 'onModelUpdate' } );
+ this.buttonSelect.connect( this, { choose: 'onChooseColor' } );
+
+ this.$element
+ .addClass( 'mw-rcfilters-ui-highlightColorPickerWidget' )
+ .append(
+ this.$label
+ .addClass( 'mw-rcfilters-ui-highlightColorPickerWidget-label' ),
+ this.buttonSelect.$element
+ );
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.rcfilters.ui.HighlightColorPickerWidget, OO.ui.Widget );
+ OO.mixinClass( mw.rcfilters.ui.HighlightColorPickerWidget, OO.ui.mixin.LabelElement );
+
+ /* Events */
+
+ /**
+ * @event chooseColor
+ * @param {string} The chosen color
+ *
+ * A color has been chosen
+ */
+
+ /* Methods */
+
+ /**
+ * Respond to item model update event
+ */
+ mw.rcfilters.ui.HighlightColorPickerWidget.prototype.onModelUpdate = function () {
+ this.selectColor( this.model.getHighlightColor() || 'none' );
+ };
+
+ /**
+ * Select the color for this widget
+ *
+ * @param {string} color Selected color
+ */
+ mw.rcfilters.ui.HighlightColorPickerWidget.prototype.selectColor = function ( color ) {
+ var previousItem = this.buttonSelect.getItemFromData( this.currentSelection ),
+ selectedItem = this.buttonSelect.getItemFromData( color );
+
+ if ( this.currentSelection !== color ) {
+ this.currentSelection = color;
+
+ this.buttonSelect.selectItem( selectedItem );
+ if ( previousItem ) {
+ previousItem.setIcon( null );
+ }
+
+ if ( selectedItem ) {
+ selectedItem.setIcon( 'check' );
+ }
+ }
+ };
+
+ mw.rcfilters.ui.HighlightColorPickerWidget.prototype.onChooseColor = function ( button ) {
+ var color = button.data;
+ if ( color === 'none' ) {
+ this.controller.clearHighlightColor( this.model.getName() );
+ } else {
+ this.controller.setHighlightColor( this.model.getName(), color );
+ }
+ this.emit( 'chooseColor', color );
+ };
+}( mediaWiki, jQuery ) );
$noRateLimitUser->expects( $this->any() )->method( 'getRights' )->willReturn( [ 'noratelimit' ] );
$this->assertFalse( $noRateLimitUser->isPingLimitable() );
}
+
+ public function provideExperienceLevel() {
+ return [
+ [ 2, 2, 'newcomer' ],
+ [ 12, 3, 'newcomer' ],
+ [ 8, 5, 'newcomer' ],
+ [ 15, 10, 'learner' ],
+ [ 450, 20, 'learner' ],
+ [ 460, 33, 'learner' ],
+ [ 525, 28, 'learner' ],
+ [ 538, 33, 'experienced' ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideExperienceLevel
+ */
+ public function testExperienceLevel( $editCount, $memberSince, $expLevel ) {
+ $this->setMwGlobals( [
+ 'wgLearnerEdits' => 10,
+ 'wgLearnerMemberSince' => 4,
+ 'wgExperiencedUserEdits' => 500,
+ 'wgExperiencedUserMemberSince' => 30,
+ ] );
+
+ $db = wfGetDB( DB_MASTER );
+
+ $data = new stdClass();
+ $data->user_id = 1;
+ $data->user_name = 'name';
+ $data->user_real_name = 'Real Name';
+ $data->user_touched = 1;
+ $data->user_token = 'token';
+ $data->user_email = 'a@a.a';
+ $data->user_email_authenticated = null;
+ $data->user_email_token = 'token';
+ $data->user_email_token_expires = null;
+ $data->user_editcount = $editCount;
+ $data->user_registration = $db->timestamp( time() - $memberSince * 86400 );
+ $user = User::newFromRow( $data );
+
+ $this->assertEquals( $expLevel, $user->getExperienceLevel() );
+ }
+
+ public function testExperienceLevelAnon() {
+ $user = User::newFromName( '10.11.12.13', false );
+
+ $this->assertFalse( $user->getExperienceLevel() );
+ }
}
'Selecting a non-conflicting filter from a conflicting group removes the conflict'
);
} );
+
+ QUnit.test( 'Filter highlights', function ( assert ) {
+ var definition = {
+ group1: {
+ title: 'Group 1',
+ type: 'string_options',
+ filters: [
+ { name: 'filter1', class: 'class1' },
+ { name: 'filter2', class: 'class2' },
+ { name: 'filter3', class: 'class3' },
+ { name: 'filter4', class: 'class4' },
+ { name: 'filter5', class: 'class5' },
+ { name: 'filter6' }
+ ]
+ }
+ },
+ model = new mw.rcfilters.dm.FiltersViewModel();
+
+ model.initializeFilters( definition );
+
+ assert.ok(
+ !model.isHighlightEnabled(),
+ 'Initially, highlight is disabled.'
+ );
+
+ model.toggleHighlight( true );
+ assert.ok(
+ model.isHighlightEnabled(),
+ 'Highlight is enabled on toggle.'
+ );
+
+ model.setHighlightColor( 'filter1', 'color1' );
+ model.setHighlightColor( 'filter2', 'color2' );
+
+ assert.deepEqual(
+ model.getHighlightedItems().map( function ( item ) {
+ return item.getName();
+ } ),
+ [
+ 'filter1',
+ 'filter2'
+ ],
+ 'Highlighted items are highlighted.'
+ );
+
+ assert.equal(
+ model.getItemByName( 'filter1' ).getHighlightColor(),
+ 'color1',
+ 'Item highlight color is set.'
+ );
+
+ model.setHighlightColor( 'filter1', 'color1changed' );
+ assert.equal(
+ model.getItemByName( 'filter1' ).getHighlightColor(),
+ 'color1changed',
+ 'Item highlight color is changed on setHighlightColor.'
+ );
+
+ model.clearHighlightColor( 'filter1' );
+ assert.deepEqual(
+ model.getHighlightedItems().map( function ( item ) {
+ return item.getName();
+ } ),
+ [
+ 'filter2'
+ ],
+ 'Clear highlight from an item results in the item no longer being highlighted.'
+ );
+
+ // Reset
+ model = new mw.rcfilters.dm.FiltersViewModel();
+ model.initializeFilters( definition );
+
+ model.setHighlightColor( 'filter1', 'color1' );
+ model.setHighlightColor( 'filter2', 'color2' );
+ model.setHighlightColor( 'filter3', 'color3' );
+
+ assert.deepEqual(
+ model.getHighlightedItems().map( function ( item ) {
+ return item.getName();
+ } ),
+ [
+ 'filter1',
+ 'filter2',
+ 'filter3'
+ ],
+ 'Even if highlights are not enabled, the items remember their highlight state'
+ // NOTE: When actually displaying the highlights, the UI checks whether
+ // highlighting is generally active and then goes over the highlighted
+ // items. The item models, however, and the view model in general, still
+ // retains the knowledge about which filters have different colors, so we
+ // can seamlessly return to the colors the user previously chose if they
+ // reapply highlights.
+ );
+
+ // Reset
+ model = new mw.rcfilters.dm.FiltersViewModel();
+ model.initializeFilters( definition );
+
+ model.setHighlightColor( 'filter1', 'color1' );
+ model.setHighlightColor( 'filter6', 'color6' );
+
+ assert.deepEqual(
+ model.getHighlightedItems().map( function ( item ) {
+ return item.getName();
+ } ),
+ [
+ 'filter1'
+ ],
+ 'Items without a specified class identifier are not highlighted.'
+ );
+ } );
}( mediaWiki, jQuery ) );