wikimedia/mediawiki-extensions-Wikibase

View on GitHub
view/resources/jquery/wikibase/jquery.wikibase.entitytermsforlanguagelistview.js

Summary

Maintainability
D
2 days
Test Coverage
/**
 * @license GPL-2.0-or-later
 * @author H. Snater < mediawiki@snater.com >
 * @author Bene* < benestar.wikimedia@gmail.com >
 */
( function () {
    'use strict';

    var PARENT = $.ui.EditableTemplatedWidget,
        datamodel = require( 'wikibase.datamodel' );

    /**
     * Displays multiple fingerprints (see jQuery.wikibase.entitytermsforlanguageview).
     *
     * @extends jQuery.ui.EditableTemplatedWidget
     *
     * @option {datamodel.Fingerprint} value
     *
     * @option {string[]} userLanguages
     *         A list of languages for which terms should be displayed initially.
     *
     * @event change
     *        - {jQuery.Event}
     *        - {string} Language code the change was made in.
     *
     * @event afterstartediting
     *       - {jQuery.Event}
     *
     * @event afterstopediting
     *        - {jQuery.Event}
     *        - {boolean} Whether to drop the value.
     *
     * @event toggleerror
     *        - {jQuery.Event}
     *        - {Error|null}
     */
    $.widget( 'wikibase.entitytermsforlanguagelistview', PARENT, {
        options: {
            template: 'wikibase-entitytermsforlanguagelistview',
            templateParams: [
                mw.msg( 'wikibase-entitytermsforlanguagelistview-language' ),
                mw.msg( 'wikibase-entitytermsforlanguagelistview-label' ),
                mw.msg( 'wikibase-entitytermsforlanguagelistview-description' ),
                mw.msg( 'wikibase-entitytermsforlanguagelistview-aliases' ),
                '' // entitytermsforlanguageview
            ],
            templateShortCuts: {
                $header: '.wikibase-entitytermsforlanguagelistview-header',
                $listview: '.wikibase-entitytermsforlanguagelistview-listview'
            },
            value: null,
            userLanguages: []
        },

        /**
         * @type {jQuery}
         */
        $listview: null,

        /**
         * @type {jQuery}
         */
        $entitytermsforlanguagelistviewMore: null,

        /**
         * @type {boolean} Has the "show all languages" button been clicked and this click been tracked?
         */
        _showAllLanguagesTracked: false,

        /**
         * @type {Object} Map of language codes pointing to list items (in the form of jQuery nodes).
         */
        _moreLanguagesItems: {},

        /**
         * @type {string[]} List of languages shown per default.
         */
        _defaultLanguages: [],

        /**
         * @type {OO.ui.PopupWidget|null}
         */
        _popup: null,

        /**
         * @see jQuery.ui.TemplatedWidget._create
         */
        _create: function () {
            if ( !( this.options.value instanceof datamodel.Fingerprint )
                || !Array.isArray( this.options.userLanguages )
            ) {
                throw new Error( 'Required option(s) missing' );
            }

            PARENT.prototype._create.call( this );
            this._defaultLanguages = this.options.userLanguages;

            this._amendDefaultLanguages();

            this._verifyExistingDom();
            this._createListView();

            this.element.addClass( 'wikibase-entitytermsforlanguagelistview' );
        },

        _amendDefaultLanguages: function () {
            if ( !mw.config.get( 'wbEnableMulLanguageCode' ) ) {
                return;
            }
            // eslint-disable-next-line es-x/no-array-prototype-includes
            if ( this._defaultLanguages.includes( 'mul' ) ) {
                return;
            }

            if ( mw.config.get( 'wbTmpAlwaysShowMulLanguageCode' ) === false ) {
                // temporarily show "mul" only if it has a label or alias, see T330217 for removing this block
                if ( !( 'mul' in this._getMoreLanguages() ) ) {
                    return;
                }
            }

            this._defaultLanguages.push( 'mul' );
        },

        /**
         * @see jQuery.ui.TemplatedWidget.destroy
         */
        destroy: function () {
            // When destroying a widget not initialized properly, shortcuts will not have been created.
            if ( this.$listview ) {
                // When destroying a widget not initialized properly, listview will not have been created.
                var listview = this.$listview.data( 'listview' );

                if ( listview ) {
                    listview.destroy();
                }
            }

            if ( this.$entitytermsforlanguagelistviewMore ) {
                this.$entitytermsforlanguagelistviewMore.remove();
            }

            this.element.removeClass( 'wikibase-entitytermsforlanguagelistview' );
            PARENT.prototype.destroy.call( this );
        },

        _verifyExistingDom: function () {
            var $entitytermsforlanguageview = this.element
                .find( '.wikibase-entitytermsforlanguageview' );

            if ( $entitytermsforlanguageview.length === 0 ) {
                // No need to verify an empty DOM
                return;
            }

            // Scrape languages from static HTML:
            var mismatchAt = null,
                languages = this._defaultLanguages;
            $entitytermsforlanguageview.each( function ( i ) {
                var match = $( this )
                    .attr( 'class' )
                    .match( /(?:^|\s)wikibase-entitytermsforlanguageview-(\S+)/ );
                if ( match && match[ 1 ] !== languages[ i ] ) {
                    if ( match[ 1 ] !== 'mul' ) {
                        // "mul" might be included in the existing term box, but we want it to be after
                        // everything else, thus discarding it is expected.
                        mw.log.warn( 'Existing entitytermsforlanguagelistview DOM does not match configured languages' );
                    }
                    mismatchAt = i;
                    return false;
                }
            } );

            if ( mismatchAt !== null ) {
                $entitytermsforlanguageview.slice( mismatchAt ).remove();
            }
        },

        /**
         * Creates the listview widget managing the entitytermsforlanguageview widgets
         *
         * @private
         */
        _createListView: function () {
            var self = this,
                listItemWidget = $.wikibase.entitytermsforlanguageview,
                prefix = listItemWidget.prototype.widgetEventPrefix;

            // Fully encapsulate child widgets by suppressing their events:
            this.element
            .on( prefix + 'change.' + this.widgetName, function ( event, lang ) {
                event.stopPropagation();
                // The only event handler for this is in entitytermsview.
                self._trigger( 'change', null, [ lang ] );
            } )
            .on( prefix + 'toggleerror.' + this.widgetName, function ( event, error ) {
                event.stopPropagation();
                self.setError( error );
            } )
            .on(
                [
                    prefix + 'create.' + this.widgetName,
                    prefix + 'afterstartediting.' + this.widgetName,
                    prefix + 'afterstopediting.' + this.widgetName,
                    prefix + 'disable.' + this.widgetName
                ].join( ' ' ),
                function ( event ) {
                    event.stopPropagation();
                }
            );

            this.$listview
            .listview( {
                listItemAdapter: new $.wikibase.listview.ListItemAdapter( {
                    listItemWidget: listItemWidget,
                    newItemOptionsFn: function ( value ) {
                        return {
                            allLanguageLabels: function () {
                                return self.options.value.getLabels();
                            },
                            value: value
                        };
                    }
                } ),
                value: this._defaultLanguages.map( function ( lang ) {
                    return self._getValueForLanguage( lang );
                } ),
                listItemNodeName: 'TR'
            } );

            if ( !this.element.find( '.wikibase-entitytermsforlanguagelistview-more' ).length ) {
                this._createEntitytermsforlanguagelistviewMore();
            }

            if (
                mw.user.isNamed() &&
                !mw.user.options.get( 'wb-dont-show-again-mul-popup' )
            ) {
                this._addPulsatingDotToMul();
            }
        },

        /**
         * Creates a button which allows the user to show terms in all languages available.
         *
         * @private
         */
        _createEntitytermsforlanguagelistviewMore: function () {
            if ( !this._hasMoreLanguages() ) {
                return;
            }

            var $moreLanguagesButton = $( '<a>' )
                .attr( 'href', '#' )
                .on( 'click', this._onMoreLanguagesButtonClicked.bind( this ) );
            this._toggleMoreLanguagesButton( $moreLanguagesButton );

            this.$entitytermsforlanguagelistviewMore = $( '<div>' )
                .addClass( 'wikibase-entitytermsforlanguagelistview-more' )
                .append( $moreLanguagesButton );

            this.element.after( this.$entitytermsforlanguagelistviewMore );
        },

        /**
         * @return {boolean} If there are more languages to display.
         * @private
         */
        _hasMoreLanguages: function () {
            var fingerprint = this.options.value,
                minLength = this._defaultLanguages.length;

            if ( fingerprint.getLabels().length > minLength
                || fingerprint.getDescriptions().length > minLength
                || fingerprint.getAliases().length > minLength
            ) {
                return true;
            }

            return !$.isEmptyObject( this._getMoreLanguages() );
        },

        /**
         * Click handler for more languages button.
         *
         * @private
         */
        _onMoreLanguagesButtonClicked: function ( event ) {
            var $button = $( event.target );

            if ( !this._isMoreLanguagesExpanded() ) {
                this._addMoreLanguages();
                this._trackAllLanguagesShown();
            } else {
                var previousTop = $button.offset().top;
                this._removeMoreLanguages();
                this._scrollUp( $button, previousTop );
            }

            this._toggleMoreLanguagesButton( $button );
            return false;
        },

        /**
         * Toggle more language button text between the "wikibase-entitytermsforlanguagelistview-less"
         * and "wikibase-entitytermsforlanguagelistview-more" messages.
         *
         * @param {jQuery} $button
         * @private
         */
        _toggleMoreLanguagesButton: function ( $button ) {
            $button.text( mw.msg(
                this._isMoreLanguagesExpanded() ?
                    'wikibase-entitytermsforlanguagelistview-less' :
                    'wikibase-entitytermsforlanguagelistview-more'
            ) );
        },

        _trackAllLanguagesShown: function () {
            if ( this._showAllLanguagesTracked ) {
                return;
            }
            mw.track( 'event.WikibaseTermboxInteraction', {
                actionType: 'all'
            } );
            this._showAllLanguagesTracked = true;
        },

        /**
         * @return {boolean}
         * @private
         */
        _isMoreLanguagesExpanded: function () {
            return !$.isEmptyObject( this._moreLanguagesItems );
        },

        /**
         * Add terms in "more" languages to the list view, ordered by language code.
         *
         * @private
         */
        _addMoreLanguages: function () {
            var listview = this.$listview.data( 'listview' ),
                lia = listview.listItemAdapter(),
                self = this;

            Object.keys( this._getMoreLanguages() ).sort().forEach( function ( languageCode ) {
                var $item = listview.addItem( self._getValueForLanguage( languageCode ) );
                if ( self.isInEditMode() ) {
                    lia.liInstance( $item ).startEditing();
                }
                self._moreLanguagesItems[ languageCode ] = $item;
            } );
        },

        /**
         * Remove terms in "more" languages from the list view.
         *
         * @private
         */
        _removeMoreLanguages: function () {
            var listview = this.$listview.data( 'listview' );

            for ( var languageCode in this._moreLanguagesItems ) {
                listview.removeItem( this._moreLanguagesItems[ languageCode ] );
            }

            this._moreLanguagesItems = {};
        },

        /**
         * @return {Object} Unsorted map of "more" language codes in this fingerprint.
         * @private
         */
        _getMoreLanguages: function () {
            var fingerprint = this.options.value,
                languages = {};

            fingerprint.getLabels().each( function ( lang ) {
                languages[ lang ] = lang;
            } );
            fingerprint.getDescriptions().each( function ( lang ) {
                languages[ lang ] = lang;
            } );
            fingerprint.getAliases().each( function ( lang ) {
                languages[ lang ] = lang;
            } );

            this._defaultLanguages.forEach( function ( lang ) {
                delete languages[ lang ];
            } );

            return languages;
        },

        /**
         * @param {jQuery} $this
         * @param {number} previousTop
         * @private
         */
        _scrollUp: function ( $this, previousTop ) {
            var top = $this.offset().top;

            if ( top < $( window ).scrollTop() ) {
                // This does not only keep the toggler visible, it also updates all stick(y)nodes.
                window.scrollBy( 0, top - previousTop );
            }
        },

        /**
         * Click handler to open Popup when clicking pulsating dot for mul language.
         *
         * @private
         */
        _onMulPulsatingDotClicked: function ( _event ) {
            if ( !this._popup ) {
                var $target = $( this.element ).find( '.mw-pulsating-dot-popup-container' );

                var dontShowMulPopupCheckbox = new OO.ui.CheckboxInputWidget( {
                    value: true,
                    selected: false
                } ).on( 'change', function ( value ) {
                    new mw.Api().saveOption( 'wb-dont-show-again-mul-popup', value ? '1' : null );
                    mw.user.options.set( 'wb-dont-show-again-mul-popup', value ? '1' : null );
                } );

                var showAgainLayout = new OO.ui.FieldLayout( dontShowMulPopupCheckbox, {
                    align: 'inline',
                    label: mw.msg( 'wikibase-entityterms-languagelistview-mul-popup-dont-show-again' )
                } );

                var $tooltipContent = $( '<div>' ).append(
                    mw.message(
                        'wikibase-entityterms-languagelistview-mul-popup-content',
                        'https://www.wikidata.org/wiki/Special:MyLanguage/Help:Default_values_for_labels_and_aliases'
                    ).parseDom(),
                    showAgainLayout.$element
                );

                this._popup = new OO.ui.PopupWidget( {
                    padded: true,
                    width: 400,
                    head: true,
                    label: mw.msg(
                        'wikibase-entityterms-languagelistview-mul-popup-title'
                    ),
                    $content: $tooltipContent,
                    classes: [ 'wikibase-entityterms-languagelistview-mul-popup' ],
                    $floatableContainer: $target,
                    position: 'below',
                    align: 'forwards'
                } );
                $( document.body ).append( this._popup.$element );
            }
            this._popup.toggle();
        },

        /**
         * @param {string} lang
         * @return {Object}
         * @private
         */
        _getValueForLanguage: function ( lang ) {
            var fingerprint = this.options.value;

            return {
                language: lang,
                label: fingerprint.getLabelFor( lang ) || new datamodel.Term( lang, '' ),
                description: fingerprint.getDescriptionFor( lang ) || new datamodel.Term( lang, '' ),
                aliases: fingerprint.getAliasesFor( lang ) || new datamodel.MultiTerm( lang, [] )
            };
        },

        _startEditing: function () {
            var self = this;
            var listview = this.$listview.data( 'listview' );
            return listview.startEditing().done( function () {
                self.updateInputSize();

                if ( $( self.element ).find( '.mw-pulsating-dot-container' ).length ) {
                    self._onMulPulsatingDotClicked();
                }
            } );
        },

        /**
         * @param {boolean} [dropValue]
         */
        _stopEditing: function ( dropValue ) {
            if ( this._popup ) {
                this._popup.toggle( false );
            }

            var listview = this.$listview.data( 'listview' );

            return $.when.apply( $, listview.value().map( function ( entitytermsforlanguageview ) {
                return entitytermsforlanguageview.stopEditing( dropValue );
            } ) );
        },

        /**
         * Updates the size of the input boxes by triggering the inputautoexpand plugin's `expand()`
         * function.
         */
        updateInputSize: function () {
            var listview = this.$listview.data( 'listview' ),
                lia = listview.listItemAdapter();

            listview.items().each( function () {
                var entitytermsforlanguageview = lia.liInstance( $( this ) );

                [ 'label', 'description', 'aliases' ].forEach( function ( name ) {
                    var $view = entitytermsforlanguageview[ '$' + name + 'view' ],
                        autoExpandInput = $view.find( 'input,textarea' ).data( 'inputautoexpand' );

                    if ( autoExpandInput ) {
                        autoExpandInput.options( {
                            maxWidth: $view.width()
                        } );
                        autoExpandInput.expand( true );
                    }
                } );
            } );
        },

        /**
         * @see jQuery.ui.TemplatedWidget.focus
         */
        focus: function () {
            var listview = this.$listview.data( 'listview' ),
                $items = listview.items();

            if ( $items.length ) {
                listview.listItemAdapter().liInstance( $items.first() ).focus();
            } else {
                this.element.trigger( 'focus' );
            }
        },

        removeError: function () {
            PARENT.prototype.removeError.call( this );

            var listview = this.$listview.data( 'listview' ),
                lia = listview.listItemAdapter();

            listview.items().each( function () {
                var entitytermsforlanguageview = lia.liInstance( $( this ) );
                entitytermsforlanguageview.removeError();
            } );
        },

        /**
         * @param {datamodel.Fingerprint} [value]
         * @return {datamodel.Fingerprint|*}
         */
        value: function ( value ) {
            if ( value !== undefined ) {
                return this.option( 'value', value );
            }

            var listview = this.$listview.data( 'listview' ),
                lia = listview.listItemAdapter();

            // Clones the current Fingerprint.
            // FIXME: This accesses the private _items property since there is no copy or clone.
            value = new datamodel.Fingerprint(
                new datamodel.TermMap( this.options.value.getLabels()._items ),
                new datamodel.TermMap( this.options.value.getDescriptions()._items ),
                new datamodel.MultiTermMap( this.options.value.getAliases()._items )
            );

            // this only adds all terms visible in the ui to the Fingerprint, all other languages get ignored
            listview.items().each( function () {
                var terms = lia.liInstance( $( this ) ).value();
                if ( terms.label.getText() === '' ) {
                    // FIXME: DataModel JavaScript should do this.
                    value.removeLabelFor( terms.language );
                } else {
                    value.setLabel( terms.language, terms.label );
                }
                if ( terms.description.getText() === '' ) {
                    // FIXME: DataModel JavaScript should do this.
                    value.removeDescriptionFor( terms.language );
                } else {
                    value.setDescription( terms.language, terms.description );
                }
                if ( terms.aliases.isEmpty() ) {
                    // FIXME: DataModel JavaScript should do this.
                    value.removeAliasesFor( terms.language );
                } else {
                    value.setAliases( terms.language, terms.aliases );
                }
            } );

            return value;
        },

        /**
         * @see jQuery.ui.TemplatedWidget._setOption
         * @return {jQuery.Widget}
         */
        _setOption: function ( key, value ) {
            var response = PARENT.prototype._setOption.apply( this, arguments );

            if ( key === 'value' ) {
                var self = this;
                this.$listview.data( 'listview' ).value().forEach( function ( entitytermsforlanguageview ) {
                    entitytermsforlanguageview.value( self._getValueForLanguage( entitytermsforlanguageview.value().language ) );
                } );
            }

            if ( key === 'disabled' ) {
                this.$listview.data( 'listview' ).option( key, value );
            }

            return response;
        },

        /**
         * Adds a clickable pulsating dot into the language column for "mul", if we have a "mul" row.
         *
         * @private
         */
        _addPulsatingDotToMul: function () {
            var $mulLanguageRow = this.element.find( '.wikibase-entitytermsforlanguageview-mul' );
            if ( $mulLanguageRow.length ) {
                var $pulsatingDot = $( '<a>' ).addClass( 'mw-pulsating-dot' );
                var $pulsatingDotPopupContainer = $( '<span>' ).addClass( 'mw-pulsating-dot-popup-container' )
                    .append( $pulsatingDot );
                var $pulsatingDotContainer = $( '<span>' ).addClass( 'mw-pulsating-dot-container' )
                    .append( $pulsatingDotPopupContainer )
                    .on( 'click', this._onMulPulsatingDotClicked.bind( this ) );
                $mulLanguageRow.find( '.wikibase-entitytermsforlanguageview-language' )
                    .append( $pulsatingDotContainer );
            }
        }
    } );

}() );