bootbox.js 40 KB

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