improved form and wizard plugins
[myslice.git] / plugins / wizard / wizard.js
1 /**
2  * Description: implements a wizard-like interface
3  * Copyright (c) 2013 UPMC Sorbonne Universite
4  * Based on SmartWizard 2.0 plugin by Dipu (http://www.techlaboratory.net)
5  * License: GPLv3
6  */
7
8 /*
9  * It's a best practice to pass jQuery to an IIFE (Immediately Invoked Function
10  * Expression) that maps it to the dollar sign so it can't be overwritten by
11  * another library in the scope of its execution.
12  */
13 (function($){
14
15     /***************************************************************************
16      * Method calling logic
17      ***************************************************************************/
18
19     $.fn.Wizard = function( method ) {
20         if ( methods[method] ) {
21             return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
22         } else if ( typeof method === 'object' || ! method ) {
23             return methods.init.apply( this, arguments );
24         } else {
25             //$.error( 'Method ' +  method + ' does not exist on jQuery.Wizard' );
26             return undefined;
27         }    
28     };
29
30     /***************************************************************************
31      * Public methods
32      ***************************************************************************/
33
34     var methods = {
35
36         /**
37          * @brief Plugin initialization
38          * @param options : an associative array of setting values
39          * @return : a jQuery collection of objects on which the plugin is
40          *     applied, which allows to maintain chainability of calls
41          */
42         init : function ( options ) {
43
44             /* Default settings */
45             var options = $.extend({}, $.fn.Wizard.defaults, options);
46
47             return this.each(function() {
48                 var $this = $(this);
49                 var obj = $(wizard, this);
50
51                 /* An object that will hold private variables and methods */
52                 var form = new Wizard(options, obj);
53                 $this.data('Wizard', form);
54
55                 form.init();
56
57 //                // Initialize the smart-wizard third-party jquery plugin
58 //                $(wizard, this).smartWizard({
59 //                    selected    : options.start_step - 1,
60 //                    errorSteps  : [5],
61 //                    onLeaveStep : leaveAStepCallback,
62 ////                  onFinish    : onFinishCallback
63 //                });
64 //
65 //                // XXX Mark some steps as done !
66 //                $(wizard, this).smartWizard('setDone',{stepnum: 1, isdone:true});
67 //
68 //                function leaveAStepCallback(obj){
69 //                    var step_num= obj.attr('rel')-1; // get the current step number
70 //                    func = options.validate_step_js[step_num];
71 //                    if (func == 'null')
72 //                        return true;
73 //                    return window[func]();
74 //                }
75 //
76 //                function onFinishCallback(){
77 //                    window.location.href('/');
78 //                }
79
80
81             }); // this.each
82         } // init
83
84     };
85
86     /***************************************************************************
87      * Wizard object
88      ***************************************************************************/
89
90     /**
91      * \param options (dict) : a dictionary of options
92      * \param obj (jQuery ref) : handler to the wizard plugin
93     */
94     function Wizard(options, obj) {
95
96         /* save a reference to this */
97         var $this = this;
98
99         /* member variables */
100         this.options = options;
101
102         /* methods */
103
104         /**
105          * \brief get a handle on a wizard step by id
106            \param stepIdx (integer) : step identifier (0-based)
107            \returns jQuery selector
108          */
109         this.get_step = function(stepIdx) 
110         {
111             return $('.plugin', $(stepIdx.attr('href')));
112         }
113
114         this.get_plugin = function(step) 
115         {
116             return step.data().plugin;
117         }
118
119         this.onLeaveStep = function(stepIdx) 
120         {
121             /* does the plugin has a validate entry ? */
122             var step = this.get_step(stepIdx);
123             var plugin = this.get_plugin(step);
124             if (plugin.validate) {
125                 plugin.validate();
126             }
127         }
128     
129
130         this.init1 = function() {
131             $this.curStepIdx = options.selected;
132             $this.steps = $("ul > li > a[href^='#step-']", obj); // Get all anchors in this array
133             $this.contentWidth = 0;
134             //this.loader,msgBox,elmActionBar,elmStepContainer,btNext,btPrevious,btFinish;
135             
136             $this.elmActionBar = $('.actionBar',obj);
137             if($this.elmActionBar.length == 0){
138               $this.elmActionBar = $('<div></div>').addClass("actionBar");                
139             }
140
141             $this.msgBox = $('.msgBox',obj);
142             if($this.msgBox.length == 0){
143               $this.msgBox = $('<div class="msgBox"><div class="content"></div><a href="#" class="close">X</a></div>');
144               $this.elmActionBar.append($this.msgBox);                
145             }
146             
147             $('.close',$this.msgBox).click(function() {
148                 $this.msgBox.fadeOut("normal");
149                 return false;
150             });
151         } // this.init1
152
153         this.init = function() {
154             this.init1();
155
156             /* additonal init */
157             this.options.onLeaveStep = this.onLeaveStep;
158
159             var allDivs =obj.children('div'); //$("div", obj);                
160             obj.children('ul').addClass("anchor");
161             allDivs.addClass("content");
162             // Create Elements
163             loader = $('<div>Loading</div>').addClass("loader");
164             elmActionBar = $('<div></div>').addClass("actionBar");
165             elmStepContainer = $('<div></div>').addClass("stepContainer");
166             btNext = $('<a>'+options.labelNext+'</a>').attr("href","#").addClass("buttonNext");
167             btPrevious = $('<a>'+options.labelPrevious+'</a>').attr("href","#").addClass("buttonPrevious");
168             btFinish = $('<a>'+options.labelFinish+'</a>').attr("href","#").addClass("buttonFinish");
169             
170             // highlight steps with errors
171             if(options.errorSteps && options.errorSteps.length>0){
172               $.each(options.errorSteps, function(i, n){
173                 $this.setError(n,true);
174               });
175             }
176
177
178             elmStepContainer.append(allDivs);
179             elmActionBar.append(loader);
180             obj.append(elmStepContainer);
181             obj.append(elmActionBar); 
182             if (options.includeFinishButton) {
183               elmActionBar.append(btFinish);
184             }
185             elmActionBar.append(btNext).append(btPrevious); 
186             contentWidth = elmStepContainer.width();
187
188             $(btNext).click(function() {
189                 if($(this).hasClass('buttonDisabled')){
190                   return false;
191                 }
192                 $this.doForwardProgress();
193                 return false;
194             }); 
195             $(btPrevious).click(function() {
196                 if($(this).hasClass('buttonDisabled')){
197                   return false;
198                 }
199                 $this.doBackwardProgress();
200                 return false;
201             }); 
202             $(btFinish).click(function() {
203                 if(!$(this).hasClass('buttonDisabled')){
204                    if($.isFunction(options.onFinish)) {
205                       if(!options.onFinish.call(this,$(this.steps))){
206                         return false;
207                       }
208                    }else{
209                      var frm = $(obj).parents('form');
210                      if(frm && frm.length){
211                        frm.submit();
212                      }                         
213                    }                      
214                 }
215
216                 return false;
217             }); 
218             
219             $(this.steps).bind("click", function(e){
220                 if(steps.index(this) == $this.curStepIdx){
221                   return false;                    
222                 }
223                 var nextStepIdx = steps.index(this);
224                 var isDone = steps.eq(nextStepIdx).attr("isDone") - 0;
225                 if(isDone == 1){
226                   $this.LoadContent(nextStepIdx);                    
227                 }
228                 return false;
229             }); 
230             
231             // Enable keyboard navigation                 
232             if(options.keyNavigation){
233                 $(document).keyup(function(e){
234                     if(e.which==39){ // Right Arrow
235                       $this.doForwardProgress();
236                     }else if(e.which==37){ // Left Arrow
237                       $this.doBackwardProgress();
238                     }
239                 });
240             }
241             //  Prepare the steps
242             this.prepareSteps();
243             // Show the first slected step
244             this.LoadContent($this.curStepIdx);                  
245         }
246
247         this.prepareSteps = function() {
248             if (!options.enableAllSteps) {
249                 $(this.steps, obj).removeClass("selected").removeClass("done").addClass("disabled"); 
250                 $(this.steps, obj).attr("isDone",0);                 
251             } else {
252                 $(this.steps, obj).removeClass("selected").removeClass("disabled").addClass("done"); 
253                 $(this.steps, obj).attr("isDone",1); 
254             }
255
256             $(this.steps, obj).each(function(i){
257                 $($(this).attr("href"), obj).hide();
258                 $(this).attr("rel",i+1);
259             });
260         }
261                     
262         this.LoadContent = function(stepIdx) {
263             var selStep = this.steps.eq(stepIdx);
264             var ajaxurl = options.contentURL;
265             var hasContent =  selStep.data('hasContent');
266             stepNum = stepIdx+1;
267             if(ajaxurl && ajaxurl.length>0){
268                if(options.contentCache && hasContent){
269                    this.showStep(stepIdx);                          
270                }else{
271                    $.ajax({
272                     url: ajaxurl,
273                     type: "POST",
274                     data: ({step_number : stepNum}),
275                     dataType: "text",
276                     beforeSend: function(){ loader.show(); },
277                     error: function(){loader.hide();},
278                     success: function(res){ 
279                       loader.hide();       
280                       if(res && res.length>0){  
281                          selStep.data('hasContent',true);            
282                          $($(selStep, obj).attr("href"), obj).html(res);
283                          this.showStep(stepIdx);
284                       }
285                     }
286                   }); 
287               }
288             }else{
289               this.showStep(stepIdx);
290             }
291         } // this.LoadContent
292                     
293         this.showStep = function(stepIdx) 
294         {
295             var selStep = this.steps.eq(stepIdx); 
296             var curStep = this.steps.eq($this.curStepIdx);
297
298
299             if(stepIdx != $this.curStepIdx){
300               if($.isFunction(options.onLeaveStep)) {
301                 if(!options.onLeaveStep.call(this,$(curStep))){
302                   return false;
303                 }
304               }
305             }     
306             if (options.updateHeight)
307                 elmStepContainer.height($($(selStep, obj).attr("href"), obj).outerHeight());               
308             if(options.transitionEffect == 'slide'){
309               $($(curStep, obj).attr("href"), obj).slideUp("fast",function(e){
310                     $($(selStep, obj).attr("href"), obj).slideDown("fast");
311                     $this.curStepIdx =  stepIdx;                        
312                     $this.SetupStep(curStep,selStep);
313                   });
314             } else if(options.transitionEffect == 'fade'){                      
315               $($(curStep, obj).attr("href"), obj).fadeOut("fast",function(e){
316                     $($(selStep, obj).attr("href"), obj).fadeIn("fast");
317                     $this.curStepIdx =  stepIdx;                        
318                     $this.SetupStep(curStep,selStep);                           
319                   });                    
320             } else if(options.transitionEffect == 'slideleft'){
321                 var nextElmLeft = 0;
322                 var curElementLeft = 0;
323                 if(stepIdx > $this.curStepIdx){
324                     nextElmLeft1 = contentWidth + 10;
325                     nextElmLeft2 = 0;
326                     curElementLeft = 0 - $($(curStep, obj).attr("href"), obj).outerWidth();
327                 } else {
328                     nextElmLeft1 = 0 - $($(selStep, obj).attr("href"), obj).outerWidth() + 20;
329                     nextElmLeft2 = 0;
330                     curElementLeft = 10 + $($(curStep, obj).attr("href"), obj).outerWidth();
331                 }
332                 if(stepIdx == $this.curStepIdx){
333                     nextElmLeft1 = $($(selStep, obj).attr("href"), obj).outerWidth() + 20;
334                     nextElmLeft2 = 0;
335                     curElementLeft = 0 - $($(curStep, obj).attr("href"), obj).outerWidth();                           
336                 }else{
337                     $($(curStep, obj).attr("href"), obj).animate({left:curElementLeft},"fast",function(e){
338                       $($(curStep, obj).attr("href"), obj).hide();
339                     });                       
340                 }
341
342                 $($(selStep, obj).attr("href"), obj).css("left",nextElmLeft1);
343                 $($(selStep, obj).attr("href"), obj).show();
344                 $($(selStep, obj).attr("href"), obj).animate({left:nextElmLeft2},"fast",function(e){
345                   $this.curStepIdx =  stepIdx;                        
346                   $this.SetupStep(curStep,selStep);                      
347                 });
348             } else{
349                 $($(curStep, obj).attr("href"), obj).hide(); 
350                 $($(selStep, obj).attr("href"), obj).show();
351                 $this.curStepIdx =  stepIdx;                        
352                 $this.SetupStep(curStep,selStep);
353             }
354             return true;
355         } // this.showStep
356                     
357         this.SetupStep = function(curStep,selStep)
358         {
359             $(curStep, obj).removeClass("selected");
360             $(curStep, obj).addClass("done");
361             
362             $(selStep, obj).removeClass("disabled");
363             $(selStep, obj).removeClass("done");
364             $(selStep, obj).addClass("selected");
365             $(selStep, obj).attr("isDone",1);
366             $this.adjustButton();
367             if($.isFunction(options.onShowStep)) {
368                if(!options.onShowStep.call(this,$(selStep))){
369                  return false;
370                }
371             } 
372         }                
373                     
374         this.doForwardProgress = function()
375         {
376             var nextStepIdx = $this.curStepIdx + 1;
377             if(this.steps.length <= nextStepIdx){
378               if(!options.cycleSteps){
379                 return false;
380               }                  
381               nextStepIdx = 0;
382             }
383             this.LoadContent(nextStepIdx);
384         }
385                     
386         this.doBackwardProgress = function()
387         {
388             var nextStepIdx = $this.curStepIdx-1;
389             if(0 > nextStepIdx){
390               if(!options.cycleSteps){
391                 return false;
392               }
393               nextStepIdx = this.steps.length - 1;
394             }
395             this.LoadContent(nextStepIdx);
396         }  
397                     
398         this.adjustButton = function()
399         {
400             if(!options.cycleSteps){                
401               if(0 >= $this.curStepIdx){
402                 $(btPrevious).addClass("buttonDisabled");
403               }else{
404                 $(btPrevious).removeClass("buttonDisabled");
405               }
406               if(($this.steps.length-1) <= $this.curStepIdx){
407                 $(btNext).addClass("buttonDisabled");
408               }else{
409                 $(btNext).removeClass("buttonDisabled");
410               }
411             }
412             // Finish Button 
413             if(!$this.steps.hasClass('disabled') || options.enableFinishButton){
414               $(btFinish).removeClass("buttonDisabled");
415             }else{
416               $(btFinish).addClass("buttonDisabled");
417             }                  
418         }
419                     
420         this.showMessage = function(msg)
421         {
422             $('.content',msgBox).html(msg);
423             msgBox.show();
424         }
425                     
426         this.setError = function(stepnum,iserror)
427         {
428             if(iserror){                    
429               $(this.steps.eq(stepnum-1), obj).addClass('error')
430             }else{
431               $(this.steps.eq(stepnum-1), obj).removeClass("error");
432             }                                   
433         }                        
434
435         this.setDone = function(stepnum,isdone)
436         {
437             if(isdone){                    
438               $(this.steps.eq(stepnum-1), obj).removeClass("selected").removeClass("disabled").addClass('done')
439             }else{
440               $(this.steps.eq(stepnum-1), obj).removeClass("done");
441             }                                   
442         }
443     }
444
445     /// KEEP BELOW
446
447     // Default Properties and Events
448     $.fn.Wizard.defaults = {
449         selected: 0,  // Selected Step, 0 = first step   
450         keyNavigation: true, // Enable/Disable key navigation(left and right keys are used if enabled)
451         enableAllSteps: false,
452         updateHeight: true,
453         transitionEffect: 'fade', // Effect on navigation, none/fade/slide/slideleft
454         contentURL:null, // content url, Enables Ajax content loading
455         contentCache:true, // cache step contents, if false content is fetched always from ajax url
456         cycleSteps: false, // cycle step navigation
457         includeFinishButton: true, // whether to show a Finish button
458         enableFinishButton: false, // make finish button enabled always
459         errorSteps:[],    // Array Steps with errors
460         labelNext:'Next',
461         labelPrevious:'Previous',
462         labelFinish:'Finish',          
463         onLeaveStep: null, // triggers when leaving a step
464         onShowStep: null,  // triggers when showing a step
465         onFinish: null  // triggers when Finish button is clicked
466     };    
467
468 })( jQuery );