wikimedia/mediawiki-extensions-Wikibase

View on GitHub
view/resources/jquery/wikibase/jquery.wikibase.statementview.RankSelector.js

Summary

Maintainability
C
7 hrs
Test Coverage
( function () {
    'use strict';

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

    /**
     * The node of the `RankSelector` menu to select a `RANK` from.
     *
     * @property {jQuery|null} [$menu=null]
     * @ignore
     */
    var $menu = null;

    /**
     * Returns a `RANK`'s serialized string.
     *
     * @see datamodel.Statement.RANK
     * @ignore
     *
     * @param {number} rank
     * @return {string|null}
     */
    function getRankName( rank ) {
        for ( var rankName in datamodel.Statement.RANK ) {
            if ( rank === datamodel.Statement.RANK[ rankName ] ) {
                return rankName.toLowerCase();
            }
        }

        return null;
    }

    /**
     * Selector for choosing a `Statement` rank.
     *
     * @see datamodel.Statement.RANK
     * @class jQuery.wikibase.statementview.RankSelector
     * @extends jQuery.ui.EditableTemplatedWidget
     * @license GPL-2.0-or-later
     * @author H. Snater < mediawiki@snater.com >
     *
     * @constructor
     *
     * @param {Object} [options]
     * @param {number} [options.value=datamodel.Statement.RANK.NORMAL]
     *        The `RANK` that shall be selected.
     * @param {boolean} [options.isRTL=false]
     *        Whether the widget is displayed in right-to-left context.
     */
    /**
     * @event change
     * Triggered when the snak type got changed.
     * @param {jQuery.Event} event
     */
    $.wikibase.statementview.RankSelector = util.inherit( PARENT, {
        namespace: 'wikibase',
        widgetName: 'rankselector',
        widgetFullName: 'wikibase-rankselector',

        /**
         * @inheritdoc
         * @protected
         * @readonly
         */
        options: {
            template: 'wikibase-rankselector',
            templateParams: [
                '',
                '',
                ''
            ],
            templateShortCuts: {
                $icon: '.ui-icon-rankselector'
            },
            value: datamodel.Statement.RANK.NORMAL,
            isRtl: false
        },

        /**
         * The `RANK` currently featured by the `RankSelector`.
         *
         * @see datamodel.Statement.RANK
         * @type {number}
         */
        _rank: null,

        /**
         * @inheritdoc
         */
        _create: function () {
            var self = this;

            PARENT.prototype._create.call( this );

            if ( !$menu ) {
                $menu = this._buildMenu().appendTo( document.body ).hide();

                $menu.on( 'click.' + this.widgetName, function ( event ) {
                    var $li = $( event.target ).closest( 'li' ),
                        rank = $li.data( self.widgetName + '-menuitem-rank' );

                    if ( rank !== undefined ) {
                        $.data( this, self.widgetName ).value( rank );
                    }
                } );
            }

            this.element
            .addClass( this.widgetFullName )
            .on( 'mouseover.' + this.widgetName, function ( event ) {
                if ( !self.option( 'disabled' ) && self.isInEditMode() ) {
                    self.element.addClass( 'ui-state-hover' );
                }
            } )
            .on( 'mouseout.' + this.widgetName, function ( event ) {
                if ( !self.option( 'disabled' ) && self.isInEditMode() ) {
                    self.element.removeClass( 'ui-state-hover' );
                }
            } )
            .on( 'click.' + this.widgetName, function ( event ) {
                // TODO: Store visibility in model
                // eslint-disable-next-line no-jquery/no-sizzle
                if ( self.option( 'disabled' ) || !self.isInEditMode() || $menu.is( ':visible' ) ) {
                    $menu.hide();
                    return;
                }

                $menu.data( self.widgetName, self );
                $menu.show();
                self._updateMenuCss();
                self.repositionMenu();

                self.element.addClass( 'ui-state-active' );

                // Close the menu when clicking, regardless of whether the click is performed on the
                // menu itself or outside of it:
                var degrade = function ( ev ) {
                    if ( ev.target !== self.element.get( 0 ) ) {
                        $menu.hide();
                        self.element.removeClass( 'ui-state-active' );
                    }
                    self._unbindGlobalEventListeners();
                };

                $( document ).on( 'mouseup.' + self.widgetName, degrade );
                $( window ).on(
                    'resize.' + self.widgetName,
                    function ( ev ) {
                        self.repositionMenu();
                    }
                );
            } );

            this._setRank( this.options.value );
        },

        /**
         * @inheritdoc
         */
        destroy: function () {
            if ( $( '.' + this.widgetFullName ).length === 0 ) {
                $menu.data( 'menu' ).destroy();
                $menu.remove();
                $menu = null;
            }
            this.$icon.remove();

            this.element.removeClass( 'ui-state-default ui-state-hover ' + this.widgetFullName );

            this._unbindGlobalEventListeners();

            PARENT.prototype.destroy.call( this );
        },

        /**
         * @inheritdoc
         * @protected
         */
        _setOption: function ( key, value ) {
            var response = PARENT.prototype._setOption.apply( this, arguments );
            if ( key === 'rank' ) {
                this._setRank( value );
                this._trigger( 'change' );
            } else if ( key === 'disabled' ) {
                this.draw();
            }
            return response;
        },

        /**
         * Removes all global event listeners generated by the `RankSelector`.
         *
         * @private
         */
        _unbindGlobalEventListeners: function () {
            $( document ).add( $( window ) ).off( '.' + this.widgetName );
        },

        /**
         * Generates the menu the `RANK` may be chosen from.
         *
         * @private
         *
         * @return {jQuery}
         */
        _buildMenu: function () {
            var self = this,
                $m = $( '<ul>' ).addClass( this.widgetFullName + '-menu' );

            // eslint-disable-next-line no-jquery/no-each-util
            $.each( datamodel.Statement.RANK, function ( rankName, rank ) {
                rankName = rankName.toLowerCase();

                $m.append(
                    $( '<li>' )
                    .addClass( self.widgetFullName + '-menuitem-' + rankName )
                    .data( self.widgetName + '-menuitem-rank', rank )
                    .append(
                        $( '<a>' )
                            // The following messages are used here:
                            // * wikibase-statementview-rank-preferred
                            // * wikibase-statementview-rank-normal
                            // * wikibase-statementview-rank-deprecated
                            .text( mw.msg( 'wikibase-statementview-rank-' + rankName ) )
                            // The following messages are used here:
                            // * wikibase-statementview-rank-tooltip-preferred
                            // * wikibase-statementview-rank-tooltip-normal
                            // * wikibase-statementview-rank-tooltip-deprecated
                            .attr( 'title', mw.msg( 'wikibase-statementview-rank-tooltip-' + rankName ) )
                            .on( 'click.' + self.widgetName, function ( event ) {
                                event.preventDefault();
                            } )
                    )
                );
            } );

            return $m.menu();
        },

        /**
         * Sets the `RANK` if a `RANK` is specified or gets the current `RANK` if parameter is
         * omitted.
         *
         * @param {number} [rank]
         * @return {number|undefined}
         */
        value: function ( rank ) {
            if ( rank === undefined ) {
                return this._rank;
            }

            this._setRank( rank );

            this._trigger( 'change' );
        },

        /**
         * Sets the `RANK` activating the menu item representing the specified `RANK`.
         *
         * @private
         *
         * @param {number} rank
         */
        _setRank: function ( rank ) {
            this._rank = rank;

            if ( $menu && $menu.data( this.widgetName ) === this ) {
                this._updateMenuCss();
            }

            this._updateIcon();
        },

        /**
         * Updates the menu's CSS classes.
         *
         * @private
         */
        _updateMenuCss: function () {
            $menu.children().removeClass( 'ui-state-active' );
            $menu
            .children( '.' + this.widgetFullName + '-menuitem-' + getRankName( this._rank ) )
            .addClass( 'ui-state-active' );
        },

        /**
         * Updates the rank icon to reflect the rank currently set.
         *
         * @private
         */
        _updateIcon: function () {
            for ( var rankId in datamodel.Statement.RANK ) {
                var rankName = rankId.toLowerCase(),
                    selected = this._rank === datamodel.Statement.RANK[ rankId ];

                this.$icon.toggleClass( this.widgetFullName + '-' + rankName, selected );

                if ( selected ) {
                    // The following messages are used here:
                    // * wikibase-statementview-rank-preferred
                    // * wikibase-statementview-rank-normal
                    // * wikibase-statementview-rank-deprecated
                    this.$icon.attr( 'title', mw.msg( 'wikibase-statementview-rank-' + rankName ) );
                }
            }
        },

        /**
         * (Re-)positions the menu.
         */
        repositionMenu: function () {
            $menu.position( {
                of: this.$icon,
                my: ( this.options.isRTL ? 'right' : 'left' ) + ' top',
                at: ( this.options.isRTL ? 'right' : 'left' ) + ' bottom',
                offset: '0 1',
                collision: 'none'
            } );
        },

        /**
         * @inheritdoc
         */
        draw: function () {
            if ( this.isInEditMode() ) {
                this.element
                .addClass( 'ui-state-default' )
                .removeClass( 'ui-state-disabled' );
            } else {
                this.element
                .removeClass( 'ui-state-default ui-state-active ui-state-hover' )
                .addClass( 'ui-state-disabled' );
            }

            return $.Deferred().resolve().promise();
        },

        _startEditing: function () {
            return this.draw();
        },

        _stopEditing: function ( dropValue ) {
            // Hide the menu the rank selector currently references to:
            if ( $menu && $menu.data( this.widgetName ) === this ) {
                $menu.hide();
            }
            if ( dropValue ) {
                this._setRank( this.options.value );
            }
            return this.draw();
        }

    } );

}() );