wikimedia/mediawiki-extensions-VisualEditor

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

Summary

Maintainability
C
7 hrs
Test Coverage
/*!
 * VisualEditor UserInterface MWCategoryInputWidget class.
 *
 * @copyright See AUTHORS.txt
 * @license The MIT License (MIT); see LICENSE.txt
 */

/**
 * Creates an ve.ui.MWCategoryInputWidget object.
 *
 * @class
 * @extends OO.ui.TextInputWidget
 * @mixes OO.ui.mixin.LookupElement
 *
 * @constructor
 * @param {ve.ui.MWCategoryWidget} categoryWidget
 * @param {Object} [config] Configuration options
 * @param {jQuery} [config.$overlay] Overlay to render dropdowns in
 * @param {mw.Api} [config.api] API object to use, uses Target#getContentApi if not specified
 */
ve.ui.MWCategoryInputWidget = function VeUiMWCategoryInputWidget( categoryWidget, config ) {
    // Config initialization
    config = ve.extendObject( {
        placeholder: ve.msg( 'visualeditor-dialog-meta-categories-input-placeholder' )
    }, config );

    // Parent constructor
    ve.ui.MWCategoryInputWidget.super.call( this, config );

    // Mixin constructors
    OO.ui.mixin.LookupElement.call( this, config );

    // Properties
    this.categoryWidget = categoryWidget;
    this.api = config.api || ve.init.target.getContentApi();

    this.$input.attr( 'aria-label', ve.msg( 'visualeditor-dialog-meta-categories-input-placeholder' ) );

    // Initialization
    this.$element.addClass( 've-ui-mwCategoryInputWidget' );
    this.lookupMenu.$element.addClass( 've-ui-mwCategoryInputWidget-menu' );
    this.lookupMenu.$element.attr( 'aria-label', ve.msg( 'visualeditor-dialog-meta-categories-data-label' ) );
};

/* Inheritance */

OO.inheritClass( ve.ui.MWCategoryInputWidget, OO.ui.TextInputWidget );

OO.mixinClass( ve.ui.MWCategoryInputWidget, OO.ui.mixin.LookupElement );

/* Events */

/**
 * A category was chosen
 *
 * @event ve.ui.MWCategoryInputWidget#choose
 * @param {OO.ui.MenuOptionWidget} item Chosen item
 */

/* Methods */

/**
 * @inheritdoc
 */
ve.ui.MWCategoryInputWidget.prototype.getLookupRequest = function () {
    let title = mw.Title.newFromText( this.value );
    if ( title && title.getNamespaceId() === mw.config.get( 'wgNamespaceIds' ).category ) {
        title = title.getMainText();
    } else {
        title = this.value;
    }
    return this.api.get( {
        action: 'query',
        generator: 'allcategories',
        gacmin: 1,
        gacprefix: title,
        prop: 'categoryinfo',
        redirects: ''
    } );
};

/**
 * @inheritdoc
 */
ve.ui.MWCategoryInputWidget.prototype.getLookupCacheDataFromResponse = function ( data ) {
    const result = [],
        linkCacheUpdate = {},
        query = data.query || {};

    ( query.pages || [] ).forEach( ( categoryPage ) => {
        result.push( mw.Title.newFromText( categoryPage.title ).getMainText() );
        linkCacheUpdate[ categoryPage.title ] = {
            missing: Object.prototype.hasOwnProperty.call( categoryPage, 'missing' ),
            hidden: (
                categoryPage.categoryinfo &&
                Object.prototype.hasOwnProperty.call( categoryPage.categoryinfo, 'missing' )
            )
        };
    } );

    ( query.redirects || [] ).forEach( ( redirect ) => {
        if ( !Object.prototype.hasOwnProperty.call( linkCacheUpdate, redirect.to ) ) {
            linkCacheUpdate[ redirect.to ] = ve.init.platform.linkCache.getCached( redirect.to ) ||
                { missing: false, redirectFrom: [ redirect.from ] };
        }
        if (
            linkCacheUpdate[ redirect.to ].redirectFrom &&
            linkCacheUpdate[ redirect.to ].redirectFrom.indexOf( redirect.from ) === -1
        ) {
            linkCacheUpdate[ redirect.to ].redirectFrom.push( redirect.from );
        } else {
            linkCacheUpdate[ redirect.to ].redirectFrom = [ redirect.from ];
        }
    } );

    ve.init.platform.linkCache.set( linkCacheUpdate );

    return result;
};

/**
 * @inheritdoc
 */
ve.ui.MWCategoryInputWidget.prototype.getLookupMenuOptionsFromData = function ( data ) {
    const itemWidgets = [],
        existingCategoryItems = [],
        matchingCategoryItems = [],
        hiddenCategoryItems = [],
        newCategoryItems = [],
        existingCategories = this.categoryWidget.getCategories(),
        linkCacheUpdate = {};

    let canonicalQueryValue = mw.Title.newFromText( this.value ),
        prefixedCanonicalQueryValue = mw.Title.newFromText(
            this.value,
            mw.config.get( 'wgNamespaceIds' ).category
        );

    prefixedCanonicalQueryValue = prefixedCanonicalQueryValue && prefixedCanonicalQueryValue.getPrefixedText();

    // Invalid titles end up with canonicalQueryValue being null.
    if ( canonicalQueryValue ) {
        canonicalQueryValue = canonicalQueryValue.getMainText();
    }

    let exactMatch = false;
    data.forEach( ( suggestedCategory ) => {
        const suggestedCategoryTitle = mw.Title.newFromText(
                suggestedCategory,
                mw.config.get( 'wgNamespaceIds' ).category
            ).getPrefixedText(),
            suggestedCacheEntry = ve.init.platform.linkCache.getCached( suggestedCategoryTitle );
        if ( canonicalQueryValue === suggestedCategory ) {
            exactMatch = true;
        }
        if ( !suggestedCacheEntry ) {
            linkCacheUpdate[ suggestedCategoryTitle ] = { missing: false };
        }
        if (
            existingCategories.indexOf( suggestedCategory ) === -1
        ) {
            if ( suggestedCacheEntry && suggestedCacheEntry.hidden ) {
                hiddenCategoryItems.push( suggestedCategory );
            } else {
                matchingCategoryItems.push( suggestedCategory );
            }
        }
    } );

    // Existing categories
    existingCategories.forEach( ( existingCategory, i ) => {
        if ( existingCategory === canonicalQueryValue ) {
            exactMatch = true;
        }
        if ( i < existingCategories.length - 1 && existingCategory.lastIndexOf( canonicalQueryValue, 0 ) === 0 ) {
            // Verify that item starts with category.value
            existingCategoryItems.push( existingCategory );
        }
    } );

    // New category
    if ( !exactMatch && canonicalQueryValue ) {
        newCategoryItems.push( canonicalQueryValue );
        linkCacheUpdate[ prefixedCanonicalQueryValue ] = { missing: true };
    }

    ve.init.platform.linkCache.set( linkCacheUpdate );

    // Add sections for non-empty groups. Each section consists of an id, a label and items
    [
        {
            id: 'newCategory',
            label: ve.msg( 'visualeditor-dialog-meta-categories-input-newcategorylabel' ),
            items: newCategoryItems
        },
        {
            id: 'inArticle',
            label: ve.msg( 'visualeditor-dialog-meta-categories-input-movecategorylabel' ),
            items: existingCategoryItems
        },
        {
            id: 'matchingCategories',
            label: ve.msg( 'visualeditor-dialog-meta-categories-input-matchingcategorieslabel' ),
            items: matchingCategoryItems
        },
        {
            id: 'hiddenCategories',
            label: ve.msg( 'visualeditor-dialog-meta-categories-input-hiddencategorieslabel' ),
            items: hiddenCategoryItems
        }
    ].forEach( ( sectionData ) => {
        if ( sectionData.items.length ) {
            itemWidgets.push( new OO.ui.MenuSectionOptionWidget( {
                data: sectionData.id,
                label: sectionData.label
            } ) );
            sectionData.items.forEach( ( categoryItem ) => {
                itemWidgets.push( this.getCategoryWidgetFromName( categoryItem ) );
            } );
        }
    } );

    return itemWidgets;
};

/**
 * @inheritdoc
 * @fires ve.ui.MWCategoryInputWidget#choose
 */
ve.ui.MWCategoryInputWidget.prototype.onLookupMenuChoose = function ( item ) {
    this.emit( 'choose', item );

    // Reset input
    this.setValue( '' );
};

/**
 * Take a category name and turn it into a menu item widget, following redirects.
 *
 * @param {string} name Category name
 * @return {OO.ui.MenuOptionWidget} Menu item widget to be shown
 */
ve.ui.MWCategoryInputWidget.prototype.getCategoryWidgetFromName = function ( name ) {
    const cachedData = ve.init.platform.linkCache.getCached( mw.Title.newFromText(
        name,
        mw.config.get( 'wgNamespaceIds' ).category
    ).getPrefixedText() );
    let optionWidget, labelText;
    if ( cachedData && cachedData.redirectFrom ) {
        labelText = mw.Title.newFromText( cachedData.redirectFrom[ 0 ] ).getMainText();
        optionWidget = new OO.ui.MenuOptionWidget( {
            data: name,
            autoFitLabel: false,
            label: $( '<span>' )
                .text( labelText )
                .append(
                    $( '<br>' ),
                    $( document.createTextNode( '↳ ' ) ),
                    $( '<span>' ).text( mw.Title.newFromText( name ).getMainText() )
                )
        } );
    } else {
        labelText = name;
        optionWidget = new OO.ui.MenuOptionWidget( {
            data: name,
            label: name
        } );
    }
    optionWidget.$element.attr( 'title', labelText );
    return optionWidget;
};