wikimedia/mediawiki-extensions-VisualEditor

View on GitHub
modules/ve-mw/ui/widgets/ve.ui.MWTransclusionOutlineParameterSelectWidget.js

Summary

Maintainability
B
4 hrs
Test Coverage
/**
 * List of template parameters, each of which can be added or removed using a
 * checkbox.
 *
 * This is modelled after {@see OO.ui.OutlineSelectWidget}.  Currently we use
 * the SelectWidget in multi-select mode, and selection maps to checked
 * checkboxes.
 *
 * @class
 * @extends OO.ui.SelectWidget
 * @mixes OO.ui.mixin.TabIndexedElement
 * @mixes ve.ui.MWAriaDescribe
 *
 * @constructor
 * @param {Object} config
 * @param {ve.ui.MWTransclusionOutlineParameterWidget[]} config.items
 * @property {string|null} activeParameter Name of the currently selected parameter
 * @property {number} stickyHeaderHeight
 */
ve.ui.MWTransclusionOutlineParameterSelectWidget = function VeUiMWTransclusionOutlineParameterSelectWidget( config ) {
    // Parent constructor
    ve.ui.MWTransclusionOutlineParameterSelectWidget.super.call( this, ve.extendObject( config, {
        classes: [ 've-ui-mwTransclusionOutlineParameterSelectWidget' ],
        multiselect: true
    } ) );

    // Mixin constructors
    OO.ui.mixin.TabIndexedElement.call( this, {
        tabIndex: this.isEmpty() ? -1 : 0
    } );
    ve.ui.MWAriaDescribe.call( this, config );

    this.$element
        .on( {
            focus: this.bindDocumentKeyDownListener.bind( this ),
            blur: this.onBlur.bind( this )
        } );

    this.activeParameter = null;
    this.stickyHeaderHeight = 0;
};

/* Inheritance */

OO.inheritClass( ve.ui.MWTransclusionOutlineParameterSelectWidget, OO.ui.SelectWidget );
OO.mixinClass( ve.ui.MWTransclusionOutlineParameterSelectWidget, OO.ui.mixin.TabIndexedElement );
OO.mixinClass( ve.ui.MWTransclusionOutlineParameterSelectWidget, ve.ui.MWAriaDescribe );

/* Events */

/**
 * This is fired instead of the "choose" event from the {@see OO.ui.SelectWidget} base class when
 * pressing space on a parameter to toggle it or scroll it into view, without losing the focus.
 *
 * @event ve.ui.MWTransclusionOutlineParameterSelectWidget#templateParameterSpaceDown
 * @param {ve.ui.MWTransclusionOutlineParameterWidget} item
 * @param {boolean} selected
 */

/* Static Methods */

/**
 * @param {Object} config
 * @param {string} config.data Parameter name
 * @param {string} config.label
 * @param {boolean} [config.required=false] Required parameters can't be unchecked
 * @param {boolean} [config.selected=false] If the parameter is currently used (checked)
 * @return {ve.ui.MWTransclusionOutlineParameterWidget}
 */
ve.ui.MWTransclusionOutlineParameterSelectWidget.static.createItem = function ( config ) {
    return new ve.ui.MWTransclusionOutlineParameterWidget( config );
};

/* Methods */

/**
 * @inheritDoc OO.ui.mixin.GroupElement
 * @param {ve.ui.MWTransclusionOutlineParameterWidget[]} items
 * @param {number} [index]
 * @return {ve.ui.MWTransclusionOutlineParameterSelectWidget}
 */
ve.ui.MWTransclusionOutlineParameterSelectWidget.prototype.addItems = function ( items, index ) {
    items.forEach( ( item ) => {
        item.connect( this, {
            change: [ 'onCheckboxChange', item ]
        } );
    } );

    ve.ui.MWTransclusionOutlineParameterSelectWidget.super.prototype.addItems.call( this, items, index );
    this.setTabIndex( this.isEmpty() ? -1 : 0 );
    return this;
};

ve.ui.MWTransclusionOutlineParameterSelectWidget.prototype.ensureVisibilityOfFirstCheckedParameter = function () {
    // TODO: Replace with {@see OO.ui.SelectWidget.findFirstSelectedItem} when available
    const firstChecked = this.findSelectedItems()[ 0 ];
    if ( firstChecked ) {
        firstChecked.ensureVisibility( this.stickyHeaderHeight );
    }
};

/**
 * @param {string|null} [paramName] Parameter name to set, e.g. "param1". Omit to remove setting.
 */
ve.ui.MWTransclusionOutlineParameterSelectWidget.prototype.setActiveParameter = function ( paramName ) {
    // Note: We know unnamed parameter placeholders never have an item here
    const newItem = paramName ? this.findItemFromData( paramName ) : null;
    // Unhighlight when called with no parameter name
    this.highlightItem( newItem );

    paramName = paramName || null;
    if ( this.activeParameter === paramName ) {
        return;
    }

    const currentItem = this.activeParameter ? this.findItemFromData( this.activeParameter ) : null;
    this.activeParameter = paramName;

    if ( currentItem ) {
        currentItem.toggleActivePageIndicator( false );
    }
    if ( newItem ) {
        newItem.toggleActivePageIndicator( true );
    }
};

/**
 * @inheritDoc OO.ui.SelectWidget
 */
ve.ui.MWTransclusionOutlineParameterSelectWidget.prototype.highlightItem = function ( item ) {
    if ( item ) {
        item.ensureVisibility( this.stickyHeaderHeight );
    }
    ve.ui.MWTransclusionOutlineParameterSelectWidget.super.prototype.highlightItem.call( this, item );
};

/**
 * @param {string} paramName
 */
ve.ui.MWTransclusionOutlineParameterSelectWidget.prototype.markParameterAsUnused = function ( paramName ) {
    // There is no OO.ui.SelectWidget.unselectItemByData(), we need to do this manually
    /** @type {ve.ui.MWTransclusionOutlineParameterWidget} */
    const item = paramName ? this.findItemFromData( paramName ) : null;
    if ( item ) {
        item.setSelected( false );
        // An unused parameter can't be the active (set) one; it doesn't exist in the content pane
        if ( this.activeParameter === paramName ) {
            this.activeParameter = null;
            item.toggleActivePageIndicator( false );
        }
    }
};

/**
 * @private
 * @param {ve.ui.MWTransclusionOutlineParameterWidget} item
 * @param {boolean} value
 */
ve.ui.MWTransclusionOutlineParameterSelectWidget.prototype.onCheckboxChange = function ( item, value ) {
    // This extra check shouldn't be necessary, but better be safe than sorry
    if ( item.isSelected() !== value ) {
        // Note: This should have been named `toggle…` as it toggles the item's selection
        this.chooseItem( item );
    }
};

/**
 * @inheritDoc OO.ui.SelectWidget
 */
ve.ui.MWTransclusionOutlineParameterSelectWidget.prototype.onFocus = function ( event ) {
    if ( event.target !== this.$element[ 0 ] || this.findHighlightedItem() ) {
        return;
    }

    let index = 0;
    if ( event.relatedTarget ) {
        const toolbarClass = 've-ui-mwTransclusionOutlineControlsWidget',
            // The only elements below a parameter list can be another part or the toolbar
            selector = '.ve-ui-mwTransclusionOutlinePartWidget, .' + toolbarClass,
            $fromPart = $( event.relatedTarget ).closest( selector ),
            $toPart = $( event.target ).closest( selector );
        // When shift+tabbing into the list, highlight the last parameter
        // eslint-disable-next-line no-jquery/no-class-state
        if ( $fromPart.hasClass( toolbarClass ) || $fromPart.index() > $toPart.index() ) {
            index = this.getItemCount() - 1;
        }
    }
    this.highlightItem( this.items[ index ] );

    // Don't call the parent. It makes assumptions what should be done here.
};

/**
 * @inheritDoc OO.ui.SelectWidget
 * @param {jQuery.Event} e
 * @fires OO.ui.SelectWidget#choose
 */
ve.ui.MWTransclusionOutlineParameterSelectWidget.prototype.onMouseDown = function ( e ) {
    if ( e.which === OO.ui.MouseButtons.LEFT ) {
        const item = this.findTargetItem( e );
        // Same as pressing enter, see below.
        if ( item && item.isSelected() ) {
            this.emit( 'choose', item, item.isSelected() );

            // Don't call the parent, i.e. can't click to unselect the item
            return false;
        }
    }

    ve.ui.MWTransclusionOutlineParameterSelectWidget.super.prototype.onMouseDown.call( this, e );
};

/**
 * @inheritDoc OO.ui.SelectWidget
 * @param {KeyboardEvent} e
 * @fires OO.ui.SelectWidget#choose
 * @fires ve.ui.MWTransclusionOutlineParameterSelectWidget#templateParameterSpaceDown
 */
ve.ui.MWTransclusionOutlineParameterSelectWidget.prototype.onDocumentKeyDown = function ( e ) {
    let item;

    switch ( e.keyCode ) {
        case OO.ui.Keys.HOME:
            item = this.items[ 0 ];
            if ( item ) {
                this.highlightItem( item );
            }
            break;
        case OO.ui.Keys.END:
            item = this.items[ this.items.length - 1 ];
            if ( item ) {
                this.highlightItem( item );
            }
            break;
        case OO.ui.Keys.SPACE:
            item = this.findHighlightedItem();
            if ( item ) {
                // Warning, this intentionally doesn't call .chooseItem() because we don't want this
                // to fire a "choose" event!
                if ( item.isSelected() ) {
                    this.unselectItem( item );
                } else {
                    this.selectItem( item );
                }
                this.emit( 'templateParameterSpaceDown', item, item.isSelected() );
            }
            e.preventDefault();
            break;
        case OO.ui.Keys.ENTER:
            item = this.findHighlightedItem();
            // Same as clicking with the mouse, see above.
            if ( item && item.isSelected() ) {
                this.emit( 'choose', item, item.isSelected() );
                e.preventDefault();

                // Don't call the parent, i.e. can't use enter to unselect the item
                return false;
            }
            break;
    }

    ve.ui.MWTransclusionOutlineParameterSelectWidget.super.prototype.onDocumentKeyDown.call( this, e );
};

ve.ui.MWTransclusionOutlineParameterSelectWidget.prototype.onBlur = function () {
    this.highlightItem();
    this.unbindDocumentKeyDownListener();
};