/*! @preserve
 * bootbox.js
 * version: 5.4.0
 * author: Nick Payne <nick@kurai.co.uk>
 * license: MIT
 * http://bootboxjs.com/
 */
(function (root, factory) {
    'use strict';
    if (typeof define === 'function' && define.amd) {
        // AMD
        define(['jquery'], factory);
    } else if (typeof exports === 'object') {
        // Node, CommonJS-like
        module.exports = factory(require('jquery'));
    } else {
        // Browser globals (root is window)
        root.bootbox = factory(root.jQuery);
    }
}(this, function init($, undefined) {
    'use strict';

    //  Polyfills Object.keys, if necessary.
    //  @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
    if (!Object.keys) {
        Object.keys = (function () {
            var hasOwnProperty = Object.prototype.hasOwnProperty,
                hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'),
                dontEnums = [
                    'toString',
                    'toLocaleString',
                    'valueOf',
                    'hasOwnProperty',
                    'isPrototypeOf',
                    'propertyIsEnumerable',
                    'constructor'
                ],
                dontEnumsLength = dontEnums.length;

            return function (obj) {
                if (typeof obj !== 'function' && (typeof obj !== 'object' || obj === null)) {
                    throw new TypeError('Object.keys called on non-object');
                }

                var result = [], prop, i;

                for (prop in obj) {
                    if (hasOwnProperty.call(obj, prop)) {
                        result.push(prop);
                    }
                }

                if (hasDontEnumBug) {
                    for (i = 0; i < dontEnumsLength; i++) {
                        if (hasOwnProperty.call(obj, dontEnums[i])) {
                            result.push(dontEnums[i]);
                        }
                    }
                }

                return result;
            };
        }());
    }

    var exports = {};

    var VERSION = '5.0.0';
    exports.VERSION = VERSION;

    var locales = {
        en: {
            OK: 'OK',
            CANCEL: 'Cancel',
            CONFIRM: 'OK'
        }
    };

    var templates = {
        dialog:
            '<div class="bootbox modal" tabindex="-1" role="dialog" aria-hidden="true">' +
            '<div class="modal-dialog">' +
            '<div class="modal-content">' +
            '<div class="modal-body"><div class="bootbox-body"></div></div>' +
            '</div>' +
            '</div>' +
            '</div>',
        header:
            '<div class="modal-header">' +
            '<h5 class="modal-title"></h5>' +
            '</div>',
        footer:
            '<div class="modal-footer"></div>',
        closeButton:
            '<button type="button" class="bootbox-close-button close" aria-hidden="true">&times;</button>',
        form:
            '<form class="bootbox-form"></form>',
        button:
            '<button type="button" class="btn"></button>',
        option:
            '<option></option>',
        promptMessage:
            '<div class="bootbox-prompt-message"></div>',
        inputs: {
            text:
                '<input class="bootbox-input bootbox-input-text form-control" autocomplete="off" type="text" />',
            textarea:
                '<textarea class="bootbox-input bootbox-input-textarea form-control"></textarea>',
            email:
                '<input class="bootbox-input bootbox-input-email form-control" autocomplete="off" type="email" />',
            select:
                '<select class="bootbox-input bootbox-input-select form-control"></select>',
            checkbox:
                '<div class="form-check checkbox"><label class="form-check-label"><input class="form-check-input bootbox-input bootbox-input-checkbox" type="checkbox" /></label></div>',
            radio:
                '<div class="form-check radio"><label class="form-check-label"><input class="form-check-input bootbox-input bootbox-input-radio" type="radio" name="bootbox-radio" /></label></div>',
            date:
                '<input class="bootbox-input bootbox-input-date form-control" autocomplete="off" type="date" />',
            time:
                '<input class="bootbox-input bootbox-input-time form-control" autocomplete="off" type="time" />',
            number:
                '<input class="bootbox-input bootbox-input-number form-control" autocomplete="off" type="number" />',
            password:
                '<input class="bootbox-input bootbox-input-password form-control" autocomplete="off" type="password" />',
            range:
                '<input class="bootbox-input bootbox-input-range form-control-range" autocomplete="off" type="range" />'
        }
    };


    var defaults = {
        // default language
        locale: 'en',
        // show backdrop or not. Default to static so user has to interact with dialog
        backdrop: 'static',
        // animate the modal in/out
        animate: true,
        // additional class string applied to the top level dialog
        className: null,
        // whether or not to include a close button
        closeButton: true,
        // show the dialog immediately by default
        show: true,
        // dialog container
        container: 'body',
        // default value (used by the prompt helper)
        value: '',
        // default input type (used by the prompt helper)
        inputType: 'text',
        // switch button order from cancel/confirm (default) to confirm/cancel
        swapButtonOrder: false,
        // center modal vertically in page
        centerVertical: false,
        // Append "multiple" property to the select when using the "prompt" helper
        multiple: false,
        // Automatically scroll modal content when height exceeds viewport height
        scrollable: false
    };


    // PUBLIC FUNCTIONS
    // *************************************************************************************************************

    // Return all currently registered locales, or a specific locale if "name" is defined
    exports.locales = function (name) {
        return name ? locales[name] : locales;
    };


    // Register localized strings for the OK, CONFIRM, and CANCEL buttons
    exports.addLocale = function (name, values) {
        $.each(['OK', 'CANCEL', 'CONFIRM'], function (_, v) {
            if (!values[v]) {
                throw new Error('Please supply a translation for "' + v + '"');
            }
        });

        locales[name] = {
            OK: values.OK,
            CANCEL: values.CANCEL,
            CONFIRM: values.CONFIRM
        };

        return exports;
    };


    // Remove a previously-registered locale
    exports.removeLocale = function (name) {
        if (name !== 'en') {
            delete locales[name];
        }
        else {
            throw new Error('"en" is used as the default and fallback locale and cannot be removed.');
        }

        return exports;
    };


    // Set the default locale
    exports.setLocale = function (name) {
        return exports.setDefaults('locale', name);
    };


    // Override default value(s) of Bootbox.
    exports.setDefaults = function () {
        var values = {};

        if (arguments.length === 2) {
            // allow passing of single key/value...
            values[arguments[0]] = arguments[1];
        } else {
            // ... and as an object too
            values = arguments[0];
        }

        $.extend(defaults, values);

        return exports;
    };


    // Hides all currently active Bootbox modals
    exports.hideAll = function () {
        $('.bootbox').modal('hide');

        return exports;
    };


    // Allows the base init() function to be overridden
    exports.init = function (_$) {
        return init(_$ || $);
    };


    // CORE HELPER FUNCTIONS
    // *************************************************************************************************************

    // Core dialog function
    exports.dialog = function (options) {
        if ($.fn.modal === undefined) {
            throw new Error(
                '"$.fn.modal" is not defined; please double check you have included ' +
                'the Bootstrap JavaScript library. See https://getbootstrap.com/docs/4.4/getting-started/javascript/ ' +
                'for more details.'
            );
        }

        options = sanitize(options);

        if ($.fn.modal.Constructor.VERSION) {
            options.fullBootstrapVersion = $.fn.modal.Constructor.VERSION;
            var i = options.fullBootstrapVersion.indexOf('.');
            options.bootstrap = options.fullBootstrapVersion.substring(0, i);
        }
        else {
            // Assuming version 2.3.2, as that was the last "supported" 2.x version
            options.bootstrap = '2';
            options.fullBootstrapVersion = '2.3.2';
            console.warn('Bootbox will *mostly* work with Bootstrap 2, but we do not officially support it. Please upgrade, if possible.');
        }

        var dialog = $(templates.dialog);
        var innerDialog = dialog.find('.modal-dialog');
        var body = dialog.find('.modal-body');
        var header = $(templates.header);
        var footer = $(templates.footer);
        var buttons = options.buttons;

        var callbacks = {
            onEscape: options.onEscape
        };

        body.find('.bootbox-body').html(options.message);

        // Only attempt to create buttons if at least one has 
        // been defined in the options object
        if (getKeyLength(options.buttons) > 0) {
            each(buttons, function (key, b) {
                var button = $(templates.button);
                button.data('bb-handler', key);
                button.addClass(b.className);

                switch (key) {
                    case 'ok':
                    case 'confirm':
                        button.addClass('bootbox-accept');
                        break;

                    case 'cancel':
                        button.addClass('bootbox-cancel');
                        break;
                }

                button.html(b.label);
                footer.append(button);

                callbacks[key] = b.callback;
            });

            body.after(footer);
        }

        if (options.animate === true) {
            dialog.addClass('fade');
        }

        if (options.className) {
            dialog.addClass(options.className);
        }

        if (options.size) {
            // Requires Bootstrap 3.1.0 or higher
            if (options.fullBootstrapVersion.substring(0, 3) < '3.1') {
                console.warn('"size" requires Bootstrap 3.1.0 or higher. You appear to be using ' + options.fullBootstrapVersion + '. Please upgrade to use this option.');
            }

            switch (options.size) {
                case 'small':
                case 'sm':
                    innerDialog.addClass('modal-sm');
                    break;

                case 'large':
                case 'lg':
                    innerDialog.addClass('modal-lg');
                    break;

                case 'extra-large':
                case 'xl':
                    innerDialog.addClass('modal-xl');

                    // Requires Bootstrap 4.2.0 or higher
                    if (options.fullBootstrapVersion.substring(0, 3) < '4.2') {
                        console.warn('Using size "xl"/"extra-large" requires Bootstrap 4.2.0 or higher. You appear to be using ' + options.fullBootstrapVersion + '. Please upgrade to use this option.');
                    }
                    break;
            }
        }

        if (options.scrollable) {
            innerDialog.addClass('modal-dialog-scrollable');

            // Requires Bootstrap 4.3.0 or higher
            if (options.fullBootstrapVersion.substring(0, 3) < '4.3') {
                console.warn('Using "scrollable" requires Bootstrap 4.3.0 or higher. You appear to be using ' + options.fullBootstrapVersion + '. Please upgrade to use this option.');
            }
        }

        if (options.title) {
            body.before(header);
            dialog.find('.modal-title').html(options.title);
        }

        if (options.closeButton) {
            var closeButton = $(templates.closeButton);

            if (options.title) {
                if (options.bootstrap > 3) {
                    dialog.find('.modal-header').append(closeButton);
                }
                else {
                    dialog.find('.modal-header').prepend(closeButton);
                }
            } else {
                closeButton.prependTo(body);
            }
        }

        if (options.centerVertical) {
            innerDialog.addClass('modal-dialog-centered');

            // Requires Bootstrap 4.0.0-beta.3 or higher
            if (options.fullBootstrapVersion < '4.0.0') {
                console.warn('"centerVertical" requires Bootstrap 4.0.0-beta.3 or higher. You appear to be using ' + options.fullBootstrapVersion + '. Please upgrade to use this option.');
            }
        }

        // Bootstrap event listeners; these handle extra
        // setup & teardown required after the underlying
        // modal has performed certain actions.

        // make sure we unbind any listeners once the dialog has definitively been dismissed
        dialog.one('hide.bs.modal', { dialog: dialog }, unbindModal);

        if (options.onHide) {
            if ($.isFunction(options.onHide)) {
                dialog.on('hide.bs.modal', options.onHide);
            }
            else {
                throw new Error('Argument supplied to "onHide" must be a function');
            }
        }

        dialog.one('hidden.bs.modal', { dialog: dialog }, destroyModal);

        if (options.onHidden) {
            if ($.isFunction(options.onHidden)) {
                dialog.on('hidden.bs.modal', options.onHidden);
            }
            else {
                throw new Error('Argument supplied to "onHidden" must be a function');
            }
        }

        if (options.onShow) {
            if ($.isFunction(options.onShow)) {
                dialog.on('show.bs.modal', options.onShow);
            }
            else {
                throw new Error('Argument supplied to "onShow" must be a function');
            }
        }

        dialog.one('shown.bs.modal', { dialog: dialog }, focusPrimaryButton);

        if (options.onShown) {
            if ($.isFunction(options.onShown)) {
                dialog.on('shown.bs.modal', options.onShown);
            }
            else {
                throw new Error('Argument supplied to "onShown" must be a function');
            }
        }

        // Bootbox event listeners; used to decouple some
        // behaviours from their respective triggers

        if (options.backdrop !== 'static') {
            // A boolean true/false according to the Bootstrap docs
            // should show a dialog the user can dismiss by clicking on
            // the background.
            // We always only ever pass static/false to the actual
            // $.modal function because with "true" we can't trap
            // this event (the .modal-backdrop swallows it)
            // However, we still want to sort-of respect true
            // and invoke the escape mechanism instead
            dialog.on('click.dismiss.bs.modal', function (e) {
                // @NOTE: the target varies in >= 3.3.x releases since the modal backdrop
                // moved *inside* the outer dialog rather than *alongside* it
                if (dialog.children('.modal-backdrop').length) {
                    e.currentTarget = dialog.children('.modal-backdrop').get(0);
                }

                if (e.target !== e.currentTarget) {
                    return;
                }

                dialog.trigger('escape.close.bb');
            });
        }

        dialog.on('escape.close.bb', function (e) {
            // the if statement looks redundant but it isn't; without it
            // if we *didn't* have an onEscape handler then processCallback
            // would automatically dismiss the dialog
            if (callbacks.onEscape) {
                processCallback(e, dialog, callbacks.onEscape);
            }
        });


        dialog.on('click', '.modal-footer button:not(.disabled)', function (e) {
            var callbackKey = $(this).data('bb-handler');

            if (callbackKey !== undefined) {
                // Only process callbacks for buttons we recognize:
                processCallback(e, dialog, callbacks[callbackKey]);
            }
        });

        dialog.on('click', '.bootbox-close-button', function (e) {
            // onEscape might be falsy but that's fine; the fact is
            // if the user has managed to click the close button we
            // have to close the dialog, callback or not
            processCallback(e, dialog, callbacks.onEscape);
        });

        dialog.on('keyup', function (e) {
            if (e.which === 27) {
                dialog.trigger('escape.close.bb');
            }
        });

        // the remainder of this method simply deals with adding our
        // dialog element to the DOM, augmenting it with Bootstrap's modal
        // functionality and then giving the resulting object back
        // to our caller

        $(options.container).append(dialog);

        dialog.modal({
            backdrop: options.backdrop ? 'static' : false,
            keyboard: false,
            show: false
        });

        if (options.show) {
            dialog.modal('show');
        }

        return dialog;
    };


    // Helper function to simulate the native alert() behavior. **NOTE**: This is non-blocking, so any
    // code that must happen after the alert is dismissed should be placed within the callback function 
    // for this alert.
    exports.alert = function () {
        var options;

        options = mergeDialogOptions('alert', ['ok'], ['message', 'callback'], arguments);

        // @TODO: can this move inside exports.dialog when we're iterating over each
        // button and checking its button.callback value instead?
        if (options.callback && !$.isFunction(options.callback)) {
            throw new Error('alert requires the "callback" property to be a function when provided');
        }

        // override the ok and escape callback to make sure they just invoke
        // the single user-supplied one (if provided)
        options.buttons.ok.callback = options.onEscape = function () {
            if ($.isFunction(options.callback)) {
                return options.callback.call(this);
            }

            return true;
        };

        return exports.dialog(options);
    };


    // Helper function to simulate the native confirm() behavior. **NOTE**: This is non-blocking, so any
    // code that must happen after the confirm is dismissed should be placed within the callback function 
    // for this confirm.
    exports.confirm = function () {
        var options;

        options = mergeDialogOptions('confirm', ['cancel', 'confirm'], ['message', 'callback'], arguments);

        // confirm specific validation; they don't make sense without a callback so make
        // sure it's present
        if (!$.isFunction(options.callback)) {
            throw new Error('confirm requires a callback');
        }

        // overrides; undo anything the user tried to set they shouldn't have
        options.buttons.cancel.callback = options.onEscape = function () {
            return options.callback.call(this, false);
        };

        options.buttons.confirm.callback = function () {
            return options.callback.call(this, true);
        };

        return exports.dialog(options);
    };


    // Helper function to simulate the native prompt() behavior. **NOTE**: This is non-blocking, so any
    // code that must happen after the prompt is dismissed should be placed within the callback function 
    // for this prompt.
    exports.prompt = function () {
        var options;
        var promptDialog;
        var form;
        var input;
        var shouldShow;
        var inputOptions;

        // we have to create our form first otherwise
        // its value is undefined when gearing up our options
        // @TODO this could be solved by allowing message to
        // be a function instead...
        form = $(templates.form);

        // prompt defaults are more complex than others in that
        // users can override more defaults
        options = mergeDialogOptions('prompt', ['cancel', 'confirm'], ['title', 'callback'], arguments);

        if (!options.value) {
            options.value = defaults.value;
        }

        if (!options.inputType) {
            options.inputType = defaults.inputType;
        }

        // capture the user's show value; we always set this to false before
        // spawning the dialog to give us a chance to attach some handlers to
        // it, but we need to make sure we respect a preference not to show it
        shouldShow = (options.show === undefined) ? defaults.show : options.show;

        // This is required prior to calling the dialog builder below - we need to 
        // add an event handler just before the prompt is shown
        options.show = false;

        // Handles the 'cancel' action
        options.buttons.cancel.callback = options.onEscape = function () {
            return options.callback.call(this, null);
        };

        // Prompt submitted - extract the prompt value. This requires a bit of work, 
        // given the different input types available.
        options.buttons.confirm.callback = function () {
            var value;

            if (options.inputType === 'checkbox') {
                value = input.find('input:checked').map(function () {
                    return $(this).val();
                }).get();
            } else if (options.inputType === 'radio') {
                value = input.find('input:checked').val();
            }
            else {
                if (input[0].checkValidity && !input[0].checkValidity()) {
                    // prevents button callback from being called
                    return false;
                } else {
                    if (options.inputType === 'select' && options.multiple === true) {
                        value = input.find('option:selected').map(function () {
                            return $(this).val();
                        }).get();
                    }
                    else {
                        value = input.val();
                    }
                }
            }

            return options.callback.call(this, value);
        };

        // prompt-specific validation
        if (!options.title) {
            throw new Error('prompt requires a title');
        }

        if (!$.isFunction(options.callback)) {
            throw new Error('prompt requires a callback');
        }

        if (!templates.inputs[options.inputType]) {
            throw new Error('Invalid prompt type');
        }

        // create the input based on the supplied type
        input = $(templates.inputs[options.inputType]);

        switch (options.inputType) {
            case 'text':
            case 'textarea':
            case 'email':
            case 'password':
                input.val(options.value);

                if (options.placeholder) {
                    input.attr('placeholder', options.placeholder);
                }

                if (options.pattern) {
                    input.attr('pattern', options.pattern);
                }

                if (options.maxlength) {
                    input.attr('maxlength', options.maxlength);
                }

                if (options.required) {
                    input.prop({ 'required': true });
                }

                if (options.rows && !isNaN(parseInt(options.rows))) {
                    if (options.inputType === 'textarea') {
                        input.attr({ 'rows': options.rows });
                    }
                }

                break;


            case 'date':
            case 'time':
            case 'number':
            case 'range':
                input.val(options.value);

                if (options.placeholder) {
                    input.attr('placeholder', options.placeholder);
                }

                if (options.pattern) {
                    input.attr('pattern', options.pattern);
                }

                if (options.required) {
                    input.prop({ 'required': true });
                }

                // These input types have extra attributes which affect their input validation.
                // Warning: For most browsers, date inputs are buggy in their implementation of 'step', so 
                // this attribute will have no effect. Therefore, we don't set the attribute for date inputs.
                // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date#Setting_maximum_and_minimum_dates
                if (options.inputType !== 'date') {
                    if (options.step) {
                        if (options.step === 'any' || (!isNaN(options.step) && parseFloat(options.step) > 0)) {
                            input.attr('step', options.step);
                        }
                        else {
                            throw new Error('"step" must be a valid positive number or the value "any". See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-step for more information.');
                        }
                    }
                }

                if (minAndMaxAreValid(options.inputType, options.min, options.max)) {
                    if (options.min !== undefined) {
                        input.attr('min', options.min);
                    }
                    if (options.max !== undefined) {
                        input.attr('max', options.max);
                    }
                }

                break;


            case 'select':
                var groups = {};
                inputOptions = options.inputOptions || [];

                if (!$.isArray(inputOptions)) {
                    throw new Error('Please pass an array of input options');
                }

                if (!inputOptions.length) {
                    throw new Error('prompt with "inputType" set to "select" requires at least one option');
                }

                // placeholder is not actually a valid attribute for select,
                // but we'll allow it, assuming it might be used for a plugin
                if (options.placeholder) {
                    input.attr('placeholder', options.placeholder);
                }

                if (options.required) {
                    input.prop({ 'required': true });
                }

                if (options.multiple) {
                    input.prop({ 'multiple': true });
                }

                each(inputOptions, function (_, option) {
                    // assume the element to attach to is the input...
                    var elem = input;

                    if (option.value === undefined || option.text === undefined) {
                        throw new Error('each option needs a "value" property and a "text" property');
                    }

                    // ... but override that element if this option sits in a group

                    if (option.group) {
                        // initialise group if necessary
                        if (!groups[option.group]) {
                            groups[option.group] = $('<optgroup />').attr('label', option.group);
                        }

                        elem = groups[option.group];
                    }

                    var o = $(templates.option);
                    o.attr('value', option.value).text(option.text);
                    elem.append(o);
                });

                each(groups, function (_, group) {
                    input.append(group);
                });

                // safe to set a select's value as per a normal input
                input.val(options.value);

                break;


            case 'checkbox':
                var checkboxValues = $.isArray(options.value) ? options.value : [options.value];
                inputOptions = options.inputOptions || [];

                if (!inputOptions.length) {
                    throw new Error('prompt with "inputType" set to "checkbox" requires at least one option');
                }

                // checkboxes have to nest within a containing element, so
                // they break the rules a bit and we end up re-assigning
                // our 'input' element to this container instead
                input = $('<div class="bootbox-checkbox-list"></div>');

                each(inputOptions, function (_, option) {
                    if (option.value === undefined || option.text === undefined) {
                        throw new Error('each option needs a "value" property and a "text" property');
                    }

                    var checkbox = $(templates.inputs[options.inputType]);

                    checkbox.find('input').attr('value', option.value);
                    checkbox.find('label').append('\n' + option.text);

                    // we've ensured values is an array so we can always iterate over it
                    each(checkboxValues, function (_, value) {
                        if (value === option.value) {
                            checkbox.find('input').prop('checked', true);
                        }
                    });

                    input.append(checkbox);
                });
                break;


            case 'radio':
                // Make sure that value is not an array (only a single radio can ever be checked)
                if (options.value !== undefined && $.isArray(options.value)) {
                    throw new Error('prompt with "inputType" set to "radio" requires a single, non-array value for "value"');
                }

                inputOptions = options.inputOptions || [];

                if (!inputOptions.length) {
                    throw new Error('prompt with "inputType" set to "radio" requires at least one option');
                }

                // Radiobuttons have to nest within a containing element, so
                // they break the rules a bit and we end up re-assigning
                // our 'input' element to this container instead
                input = $('<div class="bootbox-radiobutton-list"></div>');

                // Radiobuttons should always have an initial checked input checked in a "group".
                // If value is undefined or doesn't match an input option, select the first radiobutton
                var checkFirstRadio = true;

                each(inputOptions, function (_, option) {
                    if (option.value === undefined || option.text === undefined) {
                        throw new Error('each option needs a "value" property and a "text" property');
                    }

                    var radio = $(templates.inputs[options.inputType]);

                    radio.find('input').attr('value', option.value);
                    radio.find('label').append('\n' + option.text);

                    if (options.value !== undefined) {
                        if (option.value === options.value) {
                            radio.find('input').prop('checked', true);
                            checkFirstRadio = false;
                        }
                    }

                    input.append(radio);
                });

                if (checkFirstRadio) {
                    input.find('input[type="radio"]').first().prop('checked', true);
                }
                break;
        }

        // now place it in our form
        form.append(input);

        form.on('submit', function (e) {
            e.preventDefault();
            // Fix for SammyJS (or similar JS routing library) hijacking the form post.
            e.stopPropagation();

            // @TODO can we actually click *the* button object instead?
            // e.g. buttons.confirm.click() or similar
            promptDialog.find('.bootbox-accept').trigger('click');
        });

        if ($.trim(options.message) !== '') {
            // Add the form to whatever content the user may have added.
            var message = $(templates.promptMessage).html(options.message);
            form.prepend(message);
            options.message = form;
        }
        else {
            options.message = form;
        }

        // Generate the dialog
        promptDialog = exports.dialog(options);

        // clear the existing handler focusing the submit button...
        promptDialog.off('shown.bs.modal', focusPrimaryButton);

        // ...and replace it with one focusing our input, if possible
        promptDialog.on('shown.bs.modal', function () {
            // need the closure here since input isn't
            // an object otherwise
            input.focus();
        });

        if (shouldShow === true) {
            promptDialog.modal('show');
        }

        return promptDialog;
    };


    // INTERNAL FUNCTIONS
    // *************************************************************************************************************

    // Map a flexible set of arguments into a single returned object
    // If args.length is already one just return it, otherwise
    // use the properties argument to map the unnamed args to
    // object properties.
    // So in the latter case:
    //  mapArguments(["foo", $.noop], ["message", "callback"])
    //  -> { message: "foo", callback: $.noop }
    function mapArguments(args, properties) {
        var argn = args.length;
        var options = {};

        if (argn < 1 || argn > 2) {
            throw new Error('Invalid argument length');
        }

        if (argn === 2 || typeof args[0] === 'string') {
            options[properties[0]] = args[0];
            options[properties[1]] = args[1];
        } else {
            options = args[0];
        }

        return options;
    }


    //  Merge a set of default dialog options with user supplied arguments
    function mergeArguments(defaults, args, properties) {
        return $.extend(
            // deep merge
            true,
            // ensure the target is an empty, unreferenced object
            {},
            // the base options object for this type of dialog (often just buttons)
            defaults,
            // args could be an object or array; if it's an array properties will
            // map it to a proper options object
            mapArguments(
                args,
                properties
            )
        );
    }


    //  This entry-level method makes heavy use of composition to take a simple
    //  range of inputs and return valid options suitable for passing to bootbox.dialog
    function mergeDialogOptions(className, labels, properties, args) {
        var locale;
        if (args && args[0]) {
            locale = args[0].locale || defaults.locale;
            var swapButtons = args[0].swapButtonOrder || defaults.swapButtonOrder;

            if (swapButtons) {
                labels = labels.reverse();
            }
        }

        //  build up a base set of dialog properties
        var baseOptions = {
            className: 'bootbox-' + className,
            buttons: createLabels(labels, locale)
        };

        // Ensure the buttons properties generated, *after* merging
        // with user args are still valid against the supplied labels
        return validateButtons(
            // merge the generated base properties with user supplied arguments
            mergeArguments(
                baseOptions,
                args,
                // if args.length > 1, properties specify how each arg maps to an object key
                properties
            ),
            labels
        );
    }


    //  Checks each button object to see if key is valid. 
    //  This function will only be called by the alert, confirm, and prompt helpers. 
    function validateButtons(options, buttons) {
        var allowedButtons = {};
        each(buttons, function (key, value) {
            allowedButtons[value] = true;
        });

        each(options.buttons, function (key) {
            if (allowedButtons[key] === undefined) {
                throw new Error('button key "' + key + '" is not allowed (options are ' + buttons.join(' ') + ')');
            }
        });

        return options;
    }



    //  From a given list of arguments, return a suitable object of button labels.
    //  All this does is normalise the given labels and translate them where possible.
    //  e.g. "ok", "confirm" -> { ok: "OK", cancel: "Annuleren" }
    function createLabels(labels, locale) {
        var buttons = {};

        for (var i = 0, j = labels.length; i < j; i++) {
            var argument = labels[i];
            var key = argument.toLowerCase();
            var value = argument.toUpperCase();

            buttons[key] = {
                label: getText(value, locale)
            };
        }

        return buttons;
    }



    //  Get localized text from a locale. Defaults to 'en' locale if no locale 
    //  provided or a non-registered locale is requested
    function getText(key, locale) {
        var labels = locales[locale];

        return labels ? labels[key] : locales.en[key];
    }



    //  Filter and tidy up any user supplied parameters to this dialog.
    //  Also looks for any shorthands used and ensures that the options
    //  which are returned are all normalized properly
    function sanitize(options) {
        var buttons;
        var total;

        if (typeof options !== 'object') {
            throw new Error('Please supply an object of options');
        }

        if (!options.message) {
            throw new Error('"message" option must not be null or an empty string.');
        }

        // make sure any supplied options take precedence over defaults
        options = $.extend({}, defaults, options);

        // no buttons is still a valid dialog but it's cleaner to always have
        // a buttons object to iterate over, even if it's empty
        if (!options.buttons) {
            options.buttons = {};
        }

        buttons = options.buttons;

        total = getKeyLength(buttons);

        each(buttons, function (key, button, index) {
            if ($.isFunction(button)) {
                // short form, assume value is our callback. Since button
                // isn't an object it isn't a reference either so re-assign it
                button = buttons[key] = {
                    callback: button
                };
            }

            // before any further checks make sure by now button is the correct type
            if ($.type(button) !== 'object') {
                throw new Error('button with key "' + key + '" must be an object');
            }

            if (!button.label) {
                // the lack of an explicit label means we'll assume the key is good enough
                button.label = key;
            }

            if (!button.className) {
                var isPrimary = false;
                if (options.swapButtonOrder) {
                    isPrimary = index === 0;
                }
                else {
                    isPrimary = index === total - 1;
                }

                if (total <= 2 && isPrimary) {
                    // always add a primary to the main option in a one or two-button dialog
                    button.className = 'btn-primary';
                } else {
                    // adding both classes allows us to target both BS3 and BS4 without needing to check the version
                    button.className = 'btn-secondary btn-default';
                }
            }
        });

        return options;
    }


    //  Returns a count of the properties defined on the object
    function getKeyLength(obj) {
        return Object.keys(obj).length;
    }


    //  Tiny wrapper function around jQuery.each; just adds index as the third parameter
    function each(collection, iterator) {
        var index = 0;
        $.each(collection, function (key, value) {
            iterator(key, value, index++);
        });
    }


    function focusPrimaryButton(e) {
        e.data.dialog.find('.bootbox-accept').first().trigger('focus');
    }


    function destroyModal(e) {
        // ensure we don't accidentally intercept hidden events triggered
        // by children of the current dialog. We shouldn't need to handle this anymore, 
        // now that Bootstrap namespaces its events, but still worth doing.
        if (e.target === e.data.dialog[0]) {
            e.data.dialog.remove();
        }
    }


    function unbindModal(e) {
        if (e.target === e.data.dialog[0]) {
            e.data.dialog.off('escape.close.bb');
            e.data.dialog.off('click');
        }
    }


    //  Handle the invoked dialog callback
    function processCallback(e, dialog, callback) {
        e.stopPropagation();
        e.preventDefault();

        // by default we assume a callback will get rid of the dialog,
        // although it is given the opportunity to override this

        // so, if the callback can be invoked and it *explicitly returns false*
        // then we'll set a flag to keep the dialog active...
        var preserveDialog = $.isFunction(callback) && callback.call(dialog, e) === false;

        // ... otherwise we'll bin it
        if (!preserveDialog) {
            dialog.modal('hide');
        }
    }

    // Validate `min` and `max` values based on the current `inputType` value
    function minAndMaxAreValid(type, min, max) {
        var result = false;
        var minValid = true;
        var maxValid = true;

        if (type === 'date') {
            if (min !== undefined && !(minValid = dateIsValid(min))) {
                console.warn('Browsers which natively support the "date" input type expect date values to be of the form "YYYY-MM-DD" (see ISO-8601 https://www.iso.org/iso-8601-date-and-time-format.html). Bootbox does not enforce this rule, but your min value may not be enforced by this browser.');
            }
            else if (max !== undefined && !(maxValid = dateIsValid(max))) {
                console.warn('Browsers which natively support the "date" input type expect date values to be of the form "YYYY-MM-DD" (see ISO-8601 https://www.iso.org/iso-8601-date-and-time-format.html). Bootbox does not enforce this rule, but your max value may not be enforced by this browser.');
            }
        }
        else if (type === 'time') {
            if (min !== undefined && !(minValid = timeIsValid(min))) {
                throw new Error('"min" is not a valid time. See https://www.w3.org/TR/2012/WD-html-markup-20120315/datatypes.html#form.data.time for more information.');
            }
            else if (max !== undefined && !(maxValid = timeIsValid(max))) {
                throw new Error('"max" is not a valid time. See https://www.w3.org/TR/2012/WD-html-markup-20120315/datatypes.html#form.data.time for more information.');
            }
        }
        else {
            if (min !== undefined && isNaN(min)) {
                minValid = false;
                throw new Error('"min" must be a valid number. See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-min for more information.');
            }

            if (max !== undefined && isNaN(max)) {
                maxValid = false;
                throw new Error('"max" must be a valid number. See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-max for more information.');
            }
        }

        if (minValid && maxValid) {
            if (max <= min) {
                throw new Error('"max" must be greater than "min". See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-max for more information.');
            }
            else {
                result = true;
            }
        }

        return result;
    }

    function timeIsValid(value) {
        return /([01][0-9]|2[0-3]):[0-5][0-9]?:[0-5][0-9]/.test(value);
    }

    function dateIsValid(value) {
        return /(\d{4})-(\d{2})-(\d{2})/.test(value);
    }

    //  The Bootbox object
    return exports;
}));