bootbox-rails 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 53d96ab1df21b0acec764c8803adc535521af86c
4
+ data.tar.gz: 6d2c63ae9847cc96b476f87cfb030b70d63280e3
5
+ SHA512:
6
+ metadata.gz: 5525b7f43328f99a46060e88018b0e5c9e23f1a72245250e0be53d51284a9bfc9a1ec58e906251fdae3c71e8f81ac05f54b48c54d411770727d76af943782192
7
+ data.tar.gz: f854bf316d82974873f2fd031ebdb7a4e1b96fe63452dc3001a6592a0e92dc7b9abc718163a11f844e452bb9b5d3a298258361dc97f320a0958957a62618984e
data/Gemfile CHANGED
@@ -4,10 +4,10 @@ gemspec
4
4
  gem 'sqlite3'
5
5
 
6
6
  group :assets do
7
- gem 'sass-rails', ">= 3.1.0"
8
- gem 'coffee-rails', ">= 3.1.0"
7
+ gem 'sass-rails', "~>3.1"
8
+ gem 'coffee-rails', "~>3.1"
9
9
  gem 'uglifier'
10
- gem 'bootstrap-sass', '2.1.0.0'
10
+ gem 'bootstrap-sass', '3.0.2.0'
11
11
  end
12
12
 
13
13
  gem 'jquery-rails'
@@ -17,4 +17,4 @@ group :test do
17
17
  gem 'rspec-rails'
18
18
  gem 'capybara'
19
19
  gem 'capybara-webkit'
20
- end
20
+ end
data/README.md CHANGED
@@ -6,10 +6,16 @@ Check out how to use bootbox.js at http://bootboxjs.com/
6
6
 
7
7
  ## Installation
8
8
 
9
- First, put this line in your `Gemfile`:
9
+ First, put this line in your `Gemfile` (for Bootstrap 3):
10
10
 
11
11
  ```ruby
12
- gem 'bootbox-rails'
12
+ gem 'bootbox-rails', '~>0.2'
13
+ ```
14
+
15
+ For Bootstrap 2 use following version:
16
+
17
+ ```ruby
18
+ gem 'bootbox-rails', '~>0.1'
13
19
  ```
14
20
 
15
21
  _Don't forget to add `jquery-rails` gem into your `Gemfile`. Also you may use very handy `bootstrap-sass` gem to add full stack of Twitter Bootstrap into your app._
@@ -42,6 +48,13 @@ $(function() {
42
48
 
43
49
  Please see http://paynedigital.com/bootbox for full usage instructions, or head over to http://bootboxjs.com for a demo page
44
50
 
51
+ ## How to run tests
52
+
53
+ ```bash
54
+ $ cd spec/dummy && rake db:create
55
+ $ rspec spec/
56
+ ```
57
+
45
58
  ## License
46
59
 
47
60
  [The MIT License](https://github.com/tanraya/bootbox-rails/blob/master/MIT-LICENSE)
@@ -1,641 +1,784 @@
1
1
  /**
2
- * bootbox.js v3.2.0
2
+ * bootbox.js [v4.1.0]
3
3
  *
4
4
  * http://bootboxjs.com/license.txt
5
5
  */
6
- var bootbox = window.bootbox || (function(document, $) {
7
- /*jshint scripturl:true sub:true */
6
+ // @see https://github.com/makeusabrew/bootbox/issues/71
7
+ window.bootbox = window.bootbox || (function init($, undefined) {
8
+ "use strict";
9
+
10
+ // the base DOM structure needed to create a modal
11
+ var templates = {
12
+ dialog:
13
+ "<div class='bootbox modal' tabindex='-1' role='dialog'>" +
14
+ "<div class='modal-dialog'>" +
15
+ "<div class='modal-content'>" +
16
+ "<div class='modal-body'><div class='bootbox-body'></div></div>" +
17
+ "</div>" +
18
+ "</div>" +
19
+ "</div>",
20
+ header:
21
+ "<div class='modal-header'>" +
22
+ "<h4 class='modal-title'></h4>" +
23
+ "</div>",
24
+ footer:
25
+ "<div class='modal-footer'></div>",
26
+ closeButton:
27
+ "<button type='button' class='bootbox-close-button close'>&times;</button>",
28
+ form:
29
+ "<form class='bootbox-form'></form>",
30
+ inputs: {
31
+ text:
32
+ "<input class='bootbox-input bootbox-input-text form-control' autocomplete=off type=text />",
33
+ email:
34
+ "<input class='bootbox-input bootbox-input-email form-control' autocomplete='off' type='email' />",
35
+ select:
36
+ "<select class='bootbox-input bootbox-input-select form-control'></select>",
37
+ checkbox:
38
+ "<div class='checkbox'><label><input class='bootbox-input bootbox-input-checkbox' type='checkbox' /></label></div>"
39
+ }
40
+ };
41
+
42
+ // cache a reference to the jQueryfied body element
43
+ var appendTo = $("body");
44
+
45
+ var defaults = {
46
+ // default language
47
+ locale: "en",
48
+ // show backdrop or not
49
+ backdrop: true,
50
+ // animate the modal in/out
51
+ animate: true,
52
+ // additional class string applied to the top level dialog
53
+ className: null,
54
+ // whether or not to include a close button
55
+ closeButton: true,
56
+ // show the dialog immediately by default
57
+ show: true
58
+ };
59
+
60
+ // our public object; augmented after our private API
61
+ var exports = {};
62
+
63
+ /**
64
+ * @private
65
+ */
66
+ function _t(key) {
67
+ var locale = locales[defaults.locale];
68
+ return locale ? locale[key] : locales.en[key];
69
+ }
70
+
71
+ function processCallback(e, dialog, callback) {
72
+ e.preventDefault();
73
+
74
+ // by default we assume a callback will get rid of the dialog,
75
+ // although it is given the opportunity to override this
76
+
77
+ // so, if the callback can be invoked and it *explicitly returns false*
78
+ // then we'll set a flag to keep the dialog active...
79
+ var preserveDialog = $.isFunction(callback) && callback(e) === false;
80
+
81
+ // ... otherwise we'll bin it
82
+ if (!preserveDialog) {
83
+ dialog.modal("hide");
84
+ }
85
+ }
8
86
 
9
- var _locale = 'en',
10
- _defaultLocale = 'en',
11
- _animate = true,
12
- _backdrop = 'static',
13
- _defaultHref = 'javascript:;',
14
- _classes = '',
15
- _btnClasses = {},
16
- _icons = {},
17
- /* last var should always be the public object we'll return */
18
- that = {};
87
+ function getKeyLength(obj) {
88
+ // @TODO defer to Object.keys(x).length if available?
89
+ var k, t = 0;
90
+ for (k in obj) {
91
+ t ++;
92
+ }
93
+ return t;
94
+ }
95
+
96
+ function each(collection, iterator) {
97
+ var index = 0;
98
+ $.each(collection, function(key, value) {
99
+ iterator(key, value, index++);
100
+ });
101
+ }
102
+
103
+ function sanitize(options) {
104
+ var buttons;
105
+ var total;
106
+
107
+ if (typeof options !== "object") {
108
+ throw new Error("Please supply an object of options");
109
+ }
19
110
 
111
+ if (!options.message) {
112
+ throw new Error("Please specify a message");
113
+ }
20
114
 
21
- /**
22
- * public API
23
- */
24
- that.setLocale = function(locale) {
25
- for (var i in _locales) {
26
- if (i == locale) {
27
- _locale = locale;
28
- return;
29
- }
30
- }
31
- throw new Error('Invalid locale: '+locale);
32
- };
115
+ // make sure any supplied options take precedence over defaults
116
+ options = $.extend({}, defaults, options);
33
117
 
34
- that.addLocale = function(locale, translations) {
35
- if (typeof _locales[locale] === 'undefined') {
36
- _locales[locale] = {};
37
- }
38
- for (var str in translations) {
39
- _locales[locale][str] = translations[str];
40
- }
41
- };
118
+ if (!options.buttons) {
119
+ options.buttons = {};
120
+ }
42
121
 
43
- that.setIcons = function(icons) {
44
- _icons = icons;
45
- if (typeof _icons !== 'object' || _icons === null) {
46
- _icons = {};
47
- }
48
- };
122
+ // we only support Bootstrap's "static" and false backdrop args
123
+ // supporting true would mean you could dismiss the dialog without
124
+ // explicitly interacting with it
125
+ options.backdrop = options.backdrop ? "static" : false;
49
126
 
50
- that.setBtnClasses = function(btnClasses) {
51
- _btnClasses = btnClasses;
52
- if (typeof _btnClasses !== 'object' || _btnClasses === null) {
53
- _btnClasses = {};
54
- }
55
- };
127
+ buttons = options.buttons;
56
128
 
57
- that.alert = function(/*str, label, cb*/) {
58
- var str = "",
59
- label = _translate('OK'),
60
- cb = null;
61
-
62
- switch (arguments.length) {
63
- case 1:
64
- // no callback, default button label
65
- str = arguments[0];
66
- break;
67
- case 2:
68
- // callback *or* custom button label dependent on type
69
- str = arguments[0];
70
- if (typeof arguments[1] == 'function') {
71
- cb = arguments[1];
72
- } else {
73
- label = arguments[1];
74
- }
75
- break;
76
- case 3:
77
- // callback and custom button label
78
- str = arguments[0];
79
- label = arguments[1];
80
- cb = arguments[2];
81
- break;
82
- default:
83
- throw new Error("Incorrect number of arguments: expected 1-3");
84
- }
129
+ total = getKeyLength(buttons);
85
130
 
86
- return that.dialog(str, {
87
- // only button (ok)
88
- "label" : label,
89
- "icon" : _icons.OK,
90
- "class" : _btnClasses.OK,
91
- "callback": cb
92
- }, {
93
- // ensure that the escape key works; either invoking the user's
94
- // callback or true to just close the dialog
95
- "onEscape": cb || true
96
- });
97
- };
131
+ each(buttons, function(key, button, index) {
98
132
 
99
- that.confirm = function(/*str, labelCancel, labelOk, cb*/) {
100
- var str = "",
101
- labelCancel = _translate('CANCEL'),
102
- labelOk = _translate('CONFIRM'),
103
- cb = null;
104
-
105
- switch (arguments.length) {
106
- case 1:
107
- str = arguments[0];
108
- break;
109
- case 2:
110
- str = arguments[0];
111
- if (typeof arguments[1] == 'function') {
112
- cb = arguments[1];
113
- } else {
114
- labelCancel = arguments[1];
115
- }
116
- break;
117
- case 3:
118
- str = arguments[0];
119
- labelCancel = arguments[1];
120
- if (typeof arguments[2] == 'function') {
121
- cb = arguments[2];
122
- } else {
123
- labelOk = arguments[2];
124
- }
125
- break;
126
- case 4:
127
- str = arguments[0];
128
- labelCancel = arguments[1];
129
- labelOk = arguments[2];
130
- cb = arguments[3];
131
- break;
132
- default:
133
- throw new Error("Incorrect number of arguments: expected 1-4");
134
- }
135
-
136
- var cancelCallback = function() {
137
- if (typeof cb === 'function') {
138
- return cb(false);
139
- }
133
+ if ($.isFunction(button)) {
134
+ // short form, assume value is our callback. Since button
135
+ // isn't an object it isn't a reference either so re-assign it
136
+ button = buttons[key] = {
137
+ callback: button
140
138
  };
139
+ }
140
+
141
+ // before any further checks make sure by now button is the correct type
142
+ if ($.type(button) !== "object") {
143
+ throw new Error("button with key " + key + " must be an object");
144
+ }
145
+
146
+ if (!button.label) {
147
+ // the lack of an explicit label means we'll assume the key is good enough
148
+ button.label = key;
149
+ }
150
+
151
+ if (!button.className) {
152
+ if (total <= 2 && index === total-1) {
153
+ // always add a primary to the main option in a two-button dialog
154
+ button.className = "btn-primary";
155
+ } else {
156
+ button.className = "btn-default";
157
+ }
158
+ }
159
+ });
160
+
161
+ return options;
162
+ }
163
+
164
+ /**
165
+ * map a flexible set of arguments into a single returned object
166
+ * if args.length is already one just return it, otherwise
167
+ * use the properties argument to map the unnamed args to
168
+ * object properties
169
+ * so in the latter case:
170
+ * mapArguments(["foo", $.noop], ["message", "callback"])
171
+ * -> { message: "foo", callback: $.noop }
172
+ */
173
+ function mapArguments(args, properties) {
174
+ var argn = args.length;
175
+ var options = {};
176
+
177
+ if (argn < 1 || argn > 2) {
178
+ throw new Error("Invalid argument length");
179
+ }
141
180
 
142
- var confirmCallback = function() {
143
- if (typeof cb === 'function') {
144
- return cb(true);
145
- }
146
- };
181
+ if (argn === 2 || typeof args[0] === "string") {
182
+ options[properties[0]] = args[0];
183
+ options[properties[1]] = args[1];
184
+ } else {
185
+ options = args[0];
186
+ }
147
187
 
148
- return that.dialog(str, [{
149
- // first button (cancel)
150
- "label" : labelCancel,
151
- "icon" : _icons.CANCEL,
152
- "class" : _btnClasses.CANCEL,
153
- "callback": cancelCallback
154
- }, {
155
- // second button (confirm)
156
- "label" : labelOk,
157
- "icon" : _icons.CONFIRM,
158
- "class" : _btnClasses.CONFIRM,
159
- "callback": confirmCallback
160
- }], {
161
- // escape key bindings
162
- "onEscape": cancelCallback
163
- });
188
+ return options;
189
+ }
190
+
191
+ /**
192
+ * merge a set of default dialog options with user supplied arguments
193
+ */
194
+ function mergeArguments(defaults, args, properties) {
195
+ return $.extend(
196
+ // deep merge
197
+ true,
198
+ // ensure the target is an empty, unreferenced object
199
+ {},
200
+ // the base options object for this type of dialog (often just buttons)
201
+ defaults,
202
+ // args could be an object or array; if it's an array properties will
203
+ // map it to a proper options object
204
+ mapArguments(
205
+ args,
206
+ properties
207
+ )
208
+ );
209
+ }
210
+
211
+ /**
212
+ * this entry-level method makes heavy use of composition to take a simple
213
+ * range of inputs and return valid options suitable for passing to bootbox.dialog
214
+ */
215
+ function mergeDialogOptions(className, labels, properties, args) {
216
+ // build up a base set of dialog properties
217
+ var baseOptions = {
218
+ className: "bootbox-" + className,
219
+ buttons: createLabels.apply(null, labels)
164
220
  };
165
221
 
166
- that.prompt = function(/*str, labelCancel, labelOk, cb, defaultVal*/) {
167
- var str = "",
168
- labelCancel = _translate('CANCEL'),
169
- labelOk = _translate('CONFIRM'),
170
- cb = null,
171
- defaultVal = "";
172
-
173
- switch (arguments.length) {
174
- case 1:
175
- str = arguments[0];
176
- break;
177
- case 2:
178
- str = arguments[0];
179
- if (typeof arguments[1] == 'function') {
180
- cb = arguments[1];
181
- } else {
182
- labelCancel = arguments[1];
183
- }
184
- break;
185
- case 3:
186
- str = arguments[0];
187
- labelCancel = arguments[1];
188
- if (typeof arguments[2] == 'function') {
189
- cb = arguments[2];
190
- } else {
191
- labelOk = arguments[2];
192
- }
193
- break;
194
- case 4:
195
- str = arguments[0];
196
- labelCancel = arguments[1];
197
- labelOk = arguments[2];
198
- cb = arguments[3];
199
- break;
200
- case 5:
201
- str = arguments[0];
202
- labelCancel = arguments[1];
203
- labelOk = arguments[2];
204
- cb = arguments[3];
205
- defaultVal = arguments[4];
206
- break;
207
- default:
208
- throw new Error("Incorrect number of arguments: expected 1-5");
209
- }
222
+ // ensure the buttons properties generated, *after* merging
223
+ // with user args are still valid against the supplied labels
224
+ return validateButtons(
225
+ // merge the generated base properties with user supplied arguments
226
+ mergeArguments(
227
+ baseOptions,
228
+ args,
229
+ // if args.length > 1, properties specify how each arg maps to an object key
230
+ properties
231
+ ),
232
+ labels
233
+ );
234
+ }
235
+
236
+ /**
237
+ * from a given list of arguments return a suitable object of button labels
238
+ * all this does is normalise the given labels and translate them where possible
239
+ * e.g. "ok", "confirm" -> { ok: "OK, cancel: "Annuleren" }
240
+ */
241
+ function createLabels() {
242
+ var buttons = {};
243
+
244
+ for (var i = 0, j = arguments.length; i < j; i++) {
245
+ var argument = arguments[i];
246
+ var key = argument.toLowerCase();
247
+ var value = argument.toUpperCase();
248
+
249
+ buttons[key] = {
250
+ label: _t(value)
251
+ };
252
+ }
210
253
 
211
- var header = str;
254
+ return buttons;
255
+ }
212
256
 
213
- // let's keep a reference to the form object for later
214
- var form = $("<form></form>");
215
- form.append("<input autocomplete=off type=text value='" + defaultVal + "' />");
257
+ function validateButtons(options, buttons) {
258
+ var allowedButtons = {};
259
+ each(buttons, function(key, value) {
260
+ allowedButtons[value] = true;
261
+ });
216
262
 
217
- var cancelCallback = function() {
218
- if (typeof cb === 'function') {
219
- // yep, native prompts dismiss with null, whereas native
220
- // confirms dismiss with false...
221
- return cb(null);
222
- }
223
- };
263
+ each(options.buttons, function(key) {
264
+ if (allowedButtons[key] === undefined) {
265
+ throw new Error("button key " + key + " is not allowed (options are " + buttons.join("\n") + ")");
266
+ }
267
+ });
224
268
 
225
- var confirmCallback = function() {
226
- if (typeof cb === 'function') {
227
- return cb(form.find("input[type=text]").val());
228
- }
229
- };
269
+ return options;
270
+ }
230
271
 
231
- var div = that.dialog(form, [{
232
- // first button (cancel)
233
- "label" : labelCancel,
234
- "icon" : _icons.CANCEL,
235
- "class" : _btnClasses.CANCEL,
236
- "callback": cancelCallback
237
- }, {
238
- // second button (confirm)
239
- "label" : labelOk,
240
- "icon" : _icons.CONFIRM,
241
- "class" : _btnClasses.CONFIRM,
242
- "callback": confirmCallback
243
- }], {
244
- // prompts need a few extra options
245
- "header" : header,
246
- // explicitly tell dialog NOT to show the dialog...
247
- "show" : false,
248
- "onEscape": cancelCallback
249
- });
272
+ exports.alert = function() {
273
+ var options;
250
274
 
251
- // ... the reason the prompt needs to be hidden is because we need
252
- // to bind our own "shown" handler, after creating the modal but
253
- // before any show(n) events are triggered
254
- // @see https://github.com/makeusabrew/bootbox/issues/69
275
+ options = mergeDialogOptions("alert", ["ok"], ["message", "callback"], arguments);
255
276
 
256
- div.on("shown", function() {
257
- form.find("input[type=text]").focus();
277
+ if (options.callback && !$.isFunction(options.callback)) {
278
+ throw new Error("alert requires callback property to be a function when provided");
279
+ }
258
280
 
259
- // ensure that submitting the form (e.g. with the enter key)
260
- // replicates the behaviour of a normal prompt()
261
- form.on("submit", function(e) {
262
- e.preventDefault();
263
- div.find(".btn-primary").click();
264
- });
265
- });
281
+ /**
282
+ * overrides
283
+ */
284
+ options.buttons.ok.callback = options.onEscape = function() {
285
+ if ($.isFunction(options.callback)) {
286
+ return options.callback();
287
+ }
288
+ return true;
289
+ };
266
290
 
267
- div.modal("show");
291
+ return exports.dialog(options);
292
+ };
268
293
 
269
- return div;
294
+ exports.confirm = function() {
295
+ var options;
296
+
297
+ options = mergeDialogOptions("confirm", ["cancel", "confirm"], ["message", "callback"], arguments);
298
+
299
+ /**
300
+ * overrides; undo anything the user tried to set they shouldn't have
301
+ */
302
+ options.buttons.cancel.callback = options.onEscape = function() {
303
+ return options.callback(false);
270
304
  };
271
305
 
272
- that.dialog = function(str, handlers, options) {
273
- var buttons = "",
274
- callbacks = [];
306
+ options.buttons.confirm.callback = function() {
307
+ return options.callback(true);
308
+ };
275
309
 
276
- if (!options) {
277
- options = {};
278
- }
310
+ // confirm specific validation
311
+ if (!$.isFunction(options.callback)) {
312
+ throw new Error("confirm requires a callback");
313
+ }
279
314
 
280
- // check for single object and convert to array if necessary
281
- if (typeof handlers === 'undefined') {
282
- handlers = [];
283
- } else if (typeof handlers.length == 'undefined') {
284
- handlers = [handlers];
285
- }
315
+ return exports.dialog(options);
316
+ };
317
+
318
+ exports.prompt = function() {
319
+ var options;
320
+ var defaults;
321
+ var dialog;
322
+ var form;
323
+ var input;
324
+ var shouldShow;
325
+ var inputOptions;
326
+
327
+ // we have to create our form first otherwise
328
+ // its value is undefined when gearing up our options
329
+ // @TODO this could be solved by allowing message to
330
+ // be a function instead...
331
+ form = $(templates.form);
332
+
333
+ // prompt defaults are more complex than others in that
334
+ // users can override more defaults
335
+ // @TODO I don't like that prompt has to do a lot of heavy
336
+ // lifting which mergeDialogOptions can *almost* support already
337
+ // just because of 'value' and 'inputType' - can we refactor?
338
+ defaults = {
339
+ className: "bootbox-prompt",
340
+ buttons: createLabels("cancel", "confirm"),
341
+ value: "",
342
+ inputType: "text"
343
+ };
286
344
 
287
- var i = handlers.length;
288
- while (i--) {
289
- var label = null,
290
- href = null,
291
- _class = null,
292
- icon = '',
293
- callback = null;
294
-
295
- if (typeof handlers[i]['label'] == 'undefined' &&
296
- typeof handlers[i]['class'] == 'undefined' &&
297
- typeof handlers[i]['callback'] == 'undefined') {
298
- // if we've got nothing we expect, check for condensed format
299
-
300
- var propCount = 0, // condensed will only match if this == 1
301
- property = null; // save the last property we found
302
-
303
- // be nicer to count the properties without this, but don't think it's possible...
304
- for (var j in handlers[i]) {
305
- property = j;
306
- if (++propCount > 1) {
307
- // forget it, too many properties
308
- break;
309
- }
310
- }
311
-
312
- if (propCount == 1 && typeof handlers[i][j] == 'function') {
313
- // matches condensed format of label -> function
314
- handlers[i]['label'] = property;
315
- handlers[i]['callback'] = handlers[i][j];
316
- }
317
- }
345
+ options = validateButtons(
346
+ mergeArguments(defaults, arguments, ["title", "callback"]),
347
+ ["cancel", "confirm"]
348
+ );
318
349
 
319
- if (typeof handlers[i]['callback']== 'function') {
320
- callback = handlers[i]['callback'];
321
- }
350
+ // capture the user's show value; we always set this to false before
351
+ // spawning the dialog to give us a chance to attach some handlers to
352
+ // it, but we need to make sure we respect a preference not to show it
353
+ shouldShow = (options.show === undefined) ? true : options.show;
322
354
 
323
- if (handlers[i]['class']) {
324
- _class = handlers[i]['class'];
325
- } else if (i == handlers.length -1 && handlers.length <= 2) {
326
- // always add a primary to the main option in a two-button dialog
327
- _class = 'btn-primary';
328
- }
355
+ /**
356
+ * overrides; undo anything the user tried to set they shouldn't have
357
+ */
358
+ options.message = form;
329
359
 
330
- if (handlers[i]['label']) {
331
- label = handlers[i]['label'];
332
- } else {
333
- label = "Option "+(i+1);
334
- }
360
+ options.buttons.cancel.callback = options.onEscape = function() {
361
+ return options.callback(null);
362
+ };
335
363
 
336
- if (handlers[i]['icon']) {
337
- icon = "<i class='"+handlers[i]['icon']+"'></i> ";
338
- }
364
+ options.buttons.confirm.callback = function() {
365
+ var value;
339
366
 
340
- if (handlers[i]['href']) {
341
- href = handlers[i]['href'];
342
- }
343
- else {
344
- href = _defaultHref;
345
- }
367
+ switch (options.inputType) {
368
+ case "text":
369
+ case "email":
370
+ case "select":
371
+ value = input.val();
372
+ break;
346
373
 
347
- buttons = "<a data-handler='"+i+"' class='btn "+_class+"' href='" + href + "'>"+icon+""+label+"</a>" + buttons;
374
+ case "checkbox":
375
+ var checkedItems = input.find("input:checked");
348
376
 
349
- callbacks[i] = callback;
350
- }
377
+ // we assume that checkboxes are always multiple,
378
+ // hence we default to an empty array
379
+ value = [];
351
380
 
352
- // @see https://github.com/makeusabrew/bootbox/issues/46#issuecomment-8235302
353
- // and https://github.com/twitter/bootstrap/issues/4474
354
- // for an explanation of the inline overflow: hidden
355
- // @see https://github.com/twitter/bootstrap/issues/4854
356
- // for an explanation of tabIndex=-1
381
+ each(checkedItems, function(_, item) {
382
+ value.push($(item).val());
383
+ });
384
+ break;
385
+ }
357
386
 
358
- var parts = ["<div class='bootbox modal' tabindex='-1' style='overflow:hidden;'>"];
387
+ return options.callback(value);
388
+ };
359
389
 
360
- if (options['header']) {
361
- var closeButton = '';
362
- if (typeof options['headerCloseButton'] == 'undefined' || options['headerCloseButton']) {
363
- closeButton = "<a href='"+_defaultHref+"' class='close'>&times;</a>";
364
- }
390
+ options.show = false;
365
391
 
366
- parts.push("<div class='modal-header'>"+closeButton+"<h3>"+options['header']+"</h3></div>");
367
- }
392
+ // prompt specific validation
393
+ if (!options.title) {
394
+ throw new Error("prompt requires a title");
395
+ }
368
396
 
369
- // push an empty body into which we'll inject the proper content later
370
- parts.push("<div class='modal-body'></div>");
397
+ if (!$.isFunction(options.callback)) {
398
+ throw new Error("prompt requires a callback");
399
+ }
371
400
 
372
- if (buttons) {
373
- parts.push("<div class='modal-footer'>"+buttons+"</div>");
374
- }
401
+ if (!templates.inputs[options.inputType]) {
402
+ throw new Error("invalid prompt type");
403
+ }
375
404
 
376
- parts.push("</div>");
405
+ // create the input based on the supplied type
406
+ input = $(templates.inputs[options.inputType]);
377
407
 
378
- var div = $(parts.join("\n"));
408
+ switch (options.inputType) {
409
+ case "text":
410
+ case "email":
411
+ input.val(options.value);
412
+ break;
379
413
 
380
- // check whether we should fade in/out
381
- var shouldFade = (typeof options.animate === 'undefined') ? _animate : options.animate;
414
+ case "select":
415
+ var groups = {};
416
+ inputOptions = options.inputOptions || [];
382
417
 
383
- if (shouldFade) {
384
- div.addClass("fade");
418
+ if (!inputOptions.length) {
419
+ throw new Error("prompt with select requires options");
385
420
  }
386
421
 
387
- var optionalClasses = (typeof options.classes === 'undefined') ? _classes : options.classes;
388
- if (optionalClasses) {
389
- div.addClass(optionalClasses);
390
- }
422
+ each(inputOptions, function(_, option) {
391
423
 
392
- // now we've built up the div properly we can inject the content whether it was a string or a jQuery object
393
- div.find(".modal-body").html(str);
424
+ // assume the element to attach to is the input...
425
+ var elem = input;
394
426
 
395
- function onCancel(source) {
396
- // for now source is unused, but it will be in future
397
- var hideModal = null;
398
- if (typeof options.onEscape === 'function') {
399
- // @see https://github.com/makeusabrew/bootbox/issues/91
400
- hideModal = options.onEscape();
401
- }
427
+ if (option.value === undefined || option.text === undefined) {
428
+ throw new Error("given options in wrong format");
429
+ }
402
430
 
403
- if (hideModal !== false) {
404
- div.modal('hide');
405
- }
406
- }
407
431
 
408
- // hook into the modal's keyup trigger to check for the escape key
409
- div.on('keyup.dismiss.modal', function(e) {
410
- // any truthy value passed to onEscape will dismiss the dialog
411
- // as long as the onEscape function (if defined) doesn't prevent it
412
- if (e.which === 27 && options.onEscape) {
413
- onCancel('escape');
432
+ // ... but override that element if this option sits in a group
433
+
434
+ if (option.group) {
435
+ // initialise group if necessary
436
+ if (!groups[option.group]) {
437
+ groups[option.group] = $("<optgroup/>").attr("label", option.group);
414
438
  }
415
- });
416
439
 
417
- // handle close buttons too
418
- div.on('click', 'a.close', function(e) {
419
- e.preventDefault();
420
- onCancel('close');
421
- });
440
+ elem = groups[option.group];
441
+ }
422
442
 
423
- // well, *if* we have a primary - give the first dom element focus
424
- div.on('shown', function() {
425
- div.find("a.btn-primary:first").focus();
443
+ elem.append("<option value='" + option.value + "'>" + option.text + "</option>");
426
444
  });
427
445
 
428
- div.on('hidden', function() {
429
- div.remove();
446
+ each(groups, function(_, group) {
447
+ input.append(group);
430
448
  });
431
449
 
432
- // wire up button handlers
433
- div.on('click', '.modal-footer a', function(e) {
450
+ // safe to set a select's value as per a normal input
451
+ input.val(options.value);
452
+ break;
453
+
454
+ case "checkbox":
455
+ var values = $.isArray(options.value) ? options.value : [options.value];
456
+ inputOptions = options.inputOptions || [];
434
457
 
435
- var handler = $(this).data("handler"),
436
- cb = callbacks[handler],
437
- hideModal = null;
458
+ if (!inputOptions.length) {
459
+ throw new Error("prompt with checkbox requires options");
460
+ }
438
461
 
439
- // sort of @see https://github.com/makeusabrew/bootbox/pull/68 - heavily adapted
440
- // if we've got a custom href attribute, all bets are off
441
- if (typeof handler !== 'undefined' &&
442
- typeof handlers[handler]['href'] !== 'undefined') {
462
+ if (!inputOptions[0].value || !inputOptions[0].text) {
463
+ throw new Error("given options in wrong format");
464
+ }
443
465
 
444
- return;
445
- }
466
+ // checkboxes have to nest within a containing element, so
467
+ // they break the rules a bit and we end up re-assigning
468
+ // our 'input' element to this container instead
469
+ input = $("<div/>");
446
470
 
447
- e.preventDefault();
471
+ each(inputOptions, function(_, option) {
472
+ var checkbox = $(templates.inputs[options.inputType]);
448
473
 
449
- if (typeof cb === 'function') {
450
- hideModal = cb();
451
- }
474
+ checkbox.find("input").attr("value", option.value);
475
+ checkbox.find("label").append(option.text);
452
476
 
453
- // the only way hideModal *will* be false is if a callback exists and
454
- // returns it as a value. in those situations, don't hide the dialog
455
- // @see https://github.com/makeusabrew/bootbox/pull/25
456
- if (hideModal !== false) {
457
- div.modal("hide");
477
+ // we've ensured values is an array so we can always iterate over it
478
+ each(values, function(_, value) {
479
+ if (value === option.value) {
480
+ checkbox.find("input").prop("checked", true);
458
481
  }
459
- });
482
+ });
460
483
 
461
- // stick the modal right at the bottom of the main body out of the way
462
- $("body").append(div);
463
-
464
- div.modal({
465
- // unless explicitly overridden take whatever our default backdrop value is
466
- backdrop : (typeof options.backdrop === 'undefined') ? _backdrop : options.backdrop,
467
- // ignore bootstrap's keyboard options; we'll handle this ourselves (more fine-grained control)
468
- keyboard : false,
469
- // @ see https://github.com/makeusabrew/bootbox/issues/69
470
- // we *never* want the modal to be shown before we can bind stuff to it
471
- // this method can also take a 'show' option, but we'll only use that
472
- // later if we need to
473
- show : false
484
+ input.append(checkbox);
474
485
  });
486
+ break;
487
+ }
475
488
 
476
- // @see https://github.com/makeusabrew/bootbox/issues/64
477
- // @see https://github.com/makeusabrew/bootbox/issues/60
478
- // ...caused by...
479
- // @see https://github.com/twitter/bootstrap/issues/4781
480
- div.on("show", function(e) {
481
- $(document).off("focusin.modal");
482
- });
489
+ if (options.placeholder) {
490
+ input.attr("placeholder", options.placeholder);
491
+ }
483
492
 
484
- if (typeof options.show === 'undefined' || options.show === true) {
485
- div.modal("show");
486
- }
493
+ // now place it in our form
494
+ form.append(input);
487
495
 
488
- return div;
489
- };
496
+ form.on("submit", function(e) {
497
+ e.preventDefault();
498
+ // @TODO can we actually click *the* button object instead?
499
+ // e.g. buttons.confirm.click() or similar
500
+ dialog.find(".btn-primary").click();
501
+ });
490
502
 
491
- /**
492
- * #modal is deprecated in v3; it can still be used but no guarantees are
493
- * made - have never been truly convinced of its merit but perhaps just
494
- * needs a tidyup and some TLC
495
- */
496
- that.modal = function(/*str, label, options*/) {
497
- var str;
498
- var label;
499
- var options;
500
-
501
- var defaultOptions = {
502
- "onEscape": null,
503
- "keyboard": true,
504
- "backdrop": _backdrop
505
- };
503
+ dialog = exports.dialog(options);
506
504
 
507
- switch (arguments.length) {
508
- case 1:
509
- str = arguments[0];
510
- break;
511
- case 2:
512
- str = arguments[0];
513
- if (typeof arguments[1] == 'object') {
514
- options = arguments[1];
515
- } else {
516
- label = arguments[1];
517
- }
518
- break;
519
- case 3:
520
- str = arguments[0];
521
- label = arguments[1];
522
- options = arguments[2];
523
- break;
524
- default:
525
- throw new Error("Incorrect number of arguments: expected 1-3");
526
- }
505
+ // clear the existing handler focusing the submit button...
506
+ dialog.off("shown.bs.modal");
527
507
 
528
- defaultOptions['header'] = label;
508
+ // ...and replace it with one focusing our input, if possible
509
+ dialog.on("shown.bs.modal", function() {
510
+ input.focus();
511
+ });
529
512
 
530
- if (typeof options == 'object') {
531
- options = $.extend(defaultOptions, options);
532
- } else {
533
- options = defaultOptions;
534
- }
513
+ if (shouldShow === true) {
514
+ dialog.modal("show");
515
+ }
535
516
 
536
- return that.dialog(str, [], options);
537
- };
517
+ return dialog;
518
+ };
538
519
 
520
+ exports.dialog = function(options) {
521
+ options = sanitize(options);
539
522
 
540
- that.hideAll = function() {
541
- $(".bootbox").modal("hide");
523
+ var dialog = $(templates.dialog);
524
+ var body = dialog.find(".modal-body");
525
+ var buttons = options.buttons;
526
+ var buttonStr = "";
527
+ var callbacks = {
528
+ onEscape: options.onEscape
542
529
  };
543
530
 
544
- that.animate = function(animate) {
545
- _animate = animate;
546
- };
531
+ each(buttons, function(key, button) {
547
532
 
548
- that.backdrop = function(backdrop) {
549
- _backdrop = backdrop;
550
- };
533
+ // @TODO I don't like this string appending to itself; bit dirty. Needs reworking
534
+ // can we just build up button elements instead? slower but neater. Then button
535
+ // can just become a template too
536
+ buttonStr += "<button data-bb-handler='" + key + "' type='button' class='btn " + button.className + "'>" + button.label + "</button>";
537
+ callbacks[key] = button.callback;
538
+ });
539
+
540
+ body.find(".bootbox-body").html(options.message);
541
+
542
+ if (options.animate === true) {
543
+ dialog.addClass("fade");
544
+ }
545
+
546
+ if (options.className) {
547
+ dialog.addClass(options.className);
548
+ }
549
+
550
+ if (options.title) {
551
+ body.before(templates.header);
552
+ }
553
+
554
+ if (options.closeButton) {
555
+ var closeButton = $(templates.closeButton);
556
+
557
+ if (options.title) {
558
+ dialog.find(".modal-header").prepend(closeButton);
559
+ } else {
560
+ closeButton.css("margin-top", "-10px").prependTo(body);
561
+ }
562
+ }
563
+
564
+ if (options.title) {
565
+ dialog.find(".modal-title").html(options.title);
566
+ }
567
+
568
+ if (buttonStr.length) {
569
+ body.after(templates.footer);
570
+ dialog.find(".modal-footer").html(buttonStr);
571
+ }
551
572
 
552
- that.classes = function(classes) {
553
- _classes = classes;
554
- };
555
573
 
556
574
  /**
557
- * private API
575
+ * Bootstrap event listeners; used handle extra
576
+ * setup & teardown required after the underlying
577
+ * modal has performed certain actions
558
578
  */
559
579
 
580
+ dialog.on("hidden.bs.modal", function(e) {
581
+ // ensure we don't accidentally intercept hidden events triggered
582
+ // by children of the current dialog. We shouldn't anymore now BS
583
+ // namespaces its events; but still worth doing
584
+ if (e.target === this) {
585
+ dialog.remove();
586
+ }
587
+ });
588
+
589
+ /*
590
+ dialog.on("show.bs.modal", function() {
591
+ // sadly this doesn't work; show is called *just* before
592
+ // the backdrop is added so we'd need a setTimeout hack or
593
+ // otherwise... leaving in as would be nice
594
+ if (options.backdrop) {
595
+ dialog.next(".modal-backdrop").addClass("bootbox-backdrop");
596
+ }
597
+ });
598
+ */
599
+
600
+ dialog.on("shown.bs.modal", function() {
601
+ dialog.find(".btn-primary:first").focus();
602
+ });
603
+
604
+ /**
605
+ * Bootbox event listeners; experimental and may not last
606
+ * just an attempt to decouple some behaviours from their
607
+ * respective triggers
608
+ */
609
+
610
+ dialog.on("escape.close.bb", function(e) {
611
+ if (callbacks.onEscape) {
612
+ processCallback(e, dialog, callbacks.onEscape);
613
+ }
614
+ });
615
+
560
616
  /**
561
- * standard locales. Please add more according to ISO 639-1 standard. Multiple language variants are
562
- * unlikely to be required. If this gets too large it can be split out into separate JS files.
617
+ * Standard jQuery event listeners; used to handle user
618
+ * interaction with our dialog
563
619
  */
564
- var _locales = {
565
- 'br' : {
566
- OK : 'OK',
567
- CANCEL : 'Cancelar',
568
- CONFIRM : 'Sim'
569
- },
570
- 'da' : {
571
- OK : 'OK',
572
- CANCEL : 'Annuller',
573
- CONFIRM : 'Accepter'
574
- },
575
- 'de' : {
576
- OK : 'OK',
577
- CANCEL : 'Abbrechen',
578
- CONFIRM : 'Akzeptieren'
579
- },
580
- 'en' : {
581
- OK : 'OK',
582
- CANCEL : 'Cancel',
583
- CONFIRM : 'OK'
584
- },
585
- 'es' : {
586
- OK : 'OK',
587
- CANCEL : 'Cancelar',
588
- CONFIRM : 'Aceptar'
589
- },
590
- 'fr' : {
591
- OK : 'OK',
592
- CANCEL : 'Annuler',
593
- CONFIRM : 'D\'accord'
594
- },
595
- 'it' : {
596
- OK : 'OK',
597
- CANCEL : 'Annulla',
598
- CONFIRM : 'Conferma'
599
- },
600
- 'nl' : {
601
- OK : 'OK',
602
- CANCEL : 'Annuleren',
603
- CONFIRM : 'Accepteren'
604
- },
605
- 'pl' : {
606
- OK : 'OK',
607
- CANCEL : 'Anuluj',
608
- CONFIRM : 'Potwierdź'
609
- },
610
- 'ru' : {
611
- OK : 'OK',
612
- CANCEL : 'Отмена',
613
- CONFIRM : 'Применить'
614
- },
620
+
621
+ dialog.on("click", ".modal-footer button", function(e) {
622
+ var callbackKey = $(this).data("bb-handler");
623
+
624
+ processCallback(e, dialog, callbacks[callbackKey]);
625
+
626
+ });
627
+
628
+ dialog.on("click", ".bootbox-close-button", function(e) {
629
+ // onEscape might be falsy but that's fine; the fact is
630
+ // if the user has managed to click the close button we
631
+ // have to close the dialog, callback or not
632
+ processCallback(e, dialog, callbacks.onEscape);
633
+ });
634
+
635
+ dialog.on("keyup", function(e) {
636
+ if (e.which === 27) {
637
+ dialog.trigger("escape.close.bb");
638
+ }
639
+ });
640
+
641
+ // the remainder of this method simply deals with adding our
642
+ // dialogent to the DOM, augmenting it with Bootstrap's modal
643
+ // functionality and then giving the resulting object back
644
+ // to our caller
645
+
646
+ appendTo.append(dialog);
647
+
648
+ dialog.modal({
649
+ backdrop: options.backdrop,
650
+ keyboard: false,
651
+ show: false
652
+ });
653
+
654
+ if (options.show) {
655
+ dialog.modal("show");
656
+ }
657
+
658
+ // @TODO should we return the raw element here or should
659
+ // we wrap it in an object on which we can expose some neater
660
+ // methods, e.g. var d = bootbox.alert(); d.hide(); instead
661
+ // of d.modal("hide");
662
+
663
+ /*
664
+ function BBDialog(elem) {
665
+ this.elem = elem;
666
+ }
667
+
668
+ BBDialog.prototype = {
669
+ hide: function() {
670
+ return this.elem.modal("hide");
671
+ },
672
+ show: function() {
673
+ return this.elem.modal("show");
674
+ }
615
675
  };
676
+ */
616
677
 
617
- function _translate(str, locale) {
618
- // we assume if no target locale is probided then we should take it from current setting
619
- if (typeof locale === 'undefined') {
620
- locale = _locale;
621
- }
622
- if (typeof _locales[locale][str] === 'string') {
623
- return _locales[locale][str];
624
- }
678
+ return dialog;
625
679
 
626
- // if we couldn't find a lookup then try and fallback to a default translation
680
+ };
627
681
 
628
- if (locale != _defaultLocale) {
629
- return _translate(str, _defaultLocale);
630
- }
682
+ exports.setDefaults = function() {
683
+ var values = {};
684
+
685
+ if (arguments.length === 2) {
686
+ // allow passing of single key/value...
687
+ values[arguments[0]] = arguments[1];
688
+ } else {
689
+ // ... and as an object too
690
+ values = arguments[0];
691
+ }
631
692
 
632
- // if we can't do anything then bail out with whatever string was passed in - last resort
633
- return str;
693
+ $.extend(defaults, values);
694
+ };
695
+
696
+ exports.hideAll = function() {
697
+ $(".bootbox").modal("hide");
698
+ };
699
+
700
+
701
+ /**
702
+ * standard locales. Please add more according to ISO 639-1 standard. Multiple language variants are
703
+ * unlikely to be required. If this gets too large it can be split out into separate JS files.
704
+ */
705
+ var locales = {
706
+ br : {
707
+ OK : "OK",
708
+ CANCEL : "Cancelar",
709
+ CONFIRM : "Sim"
710
+ },
711
+ da : {
712
+ OK : "OK",
713
+ CANCEL : "Annuller",
714
+ CONFIRM : "Accepter"
715
+ },
716
+ de : {
717
+ OK : "OK",
718
+ CANCEL : "Abbrechen",
719
+ CONFIRM : "Akzeptieren"
720
+ },
721
+ en : {
722
+ OK : "OK",
723
+ CANCEL : "Cancel",
724
+ CONFIRM : "OK"
725
+ },
726
+ es : {
727
+ OK : "OK",
728
+ CANCEL : "Cancelar",
729
+ CONFIRM : "Aceptar"
730
+ },
731
+ fi : {
732
+ OK : "OK",
733
+ CANCEL : "Peruuta",
734
+ CONFIRM : "OK"
735
+ },
736
+ fr : {
737
+ OK : "OK",
738
+ CANCEL : "Annuler",
739
+ CONFIRM : "D'accord"
740
+ },
741
+ it : {
742
+ OK : "OK",
743
+ CANCEL : "Annulla",
744
+ CONFIRM : "Conferma"
745
+ },
746
+ nl : {
747
+ OK : "OK",
748
+ CANCEL : "Annuleren",
749
+ CONFIRM : "Accepteren"
750
+ },
751
+ no : {
752
+ OK : "OK",
753
+ CANCEL : "Avbryt",
754
+ CONFIRM : "OK"
755
+ },
756
+ pl : {
757
+ OK : "OK",
758
+ CANCEL : "Anuluj",
759
+ CONFIRM : "Potwierdź"
760
+ },
761
+ ru : {
762
+ OK : "OK",
763
+ CANCEL : "Отмена",
764
+ CONFIRM : "Применить"
765
+ },
766
+ zh_CN : {
767
+ OK : "OK",
768
+ CANCEL : "取消",
769
+ CONFIRM : "确认"
770
+ },
771
+ zh_TW : {
772
+ OK : "OK",
773
+ CANCEL : "取消",
774
+ CONFIRM : "確認"
634
775
  }
776
+ };
635
777
 
636
- return that;
778
+ exports.init = function(_$) {
779
+ window.bootbox = init(_$ || $);
780
+ };
637
781
 
638
- }(document, window.jQuery));
782
+ return exports;
639
783
 
640
- // @see https://github.com/makeusabrew/bootbox/issues/71
641
- window.bootbox = bootbox;
784
+ }(window.jQuery));