From 8fd37d3832ee7a34222ee190511348ca2b22143e Mon Sep 17 00:00:00 2001 From: Roan Kattouw Date: Thu, 9 Mar 2017 19:47:34 -0800 Subject: [PATCH] RCFilters UI: Create tooltips for filter states Tooltips represent the state of the filter, whether it is conflicted, included, or fully covered. If none of the above, tooltip message falls back on displaying the description of the filter. Bug: T156864 Change-Id: Ic97c7c6aae78bb6ddf51f0294eeae4b7f86a1a1d --- languages/i18n/en.json | 2 + languages/i18n/qqq.json | 2 + resources/Resources.php | 4 + .../dm/mw.rcfilters.dm.FilterItem.js | 108 +++++++++++++++++- .../dm/mw.rcfilters.dm.FiltersViewModel.js | 19 ++- .../ui/mw.rcfilters.ui.CapsuleItemWidget.js | 19 ++- 6 files changed, 137 insertions(+), 17 deletions(-) diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 2ef4f3ae8c..77e93ae0a5 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -1375,6 +1375,8 @@ "rcfilters-highlightmenu-title": "Select a color", "rcfilters-highlightmenu-help": "Select a color to highlight this property", "rcfilters-filterlist-noresults": "No filters found", + "rcfilters-state-message-subset": "This filter has no effect because its results are included with those of the following, broader {{PLURAL:$2|filter|filters}} (try highlighting to distinguish it): $1", + "rcfilters-state-message-fullcoverage": "Selecting all filters in a group is the same as selecting none, so this filter has no effect. Group includes: $1", "rcfilters-filtergroup-registration": "User registration", "rcfilters-filter-registered-label": "Registered", "rcfilters-filter-registered-description": "Logged-in editors.", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index c3a871f881..ead277d771 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -1562,6 +1562,8 @@ "rcfilters-highlightmenu-title": "Title for the highlight menu used to select the highlight color for an individual filter.", "rcfilters-highlightmenu-help": "Tooltip for the highlight menu for individual filters.", "rcfilters-filterlist-noresults": "Message showing no results found for searching a filter.", + "rcfilters-state-message-subset": "Tooltip shown when hovering over a filter tag when one or more broader filters that contain the hovered filter are also selected. This indicates that the hovered filter has no effect because all the results it matches are also matched by the broader filter(s). Parameters:\n* $1 - Comma-separated string of selected broader filters that this filter is a subset of\n* $2 - Count of filters in $1, for PLURAL", + "rcfilters-state-message-fullcoverage": "Tooltip shown when hovering over a filter tag when all the filters in its group are selected. This indicates that the hovered filter has no effect because the selected filters in the group cover all changes. Parameters:\n* $1 - Comma-separated string of selected filters in the group\n* $2 - Count of filters in $1, for PLURAL", "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}}", "rcfilters-filter-registered-description": "Description for the filter for showing edits made by logged-in users.", diff --git a/resources/Resources.php b/resources/Resources.php index 64dcf79150..6a26ca26a2 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -1815,10 +1815,14 @@ return [ 'rcfilters-highlightbutton-title', 'rcfilters-highlightmenu-title', 'rcfilters-highlightmenu-help', + 'rcfilters-state-message-subset', + 'rcfilters-state-message-fullcoverage', 'recentchanges-noresult', + 'quotation-marks', ], 'dependencies' => [ 'oojs-ui', + 'mediawiki.language', 'mediawiki.rcfilters.filters.dm', 'oojs-ui.styles.icons-moderation', 'oojs-ui.styles.icons-editing-core', diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js index 852b810c43..59f09bbb2d 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js @@ -96,6 +96,97 @@ return this.param; }; + /** + * Get the details of the active conflict on this filter + * + * @param {Object} conflicts Conflicts to examine + * @param {string} [key='contextDescription'] Message key + * @return {Object} Object with conflict message and conflict items + * @return {string} return.message Conflict message + * @return {string[]} return.names Conflicting item labels + */ + mw.rcfilters.dm.FilterItem.prototype.getConflictDetails = function ( conflicts, key ) { + var group, + conflictMessage = '', + itemLabels = []; + + key = key || 'contextDescription'; + + $.each( conflicts, function ( filterName, conflict ) { + if ( !conflict.item.isSelected() ) { + return; + } + + if ( !conflictMessage ) { + conflictMessage = conflict[ key ]; + group = conflict.group; + } + + if ( group === conflict.group ) { + itemLabels.push( mw.msg( 'quotation-marks', conflict.item.getLabel() ) ); + } + } ); + + return { + message: conflictMessage, + names: itemLabels + }; + + }; + + /** + * Get the message representing the state of this model. + * + * @return {string} State message + */ + mw.rcfilters.dm.FilterItem.prototype.getStateMessage = function () { + var messageKey, details, superset, + affectingItems = []; + + if ( this.isConflicted() ) { + // First look in filter's own conflicts + details = this.getConflictDetails( this.getOwnConflicts() ); + if ( !details.message ) { + // Fall back onto conflicts in the group + details = this.getConflictDetails( this.getGroupModel().getConflicts() ); + } + + messageKey = details.message; + affectingItems = details.names; + } else if ( this.isIncluded() ) { + superset = this.getSuperset(); + // For this message we need to collect the affecting superset + affectingItems = this.getGroupModel().getSelectedItems( this ) + .filter( function ( item ) { + return superset.indexOf( item.getName() ) !== -1; + } ) + .map( function ( item ) { + return mw.msg( 'quotation-marks', item.getLabel() ); + } ); + + messageKey = 'rcfilters-state-message-subset'; + } else if ( this.isFullyCovered() ) { + affectingItems = this.getGroupModel().getSelectedItems( this ) + .map( function ( item ) { + return mw.msg( 'quotation-marks', item.getLabel() ); + } ); + + messageKey = 'rcfilters-state-message-fullcoverage'; + } + + if ( messageKey ) { + // Build message + return mw.msg( + messageKey, + mw.language.listToText( affectingItems ), + affectingItems.length + ); + } + + // Display description + return this.getDescription(); + }; + /** * Get the model of the group this filter belongs to * @@ -200,18 +291,22 @@ }; /** - * Get filter conflicts + * Get all conflicts associated with this filter or its group * * Conflict object is set up by filter name keys and conflict * definition. For example: * { * filterName: { * filter: filterName, - * group: group1 + * group: group1, + * label: itemLabel, + * item: itemModel * } * filterName2: { * filter: filterName2, * group: group2 + * label: itemLabel2, + * item: itemModel2 * } * } * @@ -221,6 +316,15 @@ return $.extend( {}, this.conflicts, this.getGroupModel().getConflicts() ); }; + /** + * Get the conflicts associated with this filter + * + * @return {Object} Filter conflicts + */ + mw.rcfilters.dm.FilterItem.prototype.getOwnConflicts = function () { + return this.conflicts; + }; + /** * Set conflicts for this filter. See #getConflicts for the expected * structure of the definition. diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js index cf51424fe4..ca0282d607 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js @@ -188,14 +188,20 @@ adjustedConflicts = {}; conflicts.forEach( function ( conflict ) { + var filter; + if ( conflict.filter ) { filterName = model.groups[ conflict.group ].getNamePrefix() + conflict.filter; + filter = model.getItemByName( filterName ); // Rename adjustedConflicts[ filterName ] = $.extend( {}, conflict, - { filter: filterName } + { + filter: filterName, + item: filter + } ); } else { // This conflict is for an entire group. Split it up to @@ -207,7 +213,10 @@ adjustedConflicts[ groupItem.getName() ] = $.extend( {}, conflict, - { filter: groupItem.getName() } + { + filter: groupItem.getName(), + item: groupItem + } ); } ); } @@ -305,6 +314,9 @@ } } ); + // Add items to the model + this.addItems( items ); + // Expand conflicts groupConflictResult = expandConflictDefinitions( groupConflictMap ); filterConflictResult = expandConflictDefinitions( filterConflictMap ); @@ -337,9 +349,6 @@ } } ); - // Add items to the model - this.addItems( items ); - this.emit( 'initialize' ); }; diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CapsuleItemWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CapsuleItemWidget.js index f28523a765..a72af8eb8d 100644 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CapsuleItemWidget.js +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CapsuleItemWidget.js @@ -13,15 +13,12 @@ * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups */ mw.rcfilters.ui.CapsuleItemWidget = function MwRcfiltersUiCapsuleItemWidget( controller, model, config ) { - var $popupContent = $( '
' ) - .addClass( 'mw-rcfilters-ui-capsuleItemWidget-popup-content' ), - descLabelWidget = new OO.ui.LabelWidget(); - // Configuration initialization config = config || {}; this.controller = controller; this.model = model; + this.popupLabel = new OO.ui.LabelWidget(); this.$overlay = config.$overlay || this.$element; this.positioned = false; this.popupTimeoutShow = null; @@ -39,16 +36,14 @@ padded: false, align: 'center', position: 'above', - $content: $popupContent - .append( descLabelWidget.$element ), + $content: $( '
' ) + .addClass( 'mw-rcfilters-ui-capsuleItemWidget-popup-content' ) + .append( this.popupLabel.$element ), $floatableContainer: this.$element, classes: [ 'mw-rcfilters-ui-capsuleItemWidget-popup' ] } }, config ) ); - // Set initial text for the popup - the description - descLabelWidget.setLabel( this.model.getDescription() ); - this.$highlight = $( '
' ) .addClass( 'mw-rcfilters-ui-capsuleItemWidget-highlight' ); @@ -147,7 +142,11 @@ * Respond to mouse enter event */ mw.rcfilters.ui.CapsuleItemWidget.prototype.onMouseEnter = function () { - if ( this.model.getDescription() ) { + var labelText = this.model.getStateMessage(); + + if ( labelText ) { + this.popupLabel.setLabel( labelText ); + if ( !this.positioned ) { // Recalculate anchor position to be center of the capsule item this.popup.$anchor.css( 'margin-left', ( this.$element.width() / 2 ) ); -- 2.20.1