wikimedia/mediawiki-extensions-VisualEditor

View on GitHub
modules/ve-mw/ce/nodes/ve.ce.MWExtensionNode.js

Summary

Maintainability
A
1 hr
Test Coverage
/*!
 * VisualEditor ContentEditable MWExtensionNode class.
 *
 * @copyright See AUTHORS.txt
 * @license The MIT License (MIT); see LICENSE.txt
 */

/**
 * ContentEditable MediaWiki extension node.
 *
 * Configuration options for .update():
 * - extsrc: override the contents of the tag (string)
 * - attrs: override the attributes of the tag (object)
 *
 * @class
 * @abstract
 * @extends ve.ce.LeafNode
 * @mixes ve.ce.FocusableNode
 * @mixes ve.ce.GeneratedContentNode
 *
 * @constructor
 */
ve.ce.MWExtensionNode = function VeCeMWExtensionNode() {
    // Parent constructor
    ve.ce.MWExtensionNode.super.apply( this, arguments );

    // Mixin constructors
    ve.ce.FocusableNode.call( this, this.getFocusableElement() );
    ve.ce.GeneratedContentNode.call( this );
};

/* Inheritance */

OO.inheritClass( ve.ce.MWExtensionNode, ve.ce.LeafNode );
OO.mixinClass( ve.ce.MWExtensionNode, ve.ce.FocusableNode );
OO.mixinClass( ve.ce.MWExtensionNode, ve.ce.GeneratedContentNode );

/* Static properties */

/**
 * Extension renders visible content when empty
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.ce.MWExtensionNode.static.rendersEmpty = false;

ve.ce.MWExtensionNode.static.iconWhenInvisible = 'markup';

ve.ce.MWExtensionNode.static.getDescription = function ( model ) {
    const body = ve.getProp( model.getAttribute( 'mw' ), 'body', 'extsrc' ) || '';
    return body.slice( 0, 100 ) + ( body.length > 100 ? '…' : '' );
};

/* Methods */

/**
 * @inheritdoc ve.ce.GeneratedContentNode
 */
ve.ce.MWExtensionNode.prototype.generateContents = function ( config ) {
    const deferred = ve.createDeferred(),
        mwData = ve.copy( this.getModel().getAttribute( 'mw' ) ),
        extsrc = config && config.extsrc !== undefined ? config.extsrc : ( ve.getProp( mwData, 'body', 'extsrc' ) || '' ),
        attrs = config && config.attrs || mwData.attrs,
        tagName = this.getModel().getExtensionName();

    // undefined means omit the attribute, not convert it to string 'undefined'
    for ( const attr in attrs ) {
        if ( attrs[ attr ] === undefined ) {
            delete attrs[ attr ];
        }
    }

    // XML-like tags in wikitext are not actually XML and don't expect their contents to be escaped.
    const wikitext = mw.html.element( tagName, attrs, new mw.html.Raw( extsrc ) );

    if ( this.constructor.static.rendersEmpty || extsrc.trim() !== '' ) {
        const xhr = ve.init.target.parseWikitextFragment( wikitext, false, this.getModel().getDocument() )
            .done( this.onParseSuccess.bind( this, deferred ) )
            .fail( this.onParseError.bind( this, deferred ) );
        return deferred.promise( { abort: xhr.abort } );
    } else {
        deferred.resolve( $( '<span>' ).text( '\u00a0' ).get() );
        return deferred.promise();
    }
};

/**
 * @inheritdoc
 */
ve.ce.MWExtensionNode.prototype.getRenderedDomElements = function () {
    // Parent method
    const elements = ve.ce.GeneratedContentNode.prototype.getRenderedDomElements.apply( this, arguments );

    if ( this.getModelHtmlDocument() ) {
        ve.init.platform.linkCache.styleParsoidElements(
            $( elements ),
            this.getModelHtmlDocument()
        );
    }
    return elements;
};

/**
 * Handle a successful response from the parser for the wikitext fragment.
 *
 * @param {jQuery.Deferred} deferred The Deferred object created by generateContents
 * @param {Object} response Response data
 */
ve.ce.MWExtensionNode.prototype.onParseSuccess = function ( deferred, response ) {
    const data = response.visualeditor,
        contentNodes = $( data.content ).get();
    deferred.resolve( contentNodes );
};

ve.ce.MWExtensionNode.prototype.afterRender = function () {
    const $images = this.$element
        .find( 'img:not([width]),img:not([height])' )
        .addBack( 'img:not([width]),img:not([height])' );

    // Mixin method
    ve.ce.GeneratedContentNode.prototype.afterRender.call( this );

    // Images missing a dimension change size after load
    // TODO: Ignore images which have dimensions defined in CSS, if performant
    if ( $images.length ) {
        $images.on( 'load', () => {
            // Mixin method
            ve.ce.GeneratedContentNode.prototype.afterRender.call( this );
        } );
    }
};

/**
 * Handle an unsuccessful response from the parser for the wikitext fragment.
 *
 * @param {jQuery.Deferred} deferred The promise object created by generateContents
 * @param {Object} response Response data
 */
ve.ce.MWExtensionNode.prototype.onParseError = function ( deferred ) {
    deferred.reject();
};