bootbox-rails 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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));