/** * bootbox.js v4.0.0 * * http://bootboxjs.com/license.txt */ // @see https://github.com/makeusabrew/bootbox/issues/71 window.bootbox = window.bootbox || (function init($, undefined) { "use strict"; // the base DOM structure needed to create a modal var templates = { dialog: "", header: "", footer: "", closeButton: "", form: "
", inputs: { text: "" } }; // cache a reference to the jQueryfied body element var appendTo = $("body"); 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 }; // 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.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; } 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; } function mergeArguments(defaults, args, properties) { return $.extend(true, {}, defaults, mapArguments(args, properties)); } function mergeButtons(labels, args, properties) { return validateButtons( mergeArguments(createButtons.apply(null, labels), args, properties), labels ); } 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 createButtons() { return { buttons: createLabels.apply(null, arguments) }; } 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 = mergeButtons(["ok"], arguments, ["message", "callback"]); 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 = mergeButtons(["cancel", "confirm"], arguments, ["message", "callback"]); /** * 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; // 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); defaults = { buttons: createLabels("cancel", "confirm"), value: "" }; 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() { return options.callback(input.val()); }; 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"); } // create the input input = $(templates.inputs.text); input.val(options.value); // 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 += ""; 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 appendTo.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(values) { $.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" }, it : { OK : "OK", CANCEL : "Annulla", CONFIRM : "Conferma" }, nl : { OK : "OK", CANCEL : "Annuleren", CONFIRM : "Accepteren" }, pl : { OK : "OK", CANCEL : "Anuluj", CONFIRM : "Potwierdź" }, ru : { OK : "OK", CANCEL : "Отмена", CONFIRM : "Применить" }, zh_CN : { OK : "OK", CANCEL : "取消", CONFIRM : "确认" }, zh_TW : { OK : "OK", CANCEL : "取消", CONFIRM : "確認" } }; exports.init = function(_$) { window.bootbox = init(_$ || $); }; return exports; }(window.jQuery));