wikimedia/mediawiki-extensions-VisualEditor

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

Summary

Maintainability
B
4 hrs
Test Coverage
/*!
 * VisualEditor user interface MWMediaDialog class.
 *
 * @copyright See AUTHORS.txt
 * @license The MIT License (MIT); see LICENSE.txt
 */

/* global moment */

/**
 * MWMediaInfoFieldWidget widget for displaying media information from the API.
 *
 * @class
 * @extends OO.ui.Widget
 * @mixes OO.ui.mixin.IconElement
 * @mixes OO.ui.mixin.TitledElement
 *
 * @constructor
 * @param {jQuery|string|OO.ui.HtmlSnippet} content API response data from which to build the display
 * @param {Object} [config] Configuration options
 * @param {string} [config.href] A url encapsulating the field text. If a label is attached it will include the label.
 * @param {string} [config.labelMsg] A ve.msg() label string for the field.
 * @param {boolean} [config.isDate=false] Field text is a date that will be converted to 'fromNow' string.
 * @param {string} [config.type='attribute'] Field type, either 'description' or 'attribute'
 * @param {string} [config.descriptionHeight='4em'] Height limit for description fields
 */
ve.ui.MWMediaInfoFieldWidget = function VeUiMWMediaInfoFieldWidget( content, config ) {
    // Configuration initialization
    config = config || {};

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

    // Mixin constructors
    OO.ui.mixin.IconElement.call( this, config );
    OO.ui.mixin.LabelElement.call( this, ve.extendObject( { $label: $( '<div>' ) }, config ) );

    this.$text = $( '<div>' )
        .addClass( 've-ui-mwMediaInfoFieldWidget-text' );
    this.type = config.type || 'attribute';

    // Initialization
    if ( typeof content === 'string' ) {
        let datetime;
        if ( config.isDate && ( datetime = moment( content ) ).isValid() ) {
            content = datetime.fromNow();
        }

        if ( config.labelMsg ) {
            // Messages defined in ve.ui.MWMediaDialog#buildMediaInfoPanel
            // eslint-disable-next-line mediawiki/msg-doc
            content = ve.msg( config.labelMsg, content );
        }

        if ( config.href ) {
            // This variable may contain either jQuery objects or strings
            // eslint-disable-next-line no-jquery/variable-pattern
            content = $( '<a>' )
                .attr( 'href',
                    // For the cases where we get urls that are "local"
                    // without http(s) prefix, we will add that prefix
                    // ourselves
                    !/^(https?:)?\/\//.test( config.href ) ?
                        '//' + config.href :
                        config.href
                )
                .text( content );
        }
    }

    if ( typeof content === 'string' ) {
        this.$text.text( content );
    } else if ( content instanceof OO.ui.HtmlSnippet ) {
        // eslint-disable-next-line no-jquery/no-html
        this.$text.html( content.toString() );
    } else if ( content instanceof $ ) {
        // eslint-disable-next-line no-jquery/no-append-html
        this.$text.append( content );
    } else {
        throw new Error( 'Unexpected metadata field content' );
    }

    this.$element
        .append( this.$icon, this.$label )
        .addClass( 've-ui-mwMediaInfoFieldWidget' )
        // The following classes are used here:
        // * ve-ui-mwMediaInfoFieldWidget-description
        // * ve-ui-mwMediaInfoFieldWidget-attribute
        .addClass( 've-ui-mwMediaInfoFieldWidget-' + this.type );
    this.$icon.addClass( 've-ui-mwMediaInfoFieldWidget-icon' );

    if ( this.type === 'description' ) {
        // Limit height
        this.readMoreButton = new OO.ui.ButtonWidget( {
            framed: false,
            icon: 'expand',
            label: ve.msg( 'visualeditor-dialog-media-info-readmore' ),
            classes: [ 've-ui-mwMediaInfoFieldWidget-readmore' ]
        } );
        this.readMoreButton.toggle( false );
        this.readMoreButton.connect( this, { click: 'onReadMoreClick' } );

        this.$text
            .css( 'maxHeight', config.descriptionHeight || '4em' );

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

    this.setLabel( this.$text );
};

/* Setup */

OO.inheritClass( ve.ui.MWMediaInfoFieldWidget, OO.ui.Widget );
OO.mixinClass( ve.ui.MWMediaInfoFieldWidget, OO.ui.mixin.IconElement );
OO.mixinClass( ve.ui.MWMediaInfoFieldWidget, OO.ui.mixin.LabelElement );

/* Static Properties */

ve.ui.MWMediaInfoFieldWidget.static.tagName = 'div';

/**
 * Define a height threshold for the description fields.
 * If the rendered field's height is under the defined limit
 * (max-height + threshold) we should remove the max-height
 * and display the field as-is.
 * This prevents cases where "read more" appears but only
 * exposes only a few pixels or a line extra.
 *
 * @property {number} Threshold in pixels
 */
ve.ui.MWMediaInfoFieldWidget.static.threshold = 24;

/**
 * Toggle the read more button according to whether it should be
 * visible or not.
 */
ve.ui.MWMediaInfoFieldWidget.prototype.initialize = function () {
    if ( this.getType() === 'description' ) {
        const actualHeight = this.$text.prop( 'scrollHeight' );
        const containerHeight = this.$text.outerHeight( true );

        if ( actualHeight < containerHeight + this.constructor.static.threshold ) {
            // The contained result is big enough to show. Remove the maximum height
            this.$text
                .css( 'maxHeight', '' );
        } else {
            // Only show the readMore button if it should be shown
            this.readMoreButton.toggle( containerHeight < actualHeight );
        }
    }
};

/**
 * Respond to read more button click event.
 */
ve.ui.MWMediaInfoFieldWidget.prototype.onReadMoreClick = function () {
    this.readMoreButton.toggle( false );
    this.$text.css( 'maxHeight', this.$text.prop( 'scrollHeight' ) );
};

/**
 * Get field type; 'attribute' or 'description'
 *
 * @return {string} Field type
 */
ve.ui.MWMediaInfoFieldWidget.prototype.getType = function () {
    return this.type;
};