/** * bootbox.js [v4.3.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: "", header: "", footer: "", closebutton: "", form: "
", inputs: { text: "", textarea: "", email: "", select: "", checkbox: "
", date: "", time: "", number: "", 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; /** * 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] = $("").attr("label", option.group); } elem = groups[option.group]; } elem.append(""); }); 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 = $("
"); 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(); // 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 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 innerdialog = dialog.find(".modal-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 += ""; 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.size === "large") { innerdialog.addclass("modal-lg"); } if (options.size === "small") { innerdialog.addclass("modal-sm"); } 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"); return exports; }; /** * 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" }, cs : { ok : "ok", cancel : "zrušit", confirm : "potvrdit" }, da : { ok : "ok", cancel : "annuller", confirm : "accepter" }, de : { ok : "ok", cancel : "abbrechen", confirm : "akzeptieren" }, el : { ok : "εντάξει", cancel : "ακύρωση", confirm : "επιβεβαίωση" }, en : { ok : "ok", cancel : "cancel", confirm : "ok" }, es : { ok : "ok", cancel : "cancelar", confirm : "aceptar" }, et : { ok : "ok", cancel : "katkesta", confirm : "ok" }, fi : { ok : "ok", cancel : "peruuta", confirm : "ok" }, fr : { ok : "ok", cancel : "annuler", confirm : "d'accord" }, he : { ok : "אישור", cancel : "ביטול", confirm : "אישור" }, id : { ok : "ok", cancel : "batal", confirm : "ok" }, it : { ok : "ok", cancel : "annulla", confirm : "conferma" }, ja : { ok : "ok", cancel : "キャンセル", confirm : "確認" }, 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ź" }, pt : { ok : "ok", cancel : "cancelar", confirm : "confirmar" }, ru : { ok : "ok", cancel : "отмена", confirm : "применить" }, sv : { ok : "ok", cancel : "avbryt", confirm : "ok" }, tr : { ok : "tamam", cancel : "iptal", confirm : "onayla" }, zh_cn : { ok : "ok", cancel : "取消", confirm : "确认" }, zh_tw : { ok : "ok", cancel : "取消", confirm : "確認" } }; exports.init = function(_$) { return init(_$ || $); }; return exports; }));