wikimedia/mediawiki-core

View on GitHub
resources/src/mediawiki.rcfilters/dm/FilterItem.js

Summary

Maintainability
C
1 day
Test Coverage
var ItemModel = require( './ItemModel.js' ),
    FilterItem;

/**
 * Filter item model.
 *
 * @class mw.rcfilters.dm.FilterItem
 * @ignore
 * @extends mw.rcfilters.dm.ItemModel
 *
 * @param {string} param Filter param name
 * @param {mw.rcfilters.dm.FilterGroup} groupModel Filter group model
 * @param {Object} config Configuration object
 * @param {string[]} [config.excludes=[]] A list of filter names this filter, if
 *  selected, makes inactive.
 * @param {string[]} [config.subset] Defining the names of filters that are a subset of this filter
 * @param {Object} [config.conflicts] Defines the conflicts for this filter
 * @param {boolean} [config.visible=true] The visibility of the group
 */
FilterItem = function MwRcfiltersDmFilterItem( param, groupModel, config ) {
    config = config || {};

    this.groupModel = groupModel;

    // Parent
    FilterItem.super.call( this, param, $.extend( {
        namePrefix: this.groupModel.getNamePrefix()
    }, config ) );
    // Mixin constructor
    OO.EventEmitter.call( this );

    // Interaction definitions
    this.subset = config.subset || [];
    this.conflicts = config.conflicts || {};
    this.superset = [];
    this.visible = config.visible === undefined ? true : !!config.visible;

    // Interaction states
    this.included = false;
    this.conflicted = false;
    this.fullyCovered = false;
};

/* Initialization */

OO.inheritClass( FilterItem, ItemModel );

/* Methods */

/**
 * Return the representation of the state of this item.
 *
 * @return {Object} State of the object
 */
FilterItem.prototype.getState = function () {
    return {
        selected: this.isSelected(),
        included: this.isIncluded(),
        conflicted: this.isConflicted(),
        fullyCovered: this.isFullyCovered()
    };
};

/**
 * Get the message for the display area for the currently active conflict
 *
 * @return {string} Conflict result message key
 */
FilterItem.prototype.getCurrentConflictResultMessage = function () {
    var details;

    // First look in filter's own conflicts
    details = this.getConflictDetails( this.getOwnConflicts(), 'globalDescription' );
    if ( !details.message ) {
        // Fall back onto conflicts in the group
        details = this.getConflictDetails( this.getGroupModel().getConflicts(), 'globalDescription' );
    }

    return details.message;
};

/**
 * Get the details of the active conflict on this filter
 *
 * @private
 * @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
 */
FilterItem.prototype.getConflictDetails = function ( conflicts, key ) {
    var group,
        conflictMessage = '',
        itemLabels = [];

    key = key || 'contextDescription';

    // eslint-disable-next-line no-jquery/no-each-util
    $.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
    };

};

/**
 * @inheritdoc
 */
FilterItem.prototype.getStateMessage = function () {
    var messageKey, details, superset,
        affectingItems = [];

    if ( this.isSelected() ) {
        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() && !this.isHighlighted() ) {
            // We only show the 'no effect' full-coverage message
            // if the item is also not highlighted. See T161273
            superset = this.getSuperset();
            // For this message we need to collect the affecting superset
            affectingItems = this.getGroupModel().findSelectedItems( 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() && !this.isHighlighted() ) {
            affectingItems = this.getGroupModel().findSelectedItems( this )
                .map( function ( item ) {
                    return mw.msg( 'quotation-marks', item.getLabel() );
                } );

            messageKey = 'rcfilters-state-message-fullcoverage';
        }
    }

    if ( messageKey ) {
        // Build message
        // The following messages are used here:
        // * rcfilters-state-message-subset
        // * rcfilters-state-message-fullcoverage
        // * conflict.message values...
        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
 *
 * @ignore
 * @return {mw.rcfilters.dm.FilterGroup} Filter group model
 */
FilterItem.prototype.getGroupModel = function () {
    return this.groupModel;
};

/**
 * Get the group name this filter belongs to
 *
 * @return {string} Filter group name
 */
FilterItem.prototype.getGroupName = function () {
    return this.groupModel.getName();
};

/**
 * Get filter subset
 * This is a list of filter names that are defined to be included
 * when this filter is selected.
 *
 * @return {string[]} Filter subset
 */
FilterItem.prototype.getSubset = function () {
    return this.subset;
};

/**
 * Get filter superset
 * This is a generated list of filters that define this filter
 * to be included when either of them is selected.
 *
 * @return {string[]} Filter superset
 */
FilterItem.prototype.getSuperset = function () {
    return this.superset;
};

/**
 * Check whether the filter is currently in a conflict state
 *
 * @return {boolean} Filter is in conflict state
 */
FilterItem.prototype.isConflicted = function () {
    return this.conflicted;
};

/**
 * Check whether the filter is currently in an already included subset
 *
 * @return {boolean} Filter is in an already-included subset
 */
FilterItem.prototype.isIncluded = function () {
    return this.included;
};

/**
 * Check whether the filter is currently fully covered
 *
 * @return {boolean} Filter is in fully-covered state
 */
FilterItem.prototype.isFullyCovered = function () {
    return this.fullyCovered;
};

/**
 * 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,
 *          label: itemLabel,
 *          item: itemModel
 *      }
 *      filterName2: {
 *          filter: filterName2,
 *          group: group2
 *          label: itemLabel2,
 *          item: itemModel2
 *      }
 *  }
 *
 * @return {Object} Filter conflicts
 */
FilterItem.prototype.getConflicts = function () {
    return $.extend( {}, this.conflicts, this.getGroupModel().getConflicts() );
};

/**
 * Get the conflicts associated with this filter
 *
 * @return {Object} Filter conflicts
 */
FilterItem.prototype.getOwnConflicts = function () {
    return this.conflicts;
};

/**
 * Set conflicts for this filter. See #getConflicts for the expected
 * structure of the definition.
 *
 * @param {Object} conflicts Conflicts for this filter
 */
FilterItem.prototype.setConflicts = function ( conflicts ) {
    this.conflicts = conflicts || {};
};

/**
 * Set filter superset
 *
 * @param {string[]} superset Filter superset
 */
FilterItem.prototype.setSuperset = function ( superset ) {
    this.superset = superset || [];
};

/**
 * Set filter subset
 *
 * @param {string[]} subset Filter subset
 */
FilterItem.prototype.setSubset = function ( subset ) {
    this.subset = subset || [];
};

/**
 * Check whether a filter exists in the subset list for this filter
 *
 * @param {string} filterName Filter name
 * @return {boolean} Filter name is in the subset list
 */
FilterItem.prototype.existsInSubset = function ( filterName ) {
    return this.subset.indexOf( filterName ) > -1;
};

/**
 * Check whether this item has a potential conflict with the given item
 *
 * This checks whether the given item is in the list of conflicts of
 * the current item, but makes no judgment about whether the conflict
 * is currently at play (either one of the items may not be selected)
 *
 * @param {mw.rcfilters.dm.FilterItem} filterItem Filter item
 * @return {boolean} This item has a conflict with the given item
 */
FilterItem.prototype.existsInConflicts = function ( filterItem ) {
    return Object.prototype.hasOwnProperty.call( this.getConflicts(), filterItem.getName() );
};

/**
 * Set the state of this filter as being conflicted
 * (This means any filters in its conflicts are selected)
 *
 * @param {boolean} [conflicted] Filter is in conflict state
 * @fires update
 */
FilterItem.prototype.toggleConflicted = function ( conflicted ) {
    conflicted = conflicted === undefined ? !this.conflicted : conflicted;

    if ( this.conflicted !== conflicted ) {
        this.conflicted = conflicted;
        this.emit( 'update' );
    }
};

/**
 * Set the state of this filter as being already included
 * (This means any filters in its superset are selected)
 *
 * @param {boolean} [included] Filter is included as part of a subset
 * @fires update
 */
FilterItem.prototype.toggleIncluded = function ( included ) {
    included = included === undefined ? !this.included : included;

    if ( this.included !== included ) {
        this.included = included;
        this.emit( 'update' );
    }
};

/**
 * Toggle the fully covered state of the item
 *
 * @param {boolean} [isFullyCovered] Filter is fully covered
 * @fires update
 */
FilterItem.prototype.toggleFullyCovered = function ( isFullyCovered ) {
    isFullyCovered = isFullyCovered === undefined ? !this.fullycovered : isFullyCovered;

    if ( this.fullyCovered !== isFullyCovered ) {
        this.fullyCovered = isFullyCovered;
        this.emit( 'update' );
    }
};

/**
 * Toggle the visibility of this item
 *
 * @param {boolean} [isVisible] Item is visible
 */
FilterItem.prototype.toggleVisible = function ( isVisible ) {
    isVisible = isVisible === undefined ? !this.visible : !!isVisible;

    if ( this.visible !== isVisible ) {
        this.visible = isVisible;
        this.emit( 'update' );
    }
};

/**
 * Check whether the item is visible
 *
 * @return {boolean} Item is visible
 */
FilterItem.prototype.isVisible = function () {
    return this.visible;
};

module.exports = FilterItem;