PluginProbe
UpdraftCentral Dashboard / 0.8.13
UpdraftCentral Dashboard v0.8.13
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.js

bootbox.js in UpdraftCentral Dashboard 0.8.13, at js/bootbox/bootbox.js

1,231 lines 39.3 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: 5.4.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 // Polyfills Object.keys, if necessary.
24 // @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
25 if (!Object.keys) {
26 Object.keys = (function () {
27 var hasOwnProperty = Object.prototype.hasOwnProperty,
28 hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'),
29 dontEnums = [
30 'toString',
31 'toLocaleString',
32 'valueOf',
33 'hasOwnProperty',
34 'isPrototypeOf',
35 'propertyIsEnumerable',
36 'constructor'
37 ],
38 dontEnumsLength = dontEnums.length;
39
40 return function (obj) {
41 if (typeof obj !== 'function' && (typeof obj !== 'object' || obj === null)) {
42 throw new TypeError('Object.keys called on non-object');
43 }
44
45 var result = [], prop, i;
46
47 for (prop in obj) {
48 if (hasOwnProperty.call(obj, prop)) {
49 result.push(prop);
50 }
51 }
52
53 if (hasDontEnumBug) {
54 for (i = 0; i < dontEnumsLength; i++) {
55 if (hasOwnProperty.call(obj, dontEnums[i])) {
56 result.push(dontEnums[i]);
57 }
58 }
59 }
60
61 return result;
62 };
63 }());
64 }
65
66 var exports = {};
67
68 var VERSION = '5.0.0';
69 exports.VERSION = VERSION;
70
71 var locales = {
72 en : {
73 OK : 'OK',
74 CANCEL : 'Cancel',
75 CONFIRM : 'OK'
76 }
77 };
78
79 var templates = {
80 dialog:
81 '<div class="bootbox modal" tabindex="-1" role="dialog" aria-hidden="true">' +
82 '<div class="modal-dialog">' +
83 '<div class="modal-content">' +
84 '<div class="modal-body"><div class="bootbox-body"></div></div>' +
85 '</div>' +
86 '</div>' +
87 '</div>',
88 header:
89 '<div class="modal-header">' +
90 '<h5 class="modal-title"></h5>' +
91 '</div>',
92 footer:
93 '<div class="modal-footer"></div>',
94 closeButton:
95 '<button type="button" class="bootbox-close-button close" aria-hidden="true">&times;</button>',
96 form:
97 '<form class="bootbox-form"></form>',
98 button:
99 '<button type="button" class="btn"></button>',
100 option:
101 '<option></option>',
102 promptMessage:
103 '<div class="bootbox-prompt-message"></div>',
104 inputs: {
105 text:
106 '<input class="bootbox-input bootbox-input-text form-control" autocomplete="off" type="text" />',
107 textarea:
108 '<textarea class="bootbox-input bootbox-input-textarea form-control"></textarea>',
109 email:
110 '<input class="bootbox-input bootbox-input-email form-control" autocomplete="off" type="email" />',
111 select:
112 '<select class="bootbox-input bootbox-input-select form-control"></select>',
113 checkbox:
114 '<div class="form-check checkbox"><label class="form-check-label"><input class="form-check-input bootbox-input bootbox-input-checkbox" type="checkbox" /></label></div>',
115 radio:
116 '<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>',
117 date:
118 '<input class="bootbox-input bootbox-input-date form-control" autocomplete="off" type="date" />',
119 time:
120 '<input class="bootbox-input bootbox-input-time form-control" autocomplete="off" type="time" />',
121 number:
122 '<input class="bootbox-input bootbox-input-number form-control" autocomplete="off" type="number" />',
123 password:
124 '<input class="bootbox-input bootbox-input-password form-control" autocomplete="off" type="password" />',
125 range:
126 '<input class="bootbox-input bootbox-input-range form-control-range" autocomplete="off" type="range" />'
127 }
128 };
129
130
131 var defaults = {
132 // default language
133 locale: 'en',
134 // show backdrop or not. Default to static so user has to interact with dialog
135 backdrop: 'static',
136 // animate the modal in/out
137 animate: true,
138 // additional class string applied to the top level dialog
139 className: null,
140 // whether or not to include a close button
141 closeButton: true,
142 // show the dialog immediately by default
143 show: true,
144 // dialog container
145 container: 'body',
146 // default value (used by the prompt helper)
147 value: '',
148 // default input type (used by the prompt helper)
149 inputType: 'text',
150 // switch button order from cancel/confirm (default) to confirm/cancel
151 swapButtonOrder: false,
152 // center modal vertically in page
153 centerVertical: false,
154 // Append "multiple" property to the select when using the "prompt" helper
155 multiple: false,
156 // Automatically scroll modal content when height exceeds viewport height
157 scrollable: false
158 };
159
160
161 // PUBLIC FUNCTIONS
162 // *************************************************************************************************************
163
164 // Return all currently registered locales, or a specific locale if "name" is defined
165 exports.locales = function (name) {
166 return name ? locales[name] : locales;
167 };
168
169
170 // Register localized strings for the OK, CONFIRM, and CANCEL buttons
171 exports.addLocale = function (name, values) {
172 $.each(['OK', 'CANCEL', 'CONFIRM'], function (_, v) {
173 if (!values[v]) {
174 throw new Error('Please supply a translation for "' + v + '"');
175 }
176 });
177
178 locales[name] = {
179 OK: values.OK,
180 CANCEL: values.CANCEL,
181 CONFIRM: values.CONFIRM
182 };
183
184 return exports;
185 };
186
187
188 // Remove a previously-registered locale
189 exports.removeLocale = function (name) {
190 if (name !== 'en') {
191 delete locales[name];
192 }
193 else {
194 throw new Error('"en" is used as the default and fallback locale and cannot be removed.');
195 }
196
197 return exports;
198 };
199
200
201 // Set the default locale
202 exports.setLocale = function (name) {
203 return exports.setDefaults('locale', name);
204 };
205
206
207 // Override default value(s) of Bootbox.
208 exports.setDefaults = function () {
209 var values = {};
210
211 if (arguments.length === 2) {
212 // allow passing of single key/value...
213 values[arguments[0]] = arguments[1];
214 } else {
215 // ... and as an object too
216 values = arguments[0];
217 }
218
219 $.extend(defaults, values);
220
221 return exports;
222 };
223
224
225 // Hides all currently active Bootbox modals
226 exports.hideAll = function () {
227 $('.bootbox').modal('hide');
228
229 return exports;
230 };
231
232
233 // Allows the base init() function to be overridden
234 exports.init = function (_$) {
235 return init(_$ || $);
236 };
237
238
239 // CORE HELPER FUNCTIONS
240 // *************************************************************************************************************
241
242 // Core dialog function
243 exports.dialog = function (options) {
244 if ($.fn.modal === undefined) {
245 throw new Error(
246 '"$.fn.modal" is not defined; please double check you have included ' +
247 'the Bootstrap JavaScript library. See https://getbootstrap.com/docs/4.4/getting-started/javascript/ ' +
248 'for more details.'
249 );
250 }
251
252 options = sanitize(options);
253
254 if ($.fn.modal.Constructor.VERSION) {
255 options.fullBootstrapVersion = $.fn.modal.Constructor.VERSION;
256 var i = options.fullBootstrapVersion.indexOf('.');
257 options.bootstrap = options.fullBootstrapVersion.substring(0, i);
258 }
259 else {
260 // Assuming version 2.3.2, as that was the last "supported" 2.x version
261 options.bootstrap = '2';
262 options.fullBootstrapVersion = '2.3.2';
263 console.warn('Bootbox will *mostly* work with Bootstrap 2, but we do not officially support it. Please upgrade, if possible.');
264 }
265
266 var dialog = $(templates.dialog);
267 var innerDialog = dialog.find('.modal-dialog');
268 var body = dialog.find('.modal-body');
269 var header = $(templates.header);
270 var footer = $(templates.footer);
271 var buttons = options.buttons;
272
273 var callbacks = {
274 onEscape: options.onEscape
275 };
276
277 body.find('.bootbox-body').html(options.message);
278
279 // Only attempt to create buttons if at least one has
280 // been defined in the options object
281 if (getKeyLength(options.buttons) > 0) {
282 each(buttons, function (key, b) {
283 var button = $(templates.button);
284 button.data('bb-handler', key);
285 button.addClass(b.className);
286
287 switch (key) {
288 case 'ok':
289 case 'confirm':
290 button.addClass('bootbox-accept');
291 break;
292
293 case 'cancel':
294 button.addClass('bootbox-cancel');
295 break;
296 }
297
298 button.html(b.label);
299 footer.append(button);
300
301 callbacks[key] = b.callback;
302 });
303
304 body.after(footer);
305 }
306
307 if (options.animate === true) {
308 dialog.addClass('fade');
309 }
310
311 if (options.className) {
312 dialog.addClass(options.className);
313 }
314
315 if (options.size) {
316 // Requires Bootstrap 3.1.0 or higher
317 if (options.fullBootstrapVersion.substring(0, 3) < '3.1') {
318 console.warn('"size" requires Bootstrap 3.1.0 or higher. You appear to be using ' + options.fullBootstrapVersion + '. Please upgrade to use this option.');
319 }
320
321 switch (options.size) {
322 case 'small':
323 case 'sm':
324 innerDialog.addClass('modal-sm');
325 break;
326
327 case 'large':
328 case 'lg':
329 innerDialog.addClass('modal-lg');
330 break;
331
332 case 'extra-large':
333 case 'xl':
334 innerDialog.addClass('modal-xl');
335
336 // Requires Bootstrap 4.2.0 or higher
337 if (options.fullBootstrapVersion.substring(0, 3) < '4.2') {
338 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.');
339 }
340 break;
341 }
342 }
343
344 if (options.scrollable) {
345 innerDialog.addClass('modal-dialog-scrollable');
346
347 // Requires Bootstrap 4.3.0 or higher
348 if (options.fullBootstrapVersion.substring(0, 3) < '4.3') {
349 console.warn('Using "scrollable" requires Bootstrap 4.3.0 or higher. You appear to be using ' + options.fullBootstrapVersion + '. Please upgrade to use this option.');
350 }
351 }
352
353 if (options.title) {
354 body.before(header);
355 dialog.find('.modal-title').html(options.title);
356 }
357
358 if (options.closeButton) {
359 var closeButton = $(templates.closeButton);
360
361 if (options.title) {
362 if (options.bootstrap > 3) {
363 dialog.find('.modal-header').append(closeButton);
364 }
365 else {
366 dialog.find('.modal-header').prepend(closeButton);
367 }
368 } else {
369 closeButton.prependTo(body);
370 }
371 }
372
373 if (options.centerVertical) {
374 innerDialog.addClass('modal-dialog-centered');
375
376 // Requires Bootstrap 4.0.0-beta.3 or higher
377 if (options.fullBootstrapVersion < '4.0.0') {
378 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.');
379 }
380 }
381
382 // Bootstrap event listeners; these handle extra
383 // setup & teardown required after the underlying
384 // modal has performed certain actions.
385
386 // make sure we unbind any listeners once the dialog has definitively been dismissed
387 dialog.one('hide.bs.modal', { dialog: dialog }, unbindModal);
388
389 if (options.onHide) {
390 if ($.isFunction(options.onHide)) {
391 dialog.on('hide.bs.modal', options.onHide);
392 }
393 else {
394 throw new Error('Argument supplied to "onHide" must be a function');
395 }
396 }
397
398 dialog.one('hidden.bs.modal', { dialog: dialog }, destroyModal);
399
400 if (options.onHidden) {
401 if ($.isFunction(options.onHidden)) {
402 dialog.on('hidden.bs.modal', options.onHidden);
403 }
404 else {
405 throw new Error('Argument supplied to "onHidden" must be a function');
406 }
407 }
408
409 if (options.onShow) {
410 if ($.isFunction(options.onShow)) {
411 dialog.on('show.bs.modal', options.onShow);
412 }
413 else {
414 throw new Error('Argument supplied to "onShow" must be a function');
415 }
416 }
417
418 dialog.one('shown.bs.modal', { dialog: dialog }, focusPrimaryButton);
419
420 if (options.onShown) {
421 if ($.isFunction(options.onShown)) {
422 dialog.on('shown.bs.modal', options.onShown);
423 }
424 else {
425 throw new Error('Argument supplied to "onShown" must be a function');
426 }
427 }
428
429 // Bootbox event listeners; used to decouple some
430 // behaviours from their respective triggers
431
432 if (options.backdrop !== 'static') {
433 // A boolean true/false according to the Bootstrap docs
434 // should show a dialog the user can dismiss by clicking on
435 // the background.
436 // We always only ever pass static/false to the actual
437 // $.modal function because with "true" we can't trap
438 // this event (the .modal-backdrop swallows it)
439 // However, we still want to sort-of respect true
440 // and invoke the escape mechanism instead
441 dialog.on('click.dismiss.bs.modal', function (e) {
442 // @NOTE: the target varies in >= 3.3.x releases since the modal backdrop
443 // moved *inside* the outer dialog rather than *alongside* it
444 if (dialog.children('.modal-backdrop').length) {
445 e.currentTarget = dialog.children('.modal-backdrop').get(0);
446 }
447
448 if (e.target !== e.currentTarget) {
449 return;
450 }
451
452 dialog.trigger('escape.close.bb');
453 });
454 }
455
456 dialog.on('escape.close.bb', function (e) {
457 // the if statement looks redundant but it isn't; without it
458 // if we *didn't* have an onEscape handler then processCallback
459 // would automatically dismiss the dialog
460 if (callbacks.onEscape) {
461 processCallback(e, dialog, callbacks.onEscape);
462 }
463 });
464
465
466 dialog.on('click', '.modal-footer button:not(.disabled)', function (e) {
467 var callbackKey = $(this).data('bb-handler');
468
469 if (callbackKey !== undefined) {
470 // Only process callbacks for buttons we recognize:
471 processCallback(e, dialog, callbacks[callbackKey]);
472 }
473 });
474
475 dialog.on('click', '.bootbox-close-button', function (e) {
476 // onEscape might be falsy but that's fine; the fact is
477 // if the user has managed to click the close button we
478 // have to close the dialog, callback or not
479 processCallback(e, dialog, callbacks.onEscape);
480 });
481
482 dialog.on('keyup', function (e) {
483 if (e.which === 27) {
484 dialog.trigger('escape.close.bb');
485 }
486 });
487
488 // the remainder of this method simply deals with adding our
489 // dialog element to the DOM, augmenting it with Bootstrap's modal
490 // functionality and then giving the resulting object back
491 // to our caller
492
493 $(options.container).append(dialog);
494
495 dialog.modal({
496 backdrop: options.backdrop ? 'static' : false,
497 keyboard: false,
498 show: false
499 });
500
501 if (options.show) {
502 dialog.modal('show');
503 }
504
505 return dialog;
506 };
507
508
509 // Helper function to simulate the native alert() behavior. **NOTE**: This is non-blocking, so any
510 // code that must happen after the alert is dismissed should be placed within the callback function
511 // for this alert.
512 exports.alert = function () {
513 var options;
514
515 options = mergeDialogOptions('alert', ['ok'], ['message', 'callback'], arguments);
516
517 // @TODO: can this move inside exports.dialog when we're iterating over each
518 // button and checking its button.callback value instead?
519 if (options.callback && !$.isFunction(options.callback)) {
520 throw new Error('alert requires the "callback" property to be a function when provided');
521 }
522
523 // override the ok and escape callback to make sure they just invoke
524 // the single user-supplied one (if provided)
525 options.buttons.ok.callback = options.onEscape = function () {
526 if ($.isFunction(options.callback)) {
527 return options.callback.call(this);
528 }
529
530 return true;
531 };
532
533 return exports.dialog(options);
534 };
535
536
537 // Helper function to simulate the native confirm() behavior. **NOTE**: This is non-blocking, so any
538 // code that must happen after the confirm is dismissed should be placed within the callback function
539 // for this confirm.
540 exports.confirm = function () {
541 var options;
542
543 options = mergeDialogOptions('confirm', ['cancel', 'confirm'], ['message', 'callback'], arguments);
544
545 // confirm specific validation; they don't make sense without a callback so make
546 // sure it's present
547 if (!$.isFunction(options.callback)) {
548 throw new Error('confirm requires a callback');
549 }
550
551 // overrides; undo anything the user tried to set they shouldn't have
552 options.buttons.cancel.callback = options.onEscape = function () {
553 return options.callback.call(this, false);
554 };
555
556 options.buttons.confirm.callback = function () {
557 return options.callback.call(this, true);
558 };
559
560 return exports.dialog(options);
561 };
562
563
564 // Helper function to simulate the native prompt() behavior. **NOTE**: This is non-blocking, so any
565 // code that must happen after the prompt is dismissed should be placed within the callback function
566 // for this prompt.
567 exports.prompt = function () {
568 var options;
569 var promptDialog;
570 var form;
571 var input;
572 var shouldShow;
573 var inputOptions;
574
575 // we have to create our form first otherwise
576 // its value is undefined when gearing up our options
577 // @TODO this could be solved by allowing message to
578 // be a function instead...
579 form = $(templates.form);
580
581 // prompt defaults are more complex than others in that
582 // users can override more defaults
583 options = mergeDialogOptions('prompt', ['cancel', 'confirm'], ['title', 'callback'], arguments);
584
585 if (!options.value) {
586 options.value = defaults.value;
587 }
588
589 if (!options.inputType) {
590 options.inputType = defaults.inputType;
591 }
592
593 // capture the user's show value; we always set this to false before
594 // spawning the dialog to give us a chance to attach some handlers to
595 // it, but we need to make sure we respect a preference not to show it
596 shouldShow = (options.show === undefined) ? defaults.show : options.show;
597
598 // This is required prior to calling the dialog builder below - we need to
599 // add an event handler just before the prompt is shown
600 options.show = false;
601
602 // Handles the 'cancel' action
603 options.buttons.cancel.callback = options.onEscape = function () {
604 return options.callback.call(this, null);
605 };
606
607 // Prompt submitted - extract the prompt value. This requires a bit of work,
608 // given the different input types available.
609 options.buttons.confirm.callback = function () {
610 var value;
611
612 if (options.inputType === 'checkbox') {
613 value = input.find('input:checked').map(function () {
614 return $(this).val();
615 }).get();
616 } else if (options.inputType === 'radio') {
617 value = input.find('input:checked').val();
618 }
619 else {
620 if (input[0].checkValidity && !input[0].checkValidity()) {
621 // prevents button callback from being called
622 return false;
623 } else {
624 if (options.inputType === 'select' && options.multiple === true) {
625 value = input.find('option:selected').map(function () {
626 return $(this).val();
627 }).get();
628 }
629 else {
630 value = input.val();
631 }
632 }
633 }
634
635 return options.callback.call(this, value);
636 };
637
638 // prompt-specific validation
639 if (!options.title) {
640 throw new Error('prompt requires a title');
641 }
642
643 if (!$.isFunction(options.callback)) {
644 throw new Error('prompt requires a callback');
645 }
646
647 if (!templates.inputs[options.inputType]) {
648 throw new Error('Invalid prompt type');
649 }
650
651 // create the input based on the supplied type
652 input = $(templates.inputs[options.inputType]);
653
654 switch (options.inputType) {
655 case 'text':
656 case 'textarea':
657 case 'email':
658 case 'password':
659 input.val(options.value);
660
661 if (options.placeholder) {
662 input.attr('placeholder', options.placeholder);
663 }
664
665 if (options.pattern) {
666 input.attr('pattern', options.pattern);
667 }
668
669 if (options.maxlength) {
670 input.attr('maxlength', options.maxlength);
671 }
672
673 if (options.required) {
674 input.prop({ 'required': true });
675 }
676
677 if (options.rows && !isNaN(parseInt(options.rows))) {
678 if (options.inputType === 'textarea') {
679 input.attr({ 'rows': options.rows });
680 }
681 }
682
683 break;
684
685
686 case 'date':
687 case 'time':
688 case 'number':
689 case 'range':
690 input.val(options.value);
691
692 if (options.placeholder) {
693 input.attr('placeholder', options.placeholder);
694 }
695
696 if (options.pattern) {
697 input.attr('pattern', options.pattern);
698 }
699
700 if (options.required) {
701 input.prop({ 'required': true });
702 }
703
704 // These input types have extra attributes which affect their input validation.
705 // Warning: For most browsers, date inputs are buggy in their implementation of 'step', so
706 // this attribute will have no effect. Therefore, we don't set the attribute for date inputs.
707 // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date#Setting_maximum_and_minimum_dates
708 if (options.inputType !== 'date') {
709 if (options.step) {
710 if (options.step === 'any' || (!isNaN(options.step) && parseFloat(options.step) > 0)) {
711 input.attr('step', options.step);
712 }
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
728 break;
729
730
731 case 'select':
732 var groups = {};
733 inputOptions = options.inputOptions || [];
734
735 if (!$.isArray(inputOptions)) {
736 throw new Error('Please pass an array of input options');
737 }
738
739 if (!inputOptions.length) {
740 throw new Error('prompt with "inputType" set to "select" requires at least one option');
741 }
742
743 // placeholder is not actually a valid attribute for select,
744 // but we'll allow it, assuming it might be used for a plugin
745 if (options.placeholder) {
746 input.attr('placeholder', options.placeholder);
747 }
748
749 if (options.required) {
750 input.prop({ 'required': true });
751 }
752
753 if (options.multiple) {
754 input.prop({ 'multiple': true });
755 }
756
757 each(inputOptions, function (_, option) {
758 // assume the element to attach to is the input...
759 var elem = input;
760
761 if (option.value === undefined || option.text === undefined) {
762 throw new Error('each option needs a "value" property and a "text" property');
763 }
764
765 // ... but override that element if this option sits in a group
766
767 if (option.group) {
768 // initialise group if necessary
769 if (!groups[option.group]) {
770 groups[option.group] = $('<optgroup />').attr('label', option.group);
771 }
772
773 elem = groups[option.group];
774 }
775
776 var o = $(templates.option);
777 o.attr('value', option.value).text(option.text);
778 elem.append(o);
779 });
780
781 each(groups, function (_, group) {
782 input.append(group);
783 });
784
785 // safe to set a select's value as per a normal input
786 input.val(options.value);
787
788 break;
789
790
791 case 'checkbox':
792 var checkboxValues = $.isArray(options.value) ? options.value : [options.value];
793 inputOptions = options.inputOptions || [];
794
795 if (!inputOptions.length) {
796 throw new Error('prompt with "inputType" set to "checkbox" requires at least one option');
797 }
798
799 // checkboxes have to nest within a containing element, so
800 // they break the rules a bit and we end up re-assigning
801 // our 'input' element to this container instead
802 input = $('<div class="bootbox-checkbox-list"></div>');
803
804 each(inputOptions, function (_, option) {
805 if (option.value === undefined || option.text === undefined) {
806 throw new Error('each option needs a "value" property and a "text" property');
807 }
808
809 var checkbox = $(templates.inputs[options.inputType]);
810
811 checkbox.find('input').attr('value', option.value);
812 checkbox.find('label').append('\n' + option.text);
813
814 // we've ensured values is an array so we can always iterate over it
815 each(checkboxValues, function (_, value) {
816 if (value === option.value) {
817 checkbox.find('input').prop('checked', true);
818 }
819 });
820
821 input.append(checkbox);
822 });
823 break;
824
825
826 case 'radio':
827 // Make sure that value is not an array (only a single radio can ever be checked)
828 if (options.value !== undefined && $.isArray(options.value)) {
829 throw new Error('prompt with "inputType" set to "radio" requires a single, non-array value for "value"');
830 }
831
832 inputOptions = options.inputOptions || [];
833
834 if (!inputOptions.length) {
835 throw new Error('prompt with "inputType" set to "radio" requires at least one option');
836 }
837
838 // Radiobuttons have to nest within a containing element, so
839 // they break the rules a bit and we end up re-assigning
840 // our 'input' element to this container instead
841 input = $('<div class="bootbox-radiobutton-list"></div>');
842
843 // Radiobuttons should always have an initial checked input checked in a "group".
844 // If value is undefined or doesn't match an input option, select the first radiobutton
845 var checkFirstRadio = true;
846
847 each(inputOptions, function (_, option) {
848 if (option.value === undefined || option.text === undefined) {
849 throw new Error('each option needs a "value" property and a "text" property');
850 }
851
852 var radio = $(templates.inputs[options.inputType]);
853
854 radio.find('input').attr('value', option.value);
855 radio.find('label').append('\n' + option.text);
856
857 if (options.value !== undefined) {
858 if (option.value === options.value) {
859 radio.find('input').prop('checked', true);
860 checkFirstRadio = false;
861 }
862 }
863
864 input.append(radio);
865 });
866
867 if (checkFirstRadio) {
868 input.find('input[type="radio"]').first().prop('checked', true);
869 }
870 break;
871 }
872
873 // now place it in our form
874 form.append(input);
875
876 form.on('submit', function (e) {
877 e.preventDefault();
878 // Fix for SammyJS (or similar JS routing library) hijacking the form post.
879 e.stopPropagation();
880
881 // @TODO can we actually click *the* button object instead?
882 // e.g. buttons.confirm.click() or similar
883 promptDialog.find('.bootbox-accept').trigger('click');
884 });
885
886 if ($.trim(options.message) !== '') {
887 // Add the form to whatever content the user may have added.
888 var message = $(templates.promptMessage).html(options.message);
889 form.prepend(message);
890 options.message = form;
891 }
892 else {
893 options.message = form;
894 }
895
896 // Generate the dialog
897 promptDialog = exports.dialog(options);
898
899 // clear the existing handler focusing the submit button...
900 promptDialog.off('shown.bs.modal', focusPrimaryButton);
901
902 // ...and replace it with one focusing our input, if possible
903 promptDialog.on('shown.bs.modal', function () {
904 // need the closure here since input isn't
905 // an object otherwise
906 input.focus();
907 });
908
909 if (shouldShow === true) {
910 promptDialog.modal('show');
911 }
912
913 return promptDialog;
914 };
915
916
917 // INTERNAL FUNCTIONS
918 // *************************************************************************************************************
919
920 // Map a flexible set of arguments into a single returned object
921 // If args.length is already one just return it, otherwise
922 // use the properties argument to map the unnamed args to
923 // object properties.
924 // So in the latter case:
925 // mapArguments(["foo", $.noop], ["message", "callback"])
926 // -> { message: "foo", callback: $.noop }
927 function mapArguments(args, properties) {
928 var argn = args.length;
929 var options = {};
930
931 if (argn < 1 || argn > 2) {
932 throw new Error('Invalid argument length');
933 }
934
935 if (argn === 2 || typeof args[0] === 'string') {
936 options[properties[0]] = args[0];
937 options[properties[1]] = args[1];
938 } else {
939 options = args[0];
940 }
941
942 return options;
943 }
944
945
946 // Merge a set of default dialog options with user supplied arguments
947 function mergeArguments(defaults, args, properties) {
948 return $.extend(
949 // deep merge
950 true,
951 // ensure the target is an empty, unreferenced object
952 {},
953 // the base options object for this type of dialog (often just buttons)
954 defaults,
955 // args could be an object or array; if it's an array properties will
956 // map it to a proper options object
957 mapArguments(
958 args,
959 properties
960 )
961 );
962 }
963
964
965 // This entry-level method makes heavy use of composition to take a simple
966 // range of inputs and return valid options suitable for passing to bootbox.dialog
967 function mergeDialogOptions(className, labels, properties, args) {
968 var locale;
969 if (args && args[0]) {
970 locale = args[0].locale || defaults.locale;
971 var swapButtons = args[0].swapButtonOrder || defaults.swapButtonOrder;
972
973 if (swapButtons) {
974 labels = labels.reverse();
975 }
976 }
977
978 // build up a base set of dialog properties
979 var baseOptions = {
980 className: 'bootbox-' + className,
981 buttons: createLabels(labels, locale)
982 };
983
984 // Ensure the buttons properties generated, *after* merging
985 // with user args are still valid against the supplied labels
986 return validateButtons(
987 // merge the generated base properties with user supplied arguments
988 mergeArguments(
989 baseOptions,
990 args,
991 // if args.length > 1, properties specify how each arg maps to an object key
992 properties
993 ),
994 labels
995 );
996 }
997
998
999 // Checks each button object to see if key is valid.
1000 // This function will only be called by the alert, confirm, and prompt helpers.
1001 function validateButtons(options, buttons) {
1002 var allowedButtons = {};
1003 each(buttons, function (key, value) {
1004 allowedButtons[value] = true;
1005 });
1006
1007 each(options.buttons, function (key) {
1008 if (allowedButtons[key] === undefined) {
1009 throw new Error('button key "' + key + '" is not allowed (options are ' + buttons.join(' ') + ')');
1010 }
1011 });
1012
1013 return options;
1014 }
1015
1016
1017
1018 // From a given list of arguments, return a suitable object of button labels.
1019 // All this does is normalise the given labels and translate them where possible.
1020 // e.g. "ok", "confirm" -> { ok: "OK", cancel: "Annuleren" }
1021 function createLabels(labels, locale) {
1022 var buttons = {};
1023
1024 for (var i = 0, j = labels.length; i < j; i++) {
1025 var argument = labels[i];
1026 var key = argument.toLowerCase();
1027 var value = argument.toUpperCase();
1028
1029 buttons[key] = {
1030 label: getText(value, locale)
1031 };
1032 }
1033
1034 return buttons;
1035 }
1036
1037
1038
1039 // Get localized text from a locale. Defaults to 'en' locale if no locale
1040 // provided or a non-registered locale is requested
1041 function getText(key, locale) {
1042 var labels = locales[locale];
1043
1044 return labels ? labels[key] : locales.en[key];
1045 }
1046
1047
1048
1049 // Filter and tidy up any user supplied parameters to this dialog.
1050 // Also looks for any shorthands used and ensures that the options
1051 // which are returned are all normalized properly
1052 function sanitize(options) {
1053 var buttons;
1054 var total;
1055
1056 if (typeof options !== 'object') {
1057 throw new Error('Please supply an object of options');
1058 }
1059
1060 if (!options.message) {
1061 throw new Error('"message" option must not be null or an empty string.');
1062 }
1063
1064 // make sure any supplied options take precedence over defaults
1065 options = $.extend({}, defaults, options);
1066
1067 // no buttons is still a valid dialog but it's cleaner to always have
1068 // a buttons object to iterate over, even if it's empty
1069 if (!options.buttons) {
1070 options.buttons = {};
1071 }
1072
1073 buttons = options.buttons;
1074
1075 total = getKeyLength(buttons);
1076
1077 each(buttons, function (key, button, index) {
1078 if ($.isFunction(button)) {
1079 // short form, assume value is our callback. Since button
1080 // isn't an object it isn't a reference either so re-assign it
1081 button = buttons[key] = {
1082 callback: button
1083 };
1084 }
1085
1086 // before any further checks make sure by now button is the correct type
1087 if ($.type(button) !== 'object') {
1088 throw new Error('button with key "' + key + '" must be an object');
1089 }
1090
1091 if (!button.label) {
1092 // the lack of an explicit label means we'll assume the key is good enough
1093 button.label = key;
1094 }
1095
1096 if (!button.className) {
1097 var isPrimary = false;
1098 if (options.swapButtonOrder) {
1099 isPrimary = index === 0;
1100 }
1101 else {
1102 isPrimary = index === total - 1;
1103 }
1104
1105 if (total <= 2 && isPrimary) {
1106 // always add a primary to the main option in a one or two-button dialog
1107 button.className = 'btn-primary';
1108 } else {
1109 // adding both classes allows us to target both BS3 and BS4 without needing to check the version
1110 button.className = 'btn-secondary btn-default';
1111 }
1112 }
1113 });
1114
1115 return options;
1116 }
1117
1118
1119 // Returns a count of the properties defined on the object
1120 function getKeyLength(obj) {
1121 return Object.keys(obj).length;
1122 }
1123
1124
1125 // Tiny wrapper function around jQuery.each; just adds index as the third parameter
1126 function each(collection, iterator) {
1127 var index = 0;
1128 $.each(collection, function (key, value) {
1129 iterator(key, value, index++);
1130 });
1131 }
1132
1133
1134 function focusPrimaryButton(e) {
1135 e.data.dialog.find('.bootbox-accept').first().trigger('focus');
1136 }
1137
1138
1139 function destroyModal(e) {
1140 // ensure we don't accidentally intercept hidden events triggered
1141 // by children of the current dialog. We shouldn't need to handle this anymore,
1142 // now that Bootstrap namespaces its events, but still worth doing.
1143 if (e.target === e.data.dialog[0]) {
1144 e.data.dialog.remove();
1145 }
1146 }
1147
1148
1149 function unbindModal(e) {
1150 if (e.target === e.data.dialog[0]) {
1151 e.data.dialog.off('escape.close.bb');
1152 e.data.dialog.off('click');
1153 }
1154 }
1155
1156
1157 // Handle the invoked dialog callback
1158 function processCallback(e, dialog, callback) {
1159 e.stopPropagation();
1160 e.preventDefault();
1161
1162 // by default we assume a callback will get rid of the dialog,
1163 // although it is given the opportunity to override this
1164
1165 // so, if the callback can be invoked and it *explicitly returns false*
1166 // then we'll set a flag to keep the dialog active...
1167 var preserveDialog = $.isFunction(callback) && callback.call(dialog, e) === false;
1168
1169 // ... otherwise we'll bin it
1170 if (!preserveDialog) {
1171 dialog.modal('hide');
1172 }
1173 }
1174
1175 // Validate `min` and `max` values based on the current `inputType` value
1176 function minAndMaxAreValid(type, min, max) {
1177 var result = false;
1178 var minValid = true;
1179 var maxValid = true;
1180
1181 if (type === 'date') {
1182 if (min !== undefined && !(minValid = dateIsValid(min))) {
1183 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.');
1184 }
1185 else if (max !== undefined && !(maxValid = dateIsValid(max))) {
1186 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.');
1187 }
1188 }
1189 else if (type === 'time') {
1190 if (min !== undefined && !(minValid = timeIsValid(min))) {
1191 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.');
1192 }
1193 else if (max !== undefined && !(maxValid = timeIsValid(max))) {
1194 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.');
1195 }
1196 }
1197 else {
1198 if (min !== undefined && isNaN(min)) {
1199 minValid = false;
1200 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.');
1201 }
1202
1203 if (max !== undefined && isNaN(max)) {
1204 maxValid = false;
1205 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.');
1206 }
1207 }
1208
1209 if (minValid && maxValid) {
1210 if (max <= min) {
1211 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.');
1212 }
1213 else {
1214 result = true;
1215 }
1216 }
1217
1218 return result;
1219 }
1220
1221 function timeIsValid(value) {
1222 return /([01][0-9]|2[0-3]):[0-5][0-9]?:[0-5][0-9]/.test(value);
1223 }
1224
1225 function dateIsValid(value) {
1226 return /(\d{4})-(\d{2})-(\d{2})/.test(value);
1227 }
1228
1229 // The Bootbox object
1230 return exports;
1231 }));