diff --git a/templates/default/assets/js/plugins/bootbox/bootbox.js b/templates/default/assets/js/plugins/bootbox/bootbox.js
new file mode 100644
index 000000000..6da84eb7a
--- /dev/null
+++ b/templates/default/assets/js/plugins/bootbox/bootbox.js
@@ -0,0 +1,604 @@
+/**
+ * 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));
diff --git a/templates/default/assets/js/plugins/bootbox/bootbox.min.js b/templates/default/assets/js/plugins/bootbox/bootbox.min.js
new file mode 100644
index 000000000..bd1d368c0
--- /dev/null
+++ b/templates/default/assets/js/plugins/bootbox/bootbox.min.js
@@ -0,0 +1,6 @@
+/**
+ * bootbox.js v4.0.0
+ *
+ * http://bootboxjs.com/license.txt
+ */
+window.bootbox=window.bootbox||function a(b,c){"use strict";function d(a){var b=s[q.locale];return b?b[a]:s.en[a]}function e(a,c,d){a.preventDefault();var e=b.isFunction(d)&&d(a)===!1;e||c.modal("hide")}function f(a){var b,c=0;for(b in a)c++;return c}function g(a,c){var d=0;b.each(a,function(a,b){c(a,b,d++)})}function h(a){var c,d;if("object"!=typeof a)throw new Error("Please supply an object of options");if(!a.message)throw new Error("Please specify a message");return a=b.extend({},q,a),a.buttons||(a.buttons={}),a.backdrop=a.backdrop?"static":!1,c=a.buttons,d=f(c),g(c,function(a,e,f){if(b.isFunction(e)&&(e=c[a]={callback:e}),"object"!==b.type(e))throw new Error("button with key "+a+" must be an object");e.label||(e.label=a),e.className||(e.className=2>=d&&f===d-1?"btn-primary":"btn-default")}),a}function i(a,b){var c=a.length,d={};if(1>c||c>2)throw new Error("Invalid argument length");return 2===c||"string"==typeof a[0]?(d[b[0]]=a[0],d[b[1]]=a[1]):d=a[0],d}function j(a,c,d){return b.extend(!0,{},a,i(c,d))}function k(a,b,c){return n(j(m.apply(null,a),b,c),a)}function l(){for(var a={},b=0,c=arguments.length;c>b;b++){var e=arguments[b],f=e.toLowerCase(),g=e.toUpperCase();a[f]={label:d(g)}}return a}function m(){return{buttons:l.apply(null,arguments)}}function n(a,b){var d={};return g(b,function(a,b){d[b]=!0}),g(a.buttons,function(a){if(d[a]===c)throw new Error("button key "+a+" is not allowed (options are "+b.join("\n")+")")}),a}var o={dialog:"",header:"",footer:"",closeButton:"",form:"",inputs:{text:""}},p=b("body"),q={locale:"en",backdrop:!0,animate:!0,className:null,closeButton:!0,show:!0},r={};r.alert=function(){var a;if(a=k(["ok"],arguments,["message","callback"]),a.callback&&!b.isFunction(a.callback))throw new Error("alert requires callback property to be a function when provided");return a.buttons.ok.callback=a.onEscape=function(){return b.isFunction(a.callback)?a.callback():!0},r.dialog(a)},r.confirm=function(){var a;if(a=k(["cancel","confirm"],arguments,["message","callback"]),a.buttons.cancel.callback=a.onEscape=function(){return a.callback(!1)},a.buttons.confirm.callback=function(){return a.callback(!0)},!b.isFunction(a.callback))throw new Error("confirm requires a callback");return r.dialog(a)},r.prompt=function(){var a,d,e,f,g,h;if(f=b(o.form),d={buttons:l("cancel","confirm"),value:""},a=n(j(d,arguments,["title","callback"]),["cancel","confirm"]),h=a.show===c?!0:a.show,a.message=f,a.buttons.cancel.callback=a.onEscape=function(){return a.callback(null)},a.buttons.confirm.callback=function(){return a.callback(g.val())},a.show=!1,!a.title)throw new Error("prompt requires a title");if(!b.isFunction(a.callback))throw new Error("prompt requires a callback");return g=b(o.inputs.text),g.val(a.value),f.append(g),f.on("submit",function(a){a.preventDefault(),e.find(".btn-primary").click()}),e=r.dialog(a),e.off("shown.bs.modal"),e.on("shown.bs.modal",function(){g.focus()}),h===!0&&e.modal("show"),e},r.dialog=function(a){a=h(a);var c=b(o.dialog),d=c.find(".modal-body"),f=a.buttons,i="",j={onEscape:a.onEscape};if(g(f,function(a,b){i+="",j[a]=b.callback}),d.find(".bootbox-body").html(a.message),a.animate===!0&&c.addClass("fade"),a.className&&c.addClass(a.className),a.title&&d.before(o.header),a.closeButton){var k=b(o.closeButton);a.title?c.find(".modal-header").prepend(k):k.css("margin-top","-10px").prependTo(d)}return a.title&&c.find(".modal-title").html(a.title),i.length&&(d.after(o.footer),c.find(".modal-footer").html(i)),c.on("hidden.bs.modal",function(a){a.target===this&&c.remove()}),c.on("shown.bs.modal",function(){c.find(".btn-primary:first").focus()}),c.on("escape.close.bb",function(a){j.onEscape&&e(a,c,j.onEscape)}),c.on("click",".modal-footer button",function(a){var d=b(this).data("bb-handler");e(a,c,j[d])}),c.on("click",".bootbox-close-button",function(a){e(a,c,j.onEscape)}),c.on("keyup",function(a){27===a.which&&c.trigger("escape.close.bb")}),p.append(c),c.modal({backdrop:a.backdrop,keyboard:!1,show:!1}),a.show&&c.modal("show"),c},r.setDefaults=function(a){b.extend(q,a)},r.hideAll=function(){b(".bootbox").modal("hide")};var s={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:"確認"}};return r.init=function(c){window.bootbox=a(c||b)},r}(window.jQuery);
\ No newline at end of file
diff --git a/templates/default/layout.tpl b/templates/default/layout.tpl
index 679c03689..11fe879ba 100644
--- a/templates/default/layout.tpl
+++ b/templates/default/layout.tpl
@@ -27,7 +27,7 @@ URL: http://www.thelia.net
{* Meta Tags *}
-
+
{block name="meta"}{/block}
@@ -350,7 +350,11 @@ URL: http://www.thelia.net
{javascripts file='assets/js/bootstrap/bootstrap.js'}
-{/javascripts}
+{/javascripts}'}
+
+{javascripts file='assets/js/plugins/bootbox/bootbox.js'}
+
+{/javascripts}'}
{block name="after-javascript-include"}{/block}