js/widget.js

import input from './input';
import event from './event';
const range = document.createRange();

/**
 * A Widget class that can be extended to provide some of the basic widget functionality out of the box.
 */
class Widget {
    /**
     * @class
     * @param {Element} element - The DOM element the widget is applied on
     * @param {(boolean|{touch: boolean})} [options] - Options passed to the widget during instantiation
     */
    constructor( element, options ) {
        this.element = element;
        this.options = options || {};
        this.question = element.closest( '.question' );
        this._props = this._getProps();

        // Some widgets (e.g. ImageMap) initialize asynchronously and init returns a promise.
        return this._init() || this;
    }

    /**
     * Meant to be overridden, but automatically called.
     *
     */
    _init() {
        // load default value into the widget
        this.value = this.originalInputValue;
        // if widget initializes asynchronously return a promise here. Otherwise, return nothing/undefined/null.
    }

    /**
     * Not meant to be overridden, but could be. Recommend to extend `get props()` instead.
     *
     * @return {object} props object
     */
    _getProps() {
        const that = this;

        return {
            get readonly() { return that.element.nodeName.toLowerCase() === 'select' ? that.element.hasAttribute( 'readonly' ) : !!that.element.readOnly; },
            appearances: [ ...this.element.closest( '.question, form.or' ).classList ]
                .filter( cls => cls.indexOf( 'or-appearance-' ) === 0 )
                .map( cls => cls.substring( 14 ) ),
            multiple: !!this.element.multiple,
            disabled: !!this.element.disabled,
            type: this.element.getAttribute( 'data-type-xml' ),
        };
    }

    /**
     * Disallow user input into widget by making it readonly.
     */
    disable() {
        // leave empty in Widget.js
    }

    /**
     * Performs opposite action of disable() function.
     */
    enable() {
        // leave empty in Widget.js
    }

    /**
     * Updates form-defined language strings, <option>s (cascading selects, and (calculated) values.
     * Most of the times, this function needs to be overridden in the widget.
     */
    update() {}

    /**
     * Returns widget properties. May need to be extended.
     *
     * @readonly
     * @type {object}
     */
    get props() {
        return this._props;
    }

    /**
     * Returns a HTML document fragment for a reset button.
     *
     * @readonly
     * @type {Element}
     */
    get resetButtonHtml() {
        return range.createContextualFragment(
            `<button
                type="button"
                class="btn-icon-only btn-reset"
                aria-label="reset">
                <i class="icon icon-refresh"> </i>
            </button>`
        );
    }

    /**
     * Returns a HTML document fragment for a download button.
     *
     * @readonly
     * @type {Element}
     */
    get downloadButtonHtml() {
        return range.createContextualFragment(
            `<a
                class="btn-icon-only btn-download"
                aria-label="download"
                download
                href=""><i class="icon icon-download"> </i></a>`
        );
    }

    /**
     * Obtains the value from the current widget state. Should be overridden.
     *
     * @readonly
     * @type {*}
     */
    get value() {
        return undefined;
    }

    /**
     * Sets a value in the widget. Should be overridden.
     *
     * @param {*} value - value to set
     * @type {*}
     */
    set value( value ) {}

    /**
     * Obtains the value from the original form control the widget is instantiated on.
     * This form control is often hidden by the widget.
     *
     * @readonly
     * @type {*}
     */
    get originalInputValue() {
        return input.getVal( this.element );
    }

    /**
     * Updates the value in the original form control the widget is instantiated on.
     * This form control is often hidden by the widget.
     *
     * @param {*} value - value to set
     * @type {*}
     */
    set originalInputValue( value ) {
        // Avoid unnecessary change events as they could have significant negative consequences!
        // However, to add a check for this.originalInputValue !== value here would affect performance too much,
        // so we rely on widget code to only use this setter when the value changes.
        input.setVal( this.element, value, null );
        this.element.dispatchEvent( event.Change() );
    }

    /**
     * Returns its own name.
     *
     * @static
     * @readonly
     * @type {string}
     */
    static get name() {
        return this.constructor.name;
    }

    /**
     * Returns true if the widget is using a list of options.
     *
     * @readonly
     * @static
     * @type {boolean}
     */
    static get list() {
        return false;
    }

    /**
     * Tests whether widget needs to be instantiated (e.g. if not to be used for touchscreens).
     * Note that the Element (used in the constructor) will be provided as parameter.
     *
     * @static
     * @return {boolean} to instantiate or not to instantiate, that is the question
     */
    static condition() {
        return true;
    }
}

export default Widget;