Skip to content

Instantly share code, notes, and snippets.

@tiesont
Created February 24, 2020 22:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tiesont/95766bc5d9b08890eba5ec3ac531aa5f to your computer and use it in GitHub Desktop.
Save tiesont/95766bc5d9b08890eba5ec3ac531aa5f to your computer and use it in GitHub Desktop.
/*! @preserve
* bootbox.js
* version: 5.5.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,
// whether or not to destroy the modal on hide
reusable: 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.
if(!options.reusable) {
// 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');
}
}
if(!options.reusable) {
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;
}));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment