widget/radio/radiopicker.js

import Widget from '../../js/widget';
import events from '../../js/event';
import $ from 'jquery';

/**
 * Enhances radio buttons
 *
 * @augments Widget
 */
class Radiopicker extends Widget {
    /**
     * @type {string}
     */
    static get selector() {
        return 'form';
    }

    _init() {
        const $form = $( this.element );
        const that = this;

        $form
            // Applies a data-checked attribute to the parent label of a checked radiobutton.
            .on( 'click', 'input[type="radio"]:not([readonly]):checked', function() {
                $( this ).parent( 'label' ).siblings().removeAttr( 'data-checked' ).end().attr( 'data-checked', 'true' );
            } )
            // Same for checkbox.
            .on( 'click', 'input[type="checkbox"]:not([readonly])', function() {
                that._updateDataChecked( this );
            } )
            // Detect programmatic changes to update data-checked attribute.
            .on( events.InputUpdate().type, 'input[type="radio"], input[type="checkbox"]', function() {
                this.closest( '.option-wrapper' )
                    .querySelectorAll( 'input[type="radio"],input[type="checkbox"]' )
                    .forEach( input => that._updateDataChecked( input ) );
            } )
            // Readonly buttons/checkboxes will not respond to clicks.
            .on( 'click', 'input[type="checkbox"][readonly],input[type="radio"][readonly]', event => {
                event.stopImmediatePropagation();

                return false;
            } )
            /*
             * In Safari a click on a readonly checkbox/radio button sets `checked` to true, **before** the click event fires.
             * This causes the model to update. The above clickhandler will set the checked back to false, but there is
             * no way to propagate that reversion back to the model.
             *
             * Disabling change handling on readonly checkboxes/radiobuttons is not an option, because of the scenario
             * described in Form.js in the comment above the change handler.
             *
             * The solution is to detect here whether the change event was triggered by a human, by checking if the
             * originalEvent property is defined.
             *
             * See more at https://github.com/enketo/enketo-core/issues/516
             */
            .on( events.Change().type, 'input[type="checkbox"][readonly],input[type="radio"][readonly]', function( event ) {
                const byProgram = typeof event.originalEvent === 'undefined';
                if ( !byProgram ) {
                    event.stopImmediatePropagation();
                    /*
                     * For radiobuttons, this is ugly and relies on the data-checked attribute.
                     * Without this, is it still possible to check a readonly radio button (although it won't propagate to the model).
                     * I think this is the only remnant of the usage of data-checked in Enketo. However, it is
                     * also still relied upon by Esri/Survey123.
                     */
                    if ( this.checked && this.parentNode.dataset.checked !== 'true' ) {
                        this.checked = false;
                    }
                }

                return byProgram;
            } )
            // Add unselect radio button functionality.
            .on( 'click', '[data-checked]>input[type="radio"]:not(.no-unselect)', function() {
                $( this ).prop( 'checked', false ).trigger( 'change' ).parent().removeAttr( 'data-checked' );
            } );

        // Defaults
        this.element
            .querySelectorAll( 'input[type="radio"]:checked, input[type="checkbox"]:checked' )
            .forEach( input => this._updateDataChecked( input ) );

    }

    /**
     * @param {Element} el - Element to update
     */
    _updateDataChecked( el ) {
        if ( el.checked ) {
            el.parentNode.dataset.checked = true;
        } else {
            delete el.parentNode.dataset.checked;
        }
    }
}

export default Radiopicker;