wikimedia/mediawiki-extensions-VisualEditor

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

Summary

Maintainability
B
6 hrs
Test Coverage
/*!
 * VisualEditor ContentEditable MWGalleryImageNode class.
 *
 * @copyright See AUTHORS.txt
 * @license The MIT License (MIT); see LICENSE.txt
 */

/**
 * ContentEditable MediaWiki gallery image node.
 *
 * @class
 * @extends ve.ce.BranchNode
 *
 * @constructor
 * @param {ve.dm.MWGalleryImageNode} model Model to observe
 * @param {Object} [config] Configuration options
 */
ve.ce.MWGalleryImageNode = function VeCeMWGalleryImageNode( model ) {
    // Parent constructor
    ve.ce.MWGalleryImageNode.super.apply( this, arguments );

    // DOM hierarchy for MWGalleryImageNode:
    //   <li> this.$element (gallerybox)
    //     <div> thumbDiv
    //       <span> innerDiv
    //         <a> a
    //           <img> img
    //     <a> filenameA (galleryfilename)
    //     <figcaption> ve.ce.MWGalleryImageCaptionNode

    const defaults = mw.config.get( 'wgVisualEditorConfig' ).galleryOptions;
    const attributes = model.getAttributes();
    const galleryMwAttrs = model.parent.getAttribute( 'mw' ).attrs;

    // Putting all this setup in the constructor works because MWGalleryImageNodes are never updated,
    // only created from scratch

    // These dimensions are different depending on the gallery mode.
    // (This only vaguely approximates the actual rendering.)
    const mode = galleryMwAttrs.mode || defaults.mode;
    let innerDivWidth, innerDivHeight, innerDivMargin, outerDivWidth;
    if ( mode === 'traditional' || mode === 'nolines' || mode === 'slideshow' ) {
        const imagePadding = ( mode === 'traditional' ? 30 : 0 );
        innerDivWidth = parseInt( galleryMwAttrs.widths || defaults.imageWidth ) + imagePadding;
        innerDivHeight = parseInt( galleryMwAttrs.heights || defaults.imageHeight ) + imagePadding;
        if ( mode === 'traditional' ) {
            const imageHeight = parseInt( attributes.height );
            innerDivMargin = ( ( innerDivHeight - imageHeight ) / 2 ) + 'px auto';
        } else {
            innerDivMargin = 0;
        }
        outerDivWidth = innerDivWidth + 8;
    } else {
        innerDivWidth = parseInt( attributes.width );
        innerDivHeight = parseInt( galleryMwAttrs.heights || defaults.imageHeight );
        innerDivMargin = 0;
        outerDivWidth = innerDivWidth + 4;
    }

    const resourceTitle = mw.Title.newFromText( mw.libs.ve.normalizeParsoidResourceName( attributes.resource ) );

    this.$element
        .addClass( 'gallerybox' )
        .css( 'width', outerDivWidth + 'px' );
    const $thumbDiv = $( '<div>' )
        .addClass( 'thumb' )
        .css( 'width', innerDivWidth + 'px' )
        .css( 'height', innerDivHeight + 'px' );
    const $innerDiv = $( '<span>' )
        .css( 'margin', innerDivMargin );
    const $a = $( '<a>' ).addClass( 'mw-file-description' );

    let $img;
    if ( model.getAttribute( 'isError' ) ) {
        $img = $( '<span>' )
            .addClass( 'mw-file-element mw-broken-media' )
            .text( model.getAttribute( 'errorText' ) );
    } else {
        $img = $( '<img>' )
            .addClass( 'mw-file-element' )
            .attr( 'resource', attributes.resource )
            .attr( 'alt', attributes.altText )
            .attr( 'src', attributes.src )
            .attr( 'height', attributes.height )
            .attr( 'width', attributes.width );
    }

    this.$filenameA = $( '<a>' )
        .attr( 'href', '#' ) // Just to make it look like a link
        .text( resourceTitle ? resourceTitle.getMainText() : attributes.resource )
        .toggleClass( 'oo-ui-element-hidden', galleryMwAttrs.showfilename !== 'yes' );

    this.$element.prepend(
        $thumbDiv.append(
            $innerDiv.append(
                $a.append(
                    $img
                )
            )
        ),
        this.$filenameA
    );

    this.model.parent.connect( this, { attributeChange: 'onGalleryAttributeChange' } );
};

/* Inheritance */

OO.inheritClass( ve.ce.MWGalleryImageNode, ve.ce.BranchNode );

/* Static Properties */

ve.ce.MWGalleryImageNode.static.name = 'mwGalleryImage';

ve.ce.MWGalleryImageNode.static.tagName = 'li';

/* Methods */

ve.ce.MWGalleryImageNode.prototype.onGalleryAttributeChange = function ( key, from, to ) {
    if ( key !== 'mw' ) {
        return;
    }

    this.$filenameA.toggleClass( 'oo-ui-element-hidden', to.attrs.showfilename !== 'yes' );
};

ve.ce.MWGalleryImageNode.prototype.getDomPosition = function () {
    // We need to override this because this.$element can have children other than renderings of child
    // CE nodes (specifically, the image, this.$thumbDiv), which throws the calculations out of whack.
    // Luckily, MWGalleryImageNode is very simple and can contain at most one other node: its caption,
    // which is always inserted at the end.
    const domNode = this.$element.last()[ 0 ];
    return {
        node: domNode,
        offset: domNode.childNodes.length
    };
};

/* Registration */

ve.ce.nodeFactory.register( ve.ce.MWGalleryImageNode );