Lack of interest [Developer Tool] "MultiDisabler" Element Handler

This suggestion has been closed automatically because it did not receive enough votes over an extended period of time. If you wish to see this, please search for an open suggestion and, if you don't find any, post a new one.

DragonByte Tech

Well-known member
Recently, I wanted to take advantage of the disabler feature in XF2 to require two checkboxes inside the same checkboxrow to be checked before the submit button would become available. After some trial and error, I concluded that is not currently possible.

I have created the MultiDisabler JS class to support this functionality.

Proposed Example Usage:
HTML:
<xf:checkboxrow rowtype="fullWidth noLabel" data-xf-init="multi-disabler" data-input-controls=".js-confirmBoxes" data-container=".js-submitDisable">
    <xf:option name="confirm" class="js-confirmBoxes">
        <xf:label>{{ phrase('please_confirm_this') }}</xf:label>
    </xf:option>

    <xf:option name="also_confirm" class="js-confirmBoxes">
        <xf:label>{{ phrase('please_also_confirm_this') }}</xf:label>
    </xf:option>
</xf:checkboxrow>

Proposed JS Class:
JavaScript:
// ################################## CONTROL MULTI-DISABLER HANDLER ###########################################

XF.MultiDisabler = XF.Element.newHandler({
    options: {
        container: '< li | ul, ol, dl',
        controls: 'input, select, textarea, button, .js-attachmentUpload',
        inputControls: 'input[type=radio], input[type=checkbox]',
        hide: false,
        optional: false,
        invert: false // if true, system will disable on checked
    },

    $container: null,

    init: function()
    {
        this.$container = XF.findRelativeIf(this.options.container, this.$target);

        if (!this.$container.length)
        {
            if (!this.options.optional)
            {
                console.error('Could not find the disabler control container');
            }
        }

        var $inputContainer = this.$target,
            $form = $inputContainer.closest('form'),
            $self = this;
        if ($form.length)
        {
            $form.on('reset', XF.proxy(this, 'formReset'));
        }

        $inputContainer.find(this.options.inputControls).each(function()
        {
            var $input = $(this);

            if ($input.is(':radio'))
            {
                var $context = $form,
                    name = $input.attr('name');
                if (!$form.length)
                {
                    $context = $(document.body);
                }

                // radios only fire events for the element we click normally, so we need to know
                // when we move away from the value by firing every radio's handler for every click
                $context.on('click', 'input:radio[name="' + name + '"]', XF.proxy($self, 'click'));
            }
            else
            {
                $input.click(XF.proxy($self, 'click'));
            }

            // this ensures that nested disablers are disabled properly
            $input.on('control:enabled control:disabled', XF.proxy($self, 'recalculateAfter'));
        });

        // this ensures that dependent editors are initialised properly as disabled if needed
        this.$container.one('editor:init', XF.proxy(this, 'recalculateAfter'));

        this.recalculate(true);
    },

    click: function(e, options)
    {
        var noSelect = (options && options.triggered);
        this.recalculateAfter(false, noSelect);
    },

    formReset: function(e)
    {
        this.recalculateAfter(false, true);
    },

    recalculateAfter: function(init, noSelect)
    {
        var t = this;
        setTimeout(function()
                   {
            t.recalculate(init, noSelect);
        }, 0);
    },

    recalculate: function(init, noSelect)
    {
        var $container = this.$container,
            $inputContainer = this.$target,
            $input = $inputContainer.find(this.options.inputControls),
            $controls = $container.find(this.options.controls).not($input),
            speed = init ? 0 : XF.config.speed.fast,
            enable = $input.not(':enabled').length == 0 && (($input.not(':checked').length == 0 && !this.options.invert) || (this.options.invert && $input.not(':checked').length != 0)),
            select = function()
        {
            if (noSelect)
            {
                return;
            }

            $container.find('input:not([type=hidden], [type=file]), textarea, select, button').not($input)
                .first().autofocus();
        };

        if (enable)
        {
            $container
                .prop('disabled', false)
                .removeClass('is-disabled');

            $controls
                .prop('disabled', false)
                .removeClass('is-disabled')
                .each(function(i, ctrl)
                      {
                var $ctrl = $(ctrl);

                if ($ctrl.is('select.is-readonly'))
                {
                    // readonly has to be implemented through disabling so we can't undisable this
                    $ctrl.prop('disabled', true);
                }
            })
                .trigger('control:enabled');

            if (this.options.hide)
            {
                if (init)
                {
                    $container.show();
                }
                else
                {
                    var cb = function()
                    {
                        XF.layoutChange();
                        select();
                    };

                    $container.slideDown(speed, cb);
                }
                XF.layoutChange();
            }
            else if (!init)
            {
                select();
            }
        }
        else
        {
            if (this.options.hide)
            {
                if (init)
                {
                    $container.hide();
                }
                else
                {
                    $container.slideUp(speed, XF.layoutChange);
                }
                XF.layoutChange();
            }

            $container
                .prop('disabled', true)
                .addClass('is-disabled');

            $controls
                .prop('disabled', true)
                .addClass('is-disabled')
                .trigger('control:disabled')
                .each(function(i, ctrl)
                      {
                var $ctrl = $(ctrl),
                    disabledVal = $ctrl.data('disabled');

                if (disabledVal !== null && typeof(disabledVal) != 'undefined')
                {
                    $ctrl.val(disabledVal);
                }
            });
        }
    }
});

XF.Element.register('multi-disabler', 'XF.MultiDisabler');

I have tested the code with data-invert both on and off. All four permutations (invert off, 1/2 checkboxes = disabled, etc) works fine in my testing.
I have not explicitly tested data-hide but I suspect it will work fine, as I did not modify that part.
I have not tested data-controls as I don't fully understand its purpose and I couldn't find any examples of this functionality being used in XF2.
I have not tested nested disablers. I predict nested disablers will produce unexpected behaviour, unless the data-input-controls option is used to restrict which input boxes MultiDisabler triggers on.

Developers: If you are implementing this code in your own product pending this feature request, please consider prefixing the class with your own vendor prefix, such as dbtech-ecommerce-multi-disabler and DBTecheCommerce.MultiDisabler rather than copying the code exactly as written.


Fillip
 
Upvote 3
This suggestion has been closed. Votes are no longer accepted.
Top Bottom