/*! @preserve * bootbox.js * version: 6.0.4 * author: Nick Payne * 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'; let exports = {}; let VERSION = '6.0.4'; exports.VERSION = VERSION; let locales = { 'en' : { OK : 'OK', CANCEL : 'Cancel', CONFIRM : 'OK' } }; let templates = { dialog: '', header: '', footer: '', closeButton: '', form: '
', button: '', option: '', promptMessage: '
', inputs: { text: '', textarea: '', email: '', select: '', checkbox: '
', radio: '
', date: '', time: '', number: '', password: '', range: '' } }; let defaults = { // Default language used when generating buttons for alert, confirm, and prompt dialogs 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', // Custom error message to report if prompt fails validation errorMessage: null, // 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, // Whether or not to destroy the modal on hide reusable: false, // The element which triggered the dialog relatedTarget: null, // The size of the modal to generate size: null, // A unique identifier for this modal id: null }; // PUBLIC FUNCTIONS // ************************************************************************************************************* /** * Return all currently registered locales, or a specific locale if "name" is defined * @param {string} [name] * @returns {(Object|Object[])} An array of the available locale objects, or a single locale object if {name} is not null */ exports.locales = function (name) { return name ? locales[name] : locales; }; /** * Register localized strings for the OK, CONFIRM, and CANCEL buttons * @param {string} name - The key used to identify the new locale in the locales array * @param {Object} values - An object containing the localized string for each of the OK, CANCEL, and CONFIRM properties of a locale * @returns The updated bootbox object */ 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 * @param {string} name - The key identifying the locale to remove * @returns The updated bootbox object */ 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 * @param {string} name - The key identifying the locale to set as the default locale for all future bootbox calls * @returns The updated bootbox object */ exports.setLocale = function (name) { return exports.setDefaults('locale', name); }; /** * Override default value(s) of Bootbox. * @returns The updated bootbox object */ exports.setDefaults = function () { let 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 * @returns The current bootbox object */ exports.hideAll = function () { $('.bootbox').modal('hide'); return exports; }; /** * Allows the base init() function to be overridden * @param {function} _$ - A function to be called when the bootbox instance is created * @returns The current bootbox object */ exports.init = function (_$) { return init(_$ || $); }; // CORE HELPER FUNCTIONS // ************************************************************************************************************* /** * The core dialog helper function, which can be used to create any custom Bootstrap modal. * @param {Object} options - An object used to configure the various properties which define a Bootbox dialog * @returns A jQuery object upon which Bootstrap's modal function has been called */ 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/5.3/getting-started/introduction/ for more details.' ); } options = sanitize(options); if ($.fn.modal.Constructor.VERSION) { options.fullBootstrapVersion = $.fn.modal.Constructor.VERSION; let 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.'); } let dialog = $(templates.dialog); let innerDialog = dialog.find('.modal-dialog'); let body = dialog.find('.modal-body'); let header = $(templates.header); let footer = $(templates.footer); let buttons = options.buttons; let 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) { let 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); if (b.id) { button.attr({ 'id': b.id }); } if (b.disabled === true) { button.prop({ disabled: true }); } 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.id) { dialog.attr({ 'id': options.id }); } 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 || options.closeButton) { if (options.title) { header.find('.modal-title').html(options.title); } else { header.addClass('border-0'); } if (options.closeButton) { let closeButton = $(templates.closeButton); if (options.bootstrap < 5) { closeButton.html('×'); } /* Note: the close button for Bootstrap 5+ does not contain content */ if(options.bootstrap < 4) { /* Bootstrap 3 and under */ header.prepend(closeButton); } else { header.append(closeButton); } } body.before(header); } 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. if(!options.reusable) { // make sure we unbind any listeners once the dialog has definitively been dismissed dialog.one('hide.bs.modal', { dialog: dialog }, unbindModal); dialog.one('hidden.bs.modal', { dialog: dialog }, destroyModal); } if (options.onHide) { if (typeof options.onHide === 'function') { dialog.on('hide.bs.modal', options.onHide); } else { throw new Error('Argument supplied to "onHide" must be a function'); } } if (options.onHidden) { if (typeof options.onHidden === 'function') { dialog.on('hidden.bs.modal', options.onHidden); } else { throw new Error('Argument supplied to "onHidden" must be a function'); } } if (options.onShow) { if (typeof options.onShow === 'function') { 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 (typeof options.onShown === 'function') { 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 === true) { let startedOnBody = false; // Prevents the event from propagating to the backdrop, when something inside the dialog is clicked dialog.on('mousedown', '.modal-content', function(e) { e.stopPropagation(); startedOnBody = true; }); // 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) { if (startedOnBody || 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) { let 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, keyboard: false, show: false }); if (options.show) { dialog.modal('show', options.relatedTarget); } 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. * @returns A jQuery object upon which Bootstrap's modal function has been called */ exports.alert = function () { let 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 && typeof options.callback !== 'function') { 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 (typeof options.callback === 'function') { 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. * @returns A jQuery object upon which Bootstrap's modal function has been called */ exports.confirm = function () { let 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 (typeof options.callback !== 'function') { 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. * @returns A jQuery object upon which Bootstrap's modal function has been called */ exports.prompt = function () { let options; let promptDialog; let form; let input; let shouldShow; let 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 () { let value; if (options.inputType === 'checkbox') { value = input.find('input:checked').map(function () { return $(this).val(); }).get(); if(value.length === 0 && options.required === true) { // prevents button callback from being called if no checkboxes have been checked return false; } } else if (options.inputType === 'radio') { value = input.find('input:checked').val(); } else { let el = input[0]; if (el.checkValidity && !el.checkValidity()) { // If a custom error message was provided, add it now if(options.errorMessage){ el.setCustomValidity(options.errorMessage); } if(el.reportValidity) { el.reportValidity(); } // 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 (typeof options.callback !== 'function') { 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); } else { if(options.inputType === 'date') { // Add the ISO-8601 short date format as a fallback for browsers without native type="date" support input.attr('pattern', '\d{4}-\d{2}-\d{2}'); } else if(options.inputType === 'time') { // Add an HH:MM pattern as a fallback for browsers without native type="time" support input.attr('pattern', '\d{2}:\d{2}'); } } if (options.required) { input.prop({ 'required': true }); } 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': let groups = {}; inputOptions = options.inputOptions || []; if (!Array.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'); } 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... let 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] = $('').attr('label', option.group); } elem = groups[option.group]; } let 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); if (options.bootstrap < 5) { input.removeClass('form-select').addClass('form-control'); } break; case 'checkbox': let checkboxValues = Array.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 = $('
'); each(inputOptions, function (_, option) { if (option.value === undefined || option.text === undefined) { throw new Error('each option needs a "value" property and a "text" property'); } let 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 && Array.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 = $('
'); // 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 let 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'); } let 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 (options.message && options.message.trim() !== '') { // Add the form to whatever content the user may have added. let 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"]) // // results in // // { message: "foo", callback: $.noop } // function mapArguments(args, properties) { let argsLength = args.length; let options = {}; if (argsLength < 1 || argsLength > 2) { throw new Error('Invalid argument length'); } if (argsLength === 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) { let locale; if (args && args[0]) { locale = args[0].locale || defaults.locale; let swapButtons = args[0].swapButtonOrder || defaults.swapButtonOrder; if (swapButtons) { labels = labels.reverse(); } } // Build up a base set of dialog properties let 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) { let 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) { let buttons = {}; for (let i = 0, j = labels.length; i < j; i++) { let argument = labels[i]; let key = argument.toLowerCase(); let 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) { let 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) { let buttons; let 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); // Make sure backdrop is either true, false, or 'static' if (!options.backdrop) { options.backdrop = (options.backdrop === false || options.backdrop === 0) ? false : 'static'; } else { options.backdrop = typeof options.backdrop === 'string' && options.backdrop.toLowerCase() === 'static' ? 'static' : true; } // 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 (typeof button === 'function') { // 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 button is the correct type if (typeof 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) { let 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) { let 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 // If the callback can be invoked and it *explicitly returns false*, then we'll set a flag to keep the dialog active... let preserveDialog = typeof callback === 'function' && 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) { let result = false; let minValid = true; let 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 or equal to "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; }));