Created
February 24, 2020 22:29
-
-
Save tiesont/95766bc5d9b08890eba5ec3ac531aa5f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! @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">×</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