Administrator
2023-04-21 195945efc5db921a4c9eb8cf9421c172273293f5
提交 | 用户 | 时间
58d006 1 /*
A 2  * Fuel UX Wizard
3  * https://github.com/ExactTarget/fuelux
4  *
5  * Copyright (c) 2014 ExactTarget
6  * Licensed under the BSD New license.
7  */
8
9 // -- BEGIN UMD WRAPPER PREFACE --
10
11 // For more information on UMD visit:
12 // https://github.com/umdjs/umd/blob/master/jqueryPlugin.js
13
14 (function (factory) {
15     if (typeof define === 'function' && define.amd) {
16         // if AMD loader is available, register as an anonymous module.
17         define(['jquery'], factory);
18     } else if (typeof exports === 'object') {
19         // Node/CommonJS
20         module.exports = factory(require('jquery'));
21     } else {
22         // OR use browser globals if AMD is not present
23         factory(jQuery);
24     }
25 }(function ($) {
26     // -- END UMD WRAPPER PREFACE --
27
28     // -- BEGIN MODULE CODE HERE --
29
30     var old = $.fn.wizard;
31
32     // WIZARD CONSTRUCTOR AND PROTOTYPE
33
34     var Wizard = function (element, options) {
35         var kids;
36
37         this.$element = $(element);
38         this.options = $.extend({}, $.fn.wizard.defaults, options);
39         this.options.disablePreviousStep = (this.$element.attr('data-restrict') === 'previous') ? true : this.options.disablePreviousStep;
40         this.currentStep = this.options.selectedItem.step;
41         this.numSteps = this.$element.find('.steps li').length;
42         this.$prevBtn = this.$element.find('button.btn-prev');
43         this.$nextBtn = this.$element.find('button.btn-next');
44
45         // maintains backwards compatibility with < 3.8, will be removed in the future
46         if (this.$element.children('.steps-container').length === 0) {
47             this.$element.addClass('no-steps-container');
48             if (window && window.console && window.console.warn) {
49                 window.console.warn('please update your wizard markup to include ".steps-container" as seen in http://getfuelux.com/javascript.html#wizard-usage-markup');
50             }
51         }
52
53         kids = this.$nextBtn.children().detach();
54         this.nextText = $.trim(this.$nextBtn.text());
55         this.$nextBtn.append(kids);
56
57         // handle events
58         this.$prevBtn.on('click.fu.wizard', $.proxy(this.previous, this));
59         this.$nextBtn.on('click.fu.wizard', $.proxy(this.next, this));
60         this.$element.on('click.fu.wizard', 'li.complete', $.proxy(this.stepclicked, this));
61
62         this.selectedItem(this.options.selectedItem);
63
64         if (this.options.disablePreviousStep) {
65             this.$prevBtn.attr('disabled', true);
66             this.$element.find('.steps').addClass('previous-disabled');
67         }
68     };
69
70     Wizard.prototype = {
71
72         constructor: Wizard,
73
74         destroy: function () {
75             this.$element.remove();
76             // any external bindings [none]
77             // empty elements to return to original markup [none]
78             // returns string of markup
79             return this.$element[0].outerHTML;
80         },
81
82         //index is 1 based
83         //second parameter can be array of objects [{ ... }, { ... }] or you can pass n additional objects as args
84         //object structure is as follows (all params are optional): { badge: '', label: '', pane: '' }
85         addSteps: function (index) {
86             var items = [].slice.call(arguments).slice(1);
87             var $steps = this.$element.find('.steps');
88             var $stepContent = this.$element.find('.step-content');
89             var i, l, $pane, $startPane, $startStep, $step;
90
91             index = (index === -1 || (index > (this.numSteps + 1))) ? this.numSteps + 1 : index;
92             if (items[0] instanceof Array) {
93                 items = items[0];
94             }
95
96             $startStep = $steps.find('li:nth-child(' + index + ')');
97             $startPane = $stepContent.find('.step-pane:nth-child(' + index + ')');
98             if ($startStep.length < 1) {
99                 $startStep = null;
100             }
101
102             for (i = 0, l = items.length; i < l; i++) {
103                 $step = $('<li data-step="' + index + '"><span class="badge badge-info"></span></li>');
104                 $step.append(items[i].label || '').append('<span class="chevron"></span>');
105                 $step.find('.badge').append(items[i].badge || index);
106
107                 $pane = $('<div class="step-pane" data-step="' + index + '"></div>');
108                 $pane.append(items[i].pane || '');
109
110                 if (!$startStep) {
111                     $steps.append($step);
112                     $stepContent.append($pane);
113                 } else {
114                     $startStep.before($step);
115                     $startPane.before($pane);
116                 }
117
118                 index++;
119             }
120
121             this.syncSteps();
122             this.numSteps = $steps.find('li').length;
123             this.setState();
124         },
125
126         //index is 1 based, howMany is number to remove
127         removeSteps: function (index, howMany) {
128             var action = 'nextAll';
129             var i = 0;
130             var $steps = this.$element.find('.steps');
131             var $stepContent = this.$element.find('.step-content');
132             var $start;
133
134             howMany = (howMany !== undefined) ? howMany : 1;
135
136             if (index > $steps.find('li').length) {
137                 $start = $steps.find('li:last');
138             } else {
139                 $start = $steps.find('li:nth-child(' + index + ')').prev();
140                 if ($start.length < 1) {
141                     action = 'children';
142                     $start = $steps;
143                 }
144
145             }
146
147             $start[action]().each(function () {
148                 var item = $(this);
149                 var step = item.attr('data-step');
150                 if (i < howMany) {
151                     item.remove();
152                     $stepContent.find('.step-pane[data-step="' + step + '"]:first').remove();
153                 } else {
154                     return false;
155                 }
156
157                 i++;
158             });
159
160             this.syncSteps();
161             this.numSteps = $steps.find('li').length;
162             this.setState();
163         },
164
165         setState: function () {
166             var canMovePrev = (this.currentStep > 1);//remember, steps index is 1 based...
167             var isFirstStep = (this.currentStep === 1);
168             var isLastStep = (this.currentStep === this.numSteps);
169
170             // disable buttons based on current step
171             if (!this.options.disablePreviousStep) {
172                 this.$prevBtn.attr('disabled', (isFirstStep === true || canMovePrev === false));
173             }
174
175             // change button text of last step, if specified
176             var last = this.$nextBtn.attr('data-last');
177             if (last) {
178                 this.lastText = last;
179                 // replace text
180                 var text = this.nextText;
181                 if (isLastStep === true) {
182                     text = this.lastText;
183                     // add status class to wizard
184                     this.$element.addClass('complete');
185                 } else {
186                     this.$element.removeClass('complete');
187                 }
188
189                 var kids = this.$nextBtn.children().detach();
190                 this.$nextBtn.text(text).append(kids);
191             }
192
193             // reset classes for all steps
194             var $steps = this.$element.find('.steps li');
195             $steps.removeClass('active').removeClass('complete');
196             $steps.find('span.badge').removeClass('badge-info').removeClass('badge-success');
197
198             // set class for all previous steps
199             var prevSelector = '.steps li:lt(' + (this.currentStep - 1) + ')';
200             var $prevSteps = this.$element.find(prevSelector);
201             $prevSteps.addClass('complete');
202             $prevSteps.find('span.badge').addClass('badge-success');
203
204             // set class for current step
205             var currentSelector = '.steps li:eq(' + (this.currentStep - 1) + ')';
206             var $currentStep = this.$element.find(currentSelector);
207             $currentStep.addClass('active');
208             $currentStep.find('span.badge').addClass('badge-info');
209
210             // set display of target element
211             var $stepContent = this.$element.find('.step-content');
212             var target = $currentStep.attr('data-step');
213             $stepContent.find('.step-pane').removeClass('active');
214             $stepContent.find('.step-pane[data-step="' + target + '"]:first').addClass('active');
215
216             //ACE
217             /**
218             // reset the wizard position to the left
219             this.$element.find('.steps').first().attr('style', 'margin-left: 0');
220
221             // check if the steps are wider than the container div
222             var totalWidth = 0;
223             this.$element.find('.steps > li').each(function () {
224                 totalWidth += $(this).outerWidth();
225             });
226             var containerWidth = 0;
227             if (this.$element.find('.actions').length) {
228                 containerWidth = this.$element.width() - this.$element.find('.actions').first().outerWidth();
229             } else {
230                 containerWidth = this.$element.width();
231             }
232
233             if (totalWidth > containerWidth) {
234                 // set the position so that the last step is on the right
235                 var newMargin = totalWidth - containerWidth;
236                 this.$element.find('.steps').first().attr('style', 'margin-left: -' + newMargin + 'px');
237
238                 // set the position so that the active step is in a good
239                 // position if it has been moved out of view
240                 if (this.$element.find('li.active').first().position().left < 200) {
241                     newMargin += this.$element.find('li.active').first().position().left - 200;
242                     if (newMargin < 1) {
243                         this.$element.find('.steps').first().attr('style', 'margin-left: 0');
244                     } else {
245                         this.$element.find('.steps').first().attr('style', 'margin-left: -' + newMargin + 'px');
246                     }
247
248                 }
249
250             }
251             */
252
253             // only fire changed event after initializing
254             if (typeof (this.initialized) !== 'undefined') {
255                 var e = $.Event('changed.fu.wizard');
256                 this.$element.trigger(e, {
257                     step: this.currentStep
258                 });
259             }
260
261             this.initialized = true;
262         },
263
264         stepclicked: function (e) {
265             var li = $(e.currentTarget);
266             var index = this.$element.find('.steps li').index(li);
267
268             if (index < this.currentStep && this.options.disablePreviousStep) {//enforce restrictions
269                 return;
270             } else {
271                 var evt = $.Event('stepclicked.fu.wizard');
272                 this.$element.trigger(evt, {
273                     step: index + 1
274                 });
275                 if (evt.isDefaultPrevented()) {
276                     return;
277                 }
278
279                 this.currentStep = (index + 1);
280                 this.setState();
281             }
282         },
283
284         syncSteps: function () {
285             var i = 1;
286             var $steps = this.$element.find('.steps');
287             var $stepContent = this.$element.find('.step-content');
288
289             $steps.children().each(function () {
290                 var item = $(this);
291                 var badge = item.find('.badge');
292                 var step = item.attr('data-step');
293
294                 if (!isNaN(parseInt(badge.html(), 10))) {
295                     badge.html(i);
296                 }
297
298                 item.attr('data-step', i);
299                 $stepContent.find('.step-pane[data-step="' + step + '"]:last').attr('data-step', i);
300                 i++;
301             });
302         },
303
304         previous: function () {
305             if (this.options.disablePreviousStep || this.currentStep === 1) {
306                 return;
307             }
308
309             var e = $.Event('actionclicked.fu.wizard');
310             this.$element.trigger(e, {
311                 step: this.currentStep,
312                 direction: 'previous'
313             });
314             if (e.isDefaultPrevented()) {
315                 return;
316             }// don't increment ...what? Why?
317
318             this.currentStep -= 1;
319             this.setState();
320
321             // only set focus if focus is still on the $nextBtn (avoid stomping on a focus set programmatically in actionclicked callback)
322             if (this.$prevBtn.is(':focus')) {
323                 var firstFormField = this.$element.find('.active').find('input, select, textarea')[0];
324
325                 if (typeof firstFormField !== 'undefined') {
326                     // allow user to start typing immediately instead of having to click on the form field.
327                     $(firstFormField).focus();
328                 } else if (this.$element.find('.active input:first').length === 0 && this.$prevBtn.is(':disabled')) {
329                     //only set focus on a button as the last resort if no form fields exist and the just clicked button is now disabled
330                     this.$nextBtn.focus();
331                 }
332
333             }
334         },
335
336         next: function () {
337             var e = $.Event('actionclicked.fu.wizard');
338             this.$element.trigger(e, {
339                 step: this.currentStep,
340                 direction: 'next'
341             });
342             if (e.isDefaultPrevented()) {
343                 return;
344             }// respect preventDefault in case dev has attached validation to step and wants to stop propagation based on it.
345
346             if (this.currentStep < this.numSteps) {
347                 this.currentStep += 1;
348                 this.setState();
349             } else {//is last step
350                 this.$element.trigger('finished.fu.wizard');
351             }
352
353             // only set focus if focus is still on the $nextBtn (avoid stomping on a focus set programmatically in actionclicked callback)
354             if (this.$nextBtn.is(':focus')) {
355                 var firstFormField = this.$element.find('.active').find('input, select, textarea')[0];
356
357                 if (typeof firstFormField !== 'undefined') {
358                     // allow user to start typing immediately instead of having to click on the form field.
359                     $(firstFormField).focus();
360                 } else if (this.$element.find('.active input:first').length === 0 && this.$nextBtn.is(':disabled')) {
361                     //only set focus on a button as the last resort if no form fields exist and the just clicked button is now disabled
362                     this.$prevBtn.focus();
363                 }
364
365             }
366         },
367
368         selectedItem: function (selectedItem) {
369             var retVal, step;
370
371             if (selectedItem) {
372                 step = selectedItem.step || -1;
373                 //allow selection of step by data-name
374                 step = Number(this.$element.find('.steps li[data-name="' + step + '"]').first().attr('data-step')) || Number(step);
375
376                 if (1 <= step && step <= this.numSteps) {
377                     this.currentStep = step;
378                     this.setState();
379                 } else {
380                     step = this.$element.find('.steps li.active:first').attr('data-step');
381                     if (!isNaN(step)) {
382                         this.currentStep = parseInt(step, 10);
383                         this.setState();
384                     }
385
386                 }
387
388                 retVal = this;
389             } else {
390                 retVal = {
391                     step: this.currentStep
392                 };
393                 if (this.$element.find('.steps li.active:first[data-name]').length) {
394                     retVal.stepname = this.$element.find('.steps li.active:first').attr('data-name');
395                 }
396
397             }
398
399             return retVal;
400         }
401     };
402
403
404     // WIZARD PLUGIN DEFINITION
405
406     $.fn.wizard = function (option) {
407         var args = Array.prototype.slice.call(arguments, 1);
408         var methodReturn;
409
410         var $set = this.each(function () {
411             var $this = $(this);
412             var data = $this.data('fu.wizard');
413             var options = typeof option === 'object' && option;
414
415             if (!data) {
416                 $this.data('fu.wizard', (data = new Wizard(this, options)));
417             }
418
419             if (typeof option === 'string') {
420                 methodReturn = data[option].apply(data, args);
421             }
422         });
423
424         return (methodReturn === undefined) ? $set : methodReturn;
425     };
426
427     $.fn.wizard.defaults = {
428         disablePreviousStep: false,
429         selectedItem: {
430             step: -1
431         }//-1 means it will attempt to look for "active" class in order to set the step
432     };
433
434     $.fn.wizard.Constructor = Wizard;
435
436     $.fn.wizard.noConflict = function () {
437         $.fn.wizard = old;
438         return this;
439     };
440
441
442     // DATA-API
443
444     $(document).on('mouseover.fu.wizard.data-api', '[data-initialize=wizard]', function (e) {
445         var $control = $(e.target).closest('.wizard');
446         if (!$control.data('fu.wizard')) {
447             $control.wizard($control.data());
448         }
449     });
450
451     // Must be domReady for AMD compatibility
452     $(function () {
453         $('[data-initialize=wizard]').each(function () {
454             var $this = $(this);
455             if ($this.data('fu.wizard')) return;
456             $this.wizard($this.data());
457         });
458     });
459
460     // -- BEGIN UMD WRAPPER AFTERWORD --
461 }));
462 // -- END UMD WRAPPER AFTERWORD --