PluginProbe
UpdraftCentral Dashboard / trunk
UpdraftCentral Dashboard vtrunk
0.8.33 0.7.2 0.7.3 0.7.4 0.8.0 0.8.1 0.8.10 0.8.11 0.8.12 0.8.13 0.8.14 0.8.15 0.8.16 0.8.17 0.8.18 0.8.19 0.8.2 0.8.20 0.8.21 0.8.22 0.8.23 0.8.24 0.8.25 0.8.26 0.8.27 All 51 releases
updraftcentral / js / bootbox / bootbox.all.js

bootbox.all.js in UpdraftCentral Dashboard trunk, at js/bootbox/bootbox.all.js

1,527 lines 48.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /*! @preserve
2 * bootbox.js
3 * version: 6.0.0
4 * author: Nick Payne <nick@kurai.co.uk>
5 * license: MIT
6 * http://bootboxjs.com/
7 */
8 (function(root, factory) {
9 'use strict';
10 if (typeof define === 'function' && define.amd) {
11 // AMD
12 define(['jquery'], factory);
13 } else if (typeof exports === 'object') {
14 // Node, CommonJS-like
15 module.exports = factory(require('jquery'));
16 } else {
17 // Browser globals (root is window)
18 root.bootbox = factory(root.jQuery);
19 }
20 }(this, function init($, undefined) {
21 'use strict';
22
23 let exports = {};
24
25 let VERSION = '6.0.0';
26 exports.VERSION = VERSION;
27
28 let locales = {
29 'en': {
30 OK: 'OK',
31 CANCEL: 'Cancel',
32 CONFIRM: 'OK'
33 }
34 };
35
36 let templates = {
37 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>',
38 header: '<div class="modal-header"><h5 class="modal-title"></h5></div>',
39 footer: '<div class="modal-footer"></div>',
40 closeButton: '<button type="button" class="bootbox-close-button close btn-close" aria-hidden="true" aria-label="Close"></button>',
41 form: '<form class="bootbox-form"></form>',
42 button: '<button type="button" class="btn"></button>',
43 option: '<option value=""></option>',
44 promptMessage: '<div class="bootbox-prompt-message"></div>',
45 inputs: {
46 text: '<input class="bootbox-input bootbox-input-text form-control" autocomplete="off" type="text" />',
47 textarea: '<textarea class="bootbox-input bootbox-input-textarea form-control"></textarea>',
48 email: '<input class="bootbox-input bootbox-input-email form-control" autocomplete="off" type="email" />',
49 select: '<select class="bootbox-input bootbox-input-select form-select"></select>',
50 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>',
51 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>',
52 date: '<input class="bootbox-input bootbox-input-date form-control" autocomplete="off" type="date" />',
53 time: '<input class="bootbox-input bootbox-input-time form-control" autocomplete="off" type="time" />',
54 number: '<input class="bootbox-input bootbox-input-number form-control" autocomplete="off" type="number" />',
55 password: '<input class="bootbox-input bootbox-input-password form-control" autocomplete="off" type="password" />',
56 range: '<input class="bootbox-input bootbox-input-range form-control-range" autocomplete="off" type="range" />'
57 }
58 };
59
60
61 let defaults = {
62 // Default language used when generating buttons for alert, confirm, and prompt dialogs
63 locale: 'en',
64 // Show backdrop or not. Default to static so user has to interact with dialog
65 backdrop: 'static',
66 // Animate the modal in/out
67 animate: true,
68 // Additional class string applied to the top level dialog
69 className: null,
70 // Whether or not to include a close button
71 closeButton: true,
72 // Show the dialog immediately by default
73 show: true,
74 // Dialog container
75 container: 'body',
76 // Default value (used by the prompt helper)
77 value: '',
78 // Default input type (used by the prompt helper)
79 inputType: 'text',
80 // Custom error message to report if prompt fails validation
81 errorMessage: null,
82 // Switch button order from cancel/confirm (default) to confirm/cancel
83 swapButtonOrder: false,
84 // Center modal vertically in page
85 centerVertical: false,
86 // Append "multiple" property to the select when using the "prompt" helper
87 multiple: false,
88 // Automatically scroll modal content when height exceeds viewport height
89 scrollable: false,
90 // Whether or not to destroy the modal on hide
91 reusable: false,
92 // The element which triggered the dialog
93 relatedTarget: null,
94 // The size of the modal to generate
95 size: null,
96 // A unique indentifier for this modal
97 id: null
98 };
99
100
101 // PUBLIC FUNCTIONS
102 // *************************************************************************************************************
103
104 /**
105 * Return all currently registered locales, or a specific locale if "name" is defined
106 * @param {string} [name]
107 * @returns {(Object|Object[])} An array of the available locale objects, or a single locale object if {name} is not null
108 */
109 exports.locales = function(name) {
110 return name ? locales[name] : locales;
111 };
112
113
114 /**
115 * Register localized strings for the OK, CONFIRM, and CANCEL buttons
116 * @param {string} name - The key used to identify the new locale in the locales array
117 * @param {Object} values - An object containing the localized string for each of the OK, CANCEL, and CONFIRM properties of a locale
118 * @returns The updated bootbox object
119 */
120 exports.addLocale = function(name, values) {
121 $.each(['OK', 'CANCEL', 'CONFIRM'], function(_, v) {
122 if (!values[v]) {
123 throw new Error('Please supply a translation for "' + v + '"');
124 }
125 });
126
127 locales[name] = {
128 OK: values.OK,
129 CANCEL: values.CANCEL,
130 CONFIRM: values.CONFIRM
131 };
132
133 return exports;
134 };
135
136
137 /**
138 * Remove a previously-registered locale
139 * @param {string} name - The key identifying the locale to remove
140 * @returns The updated bootbox object
141 */
142 exports.removeLocale = function(name) {
143 if (name !== 'en') {
144 delete locales[name];
145 } else {
146 throw new Error('"en" is used as the default and fallback locale and cannot be removed.');
147 }
148
149 return exports;
150 };
151
152
153 /**
154 * Set the default locale
155 * @param {string} name - The key identifying the locale to set as the default locale for all future bootbox calls
156 * @returns The updated bootbox object
157 */
158 exports.setLocale = function(name) {
159 return exports.setDefaults('locale', name);
160 };
161
162
163 /**
164 * Override default value(s) of Bootbox.
165 * @returns The updated bootbox object
166 */
167 exports.setDefaults = function() {
168 let values = {};
169
170 if (arguments.length === 2) {
171 // Allow passing of single key/value...
172 values[arguments[0]] = arguments[1];
173 } else {
174 // ... and as an object too
175 values = arguments[0];
176 }
177
178 $.extend(defaults, values);
179
180 return exports;
181 };
182
183
184 /**
185 * Hides all currently active Bootbox modals
186 * @returns The current bootbox object
187 */
188 exports.hideAll = function() {
189 $('.bootbox').modal('hide');
190
191 return exports;
192 };
193
194
195 /**
196 * Allows the base init() function to be overridden
197 * @param {function} _$ - A function to be called when the bootbox instance is created
198 * @returns The current bootbox object
199 */
200 exports.init = function(_$) {
201 return init(_$ || $);
202 };
203
204
205 // CORE HELPER FUNCTIONS
206 // *************************************************************************************************************
207
208 /**
209 * The core dialog helper function, which can be used to create any custom Bootstrap modal.
210 * @param {Object} options - An object used to configure the various properties which define a Bootbox dialog
211 * @returns A jQuery object upon which Bootstrap's modal function has been called
212 */
213 exports.dialog = function(options) {
214 if ($.fn.modal === undefined) {
215 throw new Error(
216 '"$.fn.modal" is not defined; please double check you have included the Bootstrap JavaScript library. See https://getbootstrap.com/docs/5.1/getting-started/introduction/ for more details.'
217 );
218 }
219
220 options = sanitize(options);
221
222 if ($.fn.modal.Constructor.VERSION) {
223 options.fullBootstrapVersion = $.fn.modal.Constructor.VERSION;
224 let i = options.fullBootstrapVersion.indexOf('.');
225 options.bootstrap = options.fullBootstrapVersion.substring(0, i);
226 } else {
227 // Assuming version 2.3.2, as that was the last "supported" 2.x version
228 options.bootstrap = '2';
229 options.fullBootstrapVersion = '2.3.2';
230 console.warn('Bootbox will *mostly* work with Bootstrap 2, but we do not officially support it. Please upgrade, if possible.');
231 }
232
233 let dialog = $(templates.dialog);
234 let innerDialog = dialog.find('.modal-dialog');
235 let body = dialog.find('.modal-body');
236 let header = $(templates.header);
237 let footer = $(templates.footer);
238 let buttons = options.buttons;
239
240 let callbacks = {
241 onEscape: options.onEscape
242 };
243
244 body.find('.bootbox-body').html(options.message);
245
246 // Only attempt to create buttons if at least one has been defined in the options object
247 if (getKeyLength(options.buttons) > 0) {
248 each(buttons, function(key, b) {
249 let button = $(templates.button);
250 button.data('bb-handler', key);
251 button.addClass(b.className);
252
253 switch (key) {
254 case 'ok':
255 case 'confirm':
256 button.addClass('bootbox-accept');
257 break;
258
259 case 'cancel':
260 button.addClass('bootbox-cancel');
261 break;
262 }
263
264 button.html(b.label);
265
266 if (b.id) {
267 button.attr({
268 'id': b.id
269 });
270 }
271
272 if (b.disabled === true) {
273 button.prop({
274 disabled: true
275 });
276 }
277
278 footer.append(button);
279
280 callbacks[key] = b.callback;
281 });
282
283 body.after(footer);
284 }
285
286 if (options.animate === true) {
287 dialog.addClass('fade');
288 }
289
290 if (options.className) {
291 dialog.addClass(options.className);
292 }
293
294 if (options.id) {
295 dialog.attr({
296 'id': options.id
297 });
298 }
299
300 if (options.size) {
301 // Requires Bootstrap 3.1.0 or higher
302 if (options.fullBootstrapVersion.substring(0, 3) < '3.1') {
303 console.warn('"size" requires Bootstrap 3.1.0 or higher. You appear to be using ' + options.fullBootstrapVersion + '. Please upgrade to use this option.');
304 }
305
306 switch (options.size) {
307 case 'small':
308 case 'sm':
309 innerDialog.addClass('modal-sm');
310 break;
311
312 case 'large':
313 case 'lg':
314 innerDialog.addClass('modal-lg');
315 break;
316
317 case 'extra-large':
318 case 'xl':
319 innerDialog.addClass('modal-xl');
320
321 // Requires Bootstrap 4.2.0 or higher
322 if (options.fullBootstrapVersion.substring(0, 3) < '4.2') {
323 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.');
324 }
325 break;
326 }
327 }
328
329 if (options.scrollable) {
330 innerDialog.addClass('modal-dialog-scrollable');
331
332 // Requires Bootstrap 4.3.0 or higher
333 if (options.fullBootstrapVersion.substring(0, 3) < '4.3') {
334 console.warn('Using "scrollable" requires Bootstrap 4.3.0 or higher. You appear to be using ' + options.fullBootstrapVersion + '. Please upgrade to use this option.');
335 }
336 }
337
338 if (options.title || options.closeButton) {
339 if (options.title) {
340 header.find('.modal-title').html(options.title);
341 } else {
342 header.addClass('border-0');
343 }
344
345 if (options.closeButton) {
346 let closeButton = $(templates.closeButton);
347 if (options.bootstrap < 5) {
348 closeButton.html('&times;');
349 }
350
351 /* Note: the close button for Bootstrap 5+ does not contain content */
352 if (options.bootstrap < 4) {
353 /* Bootstrap 3 and under */
354 header.prepend(closeButton);
355 } else {
356 header.append(closeButton);
357 }
358 }
359
360 body.before(header);
361 }
362
363 if (options.centerVertical) {
364 innerDialog.addClass('modal-dialog-centered');
365
366 // Requires Bootstrap 4.0.0-beta.3 or higher
367 if (options.fullBootstrapVersion < '4.0.0') {
368 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.');
369 }
370 }
371
372 // Bootstrap event listeners; these handle extra setup & teardown required after the underlying modal has performed certain actions.
373
374 if (!options.reusable) {
375 // make sure we unbind any listeners once the dialog has definitively been dismissed
376 dialog.one('hide.bs.modal', {
377 dialog: dialog
378 }, unbindModal);
379 dialog.one('hidden.bs.modal', {
380 dialog: dialog
381 }, destroyModal);
382 }
383
384 if (options.onHide) {
385 if ($.isFunction(options.onHide)) {
386 dialog.on('hide.bs.modal', options.onHide);
387 } else {
388 throw new Error('Argument supplied to "onHide" must be a function');
389 }
390 }
391
392 if (options.onHidden) {
393 if ($.isFunction(options.onHidden)) {
394 dialog.on('hidden.bs.modal', options.onHidden);
395 } else {
396 throw new Error('Argument supplied to "onHidden" must be a function');
397 }
398 }
399
400 if (options.onShow) {
401 if ($.isFunction(options.onShow)) {
402 dialog.on('show.bs.modal', options.onShow);
403 } else {
404 throw new Error('Argument supplied to "onShow" must be a function');
405 }
406 }
407
408 dialog.one('shown.bs.modal', {
409 dialog: dialog
410 }, focusPrimaryButton);
411
412 if (options.onShown) {
413 if ($.isFunction(options.onShown)) {
414 dialog.on('shown.bs.modal', options.onShown);
415 } else {
416 throw new Error('Argument supplied to "onShown" must be a function');
417 }
418 }
419
420 // Bootbox event listeners; used to decouple some behaviours from their respective triggers
421
422 if (options.backdrop === true) {
423 let startedOnBody = false;
424
425 // Prevents the event from propagating to the backdrop, when something inside the dialog is clicked
426 dialog.on('mousedown', '.modal-content', function(e) {
427 e.stopPropagation();
428
429 startedOnBody = true;
430 });
431
432 // A boolean true/false according to the Bootstrap docs should show a dialog the user can dismiss by clicking on the background.
433 // 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).
434 // However, we still want to sort-of respect true and invoke the escape mechanism instead
435 dialog.on('click.dismiss.bs.modal', function(e) {
436 if (startedOnBody || e.target !== e.currentTarget) {
437 return;
438 }
439
440 dialog.trigger('escape.close.bb');
441 });
442 }
443
444 dialog.on('escape.close.bb', function(e) {
445 // 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
446 if (callbacks.onEscape) {
447 processCallback(e, dialog, callbacks.onEscape);
448 }
449 });
450
451 dialog.on('click', '.modal-footer button:not(.disabled)', function(e) {
452 let callbackKey = $(this).data('bb-handler');
453
454 if (callbackKey !== undefined) {
455 // Only process callbacks for buttons we recognize:
456 processCallback(e, dialog, callbacks[callbackKey]);
457 }
458 });
459
460 dialog.on('click', '.bootbox-close-button', function(e) {
461 // 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
462 processCallback(e, dialog, callbacks.onEscape);
463 });
464
465 dialog.on('keyup', function(e) {
466 if (e.which === 27) {
467 dialog.trigger('escape.close.bb');
468 }
469 });
470
471 /*
472 The remainder of this method simply deals with adding our dialog element to the DOM, augmenting it with
473 Bootstrap's modal functionality and then giving the resulting object back to our caller
474 */
475
476 $(options.container).append(dialog);
477
478 dialog.modal({
479 backdrop: options.backdrop,
480 keyboard: false,
481 show: false
482 });
483
484 if (options.show) {
485 dialog.modal('show', options.relatedTarget);
486 }
487
488 return dialog;
489 };
490
491
492 /**
493 * 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.
494 * @returns A jQuery object upon which Bootstrap's modal function has been called
495 */
496 exports.alert = function() {
497 let options;
498
499 options = mergeDialogOptions('alert', ['ok'], ['message', 'callback'], arguments);
500
501 // @TODO: can this move inside exports.dialog when we're iterating over each button and checking its button.callback value instead?
502 if (options.callback && !$.isFunction(options.callback)) {
503 throw new Error('alert requires the "callback" property to be a function when provided');
504 }
505
506 // Override the ok and escape callback to make sure they just invoke the single user-supplied one (if provided)
507 options.buttons.ok.callback = options.onEscape = function() {
508 if ($.isFunction(options.callback)) {
509 return options.callback.call(this);
510 }
511
512 return true;
513 };
514
515 return exports.dialog(options);
516 };
517
518
519 /**
520 * 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.
521 * @returns A jQuery object upon which Bootstrap's modal function has been called
522 */
523 exports.confirm = function() {
524 let options;
525
526 options = mergeDialogOptions('confirm', ['cancel', 'confirm'], ['message', 'callback'], arguments);
527
528 // confirm specific validation; they don't make sense without a callback so make sure it's present
529 if (!$.isFunction(options.callback)) {
530 throw new Error('confirm requires a callback');
531 }
532
533 // Overrides; undo anything the user tried to set they shouldn't have
534 options.buttons.cancel.callback = options.onEscape = function() {
535 return options.callback.call(this, false);
536 };
537
538 options.buttons.confirm.callback = function() {
539 return options.callback.call(this, true);
540 };
541
542 return exports.dialog(options);
543 };
544
545
546 /**
547 * 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.
548 * @returns A jQuery object upon which Bootstrap's modal function has been called
549 */
550 exports.prompt = function() {
551 let options;
552 let promptDialog;
553 let form;
554 let input;
555 let shouldShow;
556 let inputOptions;
557
558 // We have to create our form first, otherwise its value is undefined when gearing up our options.
559 // @TODO this could be solved by allowing message to be a function instead...
560 form = $(templates.form);
561
562 // prompt defaults are more complex than others in that users can override more defaults
563 options = mergeDialogOptions('prompt', ['cancel', 'confirm'], ['title', 'callback'], arguments);
564
565 if (!options.value) {
566 options.value = defaults.value;
567 }
568
569 if (!options.inputType) {
570 options.inputType = defaults.inputType;
571 }
572
573 // 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
574 shouldShow = (options.show === undefined) ? defaults.show : options.show;
575
576 // This is required prior to calling the dialog builder below - we need to add an event handler just before the prompt is shown
577 options.show = false;
578
579 // Handles the 'cancel' action
580 options.buttons.cancel.callback = options.onEscape = function() {
581 return options.callback.call(this, null);
582 };
583
584 // Prompt submitted - extract the prompt value. This requires a bit of work, given the different input types available.
585 options.buttons.confirm.callback = function() {
586 let value;
587
588 if (options.inputType === 'checkbox') {
589 value = input.find('input:checked').map(function() {
590 return $(this).val();
591 }).get();
592 } else if (options.inputType === 'radio') {
593 value = input.find('input:checked').val();
594 } else {
595 let el = input[0];
596
597 // Clear any previous custom error message
598 if (options.errorMessage) {
599 el.setCustomValidity('');
600 }
601
602 if (el.checkValidity && !el.checkValidity()) {
603 // If a custom error message was provided, add it now
604 if (options.errorMessage) {
605 el.setCustomValidity(options.errorMessage);
606 }
607
608 if (el.reportValidity) {
609 el.reportValidity();
610 }
611
612 // prevents button callback from being called
613 return false;
614 } else {
615 if (options.inputType === 'select' && options.multiple === true) {
616 value = input.find('option:selected').map(function() {
617 return $(this).val();
618 }).get();
619 } else {
620 value = input.val();
621 }
622 }
623 }
624
625 return options.callback.call(this, value);
626 };
627
628 // prompt-specific validation
629 if (!options.title) {
630 throw new Error('prompt requires a title');
631 }
632
633 if (!$.isFunction(options.callback)) {
634 throw new Error('prompt requires a callback');
635 }
636
637 if (!templates.inputs[options.inputType]) {
638 throw new Error('Invalid prompt type');
639 }
640
641 // Create the input based on the supplied type
642 input = $(templates.inputs[options.inputType]);
643
644 switch (options.inputType) {
645 case 'text':
646 case 'textarea':
647 case 'email':
648 case 'password':
649 input.val(options.value);
650
651 if (options.placeholder) {
652 input.attr('placeholder', options.placeholder);
653 }
654
655 if (options.pattern) {
656 input.attr('pattern', options.pattern);
657 }
658
659 if (options.maxlength) {
660 input.attr('maxlength', options.maxlength);
661 }
662
663 if (options.required) {
664 input.prop({
665 'required': true
666 });
667 }
668
669 if (options.rows && !isNaN(parseInt(options.rows))) {
670 if (options.inputType === 'textarea') {
671 input.attr({
672 'rows': options.rows
673 });
674 }
675 }
676 break;
677
678 case 'date':
679 case 'time':
680 case 'number':
681 case 'range':
682 input.val(options.value);
683
684 if (options.placeholder) {
685 input.attr('placeholder', options.placeholder);
686 }
687
688 if (options.pattern) {
689 input.attr('pattern', options.pattern);
690 } else {
691 if (options.inputType === 'date') {
692 // Add the ISO-8601 short date format as a fallback for browsers without native type="date" support
693 input.attr('pattern', '\d{4}-\d{2}-\d{2}');
694 } else if (options.inputType === 'time') {
695 // Add an HH:MM pattern as a fallback for browsers without native type="time" support
696 input.attr('pattern', '\d{2}:\d{2}');
697 }
698 }
699
700 if (options.required) {
701 input.prop({
702 'required': true
703 });
704 }
705
706 // These input types have extra attributes which affect their input validation.
707 // 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.
708 // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date#Setting_maximum_and_minimum_dates
709 if (options.inputType !== 'date') {
710 if (options.step) {
711 if (options.step === 'any' || (!isNaN(options.step) && parseFloat(options.step) > 0)) {
712 input.attr('step', options.step);
713 } else {
714 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.');
715 }
716 }
717 }
718
719 if (minAndMaxAreValid(options.inputType, options.min, options.max)) {
720 if (options.min !== undefined) {
721 input.attr('min', options.min);
722 }
723 if (options.max !== undefined) {
724 input.attr('max', options.max);
725 }
726 }
727 break;
728
729 case 'select':
730 let groups = {};
731 inputOptions = options.inputOptions || [];
732
733 if (!$.isArray(inputOptions)) {
734 throw new Error('Please pass an array of input options');
735 }
736
737 if (!inputOptions.length) {
738 throw new Error('prompt with "inputType" set to "select" requires at least one option');
739 }
740
741 if (options.required) {
742 input.prop({
743 'required': true
744 });
745 }
746
747 if (options.multiple) {
748 input.prop({
749 'multiple': true
750 });
751 }
752
753 each(inputOptions, function(_, option) {
754 // Assume the element to attach to is the input...
755 let elem = input;
756
757 if (option.value === undefined || option.text === undefined) {
758 throw new Error('each option needs a "value" property and a "text" property');
759 }
760
761 // ... but override that element if this option sits in a group
762
763 if (option.group) {
764 // Initialise group if necessary
765 if (!groups[option.group]) {
766 groups[option.group] = $('<optgroup />').attr('label', option.group);
767 }
768
769 elem = groups[option.group];
770 }
771
772 let o = $(templates.option);
773 o.attr('value', option.value).text(option.text);
774 elem.append(o);
775 });
776
777 each(groups, function(_, group) {
778 input.append(group);
779 });
780
781 // Safe to set a select's value as per a normal input
782 input.val(options.value);
783 if (options.bootstrap < 5) {
784 input.removeClass('form-select').addClass('form-control');
785 }
786 break;
787
788 case 'checkbox':
789 let checkboxValues = $.isArray(options.value) ? options.value : [options.value];
790 inputOptions = options.inputOptions || [];
791
792 if (!inputOptions.length) {
793 throw new Error('prompt with "inputType" set to "checkbox" requires at least one option');
794 }
795
796 // 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
797 input = $('<div class="bootbox-checkbox-list"></div>');
798
799 each(inputOptions, function(_, option) {
800 if (option.value === undefined || option.text === undefined) {
801 throw new Error('each option needs a "value" property and a "text" property');
802 }
803
804 let checkbox = $(templates.inputs[options.inputType]);
805
806 checkbox.find('input').attr('value', option.value);
807 checkbox.find('label').append('\n' + option.text);
808
809 // We've ensured values is an array, so we can always iterate over it
810 each(checkboxValues, function(_, value) {
811 if (value === option.value) {
812 checkbox.find('input').prop('checked', true);
813 }
814 });
815
816 input.append(checkbox);
817 });
818 break;
819
820 case 'radio':
821 // Make sure that value is not an array (only a single radio can ever be checked)
822 if (options.value !== undefined && $.isArray(options.value)) {
823 throw new Error('prompt with "inputType" set to "radio" requires a single, non-array value for "value"');
824 }
825
826 inputOptions = options.inputOptions || [];
827
828 if (!inputOptions.length) {
829 throw new Error('prompt with "inputType" set to "radio" requires at least one option');
830 }
831
832 // 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
833 input = $('<div class="bootbox-radiobutton-list"></div>');
834
835 // Radiobuttons should always have an initial checked input checked in a "group".
836 // If value is undefined or doesn't match an input option, select the first radiobutton
837 let checkFirstRadio = true;
838
839 each(inputOptions, function(_, option) {
840 if (option.value === undefined || option.text === undefined) {
841 throw new Error('each option needs a "value" property and a "text" property');
842 }
843
844 let radio = $(templates.inputs[options.inputType]);
845
846 radio.find('input').attr('value', option.value);
847 radio.find('label').append('\n' + option.text);
848
849 if (options.value !== undefined) {
850 if (option.value === options.value) {
851 radio.find('input').prop('checked', true);
852 checkFirstRadio = false;
853 }
854 }
855
856 input.append(radio);
857 });
858
859 if (checkFirstRadio) {
860 input.find('input[type="radio"]').first().prop('checked', true);
861 }
862 break;
863 }
864
865 // Now place it in our form
866 form.append(input);
867
868 form.on('submit', function(e) {
869 e.preventDefault();
870 // Fix for SammyJS (or similar JS routing library) hijacking the form post.
871 e.stopPropagation();
872
873 // @TODO can we actually click *the* button object instead?
874 // e.g. buttons.confirm.click() or similar
875 promptDialog.find('.bootbox-accept').trigger('click');
876 });
877
878 if ($.trim(options.message) !== '') {
879 // Add the form to whatever content the user may have added.
880 let message = $(templates.promptMessage).html(options.message);
881 form.prepend(message);
882 options.message = form;
883 } else {
884 options.message = form;
885 }
886
887 // Generate the dialog
888 promptDialog = exports.dialog(options);
889
890 // Clear the existing handler focusing the submit button...
891 promptDialog.off('shown.bs.modal', focusPrimaryButton);
892
893 // ...and replace it with one focusing our input, if possible
894 promptDialog.on('shown.bs.modal', function() {
895 // Need the closure here since input isn'tcan object otherwise
896 input.focus();
897 });
898
899 if (shouldShow === true) {
900 promptDialog.modal('show');
901 }
902
903 return promptDialog;
904 };
905
906
907 // INTERNAL FUNCTIONS
908 // *************************************************************************************************************
909
910 // Map a flexible set of arguments into a single returned object.
911 // If args.length is already one just return it, otherwise use the properties argument to map the unnamed args to object properties.
912 // So in the latter case:
913 //
914 // mapArguments(["foo", $.noop], ["message", "callback"])
915 //
916 // results in
917 //
918 // { message: "foo", callback: $.noop }
919 //
920 function mapArguments(args, properties) {
921 let argsLength = args.length;
922 let options = {};
923
924 if (argsLength < 1 || argsLength > 2) {
925 throw new Error('Invalid argument length');
926 }
927
928 if (argsLength === 2 || typeof args[0] === 'string') {
929 options[properties[0]] = args[0];
930 options[properties[1]] = args[1];
931 } else {
932 options = args[0];
933 }
934
935 return options;
936 }
937
938
939 // Merge a set of default dialog options with user supplied arguments
940 function mergeArguments(defaults, args, properties) {
941 return $.extend(
942 // Deep merge
943 true,
944 // Ensure the target is an empty, unreferenced object
945 {},
946 // The base options object for this type of dialog (often just buttons)
947 defaults,
948 // 'args' could be an object or array; if it's an array properties will map it to a proper options object
949 mapArguments(args, properties)
950 );
951 }
952
953
954 // 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
955 function mergeDialogOptions(className, labels, properties, args) {
956 let locale;
957 if (args && args[0]) {
958 locale = args[0].locale || defaults.locale;
959 let swapButtons = args[0].swapButtonOrder || defaults.swapButtonOrder;
960
961 if (swapButtons) {
962 labels = labels.reverse();
963 }
964 }
965
966 // Build up a base set of dialog properties
967 let baseOptions = {
968 className: 'bootbox-' + className,
969 buttons: createLabels(labels, locale)
970 };
971
972 // Ensure the buttons properties generated, *after* merging with user args are still valid against the supplied labels
973 return validateButtons(
974 // Merge the generated base properties with user supplied arguments
975 mergeArguments(
976 baseOptions,
977 args,
978 // If args.length > 1, properties specify how each arg maps to an object key
979 properties
980 ),
981 labels
982 );
983 }
984
985
986 // Checks each button object to see if key is valid.
987 // This function will only be called by the alert, confirm, and prompt helpers.
988 function validateButtons(options, buttons) {
989 let allowedButtons = {};
990 each(buttons, function(key, value) {
991 allowedButtons[value] = true;
992 });
993
994 each(options.buttons, function(key) {
995 if (allowedButtons[key] === undefined) {
996 throw new Error('button key "' + key + '" is not allowed (options are ' + buttons.join(' ') + ')');
997 }
998 });
999
1000 return options;
1001 }
1002
1003
1004 // From a given list of arguments, return a suitable object of button labels.
1005 // All this does is normalise the given labels and translate them where possible.
1006 // e.g. "ok", "confirm" -> { ok: "OK", cancel: "Annuleren" }
1007 function createLabels(labels, locale) {
1008 let buttons = {};
1009
1010 for (let i = 0, j = labels.length; i < j; i++) {
1011 let argument = labels[i];
1012 let key = argument.toLowerCase();
1013 let value = argument.toUpperCase();
1014
1015 buttons[key] = {
1016 label: getText(value, locale)
1017 };
1018 }
1019
1020 return buttons;
1021 }
1022
1023
1024 // Get localized text from a locale. Defaults to 'en' locale if no locale provided or a non-registered locale is requested
1025 function getText(key, locale) {
1026 let labels = locales[locale];
1027
1028 return labels ? labels[key] : locales.en[key];
1029 }
1030
1031
1032 // Filter and tidy up any user supplied parameters to this dialog.
1033 // Also looks for any shorthands used and ensures that the options which are returned are all normalized properly
1034 function sanitize(options) {
1035 let buttons;
1036 let total;
1037
1038 if (typeof options !== 'object') {
1039 throw new Error('Please supply an object of options');
1040 }
1041
1042 if (!options.message) {
1043 throw new Error('"message" option must not be null or an empty string.');
1044 }
1045
1046 // Make sure any supplied options take precedence over defaults
1047 options = $.extend({}, defaults, options);
1048
1049 // Make sure backdrop is either true, false, or 'static'
1050 if (!options.backdrop) {
1051 options.backdrop = (options.backdrop === false || options.backdrop === 0) ? false : 'static';
1052 } else {
1053 options.backdrop = typeof options.backdrop === 'string' && options.backdrop.toLowerCase() === 'static' ? 'static' : true;
1054 }
1055
1056 // 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
1057 if (!options.buttons) {
1058 options.buttons = {};
1059 }
1060
1061 buttons = options.buttons;
1062
1063 total = getKeyLength(buttons);
1064
1065 each(buttons, function(key, button, index) {
1066 if ($.isFunction(button)) {
1067 // Short form, assume value is our callback. Since button isn't an object it isn't a reference either so re-assign it
1068 button = buttons[key] = {
1069 callback: button
1070 };
1071 }
1072
1073 // Before any further checks, make sure button is the correct type
1074 if ($.type(button) !== 'object') {
1075 throw new Error('button with key "' + key + '" must be an object');
1076 }
1077
1078 if (!button.label) {
1079 // The lack of an explicit label means we'll assume the key is good enough
1080 button.label = key;
1081 }
1082
1083 if (!button.className) {
1084 let isPrimary = false;
1085 if (options.swapButtonOrder) {
1086 isPrimary = index === 0;
1087 } else {
1088 isPrimary = index === total - 1;
1089 }
1090
1091 if (total <= 2 && isPrimary) {
1092 // always add a primary to the main option in a one or two-button dialog
1093 button.className = 'btn-primary';
1094 } else {
1095 // adding both classes allows us to target both BS3 and BS4+ without needing to check the version
1096 button.className = 'btn-secondary btn-default';
1097 }
1098 }
1099 });
1100
1101 return options;
1102 }
1103
1104
1105 // Returns a count of the properties defined on the object
1106 function getKeyLength(obj) {
1107 return Object.keys(obj).length;
1108 }
1109
1110
1111 // Tiny wrapper function around jQuery.each; just adds index as the third parameter
1112 function each(collection, iterator) {
1113 let index = 0;
1114 $.each(collection, function(key, value) {
1115 iterator(key, value, index++);
1116 });
1117 }
1118
1119
1120 function focusPrimaryButton(e) {
1121 e.data.dialog.find('.bootbox-accept').first().trigger('focus');
1122 }
1123
1124
1125 function destroyModal(e) {
1126 // Ensure we don't accidentally intercept hidden events triggered by children of the current dialog.
1127 // We shouldn't need to handle this anymore, now that Bootstrap namespaces its events, but still worth doing.
1128 if (e.target === e.data.dialog[0]) {
1129 e.data.dialog.remove();
1130 }
1131 }
1132
1133
1134 function unbindModal(e) {
1135 if (e.target === e.data.dialog[0]) {
1136 e.data.dialog.off('escape.close.bb');
1137 e.data.dialog.off('click');
1138 }
1139 }
1140
1141
1142 // Handle the invoked dialog callback
1143 function processCallback(e, dialog, callback) {
1144 e.stopPropagation();
1145 e.preventDefault();
1146
1147 // By default we assume a callback will get rid of the dialog, although it is given the opportunity to override this
1148
1149 // If the callback can be invoked and it *explicitly returns false*, then we'll set a flag to keep the dialog active...
1150 let preserveDialog = $.isFunction(callback) && callback.call(dialog, e) === false;
1151
1152 // ... otherwise we'll bin it
1153 if (!preserveDialog) {
1154 dialog.modal('hide');
1155 }
1156 }
1157
1158 // Validate `min` and `max` values based on the current `inputType` value
1159 function minAndMaxAreValid(type, min, max) {
1160 let result = false;
1161 let minValid = true;
1162 let maxValid = true;
1163
1164 if (type === 'date') {
1165 if (min !== undefined && !(minValid = dateIsValid(min))) {
1166 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.');
1167 } else if (max !== undefined && !(maxValid = dateIsValid(max))) {
1168 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.');
1169 }
1170 } else if (type === 'time') {
1171 if (min !== undefined && !(minValid = timeIsValid(min))) {
1172 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.');
1173 } else if (max !== undefined && !(maxValid = timeIsValid(max))) {
1174 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.');
1175 }
1176 } else {
1177 if (min !== undefined && isNaN(min)) {
1178 minValid = false;
1179 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.');
1180 }
1181
1182 if (max !== undefined && isNaN(max)) {
1183 maxValid = false;
1184 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.');
1185 }
1186 }
1187
1188 if (minValid && maxValid) {
1189 if (max <= min) {
1190 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.');
1191 } else {
1192 result = true;
1193 }
1194 }
1195
1196 return result;
1197 }
1198
1199 function timeIsValid(value) {
1200 return /([01][0-9]|2[0-3]):[0-5][0-9]?:[0-5][0-9]/.test(value);
1201 }
1202
1203 function dateIsValid(value) {
1204 return /(\d{4})-(\d{2})-(\d{2})/.test(value);
1205 }
1206
1207 // The Bootbox object
1208 return exports;
1209 }));
1210
1211 /*! @preserve
1212 * bootbox.locales.js
1213 * version: 6.0.0
1214 * author: Nick Payne <nick@kurai.co.uk>
1215 * license: MIT
1216 * http://bootboxjs.com/
1217 */
1218 (function(global, factory) {
1219 'use strict';
1220 if (typeof define === 'function' && define.amd) {
1221 define(['bootbox'], factory);
1222 } else if (typeof module === 'object' && module.exports) {
1223 factory(require('./bootbox'));
1224 } else {
1225 factory(global.bootbox);
1226 }
1227 }(this, function(bootbox) {
1228 'use strict';
1229
1230 // locale : Arabic
1231 // author : Emad Omar
1232 bootbox.addLocale('ar', {
1233 OK: '�
1234 وافق',
1235 CANCEL: 'الغاء',
1236 CONFIRM: 'تأكيد'
1237 });
1238 // locale : Azerbaijani
1239 // author : Valentin Belousov
1240 bootbox.addLocale('az', {
1241 OK: 'OK',
1242 CANCEL: 'İmtina et',
1243 CONFIRM: 'Təsdiq et'
1244 });
1245 // locale : Bulgarian
1246 // author : mraiur
1247 bootbox.addLocale('bg-BG', {
1248 OK: 'Ок',
1249 CANCEL: 'Отказ',
1250 CONFIRM: 'Потвърждавам'
1251 });
1252 // locale : Czech
1253 // author : Lukáš Fryč
1254 bootbox.addLocale('cs', {
1255 OK: 'OK',
1256 CANCEL: 'Zrušit',
1257 CONFIRM: 'Potvrdit'
1258 });
1259 // locale : Danish
1260 // author : Frederik Alkærsig
1261 bootbox.addLocale('da', {
1262 OK: 'OK',
1263 CANCEL: 'Annuller',
1264 CONFIRM: 'Accepter'
1265 });
1266 // locale : German
1267 // author : Nick Payne
1268 bootbox.addLocale('de', {
1269 OK: 'OK',
1270 CANCEL: 'Abbrechen',
1271 CONFIRM: 'Akzeptieren'
1272 });
1273 // locale : Greek
1274 // author : Tolis Emmanouilidis
1275 bootbox.addLocale('el', {
1276 OK: 'Εντάξει',
1277 CANCEL: 'Ακύρωση',
1278 CONFIRM: 'Επιβεβαίωση'
1279 });
1280 // locale : English
1281 // author : Nick Payne
1282 bootbox.addLocale('en', {
1283 OK: 'OK',
1284 CANCEL: 'Cancel',
1285 CONFIRM: 'OK'
1286 });
1287 // locale : Spanish
1288 // author : Ian Leckey
1289 bootbox.addLocale('es', {
1290 OK: 'OK',
1291 CANCEL: 'Cancelar',
1292 CONFIRM: 'Aceptar'
1293 });
1294 // locale : Estonian
1295 // author : Pavel Krõlov
1296 bootbox.addLocale('et', {
1297 OK: 'OK',
1298 CANCEL: 'Katkesta',
1299 CONFIRM: 'OK'
1300 });
1301 // locale : Basque
1302 // author : Iker Ibarguren
1303 bootbox.addLocale('eu', {
1304 OK: 'OK',
1305 CANCEL: 'Ezeztatu',
1306 CONFIRM: 'Onartu'
1307 });
1308 // locale : Persian
1309 // author : Touhid Arastu
1310 bootbox.addLocale('fa', {
1311 OK: 'قبول',
1312 CANCEL: 'لغو',
1313 CONFIRM: 'تایید'
1314 });
1315 // locale : Finnish
1316 // author : Nick Payne
1317 bootbox.addLocale('fi', {
1318 OK: 'OK',
1319 CANCEL: 'Peruuta',
1320 CONFIRM: 'OK'
1321 });
1322 // locale : French
1323 // author : Nick Payne, Sebastien Andary
1324 bootbox.addLocale('fr', {
1325 OK: 'OK',
1326 CANCEL: 'Annuler',
1327 CONFIRM: 'Confirmer'
1328 });
1329 // locale : Hebrew
1330 // author : Chen Alon
1331 bootbox.addLocale('he', {
1332 OK: 'אישור',
1333 CANCEL: 'ביטול',
1334 CONFIRM: 'אישור'
1335 });
1336 // locale : Croatian
1337 // author : Mario Bašić
1338 bootbox.addLocale('hr', {
1339 OK: 'OK',
1340 CANCEL: 'Odustani',
1341 CONFIRM: 'Potvrdi'
1342 });
1343 // locale : Hungarian
1344 // author : Márk Sági-Kazár
1345 bootbox.addLocale('hu', {
1346 OK: 'OK',
1347 CANCEL: 'Mégsem',
1348 CONFIRM: 'Megerősít'
1349 });
1350 // locale : Indonesian
1351 // author : Budi Irawan
1352 bootbox.addLocale('id', {
1353 OK: 'OK',
1354 CANCEL: 'Batal',
1355 CONFIRM: 'OK'
1356 });
1357 // locale : Italian
1358 // author : Mauro
1359 bootbox.addLocale('it', {
1360 OK: 'OK',
1361 CANCEL: 'Annulla',
1362 CONFIRM: 'Conferma'
1363 });
1364 // locale : Japanese
1365 // author : ms183
1366 bootbox.addLocale('ja', {
1367 OK: 'OK',
1368 CANCEL: 'キャンセル',
1369 CONFIRM: '確認'
1370 });
1371 // locale : Georgian
1372 // author : Avtandil Kikabidze aka LONGMAN (@akalongman)
1373 bootbox.addLocale('ka', {
1374 OK: 'OK',
1375 CANCEL: 'გაუქმება',
1376 CONFIRM: 'დადასტურება'
1377 });
1378 // locale : Korean
1379 // author : rigning
1380 bootbox.addLocale('ko', {
1381 OK: 'OK',
1382 CANCEL: '취소',
1383 CONFIRM: '확인'
1384 });
1385 // locale : Lithuanian
1386 // author : Tomas
1387 bootbox.addLocale('lt', {
1388 OK: 'Gerai',
1389 CANCEL: 'Atšaukti',
1390 CONFIRM: 'Patvirtinti'
1391 });
1392 // locale : Latvian
1393 // author : Dmitry Bogatykh, Lauris BH
1394 bootbox.addLocale('lv', {
1395 OK: 'Labi',
1396 CANCEL: 'Atcelt',
1397 CONFIRM: 'Apstiprināt'
1398 });
1399 // locale : Dutch
1400 // author : Bas ter Vrugt
1401 bootbox.addLocale('nl', {
1402 OK: 'OK',
1403 CANCEL: 'Annuleren',
1404 CONFIRM: 'Accepteren'
1405 });
1406 // locale : Norwegian
1407 // author : Nils Magnus Englund
1408 bootbox.addLocale('no', {
1409 OK: 'OK',
1410 CANCEL: 'Avbryt',
1411 CONFIRM: 'OK'
1412 });
1413 // locale : Polish
1414 // author : Szczepan Cieślik
1415 bootbox.addLocale('pl', {
1416 OK: 'OK',
1417 CANCEL: 'Anuluj',
1418 CONFIRM: 'Potwierdź'
1419 });
1420 // locale : Portuguese (Brasil)
1421 // author : Nick Payne
1422 bootbox.addLocale('pt-BR', {
1423 OK: 'OK',
1424 CANCEL: 'Cancelar',
1425 CONFIRM: 'Sim'
1426 });
1427 // locale : Portuguese
1428 // author : Cláudio Medina
1429 bootbox.addLocale('pt', {
1430 OK: 'OK',
1431 CANCEL: 'Cancelar',
1432 CONFIRM: 'Confirmar'
1433 });
1434 // locale : Russian
1435 // author : ionian-wind
1436 bootbox.addLocale('ru', {
1437 OK: 'OK',
1438 CANCEL: 'Отмена',
1439 CONFIRM: 'Применить'
1440 });
1441 // locale : Slovak
1442 // author : Stano Paška
1443 bootbox.addLocale('sk', {
1444 OK: 'OK',
1445 CANCEL: 'Zrušiť',
1446 CONFIRM: 'Potvrdiť'
1447 });
1448 // locale : Slovenian
1449 // author : @metalcamp
1450 bootbox.addLocale('sl', {
1451 OK: 'OK',
1452 CANCEL: 'Prekliči',
1453 CONFIRM: 'Potrdi'
1454 });
1455 // locale : Albanian
1456 // author : Knut Hühne
1457 bootbox.addLocale('sq', {
1458 OK: 'OK',
1459 CANCEL: 'Anulo',
1460 CONFIRM: 'Prano'
1461 });
1462 // locale : Swedish
1463 // author : Mattias Reichel
1464 bootbox.addLocale('sv', {
1465 OK: 'OK',
1466 CANCEL: 'Avbryt',
1467 CONFIRM: 'OK'
1468 });
1469 // locale : Swahili
1470 // author : Timothy Anyona
1471 bootbox.addLocale('sw', {
1472 OK: 'Sawa',
1473 CANCEL: 'Ghairi',
1474 CONFIRM: 'Thibitisha'
1475 });
1476 // locale : Tamil
1477 // author : Kolappan Nathan
1478 bootbox.addLocale('ta', {
1479 OK: 'சரி',
1480 CANCEL: 'ரத்து செய்',
1481 CONFIRM: 'உறுதி செய்'
1482 });
1483 // locale : Thai
1484 // author : Ishmael๛
1485 bootbox.addLocale('th', {
1486 OK: 'ตกลง',
1487 CANCEL: 'ยกเลิก',
1488 CONFIRM: 'ยืนยัน'
1489 });
1490 // locale : Turkish
1491 // author : Enes Karaca
1492 bootbox.addLocale('tr', {
1493 OK: 'Tamam',
1494 CANCEL: 'İptal',
1495 CONFIRM: 'Onayla'
1496 });
1497 // locale : Ukrainian
1498 // author : OlehBoiko
1499 bootbox.addLocale('uk', {
1500 OK: 'OK',
1501 CANCEL: 'Відміна',
1502 CONFIRM: 'Прийняти'
1503 });
1504 // locale : Vietnamese
1505 // author : Anh Tu Nguyen
1506 bootbox.addLocale('vi', {
1507 OK: 'OK',
1508 CANCEL: 'Hủy bỏ',
1509 CONFIRM: 'Xác nhận'
1510 });
1511 // locale : Chinese (China / People's Republic of China)
1512 // author : Nick Payne
1513 bootbox.addLocale('zh-CN', {
1514 OK: 'OK',
1515 CANCEL: '取消',
1516 CONFIRM: '确认'
1517 });
1518 // locale : Chinese (Taiwan / Republic of China)
1519 // author : Nick Payne
1520 bootbox.addLocale('zh-TW', {
1521 OK: 'OK',
1522 CANCEL: '取消',
1523 CONFIRM: '確認'
1524 });
1525
1526 }));
1527