You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

859 lines
23 KiB
JavaScript

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

/**
* bootbox.js [v4.2.0]
*
* http://bootboxjs.com/license.txt
*/
// @see https://github.com/makeusabrew/bootbox/issues/180
// @see https://github.com/makeusabrew/bootbox/issues/186
(function (root, factory) {
"use strict";
if (typeof define === "function" && define.amd) {
// AMD. Register as an anonymous module.
define(["jquery"], factory);
} else if (typeof exports === "object") {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory(require("jquery"));
} else {
// Browser globals (root is window)
root.bootbox = factory(root.jQuery);
}
}(this, function init($, undefined) {
"use strict";
// the base DOM structure needed to create a modal
var templates = {
dialog:
"<div class='bootbox modal' tabindex='-1' role='dialog'>" +
"<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'>" +
"<h4 class='modal-title'></h4>" +
"</div>",
footer:
"<div class='modal-footer'></div>",
closeButton:
"<button type='button' class='bootbox-close-button close' data-dismiss='modal' aria-hidden='true'>&times;</button>",
form:
"<form class='bootbox-form'></form>",
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='checkbox'><label><input class='bootbox-input bootbox-input-checkbox' type='checkbox' /></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' />"
}
};
var defaults = {
// default language
locale: "en",
// show backdrop or not
backdrop: true,
// 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"
};
// our public object; augmented after our private API
var exports = {};
/**
* @private
*/
function _t(key) {
var locale = locales[defaults.locale];
return locale ? locale[key] : locales.en[key];
}
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(e) === false;
// ... otherwise we'll bin it
if (!preserveDialog) {
dialog.modal("hide");
}
}
function getKeyLength(obj) {
// @TODO defer to Object.keys(x).length if available?
var k, t = 0;
for (k in obj) {
t ++;
}
return t;
}
function each(collection, iterator) {
var index = 0;
$.each(collection, function(key, value) {
iterator(key, value, index++);
});
}
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("Please specify a message");
}
// make sure any supplied options take precedence over defaults
options = $.extend({}, defaults, options);
if (!options.buttons) {
options.buttons = {};
}
// we only support Bootstrap's "static" and false backdrop args
// supporting true would mean you could dismiss the dialog without
// explicitly interacting with it
options.backdrop = options.backdrop ? "static" : false;
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) {
if (total <= 2 && index === total-1) {
// always add a primary to the main option in a two-button dialog
button.className = "btn-primary";
} else {
button.className = "btn-default";
}
}
});
return options;
}
/**
* 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) {
// build up a base set of dialog properties
var baseOptions = {
className: "bootbox-" + className,
buttons: createLabels.apply(null, labels)
};
// 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
);
}
/**
* 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() {
var buttons = {};
for (var i = 0, j = arguments.length; i < j; i++) {
var argument = arguments[i];
var key = argument.toLowerCase();
var value = argument.toUpperCase();
buttons[key] = {
label: _t(value)
};
}
return buttons;
}
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("\n") + ")");
}
});
return options;
}
exports.alert = function() {
var options;
options = mergeDialogOptions("alert", ["ok"], ["message", "callback"], arguments);
if (options.callback && !$.isFunction(options.callback)) {
throw new Error("alert requires callback property to be a function when provided");
}
/**
* overrides
*/
options.buttons.ok.callback = options.onEscape = function() {
if ($.isFunction(options.callback)) {
return options.callback();
}
return true;
};
return exports.dialog(options);
};
exports.confirm = function() {
var options;
options = mergeDialogOptions("confirm", ["cancel", "confirm"], ["message", "callback"], arguments);
/**
* overrides; undo anything the user tried to set they shouldn't have
*/
options.buttons.cancel.callback = options.onEscape = function() {
return options.callback(false);
};
options.buttons.confirm.callback = function() {
return options.callback(true);
};
// confirm specific validation
if (!$.isFunction(options.callback)) {
throw new Error("confirm requires a callback");
}
return exports.dialog(options);
};
exports.prompt = function() {
var options;
var defaults;
var dialog;
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
// @TODO I don't like that prompt has to do a lot of heavy
// lifting which mergeDialogOptions can *almost* support already
// just because of 'value' and 'inputType' - can we refactor?
defaults = {
className: "bootbox-prompt",
buttons: createLabels("cancel", "confirm"),
value: "",
inputType: "text"
};
options = validateButtons(
mergeArguments(defaults, arguments, ["title", "callback"]),
["cancel", "confirm"]
);
// 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) ? true : options.show;
// check if the browser supports the option.inputType
var html5inputs = ["date","time","number"];
var i = document.createElement("input");
i.setAttribute("type", options.inputType);
if(html5inputs[options.inputType]){
options.inputType = i.type;
}
/**
* overrides; undo anything the user tried to set they shouldn't have
*/
options.message = form;
options.buttons.cancel.callback = options.onEscape = function() {
return options.callback(null);
};
options.buttons.confirm.callback = function() {
var value;
switch (options.inputType) {
case "text":
case "textarea":
case "email":
case "select":
case "date":
case "time":
case "number":
case "password":
value = input.val();
break;
case "checkbox":
var checkedItems = input.find("input:checked");
// we assume that checkboxes are always multiple,
// hence we default to an empty array
value = [];
each(checkedItems, function(_, item) {
value.push($(item).val());
});
break;
}
return options.callback(value);
};
options.show = false;
// 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 "date":
case "time":
case "number":
case "password":
input.val(options.value);
break;
case "select":
var groups = {};
inputOptions = options.inputOptions || [];
if (!inputOptions.length) {
throw new Error("prompt with select requires options");
}
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("given options in wrong format");
}
// ... 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];
}
elem.append("<option value='" + option.value + "'>" + option.text + "</option>");
});
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 values = $.isArray(options.value) ? options.value : [options.value];
inputOptions = options.inputOptions || [];
if (!inputOptions.length) {
throw new Error("prompt with checkbox requires options");
}
if (!inputOptions[0].value || !inputOptions[0].text) {
throw new Error("given options in wrong format");
}
// 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/>");
each(inputOptions, function(_, option) {
var checkbox = $(templates.inputs[options.inputType]);
checkbox.find("input").attr("value", option.value);
checkbox.find("label").append(option.text);
// we've ensured values is an array so we can always iterate over it
each(values, function(_, value) {
if (value === option.value) {
checkbox.find("input").prop("checked", true);
}
});
input.append(checkbox);
});
break;
}
if (options.placeholder) {
input.attr("placeholder", options.placeholder);
}
if(options.pattern){
input.attr("pattern", options.pattern);
}
// now place it in our form
form.append(input);
form.on("submit", function(e) {
e.preventDefault();
// @TODO can we actually click *the* button object instead?
// e.g. buttons.confirm.click() or similar
dialog.find(".btn-primary").click();
});
dialog = exports.dialog(options);
// clear the existing handler focusing the submit button...
dialog.off("shown.bs.modal");
// ...and replace it with one focusing our input, if possible
dialog.on("shown.bs.modal", function() {
input.focus();
});
if (shouldShow === true) {
dialog.modal("show");
}
return dialog;
};
exports.dialog = function(options) {
options = sanitize(options);
var dialog = $(templates.dialog);
var body = dialog.find(".modal-body");
var buttons = options.buttons;
var buttonStr = "";
var callbacks = {
onEscape: options.onEscape
};
each(buttons, function(key, button) {
// @TODO I don't like this string appending to itself; bit dirty. Needs reworking
// can we just build up button elements instead? slower but neater. Then button
// can just become a template too
buttonStr += "<button data-bb-handler='" + key + "' type='button' class='ml5 btn " + button.className + "'>" + button.label + "</button>";
callbacks[key] = button.callback;
});
body.find(".bootbox-body").html(options.message);
if (options.animate === true) {
dialog.addClass("fade");
}
if (options.className) {
dialog.addClass(options.className);
}
if (options.title) {
body.before(templates.header);
}
if (options.closeButton) {
var closeButton = $(templates.closeButton);
if (options.title) {
dialog.find(".modal-header").prepend(closeButton);
} else {
closeButton.css("margin-top", "-10px").prependTo(body);
}
}
if (options.title) {
dialog.find(".modal-title").html(options.title);
}
if (buttonStr.length) {
body.after(templates.footer);
dialog.find(".modal-footer").html(buttonStr);
}
/**
* Bootstrap event listeners; used handle extra
* setup & teardown required after the underlying
* modal has performed certain actions
*/
dialog.on("hidden.bs.modal", function(e) {
// ensure we don't accidentally intercept hidden events triggered
// by children of the current dialog. We shouldn't anymore now BS
// namespaces its events; but still worth doing
if (e.target === this) {
dialog.remove();
}
});
/*
dialog.on("show.bs.modal", function() {
// sadly this doesn't work; show is called *just* before
// the backdrop is added so we'd need a setTimeout hack or
// otherwise... leaving in as would be nice
if (options.backdrop) {
dialog.next(".modal-backdrop").addClass("bootbox-backdrop");
}
});
*/
dialog.on("shown.bs.modal", function() {
dialog.find(".btn-primary:first").focus();
});
/**
* Bootbox event listeners; experimental and may not last
* just an attempt to decouple some behaviours from their
* respective triggers
*/
dialog.on("escape.close.bb", function(e) {
if (callbacks.onEscape) {
processCallback(e, dialog, callbacks.onEscape);
}
});
/**
* Standard jQuery event listeners; used to handle user
* interaction with our dialog
*/
dialog.on("click", ".modal-footer button", function(e) {
var callbackKey = $(this).data("bb-handler");
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
// dialogent 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");
}
// @TODO should we return the raw element here or should
// we wrap it in an object on which we can expose some neater
// methods, e.g. var d = bootbox.alert(); d.hide(); instead
// of d.modal("hide");
/*
function BBDialog(elem) {
this.elem = elem;
}
BBDialog.prototype = {
hide: function() {
return this.elem.modal("hide");
},
show: function() {
return this.elem.modal("show");
}
};
*/
return dialog;
};
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);
};
exports.hideAll = function() {
$(".bootbox").modal("hide");
};
/**
* standard locales. Please add more according to ISO 639-1 standard. Multiple language variants are
* unlikely to be required. If this gets too large it can be split out into separate JS files.
*/
var locales = {
br : {
OK : "OK",
CANCEL : "Cancelar",
CONFIRM : "Sim"
},
da : {
OK : "OK",
CANCEL : "Annuller",
CONFIRM : "Accepter"
},
de : {
OK : "OK",
CANCEL : "Abbrechen",
CONFIRM : "Akzeptieren"
},
en : {
OK : "OK",
CANCEL : "Cancel",
CONFIRM : "OK"
},
es : {
OK : "OK",
CANCEL : "Cancelar",
CONFIRM : "Aceptar"
},
fi : {
OK : "OK",
CANCEL : "Peruuta",
CONFIRM : "OK"
},
fr : {
OK : "OK",
CANCEL : "Annuler",
CONFIRM : "D'accord"
},
he : {
OK : "אישור",
CANCEL : "ביטול",
CONFIRM : "אישור"
},
it : {
OK : "OK",
CANCEL : "Annulla",
CONFIRM : "Conferma"
},
lt : {
OK : "Gerai",
CANCEL : "Atšaukti",
CONFIRM : "Patvirtinti"
},
lv : {
OK : "Labi",
CANCEL : "Atcelt",
CONFIRM : "Apstiprināt"
},
nl : {
OK : "OK",
CANCEL : "Annuleren",
CONFIRM : "Accepteren"
},
no : {
OK : "OK",
CANCEL : "Avbryt",
CONFIRM : "OK"
},
pl : {
OK : "OK",
CANCEL : "Anuluj",
CONFIRM : "Potwierdź"
},
ru : {
OK : "OK",
CANCEL : "Отмена",
CONFIRM : "Применить"
},
sv : {
OK : "OK",
CANCEL : "Avbryt",
CONFIRM : "OK"
},
tr : {
OK : "Tamam",
CANCEL : "Ä°ptal",
CONFIRM : "Onayla"
},
zh_CN : {
OK : "OK",
CANCEL : "取消",
CONFIRM : "确认"
},
zh_TW : {
OK : "OK",
CANCEL : "取消",
CONFIRM : "確認"
}
};
exports.init = function(_$) {
return init(_$ || $);
};
return exports;
}));