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.js

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

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