widget/rank/rank-widget.js

import $ from 'jquery';
import Widget from '../../js/widget';
import support from '../../js/support';
import events from '../../js/event';
import sortable from 'html5sortable/dist/html5sortable.cjs';
import { t } from 'enketo/translator';

/**
 * @augments Widget
 */
class RankWidget extends Widget {
    /**
     * @type {string}
     */
    static get selector() {
        return '.question input.rank';
    }

    /**
     * @type {boolean}
     */
    static get list() {
        return true;
    }

    _init() {
        const that = this;
        const loadedValue = this.originalInputValue;
        const startTextKey = support.touch ? 'rankwidget.tapstart' : 'rankwidget.clickstart';

        this.itemSelector = 'label:not(.itemset-template)';
        this.list = $( this.element ).next( '.option-wrapper' ).addClass( 'widget rank-widget' )[ 0 ];

        $( this.list )
            .toggleClass( 'rank-widget--empty', !loadedValue )
            .append( this.resetButtonHtml )
            .append( `<div class="rank-widget__overlay"><span class="rank-widget__overlay__content" data-i18n="${startTextKey}">${support.touch ? t( 'rankwidget.tapstart' ) : t( 'rankwidget.clickstart' )}</span></div>` )
            .on( 'click', function() {
                if ( !that.element.disabled ) {
                    this.classList.remove( 'rank-widget--empty' );
                    that.originalInputValue = that.value;
                    that.element.dispatchEvent( events.FakeFocus() );
                }
            } );

        this.list.querySelector( '.btn-reset' ).addEventListener( 'click', ( evt ) => {
            this._reset();
            evt.stopPropagation();
        } );

        this.element.classList.add( 'hide' );

        this.value = loadedValue;

        // Create the sortable drag-and-drop functionality
        sortable( this.list, {
            items: this.itemSelector,
            //hoverClass: 'rank-widget__item--hover',
            containerSerializer( container ) {
                return {
                    value: [].slice.call( container.node.querySelectorAll( `${that.itemSelector} input` ) ).map( input => input.value ).join( ' ' )
                };
            }
        } )[ 0 ].addEventListener( 'sortupdate', () => {
            this.originalInputValue = this.value;
            this.element.dispatchEvent( events.FakeFocus() );
        } );

        if ( this.props.readonly ) {
            this.disable();
        }
    }

    /**
     * Resets widget
     */
    _reset() {
        this.originalInputValue = '';
    }

    /**
     * @type {string}
     */
    get value() {
        const result = sortable( this.list, 'serialize' );

        return result[ 0 ].container.value;
    }

    set value( value ) {
        if ( !value ) {
            this._reset();
        } else {
            const that = this;
            const values = value.split( ' ' );
            const items = [ ...this.list.querySelectorAll( `${this.itemSelector} input` ) ];

            // Basic error check
            if ( values.length !== items.length ) {
                throw new Error( 'Could not load rank widget value. Number of items mismatch.' );
            }

            // Don't even attempt to rectify a mismatch between the value and the available items.
            items.sort( ( a, b ) => {
                const aIndex = values.indexOf( a.value );
                const bIndex = values.indexOf( b.value );
                if ( aIndex === -1 || bIndex === -1 ) {
                    throw new Error( 'Could not load rank widget value. Mismatch in item values.' );
                }

                return aIndex - bIndex;
            } );

            items.forEach( item => {
                $( that.list ).find( '.btn-reset' ).before( $( item.parentNode ).detach() );
            } );
        }
    }

    /**
     * Disables widget
     */
    disable() {
        $( this.element )
            .prop( 'disabled', true )
            .next( '.widget' )
            .find( 'input, button' )
            .prop( 'disabled', true );

        sortable( this.list, 'disable' );
    }

    /**
     * Enables widget
     */
    enable() {
        $( this.element )
            .prop( 'disabled', false )
            .next( '.widget' )
            .find( 'input, button' )
            .prop( 'disabled', false );

        sortable( this.list, 'enable' );
    }

    /**
     * Updates widget
     */
    update() {
        const value = this.element.value;
        // re-initalize sortable because the options may have changed
        sortable( this.list );

        if ( value ) {
            this.value = value;
            this.originalInputValue = value;
        } else {
            this._reset();
        }
    }

    // Since we're overriding the setter we also have to overwrite the getter
    // https://stackoverflow.com/questions/28950760/override-a-setter-and-the-getter-must-also-be-overridden
    /**
     * @type {string}
     */
    get originalInputValue() {
        return super.originalInputValue;
    }

    /**
     * This is the input that Enketo's engine listens on.
     *
     * @type {string}
     */
    set originalInputValue( value ) {
        super.originalInputValue = value;
        this.list.classList.toggle( 'rank-widget--empty', !value );
    }

}

export default RankWidget;