wikimedia/mediawiki-extensions-VisualEditor

View on GitHub
modules/ve-mw/ui/pages/ve.ui.MWSettingsPage.js

Summary

Maintainability
F
5 days
Test Coverage
/*!
 * VisualEditor user interface MWSettingsPage class.
 *
 * @copyright See AUTHORS.txt
 * @license The MIT License (MIT); see LICENSE.txt
 */

/**
 * MediaWiki meta dialog settings page.
 *
 * @class
 * @extends OO.ui.PageLayout
 *
 * @constructor
 * @param {string} name Unique symbolic name of page
 * @param {Object} [config] Configuration options
 * @param {jQuery} [config.$overlay] Overlay to render dropdowns in
 */
ve.ui.MWSettingsPage = function VeUiMWSettingsPage( name, config ) {
    // Parent constructor
    ve.ui.MWSettingsPage.super.apply( this, arguments );

    // Properties
    this.fragment = null;
    this.tocOptionTouched = false;
    this.redirectOptionsTouched = false;
    this.tableOfContentsTouched = false;
    this.label = ve.msg( 'visualeditor-dialog-meta-settings-section' );

    this.settingsFieldset = new OO.ui.FieldsetLayout( {
        label: ve.msg( 'visualeditor-dialog-meta-settings-label' ),
        icon: 'pageSettings'
    } );

    // Initialization

    // Table of Contents items
    this.tableOfContents = new OO.ui.FieldLayout(
        new OO.ui.ButtonSelectWidget( {
            classes: [ 've-test-page-settings-table-of-contents' ]
        } )
            .addItems( [
                new OO.ui.ButtonOptionWidget( {
                    data: 'mw:PageProp/forcetoc',
                    label: ve.msg( 'visualeditor-dialog-meta-settings-toc-force' )
                } ),
                new OO.ui.ButtonOptionWidget( {
                    data: 'default',
                    label: ve.msg( 'visualeditor-dialog-meta-settings-toc-default' )
                } ),
                new OO.ui.ButtonOptionWidget( {
                    data: 'mw:PageProp/notoc',
                    label: ve.msg( 'visualeditor-dialog-meta-settings-toc-disable' )
                } )
            ] )
            .connect( this, { select: 'onTableOfContentsFieldChange' } ),
        {
            $overlay: config.$overlay,
            align: 'top',
            label: ve.msg( 'visualeditor-dialog-meta-settings-toc-label' ),
            help: ve.msg( 'visualeditor-dialog-meta-settings-toc-help' )
        }
    );

    // Redirect items
    this.enableRedirectInput = new OO.ui.CheckboxInputWidget();
    this.enableRedirectField = new OO.ui.FieldLayout(
        this.enableRedirectInput,
        {
            $overlay: config.$overlay,
            classes: [ 've-test-page-settings-enable-redirect' ],
            align: 'inline',
            label: ve.msg( 'visualeditor-dialog-meta-settings-redirect-label' ),
            help: ve.msg( 'visualeditor-dialog-meta-settings-redirect-help' )
        }
    );
    this.redirectTargetInput = new mw.widgets.TitleInputWidget( {
        placeholder: ve.msg( 'visualeditor-dialog-meta-settings-redirect-placeholder' ),
        $overlay: config.$overlay,
        api: ve.init.target.getContentApi()
    } );
    this.redirectTargetInput.$input.attr( 'aria-label', ve.msg( 'visualeditor-dialog-meta-settings-redirect-placeholder' ) );

    this.redirectTargetField = new OO.ui.FieldLayout(
        this.redirectTargetInput,
        { align: 'top' }
    );
    this.enableStaticRedirectInput = new OO.ui.CheckboxInputWidget();
    this.enableStaticRedirectField = new OO.ui.FieldLayout(
        this.enableStaticRedirectInput,
        {
            $overlay: config.$overlay,
            classes: [ 've-test-page-settings-prevent-redirect' ],
            align: 'inline',
            label: ve.msg( 'visualeditor-dialog-meta-settings-redirect-staticlabel' ),
            help: ve.msg( 'visualeditor-dialog-meta-settings-redirect-statichelp' )
        }
    );
    this.enableRedirectInput.connect( this, { change: 'onEnableRedirectChange' } );
    this.redirectTargetInput.connect( this, { change: 'onRedirectTargetChange' } );
    this.enableStaticRedirectInput.connect( this, { change: 'onEnableStaticRedirectChange' } );

    this.metaItemCheckboxes = [
        {
            metaName: 'mwNoEditSection',
            label: ve.msg( 'visualeditor-dialog-meta-settings-noeditsection-label' ),
            help: ve.msg( 'visualeditor-dialog-meta-settings-noeditsection-help' ),
            classes: [ 've-test-page-settings-noeditsection' ]
        },
        ...ve.ui.MWSettingsPage.static.extraMetaCheckboxes
    ];

    if ( mw.config.get( 'wgNamespaceNumber' ) === mw.config.get( 'wgNamespaceIds' ).category ) {
        this.metaItemCheckboxes.push(
            {
                metaName: 'mwHiddenCategory',
                label: ve.msg( 'visualeditor-dialog-meta-settings-hiddencat-label' ),
                help: ve.msg( 'visualeditor-dialog-meta-settings-hiddencat-help' )
            },
            {
                metaName: 'mwNoGallery',
                label: ve.msg( 'visualeditor-dialog-meta-settings-nogallery-label' ),
                help: ve.msg( 'visualeditor-dialog-meta-settings-nogallery-help' )
            }
        );
    }

    this.settingsFieldset.addItems( [
        this.enableRedirectField,
        this.redirectTargetField,
        this.enableStaticRedirectField,
        this.tableOfContents
    ] );

    this.metaItemCheckboxes.forEach( ( metaItemCheckbox ) => {
        metaItemCheckbox.fieldLayout = new OO.ui.FieldLayout(
            new OO.ui.CheckboxInputWidget(),
            // See above for classes
            // eslint-disable-next-line mediawiki/class-doc
            {
                $overlay: config.$overlay,
                classes: metaItemCheckbox.classes,
                align: 'inline',
                label: metaItemCheckbox.label,
                help: metaItemCheckbox.help || ''
            }
        );
        this.settingsFieldset.addItems( [ metaItemCheckbox.fieldLayout ] );
    } );

    this.$element.append( this.settingsFieldset.$element );
};

/* Inheritance */

OO.inheritClass( ve.ui.MWSettingsPage, OO.ui.PageLayout );

/* Allow extra meta item checkboxes to be added by extensions etc. */
ve.ui.MWSettingsPage.static.extraMetaCheckboxes = [];

/**
 * Add a checkbox to the list of changeable page settings
 *
 * @param {string} metaName The name of the DM meta item
 * @param {string} label The label to show next to the checkbox
 */
ve.ui.MWSettingsPage.static.addMetaCheckbox = function ( metaName, label ) {
    this.extraMetaCheckboxes.push( { metaName: metaName, label: label } );
};

/* Methods */

/* Table of Contents methods */

/**
 * @inheritdoc
 */
ve.ui.MWSettingsPage.prototype.setupOutlineItem = function () {
    this.outlineItem
        .setIcon( 'pageSettings' )
        .setLabel( ve.msg( 'visualeditor-dialog-meta-settings-section' ) );
};

/**
 * Handle Table Of Contents display change events.
 */
ve.ui.MWSettingsPage.prototype.onTableOfContentsFieldChange = function () {
    this.tableOfContentsTouched = true;
};

/* Redirect methods */

/**
 * Handle redirect state change events.
 *
 * @param {boolean} value Whether a redirect is to be set for this page
 */
ve.ui.MWSettingsPage.prototype.onEnableRedirectChange = function ( value ) {
    this.redirectTargetInput.setDisabled( !value );
    this.enableStaticRedirectInput.setDisabled( !value );
    if ( value ) {
        /*
         * HACK: When editing a page which has a redirect, the meta dialog
         * automatically opens with the settings page's redirect field focused.
         * When this happens, we don't want the lookup dropdown to appear until
         * the user actually does something.
         * Using setTimeout because we need to defer this until after the
         * dialog has opened - otherwise its internal lookupDisabled logic will
         * fail to have any effect during the actual focusing and calling of
         * OO.ui.LookupElement#onLookupInputFocus/OO.ui.LookupElement#populateLookupMenu.
         * https://phabricator.wikimedia.org/T137309
         */
        setTimeout( () => {
            this.redirectTargetInput.focus();
        } );
    } else {
        this.redirectTargetInput.setValue( '' );
        this.enableStaticRedirectInput.setSelected( false );
    }
    this.redirectOptionsTouched = true;
};

/**
 * @return {boolean} Whether redirect link is valid.
 */
ve.ui.MWSettingsPage.prototype.checkValidRedirect = function () {
    if ( this.enableRedirectInput.isSelected() ) {
        const title = this.redirectTargetInput.getValue();

        if ( !mw.Title.newFromText( title ) ) {
            /*
             * TODO more precise error message. Modify the Title.newFromText method in Title.js
             * my idea is to in the parse method instead of a boolean return a string with an error message (not an error code since the error string can have parameters),
             * then in Title.newFromText instead of returning null, return the error string. Use that string there in setErrors.
             * Problem: some methods might depend on it returning null.
             * Solution: either make it a new method (Title.newFromTextThrow), or add a an optional parameter to return the error message.
             */
            this.redirectTargetField.setErrors( [ mw.msg( 'visualeditor-title-error' ) ] );
            return false;

        } else {
            this.redirectTargetField.setErrors( [] );
        }
    } else {
        this.redirectTargetField.setErrors( [] );
    }

    return true;
};

/**
 * Handle redirect target change events.
 */
ve.ui.MWSettingsPage.prototype.onRedirectTargetChange = function () {
    this.redirectOptionsTouched = true;
};

/**
 * Handle static redirect state change events.
 */
ve.ui.MWSettingsPage.prototype.onEnableStaticRedirectChange = function () {
    this.redirectOptionsTouched = true;
};

/**
 * Get the first meta item of a given name
 *
 * @param {string} name Name of the meta item
 * @return {Object|null} Meta item, if any
 */
ve.ui.MWSettingsPage.prototype.getMetaItem = function ( name ) {
    return this.fragment.getDocument().getMetaList().getItemsInGroup( name )[ 0 ] || null;
};

/**
 * Setup settings page.
 *
 * @param {ve.dm.SurfaceFragment} fragment Surface fragment
 * @param {Object} config
 * @param {Object} [config.data] Dialog setup data
 * @param {boolean} [config.isReadOnly=false] Dialog is in read-only mode
 * @return {jQuery.Promise}
 */
ve.ui.MWSettingsPage.prototype.setup = function ( fragment, config ) {
    this.fragment = fragment;

    // Table of Contents items
    const tableOfContentsField = this.tableOfContents.getField();
    const tableOfContentsMetaItem = this.getMetaItem( 'mwTOC' );
    const tableOfContentsMode = tableOfContentsMetaItem && tableOfContentsMetaItem.getAttribute( 'property' ) || 'default';
    tableOfContentsField
        .selectItemByData( tableOfContentsMode )
        .setDisabled( config.isReadOnly );
    this.tableOfContentsTouched = false;

    // Redirect items (disabled states set by change event)
    const redirectTargetItem = this.getMetaItem( 'mwRedirect' );
    const redirectTarget = redirectTargetItem && redirectTargetItem.getAttribute( 'title' ) || '';
    const redirectStatic = this.getMetaItem( 'mwStaticRedirect' );
    this.enableRedirectInput
        .setSelected( !!redirectTargetItem )
        .setDisabled( config.isReadOnly );
    this.redirectTargetInput
        .setValue( redirectTarget )
        .setDisabled( !redirectTargetItem )
        .setReadOnly( config.isReadOnly );
    this.enableStaticRedirectInput
        .setSelected( !!redirectStatic )
        .setDisabled( !redirectTargetItem || config.isReadOnly );
    this.redirectOptionsTouched = false;

    // Simple checkbox items
    this.metaItemCheckboxes.forEach( ( metaItemCheckbox ) => {
        const isSelected = !!this.getMetaItem( metaItemCheckbox.metaName );
        metaItemCheckbox.fieldLayout.getField()
            .setSelected( isSelected )
            .setDisabled( config.isReadOnly );
    } );

    return ve.createDeferred().resolve().promise();
};

/**
 * Tear down settings page.
 *
 * @param {Object} [data] Dialog tear down data
 */
ve.ui.MWSettingsPage.prototype.teardown = function ( data ) {
    // Data initialisation
    data = data || {};
    if ( data.action !== 'done' ) {
        return;
    }

    // Table of Contents items
    const currentTableOfContents = this.getMetaItem( 'mwTOC' );
    const newTableOfContentsData = this.tableOfContents.getField().findSelectedItem();

    // Redirect items
    const currentRedirectTargetItem = this.getMetaItem( 'mwRedirect' );
    const newRedirectData = this.redirectTargetInput.getValue();
    const newRedirectItemData = { type: 'mwRedirect', attributes: { title: newRedirectData } };

    const currentStaticRedirectItem = this.getMetaItem( 'mwStaticRedirect' );
    const newStaticRedirectState = this.enableStaticRedirectInput.isSelected();

    // Alter the TOC option flag iff it's been touched & is actually different
    if ( this.tableOfContentsTouched ) {
        if ( newTableOfContentsData.data === 'default' ) {
            if ( currentTableOfContents ) {
                this.fragment.removeMeta( currentTableOfContents );
            }
        } else {
            const newTableOfContentsItem = { type: 'mwTOC', attributes: { property: newTableOfContentsData.data } };

            if ( !currentTableOfContents ) {
                this.fragment.insertMeta( newTableOfContentsItem );
            } else if ( currentTableOfContents.getAttribute( 'property' ) !== newTableOfContentsData.data ) {
                this.fragment.replaceMeta(
                    currentTableOfContents,
                    ve.extendObject( true, {}, currentTableOfContents.getElement(), newTableOfContentsItem )
                );
            }
        }
    }

    // Alter the redirect options iff they've been touched & are different
    if ( this.redirectOptionsTouched ) {
        if ( currentRedirectTargetItem ) {
            if ( newRedirectData ) {
                if ( currentRedirectTargetItem.getAttribute( 'title' ) !== newRedirectData ) {
                    // There was a redirect and is a new one, but they differ, so replace
                    this.fragment.replaceMeta(
                        currentRedirectTargetItem,
                        ve.extendObject( true, {},
                            currentRedirectTargetItem.getElement(),
                            newRedirectItemData
                        )
                    );
                }
            } else {
                // There was a redirect and is no new one, so remove
                this.fragment.removeMeta( currentRedirectTargetItem );
            }
        } else {
            if ( newRedirectData ) {
                // There's no existing redirect but there is a new one, so create
                // HACK: Putting this at position 0 so that it works – T63862
                this.fragment.insertMeta( newRedirectItemData, 0 );
            }
        }

        if ( currentStaticRedirectItem && ( !newStaticRedirectState || !newRedirectData ) ) {
            this.fragment.removeMeta( currentStaticRedirectItem );
        }
        if ( !currentStaticRedirectItem && newStaticRedirectState && newRedirectData ) {
            this.fragment.insertMeta( { type: 'mwStaticRedirect' } );
        }
    }

    this.metaItemCheckboxes.forEach( ( metaItemCheckbox ) => {
        const currentItem = this.getMetaItem( metaItemCheckbox.metaName ),
            isSelected = metaItemCheckbox.fieldLayout.getField().isSelected();

        if ( currentItem && !isSelected ) {
            this.fragment.removeMeta( currentItem );
        } else if ( !currentItem && isSelected ) {
            this.fragment.insertMeta( { type: metaItemCheckbox.metaName } );
        }
    } );

    this.fragment = null;
};

ve.ui.MWSettingsPage.prototype.getFieldsets = function () {
    return [
        this.settingsFieldset
    ];
};