bootstrap_wizard_rails 0.1.1

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: cfa55fbc433cdfaaca8f109390f57098faae2a74
4
+ data.tar.gz: 30fc59864b37f207583ccfed3666460804571e67
5
+ SHA512:
6
+ metadata.gz: 7e598d2220187f0480ffdf303d69818e0f2eb6db123fba360d8b68c849e3703420589af48a179c2f0df4f18787779cc82ec3050a4e7a2f5b87eff9080c5bd974
7
+ data.tar.gz: dc18b3ea0006c21d61ac541cc20630fb3b9697ed3bf907affcb984387ba1d28ce7968eed4dcea4dea1f2f943697dbd203654ba16acc59ea347731adf393891ba
@@ -0,0 +1,4 @@
1
+ module BootstrapWizardRails
2
+ class Engine < ::Rails::Engine
3
+ end
4
+ end
@@ -0,0 +1,1171 @@
1
+ /*
2
+ * Copyright (C) 2013 Panopta, Andrew Moffat
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ * of this software and associated documentation files (the "Software"), to deal
6
+ * in the Software without restriction, including without limitation the rights
7
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ * copies of the Software, and to permit persons to whom the Software is
9
+ * furnished to do so, subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in
12
+ * all copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ * SOFTWARE.
21
+ */
22
+ (function ($) {
23
+ $.fn.wizard = function(args) {
24
+ return new Wizard(this, args);
25
+ };
26
+
27
+ $.fn.wizard.logging = false;
28
+
29
+ var WizardCard = function(wizard, card, index, prev, next) {
30
+ this.wizard = wizard;
31
+ this.index = index;
32
+ this.prev = prev;
33
+ this.next = next;
34
+ this.el = card;
35
+ this.title = card.find("h3").first().text();
36
+ this.name = card.data("cardname") || this.title;
37
+
38
+ this.nav = this._createNavElement(this.title, index);
39
+
40
+ this._disabled = false;
41
+ this._loaded = false;
42
+ this._events = {};
43
+ };
44
+
45
+ WizardCard.prototype = {
46
+ select: function() {
47
+ this.log("selecting");
48
+ if (!this.isSelected()) {
49
+ this.nav.addClass("active");
50
+ this.el.show();
51
+
52
+ if (!this._loaded) {
53
+ this.trigger("loaded");
54
+ this.reload();
55
+ }
56
+
57
+ this.trigger("selected");
58
+ }
59
+
60
+
61
+ /*
62
+ * this is ugly, but we're handling the changing of the wizard's
63
+ * buttons here, in the WizardCard select. so when a card is
64
+ * selected, we're figuring out if we're the first card or the
65
+ * last card and changing the wizard's buttons via the guts of
66
+ * the wizard
67
+ *
68
+ * ideally this logic should be encapsulated by some wizard methods
69
+ * that we can call from here, instead of messing with the guts
70
+ */
71
+ var w = this.wizard;
72
+
73
+ // The back button is only disabled on this first card...
74
+ w.backButton.toggleClass("disabled", this.index == 0);
75
+
76
+ if (this.index >= w._cards.length-1) {
77
+ this.log("on last card, changing next button to submit");
78
+
79
+ w.changeNextButton(w.args.buttons.submitText, "btn-success");
80
+ w._readyToSubmit = true;
81
+ w.trigger("readySubmit");
82
+ }
83
+ else {
84
+ w._readyToSubmit = false;
85
+ w.changeNextButton(w.args.buttons.nextText, "btn-primary");
86
+ }
87
+
88
+ return this;
89
+ },
90
+
91
+ _createNavElement: function(name, i) {
92
+ var li = $('<li class="wizard-nav-item"></li>');
93
+ var a = $('<a class="wizard-nav-link"></a>');
94
+ a.data("navindex", i);
95
+ li.append(a);
96
+ a.append('<span class="glyphicon glyphicon-chevron-right"></span> ');
97
+ a.append(name);
98
+ return li;
99
+ },
100
+
101
+ markVisited: function() {
102
+ this.log("marking as visited");
103
+ this.nav.addClass("already-visited");
104
+ this.trigger("markVisited");
105
+ return this;
106
+ },
107
+
108
+ unmarkVisited: function() {
109
+ this.log("unmarking as visited");
110
+ this.nav.removeClass("already-visited");
111
+ this.trigger("unmarkVisited");
112
+ return this;
113
+ },
114
+
115
+ deselect: function() {
116
+ this.nav.removeClass("active");
117
+ this.el.hide();
118
+ this.trigger("deselect");
119
+ return this;
120
+ },
121
+
122
+ enable: function() {
123
+ this.log("enabling");
124
+
125
+ // Issue #38 Hiding navigation link when hide card
126
+ // Awaiting approval
127
+ //
128
+ // this.nav.removeClass('hide');
129
+
130
+ this.nav.addClass("active");
131
+ this._disabled = false;
132
+ this.trigger("enabled");
133
+ return this;
134
+ },
135
+
136
+ disable: function(hideCard) {
137
+ this.log("disabling");
138
+ this._disabled = true;
139
+ this.nav.removeClass("active already-visited");
140
+ if (hideCard) {
141
+ this.el.hide();
142
+ // Issue #38 Hiding navigation link when hide card
143
+ // Awaiting approval
144
+ //
145
+ // this.nav.addClass('hide');
146
+ }
147
+ this.trigger("disabled");
148
+ return this;
149
+ },
150
+
151
+ isDisabled: function() {
152
+ return this._disabled;
153
+ },
154
+
155
+ alreadyVisited: function() {
156
+ return this.nav.hasClass("already-visited");
157
+ },
158
+
159
+ isSelected: function() {
160
+ return this.nav.hasClass("active");
161
+ },
162
+
163
+ reload: function() {
164
+ this._loaded = true;
165
+ this.trigger("reload");
166
+ return this;
167
+ },
168
+
169
+ on: function() {
170
+ return this.wizard.on.apply(this, arguments);
171
+ },
172
+
173
+ trigger: function() {
174
+ this.callListener("on"+arguments[0]);
175
+ return this.wizard.trigger.apply(this, arguments);
176
+ },
177
+
178
+ /*
179
+ * displays an alert box on the current card
180
+ */
181
+ toggleAlert: function(msg, toggle) {
182
+ this.log("toggling alert to: " + toggle);
183
+
184
+ toggle = typeof(toggle) == "undefined" ? true : toggle;
185
+
186
+ if (toggle) {this.trigger("showAlert");}
187
+ else {this.trigger("hideAlert");}
188
+
189
+ var div;
190
+ var alert = this.el.children("h3").first().next("div.alert");
191
+
192
+ if (alert.length == 0) {
193
+ /*
194
+ * we're hiding anyways, so no need to create anything.
195
+ * we'll do that if we ever are actually showing the alert
196
+ */
197
+ if (!toggle) {return this;}
198
+
199
+ this.log("couldn't find existing alert div, creating one");
200
+ div = $("<div />");
201
+ div.addClass("alert");
202
+ div.addClass("hide");
203
+ div.insertAfter(this.el.find("h3").first());
204
+ }
205
+ else {
206
+ this.log("found existing alert div");
207
+ div = alert.first();
208
+ }
209
+
210
+ if (toggle) {
211
+ if (msg != null) {
212
+ this.log("setting alert msg to", msg);
213
+ div.html(msg);
214
+ }
215
+ div.show();
216
+ }
217
+ else {
218
+ div.hide();
219
+ }
220
+ return this;
221
+ },
222
+
223
+ /*
224
+ * this looks for event handlers embedded into the html of the
225
+ * wizard card itself, in the form of a data- attribute
226
+ */
227
+ callListener: function(name) {
228
+ // a bug(?) in jquery..can't access data-<name> if name is camelCase
229
+ name = name.toLowerCase();
230
+
231
+ this.log("looking for listener " + name);
232
+ var listener = window[this.el.data(name)];
233
+ if (listener) {
234
+ this.log("calling listener " + name);
235
+ var wizard = this.wizard;
236
+
237
+ try {
238
+ var vret = listener(this);
239
+ }
240
+ catch (e) {
241
+ this.log("exception calling listener " + name + ": ", e);
242
+ }
243
+ }
244
+ else {
245
+ this.log("didn't find listener " + name);
246
+ }
247
+ },
248
+
249
+ problem: function(toggle) {
250
+ this.nav.find("a").toggleClass("wizard-step-error", toggle);
251
+ },
252
+
253
+ validate: function() {
254
+ var failures = false;
255
+ var self = this;
256
+
257
+ /*
258
+ * run all the validators embedded on the inputs themselves
259
+ */
260
+ this.el.find("[data-validate]").each(function(i, el) {
261
+ self.log("validating individiual inputs");
262
+ el = $(el);
263
+
264
+ var v = el.data("validate");
265
+ if (!v) {return;}
266
+
267
+ var ret = {
268
+ status: true,
269
+ title: "Error",
270
+ msg: ""
271
+ };
272
+
273
+ var vret = window[v](el);
274
+ $.extend(ret, vret);
275
+
276
+ // Add-On
277
+ // This allows the use of a INPUT+BTN used as one according to boostrap layout
278
+ // for the wizard it is required to add an id with btn-(ID of Input)
279
+ // this will make sure the popover is drawn on the correct element
280
+ if ( $('#btn-' + el.attr('id')).length === 1 ) {
281
+ el = $('#btn-' + el.attr('id'));
282
+ }
283
+
284
+ if (!ret.status) {
285
+ failures = true;
286
+
287
+ // Updated to show error on correct form-group
288
+ el.parents("div.form-group").toggleClass("has-error", true);
289
+
290
+ // This allows the use of a INPUT+BTN used as one according to boostrap layout
291
+ // for the wizard it is required to add an id with btn-(ID of Input)
292
+ // this will make sure the popover is drawn on the correct element
293
+ if ( $('#btn-' + el.attr('id')).length === 1 ) {
294
+ el = $('#btn-' + el.attr('id'));
295
+ }
296
+
297
+ self.wizard.errorPopover(el, ret.msg);
298
+ } else {
299
+ el.parents("div.form-group").toggleClass("has-error", false);
300
+
301
+ // This allows the use of a INPUT+BTN used as one according to boostrap layout
302
+ // for the wizard it is required to add an id with btn-(ID of Input)
303
+ // this will make sure the popover is drawn on the correct element
304
+ if ( $('#btn-' + el.attr('id')).length === 1 ) {
305
+ el = $('#btn-' + el.attr('id'));
306
+ }
307
+
308
+ try {
309
+ el.popover("destroy");
310
+ }
311
+ /*
312
+ * older versions of bootstrap don't have a destroy call
313
+ * for popovers
314
+ */
315
+ catch (e) {
316
+ el.popover("hide");
317
+ }
318
+ }
319
+ });
320
+ this.log("after validating inputs, failures is", failures);
321
+
322
+ /*
323
+ * run the validator embedded in the card
324
+ */
325
+ var cardValidator = window[this.el.data("validate")];
326
+ if (cardValidator) {
327
+ this.log("running html-embedded card validator");
328
+ var cardValidated = cardValidator(this);
329
+ if (typeof(cardValidated) == "undefined" || cardValidated == null) {
330
+ cardValidated = true;
331
+ }
332
+ if (!cardValidated) failures = true;
333
+ this.log("after running html-embedded card validator, failures is", failures);
334
+ }
335
+
336
+ /*
337
+ * run the validate listener
338
+ */
339
+ this.log("running listener validator");
340
+ var listenerValidated = this.trigger("validate");
341
+ if (typeof(listenerValidated) == "undefined" || listenerValidated == null) {
342
+ listenerValidated = true;
343
+ }
344
+ if (!listenerValidated) failures = true;
345
+ this.log("after running listener validator, failures is", failures);
346
+
347
+ var validated = !failures;
348
+ if (validated) {
349
+ this.log("validated, calling listeners");
350
+ this.trigger("validated");
351
+ }
352
+ else {
353
+ this.log("invalid");
354
+ this.trigger("invalid");
355
+ }
356
+ return validated;
357
+ },
358
+
359
+ log: function() {
360
+ if (!window.console || !$.fn.wizard.logging) {return;}
361
+ var prepend = "card '"+this.name+"': ";
362
+ var args = [prepend];
363
+ args.push.apply(args, arguments);
364
+
365
+ console.log.apply(console, args);
366
+ },
367
+
368
+ isActive: function() {
369
+ return this.nav.hasClass("active");
370
+ }
371
+ };
372
+
373
+ Wizard = function(markup, args) {
374
+
375
+ /* TEMPLATE */
376
+ this.wizard_template = [
377
+ '<div class="modal fade wizard">',
378
+ '<div class="modal-dialog wizard-dialog">',
379
+ '<div class="modal-content wizard-content">',
380
+ '<div class="modal-header wizard-header">',
381
+ '<button type="button" class="close wizard-close" aria-hidden="true">&times;</button>',
382
+ '<h3 class="modal-title wizard-title"></h3>',
383
+ '<span class="wizard-subtitle"></span>',
384
+ '</div>',
385
+ '<div class="modal-body wizard-body">',
386
+ '<div class="pull-left wizard-steps">',
387
+ '<div class="wizard-nav-container">',
388
+ '<ul class="nav wizard-nav-list">',
389
+ '</ul>',
390
+ '</div>',
391
+ '<div class="wizard-progress-container">',
392
+ '<div class="progress progress-striped">',
393
+ '<div class="progress-bar" style="width: 0%;"></div>',
394
+ '</div>',
395
+ '</div>',
396
+ '</div>',
397
+ '<form>',
398
+ '<div class="wizard-cards">',
399
+ '<div class="wizard-card-container">',
400
+ '</div>',
401
+ '<div class="wizard-footer">',
402
+ '<div class="wizard-buttons-container">',
403
+ '<button class="btn wizard-cancel wizard-close" type="button">Cancel</button>',
404
+ '<div class="btn-group-single pull-right">',
405
+ '<button class="btn wizard-back" type="button">Back</button>',
406
+ '<button class="btn btn-primary wizard-next" type="button">Next</button>',
407
+ '</div>',
408
+ '</div>',
409
+ '</div>',
410
+ '</div>',
411
+ '</form>',
412
+ '</div>',
413
+
414
+ '</div>',
415
+ '</div>',
416
+ '</div>'
417
+ ];
418
+
419
+ this.args = {
420
+ keyboard: true,
421
+ backdrop: true,
422
+ show: false,
423
+ submitUrl: "",
424
+ showCancel: false,
425
+ showClose: true,
426
+ progressBarCurrent: false,
427
+ increaseHeight: 0,
428
+ contentHeight: 300,
429
+ contentWidth: 580,
430
+ buttons: {
431
+ cancelText: "Cancel",
432
+ nextText: "Next",
433
+ backText: "Back",
434
+ submitText: "Submit",
435
+ submittingText: "Submitting...",
436
+ },
437
+ formClass: "form-horizontal"
438
+ };
439
+
440
+ $.extend(this.args, args || {});
441
+
442
+ this._create(markup);
443
+ };
444
+
445
+ Wizard.prototype = {
446
+ log: function() {
447
+ if (!window.console || !$.fn.wizard.logging) {return;}
448
+ var prepend = "wizard "+this.el.id+": ";
449
+ var args = [prepend];
450
+ args.push.apply(args, arguments);
451
+ console.log.apply(console, args);
452
+ },
453
+
454
+ _create: function(markup) {
455
+ this.markup = $(markup);
456
+ this.title = this.markup.data('title');
457
+ this.submitCards = this.markup.find(".wizard-error,.wizard-failure,.wizard-success,.wizard-loading");
458
+ this.el = $(this.wizard_template.join('\n'));
459
+ $('body').append(this.el);
460
+
461
+ this.modal = this.el.modal({
462
+ keyboard: this.args.keyboard,
463
+ show: this.args.show,
464
+ backdrop: this.args.backdrop
465
+ });
466
+
467
+ this.dimensions = {
468
+ contentHeight: this.args.contentHeight,
469
+ contentWidth: this.args.contentWidth
470
+ };
471
+ this.dialog = this.modal.find('.wizard-dialog');
472
+ this.content = this.modal.find('.wizard-content');
473
+ this.header = this.modal.find('.wizard-header');
474
+ this.body = this.modal.find('.wizard-body');
475
+ this.wizardSteps = this.modal.find('.wizard-steps');
476
+ this.wizardCards = this.modal.find('.wizard-cards');
477
+ this.wizardCardContainer = this.modal.find('.wizard-card-container');
478
+ this.wizardCardContainer
479
+ .append(this.markup.find('.wizard-card'))
480
+ .append(this.submitCards);
481
+ this.navContainer = this.modal.find('.wizard-nav-container');
482
+ this.navList = this.modal.find('.wizard-nav-list');
483
+ this.progressContainer = this.modal.find('.wizard-progress-container');
484
+ this.progress = this.progressContainer.find('.progress-bar');
485
+ this.closeButton = this.modal.find('button.wizard-close.close');
486
+ this.cardsContainer = this.modal.find('wizard-cards-container');
487
+ this.form = this.modal.find('form');
488
+ this.footer = this.modal.find(".wizard-footer");
489
+ this.cancelButton = this.footer.find(".wizard-cancel");
490
+ this.backButton = this.footer.find(".wizard-back");
491
+ this.nextButton = this.footer.find(".wizard-next");
492
+
493
+ this._cards = [];
494
+ this.cards = {};
495
+ this._readyToSubmit = false;
496
+ this.percentComplete = 0;
497
+ this._submitting = false;
498
+ this._events = {};
499
+ this._firstShow = true;
500
+
501
+ this._createCards();
502
+
503
+ this.nextButton.click(this, this._handleNextClick);
504
+ this.backButton.click(this, this._handleBackClick);
505
+
506
+ this.cancelButton.text(this.args.buttons.cancelText);
507
+ this.backButton.text(this.args.buttons.backText);
508
+ this.nextButton.text(this.args.buttons.nextText);
509
+
510
+ // Apply Form Class(es)
511
+ this.form.addClass(this.args.formClass);
512
+
513
+ // Register Array Holder for popovers
514
+ this.popovers = [];
515
+
516
+ var self = this;
517
+ var _close = function() {
518
+ self.reset();
519
+ self.close();
520
+ self.trigger("closed");
521
+ };
522
+
523
+ // Register Close Button
524
+ this.closeButton.click(_close);
525
+ this.cancelButton.click(_close);
526
+
527
+ this.wizardSteps.on("click", "li.already-visited a.wizard-nav-link", this,
528
+ function(event) {
529
+ var index = parseInt($(event.target).data("navindex"));
530
+ event.data.setCard(index);
531
+ });
532
+
533
+ if ( this.title.length != 0 ) {
534
+ this.setTitle(this.title);
535
+ }
536
+
537
+ this.on("submit", this._defaultSubmit);
538
+
539
+ // Set Modal Dimensions
540
+ this.autoDimensions();
541
+ },
542
+
543
+ autoDimensions: function() {
544
+ // DO NOT REMOVE DISPLAY ; Temporary display is required for calculation
545
+ this.modal.css('display', 'block');
546
+
547
+ this.dimensions.header = this.header.outerHeight(true);
548
+
549
+ // Navigation Pane is dyanmic build on card content
550
+ // Navigation Pane === BASE Inner Content Height
551
+ this.dimensions.navigation = this.wizardSteps.outerHeight(true);
552
+ if ( this.dimensions.navigation < this.dimensions.contentHeight ) {
553
+ this.dimensions.navigation = this.dimensions.contentHeight;
554
+ this.navContainer.height( (this.dimensions.contentHeight-30) - this.progressContainer.outerHeight(true));
555
+ }
556
+
557
+ // Dimension Alias ( Body Height === (Navigation Height) )
558
+ this.dimensions.body = this.dimensions.navigation;
559
+
560
+ // Apply OuterHeight of navigation to it's parent wizardSteps
561
+ this.wizardSteps.height(this.dimensions.body);
562
+
563
+ // Modal Height === (Header + Content)
564
+ this.dimensions.modal = (this.dimensions.header + this.dimensions.navigation);
565
+ this.content.height(this.dimensions.modal + 'px');
566
+ this.dialog.width(this.dimensions.contentWidth);
567
+
568
+ this.body.height(this.dimensions.body + 'px');
569
+ this.wizardCards.height(this.dimensions.body + 'px');
570
+
571
+ // Footer Height
572
+ this.dimensions.footer = this.footer.outerHeight(true);
573
+
574
+ // Card Container === (Body - Footer)
575
+ this.dimensions.cardContainer = (this.dimensions.body - this.dimensions.footer);
576
+ this.wizardCardContainer.height(this.dimensions.cardContainer);
577
+
578
+ // Reposition
579
+ this.dimensions.offset = ($(window).height() - this.dialog.height()) / 2;
580
+ this.dialog.css({
581
+ 'margin-top': this.dimensions.offset + 'px',
582
+ 'padding-top': 0
583
+ });
584
+
585
+ // DO NOT REMOVE NEXT LINE
586
+ this.modal.css('display', '');
587
+ },
588
+
589
+ setTitle: function(title) {
590
+ this.log("setting title to", title);
591
+ this.modal.find(".wizard-title").first().text(title);
592
+ return this;
593
+ },
594
+
595
+ setSubtitle: function(title) {
596
+ this.log("setting subtitle to", title);
597
+ this.modal.find(".wizard-subtitle").first().text(title);
598
+ return this;
599
+ },
600
+
601
+ errorPopover: function(el, msg, allowHtml) {
602
+ this.log("launching popover on", el);
603
+ allowHtml = typeof allowHtml !== "undefined" ? allowHtml : false;
604
+ var popover = el.popover({
605
+ content: msg,
606
+ trigger: "manual",
607
+ html: allowHtml,
608
+ container: el.parents('.form-group')
609
+ }).addClass("error-popover").popover("show").next(".popover");
610
+
611
+ el.parents('.form-group').find('.popover').addClass("error-popover");
612
+
613
+ this.popovers.push(el);
614
+
615
+ return popover;
616
+ },
617
+
618
+ destroyPopover: function(pop) {
619
+ pop = $(pop);
620
+
621
+ /*
622
+ * this is the element that the popover was created for
623
+ */
624
+ try {
625
+ pop.popover("destroy");
626
+ }
627
+ /*
628
+ * older versions of bootstrap don't have a destroy call
629
+ * for popovers
630
+ */
631
+ catch (e) {
632
+ pop.popover("hide");
633
+ }
634
+ },
635
+
636
+ hidePopovers: function(el) {
637
+ this.log("hiding all popovers");
638
+ var self = this;
639
+
640
+ $.each(this.popovers, function(i, p) {
641
+ self.destroyPopover(p);
642
+ });
643
+
644
+ this.modal.find('.has-error').removeClass('has-error');
645
+ this.popovers = [];
646
+ },
647
+
648
+ eachCard: function(fn) {
649
+ $.each(this._cards, fn);
650
+ return this;
651
+ },
652
+
653
+ getActiveCard: function() {
654
+ this.log("getting active card");
655
+ var currentCard = null;
656
+
657
+ $.each(this._cards, function(i, card) {
658
+ if (card.isActive()) {
659
+ currentCard = card;
660
+ return false;
661
+ }
662
+ });
663
+ if (currentCard) {this.log("found active card", currentCard);}
664
+ else {this.log("couldn't find an active card");}
665
+ return currentCard;
666
+ },
667
+
668
+ changeNextButton: function(text, cls) {
669
+ this.log("changing next button, text: " + text, "class: " + cls);
670
+ if (typeof(cls) != "undefined") {
671
+ this.nextButton.removeClass("btn-success btn-primary");
672
+ }
673
+
674
+ if (cls) {
675
+ this.nextButton.addClass(cls);
676
+ }
677
+ this.nextButton.text(text);
678
+ return this;
679
+ },
680
+
681
+ hide: function() {
682
+ this.log("hiding");
683
+ this.modal.modal("hide");
684
+ return this;
685
+ },
686
+
687
+ close: function() {
688
+ this.log("closing");
689
+ this.modal.modal("hide");
690
+ return this;
691
+ },
692
+
693
+
694
+ show: function(modalOptions) {
695
+ this.log("showing");
696
+ if (this._firstShow) {
697
+ this.setCard(0);
698
+ this._firstShow = false;
699
+ }
700
+ if (this.args.showCancel) {
701
+ this.cancelButton.show();
702
+ } else {
703
+ this.cancelButton.hide();
704
+ }
705
+ if (this.args.showClose) { this.closeButton.show(); }
706
+ this.modal.modal('show');
707
+
708
+ return this;
709
+ },
710
+
711
+ on: function(name, fn) {
712
+ this.log("adding listener to event " + name);
713
+ this._events[name] = fn;
714
+ return this;
715
+ },
716
+
717
+ trigger: function() {
718
+ var name = arguments[0];
719
+ var args = Array.prototype.slice.call(arguments);
720
+ args.shift();
721
+ args.unshift(this);
722
+
723
+ this.log("firing event " + name);
724
+ var handler = this._events[name];
725
+ if (handler === undefined && this.wizard !== undefined) {
726
+ handler = this.wizard._events[name];
727
+ }
728
+ var ret = null;
729
+
730
+ if (typeof(handler) == "function") {
731
+ this.log("found event handler, calling " + name);
732
+ try {
733
+ ret = handler.apply(this, args);
734
+ }
735
+ catch (e) {
736
+ this.log("event handler " + name + " had an exception");
737
+ }
738
+ }
739
+ else {
740
+ this.log("couldn't find an event handler for " + name);
741
+ }
742
+ return ret;
743
+ },
744
+
745
+
746
+ reset: function() {
747
+ this.log("resetting");
748
+
749
+ this.updateProgressBar(0);
750
+ this.hideSubmitCards();
751
+
752
+ this.setCard(0);
753
+ this.lockCards();
754
+
755
+ this.enableNextButton();
756
+ this.showButtons();
757
+
758
+ this.hidePopovers();
759
+
760
+ this.trigger("reset");
761
+ return this;
762
+ },
763
+
764
+ /*
765
+ * this handles switching to the next card or previous card, taking
766
+ * care to skip over disabled cards
767
+ */
768
+ _abstractIncrementStep: function(direction, getNext) {
769
+ var current = this.getActiveCard();
770
+ var next;
771
+
772
+ if (current) {
773
+ /*
774
+ * loop until we find a card that isn't disabled
775
+ */
776
+ this.log("searching for valid next card");
777
+ while (true) {
778
+ next = getNext(current);
779
+ if (next) {
780
+ this.log("looking at card", next.index);
781
+ if (next.isDisabled()) {
782
+ this.log("card " + next.index + " is disabled/locked, continuing");
783
+ current = next;
784
+ continue;
785
+ }
786
+ else {
787
+ return this.setCard(current.index+direction);
788
+ }
789
+ }
790
+ else {
791
+ this.log("next card is not defined, breaking");
792
+ break;
793
+ }
794
+ }
795
+ }
796
+ else {
797
+ this.log("current card is undefined");
798
+ }
799
+ },
800
+
801
+
802
+ incrementCard: function() {
803
+ this.log("incrementing card");
804
+ var card = this._abstractIncrementStep(1, function(current){return current.next;});
805
+ this.trigger("incrementCard");
806
+ return card;
807
+ },
808
+
809
+ decrementCard: function() {
810
+ this.log("decrementing card");
811
+ var card = this._abstractIncrementStep(-1, function(current){return current.prev;});
812
+ this.trigger("decrementCard");
813
+ return card;
814
+ },
815
+
816
+ setCard: function(i) {
817
+ this.log("setting card to " + i);
818
+ this.hideSubmitCards();
819
+ var currentCard = this.getActiveCard();
820
+
821
+ if (this._submitting) {
822
+ this.log("we're submitting the wizard already, can't change cards");
823
+ return currentCard;
824
+ }
825
+
826
+ var newCard = this._cards[i];
827
+ if (newCard) {
828
+ if (newCard.isDisabled()) {
829
+ this.log("new card is currently disabled, returning");
830
+ return currentCard;
831
+ }
832
+
833
+ if (currentCard) {
834
+
835
+ /*
836
+ * here, we're only validating if we're going forward,
837
+ * not if we're going backwards in a step
838
+ */
839
+ if (i > currentCard.index) {
840
+ var cardToValidate = currentCard;
841
+ var ok = false;
842
+
843
+ /*
844
+ * we need to loop over every card between our current
845
+ * card and the card that we clicked, and re-validate
846
+ * them. if there's an error, we need to select the
847
+ * first card to have an error
848
+ */
849
+ while (cardToValidate.index != newCard.index) {
850
+ /*
851
+ * unless we're validating the card that we're
852
+ * leaving, we need to select the card, so that
853
+ * any validators that trigger errorPopovers can
854
+ * display correctly
855
+ */
856
+ if (cardToValidate.index != currentCard.index) {
857
+ cardToValidate.prev.deselect();
858
+ cardToValidate.prev.markVisited();
859
+ cardToValidate.select();
860
+ }
861
+ ok = cardToValidate.validate();
862
+ if (!ok) {
863
+ return cardToValidate;
864
+ }
865
+ cardToValidate = cardToValidate.next;
866
+ }
867
+
868
+ cardToValidate.prev.deselect();
869
+ cardToValidate.prev.markVisited();
870
+ }
871
+
872
+ currentCard.deselect();
873
+ currentCard.markVisited();
874
+ }
875
+
876
+ newCard.select();
877
+
878
+ if (this.args.progressBarCurrent) {
879
+ this.percentComplete = i * 100.0 / this._cards.length;
880
+ this.updateProgressBar(this.percentComplete);
881
+ }
882
+ else {
883
+ var lastPercent = this.percentComplete;
884
+ this.percentComplete = i * 100.0 / this._cards.length;
885
+ this.percentComplete = Math.max(lastPercent, this.percentComplete);
886
+ this.updateProgressBar(this.percentComplete);
887
+ }
888
+
889
+ return newCard;
890
+ }
891
+ else {
892
+ this.log("couldn't find card " + i);
893
+ }
894
+ },
895
+
896
+ updateProgressBar: function(percent) {
897
+ this.log("updating progress to " + percent + "%");
898
+ this.progress.css({width: percent + "%"});
899
+ this.percentComplete = percent;
900
+
901
+ this.trigger("progressBar", percent);
902
+
903
+ if (percent == 100) {
904
+ this.log("progress is 100, animating progress bar");
905
+ this.progressContainer.find('.progress').addClass("active");
906
+ }
907
+ else if (percent == 0) {
908
+ this.log("progress is 0, disabling animation");
909
+ this.progressContainer.find('.progress').removeClass("active");
910
+ }
911
+ },
912
+
913
+ getNextCard: function() {
914
+ var currentCard = this.getActiveCard();
915
+ if (currentCard) return currentCard.next;
916
+ },
917
+
918
+ lockCards: function() {
919
+ this.log("locking nav cards");
920
+ this.eachCard(function(i,card){card.unmarkVisited();});
921
+ return this;
922
+ },
923
+
924
+ disableCards: function() {
925
+ this.log("disabling all nav cards");
926
+ this.eachCard(function(i,card){card.disable();});
927
+ return this;
928
+ },
929
+
930
+ enableCards: function() {
931
+ this.log("enabling all nav cards");
932
+ this.eachCard(function(i,card){card.enable();});
933
+ return this;
934
+ },
935
+
936
+ hideCards: function() {
937
+ this.log("hiding cards");
938
+ this.eachCard(function(i,card){card.deselect();});
939
+ this.hideSubmitCards();
940
+ return this;
941
+ },
942
+
943
+ hideButtons: function() {
944
+ this.log("hiding buttons");
945
+ this.cancelButton.hide();
946
+ this.closeButton.hide();
947
+ this.nextButton.hide();
948
+ this.backButton.hide();
949
+ return this;
950
+ },
951
+
952
+ showButtons: function() {
953
+ this.log("showing buttons");
954
+ if (this.args.showCancel) {
955
+ this.cancelButton.show();
956
+ } else {
957
+ this.cancelButton.hide();
958
+ }
959
+ if (this.args.showClose) { this.closeButton.show(); };
960
+ this.nextButton.show();
961
+ this.backButton.show();
962
+ return this;
963
+ },
964
+
965
+ getCard: function(el) {
966
+ var cardDOMEl = $(el).parents(".wizard-card").first()[0];
967
+ if (cardDOMEl) {
968
+ var foundCard = null;
969
+ this.eachCard(function(i, card) {
970
+ if (cardDOMEl == card.el[0]) {
971
+ foundCard = card;
972
+ return false;
973
+ }
974
+ return true;
975
+ });
976
+ return foundCard;
977
+ }
978
+ else {
979
+ return null;
980
+ }
981
+ },
982
+
983
+ _createCards: function() {
984
+ var prev = null;
985
+ var next = null;
986
+ var currentCard = null;
987
+ var wizard = this;
988
+ var self = this;
989
+
990
+ self.log("Creating Cards");
991
+
992
+ var cards = this.modal.find(".wizard-cards .wizard-card");
993
+ $.each(cards, function(i, card) {
994
+ card = $(card);
995
+
996
+ prev = currentCard;
997
+ currentCard = new WizardCard(wizard, card, i, prev, next);
998
+ self._cards.push(currentCard);
999
+ if (currentCard.name) {
1000
+ self.cards[currentCard.name] = currentCard;
1001
+ }
1002
+ if (prev) {prev.next = currentCard;}
1003
+
1004
+ self.modal.find(".wizard-steps .wizard-nav-list").append(currentCard.nav);
1005
+ });
1006
+ },
1007
+
1008
+ showSubmitCard: function(name) {
1009
+ this.log("showing "+name+" submit card");
1010
+
1011
+ var card = this.el.find(".wizard-"+name);
1012
+ if (card.length) {
1013
+ this.hideCards();
1014
+ this.el.find(".wizard-"+name).show();
1015
+ }
1016
+ else {
1017
+ this.log("couldn't find submit card "+name);
1018
+ }
1019
+ },
1020
+
1021
+ hideSubmitCard: function(name) {
1022
+ this.log("hiding "+name+" submit card");
1023
+ this.el.find(".wizard-"+name).hide();
1024
+ },
1025
+
1026
+ hideSubmitCards: function() {
1027
+ var wizard = this;
1028
+ $.each(["success", "error", "failure", "loading"], function(i, name) {
1029
+ wizard.hideSubmitCard(name);
1030
+ });
1031
+ },
1032
+
1033
+ enableNextButton: function() {
1034
+ this.log("enabling next button");
1035
+ this.nextButton.removeAttr("disabled");
1036
+ return this;
1037
+ },
1038
+
1039
+ disableNextButton: function() {
1040
+ this.log("disabling next button");
1041
+ this.nextButton.attr("disabled", "disabled");
1042
+ return this;
1043
+ },
1044
+
1045
+ serializeArray: function() {
1046
+ var form = this.form.serializeArray();
1047
+ this.form.find('input[disabled][data-serialize="1"]').each(function() {
1048
+ formObj = {
1049
+ name: $(this).attr('name'),
1050
+ value: $(this).val()
1051
+ };
1052
+
1053
+ form.push(formObj);
1054
+ });
1055
+
1056
+ return form;
1057
+ },
1058
+
1059
+ serialize: function() {
1060
+ var form = this.form.serialize();
1061
+ this.form.find('input[disabled][data-serialize="1"]').each(function() {
1062
+ form = form + '&' + $(this).attr('name') + '=' + $(this).val();
1063
+ });
1064
+
1065
+ return form;
1066
+ },
1067
+
1068
+ find: function(selector) {
1069
+ return this.modal.find(selector);
1070
+ },
1071
+
1072
+
1073
+ /*
1074
+ * the next 3 functions are to be called by the custom submit event
1075
+ * handler. the idea is that after you make an ajax call to submit
1076
+ * your wizard data (or whatever it is you want to do at the end of
1077
+ * the wizard), you call one of these 3 handlers to display a specific
1078
+ * card for either success, failure, or error
1079
+ */
1080
+ submitSuccess: function() {
1081
+ this.log("submit success");
1082
+ this._submitting = false;
1083
+ this.showSubmitCard("success");
1084
+ this.trigger("submitSuccess");
1085
+ },
1086
+
1087
+ submitFailure: function() {
1088
+ this.log("submit failure");
1089
+ this._submitting = false;
1090
+ this.showSubmitCard("failure");
1091
+ this.trigger("submitFailure");
1092
+ },
1093
+
1094
+ submitError: function() {
1095
+ this.log("submit error");
1096
+ this._submitting = false;
1097
+ this.showSubmitCard("error");
1098
+ this.trigger("submitError");
1099
+ },
1100
+
1101
+
1102
+ _submit: function() {
1103
+ this.log("submitting wizard");
1104
+ this._submitting = true;
1105
+
1106
+ this.lockCards();
1107
+ this.cancelButton.hide();
1108
+ this.closeButton.hide();
1109
+ this.backButton.hide();
1110
+
1111
+ this.showSubmitCard("loading");
1112
+ this.updateProgressBar(100);
1113
+
1114
+ this.changeNextButton(this.args.buttons.submittingText, false);
1115
+ this.disableNextButton();
1116
+
1117
+ var ret = this.trigger("submit");
1118
+ this.trigger("loading");
1119
+ },
1120
+
1121
+ _onNextClick: function() {
1122
+ this.log("handling 'next' button click");
1123
+ var currentCard = this.getActiveCard();
1124
+ if (this._readyToSubmit && currentCard.validate()) {
1125
+ this._submit();
1126
+ }
1127
+ else {
1128
+ currentCard = this.incrementCard();
1129
+ }
1130
+ },
1131
+
1132
+ _onBackClick: function() {
1133
+ this.log("handling 'back' button click");
1134
+ var currentCard = this.decrementCard();
1135
+ },
1136
+
1137
+ _handleNextClick: function(event) {
1138
+ var wizard = event.data;
1139
+ wizard._onNextClick.call(wizard);
1140
+ },
1141
+
1142
+ _handleBackClick: function(event) {
1143
+ var wizard = event.data;
1144
+ wizard._onBackClick.call(wizard);
1145
+ },
1146
+
1147
+
1148
+ /*
1149
+ * this function is attached by default to the wizard's "submit" event.
1150
+ * if you choose to implement your own custom submit logic, you should
1151
+ * copy how this function works
1152
+ */
1153
+ _defaultSubmit: function(wizard) {
1154
+ $.ajax({
1155
+ type: "POST",
1156
+ url: wizard.args.submitUrl,
1157
+ data: wizard.serialize(),
1158
+ dataType: "json"
1159
+ }).done(function(response) {
1160
+ wizard.submitSuccess();
1161
+ wizard.hideButtons();
1162
+ wizard.updateProgressBar(0);
1163
+ }).fail(function() {
1164
+ wizard.submitFailure();
1165
+ wizard.hideButtons();
1166
+ });
1167
+ }
1168
+ };
1169
+
1170
+
1171
+ }(window.jQuery));