Initialize the UI code
[holmes/rule-management.git] / rulemgt / src / main / frontend / src / public / common / js / jquery.sly.js
1 /*!
2  * jQuery Sly v0.9.6
3  * https://github.com/Darsain/sly
4  *
5  * Licensed under the MIT license.
6  * http://www.opensource.org/licenses/MIT
7  */
8
9 /*jshint eqeqeq: true, noempty: true, strict: true, undef: true, expr: true, smarttabs: true, browser: true */
10 /*global jQuery:false */
11
12 ;(function($, undefined){
13 'use strict';
14
15 // Plugin names
16 var pluginName = 'sly',
17         namespace = 'plugin_' + pluginName;
18
19 /**
20  * Plugin class
21  *
22  * @class
23  * @param {Element} frame DOM element of sly container
24  * @param {Object}  o Object with plugin options
25  */
26 function Plugin( frame, o ){
27
28         // Alias for this
29         var self = this,
30
31         // Frame variables
32                 $frame = $(frame),
33                 $slidee = $frame.children().eq(0),
34                 frameSize = 0,
35                 slideeSize = 0,
36                 pos = {
37                         cur: 0,
38                         max: 0,
39                         min: 0
40                 },
41
42         // Scrollbar variables
43                 $sb = $(o.scrollBar).eq(0),
44                 $handle = $sb.length ? $sb.children().eq(0) : 0,
45                 sbSize = 0,
46                 handleSize = 0,
47                 hPos = {
48                         cur: 0,
49                         max: 0,
50                         min: 0
51                 },
52
53         // Pagesbar variables
54                 $pb = $(o.pagesBar),
55                 $pages = 0,
56                 pages = [],
57
58         // Navigation type booleans
59                 basicNav = o.itemNav === 'basic',
60                 smartNav = o.itemNav === 'smart',
61                 forceCenteredNav = o.itemNav === 'forceCentered',
62                 centeredNav = o.itemNav === 'centered' || forceCenteredNav,
63                 itemNav = basicNav || smartNav || centeredNav || forceCenteredNav,
64
65         // Other variables
66                 $items = 0,
67                 items = [],
68                 rel = {
69                         firstItem: 0,
70                         lastItem: 1,
71                         centerItem: 1,
72                         activeItem: -1,
73                         activePage: 0,
74                         items: 0,
75                         pages: 0
76                 },
77                 $scrollSource = o.scrollSource ? $( o.scrollSource ) : $frame,
78                 $dragSource = o.dragSource ? $( o.dragSource ) : $frame,
79                 $prevButton = $(o.prev),
80                 $nextButton = $(o.next),
81                 $prevPageButton = $(o.prevPage),
82                 $nextPageButton = $(o.nextPage),
83                 cycleIndex = 0,
84                 cycleIsPaused = 0,
85                 isDragging = 0,
86                 callbacks = {};
87
88
89         /**
90          * (Re)Loading function
91          *
92          * Populates arrays, sets sizes, binds events, ...
93          *
94          * @public
95          */
96         var load = this.reload = function(){
97
98                 // Local variables
99                 var ignoredMargin = 0,
100                         oldPos = $.extend({}, pos);
101
102                 // Clear cycling timeout
103                 clearTimeout( cycleIndex );
104
105                 // Reset global variables
106                 frameSize = o.horizontal ? $frame.width() : $frame.height();
107                 sbSize = o.horizontal ? $sb.width() : $sb.height();
108                 slideeSize = o.horizontal ? $slidee.outerWidth() : $slidee.outerHeight();
109                 $items = $slidee.children();
110                 items = [];
111                 pages = [];
112
113                 // Set position limits & relatives
114                 pos.min = 0;
115                 pos.max = slideeSize > frameSize ? slideeSize - frameSize : 0;
116                 rel.items = $items.length;
117
118                 // Sizes & offsets logic, but only when needed
119                 if( itemNav ){
120
121                         var marginStart = getPx( $items, o.horizontal ? 'marginLeft' : 'marginTop' ),
122                                 marginEnd = getPx( $items.slice(-1), o.horizontal ? 'marginRight' : 'marginBottom' ),
123                                 centerOffset = 0,
124                                 paddingStart = getPx( $slidee, o.horizontal ? 'paddingLeft' : 'paddingTop' ),
125                                 paddingEnd = getPx( $slidee, o.horizontal ? 'paddingRight' : 'paddingBottom' ),
126                                 areFloated = $items.css('float') !== 'none';
127
128                         // Update ignored margin
129                         ignoredMargin = marginStart ? 0 : marginEnd;
130
131                         // Reset slideeSize
132                         slideeSize = 0;
133
134                         // Iterate through items
135                         $items.each(function(i,e){
136
137                                 // Item
138                                 var item = $(e),
139                                         itemSize = o.horizontal ? item.outerWidth(true) : item.outerHeight(true),
140                                         marginTop    = getPx( item, 'marginTop' ),
141                                         marginBottom = getPx( item, 'marginBottom'),
142                                         marginLeft   = getPx( item, 'marginLeft'),
143                                         marginRight  = getPx( item, 'marginRight'),
144                                         itemObj = {
145                                                 size: itemSize,
146                                                 offStart: slideeSize - ( !i || o.horizontal ? 0 : marginTop ),
147                                                 offCenter: slideeSize - Math.round( frameSize / 2 - itemSize / 2 ),
148                                                 offEnd: slideeSize - frameSize + itemSize - ( marginStart ? 0 : marginRight ),
149                                                 margins: {
150                                                         top:    marginTop,
151                                                         bottom: marginBottom,
152                                                         left:   marginLeft,
153                                                         right:  marginRight
154                                                 }
155                                         };
156
157                                 // Account for centerOffset & slidee padding
158                                 if( !i ){
159                                         centerOffset = -( forceCenteredNav ? Math.round( frameSize / 2 - itemSize / 2 ) : 0 ) + paddingStart;
160                                         slideeSize += paddingStart;
161                                 }
162
163                                 // Increment slidee size for size of the active element
164                                 slideeSize += itemSize;
165
166                                 // Try to account for vertical margin collapsing in vertical mode
167                                 // It's not bulletproof, but should work in 99% of cases
168                                 if( !o.horizontal && !areFloated ){
169
170                                         // Subtract smaller margin, but only when top margin is not 0, and this is not the first element
171                                         if( marginBottom && marginTop && i > 0 ){
172                                                 slideeSize -= marginTop < marginBottom ? marginTop : marginBottom;
173                                         }
174
175                                 }
176
177                                 // Things to be done at last item
178                                 if( i === $items.length - 1 ){
179                                         slideeSize += paddingEnd;
180                                 }
181
182                                 // Add item object to items array
183                                 items.push(itemObj);
184
185                         });
186
187                         // Resize slidee
188                         $slidee.css( o.horizontal ? { width: slideeSize+'px' } : { height: slideeSize+'px' } );
189
190                         // Adjust slidee size for last margin
191                         slideeSize -= ignoredMargin;
192
193                         // Set limits
194                         pos.min = centerOffset;
195                         pos.max = forceCenteredNav ? items[items.length-1].offCenter : slideeSize > frameSize ? slideeSize - frameSize : 0;
196
197                         // Fix overflowing activeItem
198                         rel.activeItem >= items.length && self.activate( items.length-1 );
199
200                 }
201
202                 // Assign relative position indexes
203                 assignRelatives();
204
205                 // Scrollbar
206                 if( $handle ){
207
208                         // Stretch scrollbar handle to represent the visible area
209                         handleSize = o.dynamicHandle ? Math.round( sbSize * frameSize / slideeSize ) : o.horizontal ? $handle.width() : $handle.height();
210                         handleSize = handleSize > sbSize ? sbSize : handleSize;
211                         handleSize = handleSize < o.minHandleSize ? o.minHandleSize : handleSize;
212                         hPos.max = sbSize - handleSize;
213
214                         // Resize handle
215                         $handle.css( o.horizontal ? { width: handleSize+'px' } : { height: handleSize+'px' } );
216
217                 }
218
219                 // Pages
220                 var tempPagePos = 0,
221                         pagesHtml = '',
222                         pageIndex = 0;
223
224                 // Populate pages array
225                 if( forceCenteredNav ){
226                         pages = $.map( items, function( o ){ return o.offCenter; } );
227                 } else {
228                         while( tempPagePos - frameSize < pos.max ){
229
230                                 var pagePos = tempPagePos > pos.max ? pos.max : tempPagePos;
231
232                                 pages.push( pagePos );
233                                 tempPagePos += frameSize;
234
235                                 // When item navigation, and last page is smaller than half of the last item size,
236                                 // adjust the last page position to pos.max and break the loop
237                                 if( tempPagePos > pos.max && itemNav && pos.max - pagePos < ( items[items.length-1].size - ignoredMargin ) / 2 ){
238
239                                         pages[pages.length-1] = pos.max;
240                                         break;
241
242                                 }
243
244                         }
245                 }
246
247                 // Pages bar
248                 if( $pb.length ){
249
250                         for( var i = 0; i < pages.length; i++ ){
251                                 pagesHtml += o.pageBuilder( pageIndex++ );
252                         }
253
254                         // Bind page navigation, append to pagesbar, and save to $pages variable
255                         $pages = $(pagesHtml).bind('click.' + namespace, function(){
256
257                                 self.activatePage( $pages.index(this) );
258
259                         }).appendTo( $pb.empty() );
260
261                 }
262
263                 // Bind activating to items
264                 $items.unbind('.' + namespace).bind('mouseup.' + namespace, function(e){
265
266                         e.which === 1 && !isDragging && self.activate( this );
267
268                 });
269
270                 // Fix overflowing
271                 pos.cur < pos.min && slide( pos.min );
272                 pos.cur > pos.max && slide( pos.max );
273
274                 // Extend relative variables object with some useful info
275                 rel.pages = pages.length;
276                 rel.slideeSize = slideeSize;
277                 rel.frameSize = frameSize;
278                 rel.sbSize = sbSize;
279                 rel.handleSize = handleSize;
280
281                 // Synchronize scrollbar
282                 syncBars(0);
283
284                 // Disable buttons
285                 disableButtons();
286
287                 // Automatic cycling
288                 if( itemNav && o.cycleBy ){
289
290                         var pauseEvents = 'mouseenter.' + namespace + ' mouseleave.' + namespace;
291
292                         // Pause on hover
293                         o.pauseOnHover && $frame.unbind(pauseEvents).bind(pauseEvents, function(e){
294
295                                 !cycleIsPaused && self.cycle( e.type === 'mouseenter', 1 );
296
297                         });
298
299                         // Initiate cycling
300                         self.cycle( o.startPaused );
301
302                 }
303
304                 // Trigger :load event
305                 $frame.trigger( pluginName + ':load', [ $.extend({}, pos, { old: oldPos }), $items, rel ] );
306
307         };
308
309
310         /**
311          * Slide the slidee
312          *
313          * @private
314          *
315          * @param {Int} newPos New slidee position in relation to frame
316          * @param {Bool} align  Whetner to Align elements to the frame border
317          * @param {Int} speed  Animation speed in milliseconds
318          */
319         function slide( newPos, align, speed ){
320
321                 speed = isNumber( speed ) ? speed : o.speed;
322
323                 // Align items
324                 if( align && itemNav ){
325
326                         var tempRel = getRelatives( newPos );
327
328                         if( centeredNav ){
329
330                                 newPos = items[tempRel.centerItem].offCenter;
331                                 self[ forceCenteredNav ? 'activate' : 'toCenter']( tempRel.centerItem, 1 );
332
333                         } else if( newPos > pos.min && newPos < pos.max ){
334
335                                 newPos = items[tempRel.firstItem].offStart;
336
337                         }
338
339                 }
340
341                 // Fix overflowing position
342                 if( !isDragging || !o.elasticBounds ){
343                         newPos = newPos < pos.min ? pos.min : newPos;
344                         newPos = newPos > pos.max ? pos.max : newPos;
345                 }
346
347                 // Stop if position has not changed
348                 if( newPos === pos.cur ) {
349                         return;
350                 } else {
351                         pos.cur = newPos;
352                 }
353
354                 // Reassign relative indexes
355                 assignRelatives();
356
357                 // Add disabled classes
358                 disableButtons();
359
360                 // halt ongoing animations
361                 stop();
362
363                 // Trigger :move event
364                 !isDragging && $frame.trigger( pluginName + ':move', [ pos, $items, rel ] );
365
366                 var newProp = o.horizontal ? { left: -pos.cur+'px' } : { top: -pos.cur+'px' };
367
368                 // Slidee move
369                 if( speed > 16 ){
370
371                         $slidee.animate( newProp, speed, isDragging ? 'swing' : o.easing, function(e){
372
373                                 // Trigger :moveEnd event
374                                 !isDragging && $frame.trigger( pluginName + ':moveEnd', [ pos, $items, rel ] );
375
376                         });
377
378                 } else {
379
380                         $slidee.css( newProp );
381
382                         // Trigger :moveEnd event
383                         !isDragging && $frame.trigger( pluginName + ':moveEnd', [ pos, $items, rel ] );
384
385                 }
386
387         }
388
389
390         /**
391          * Synchronizes scrollbar & pagesbar positions with the slidee
392          *
393          * @private
394          *
395          * @param {Int} speed Animation speed for scrollbar synchronization
396          */
397         function syncBars( speed ){
398
399                 // Scrollbar synchronization
400                 if ($handle) {
401
402                         hPos.cur = Math.round( ( pos.cur - pos.min ) / ( pos.max - pos.min ) * hPos.max );
403                         hPos.cur = hPos.cur < hPos.min ? hPos.min : hPos.cur > hPos.max ? hPos.max : hPos.cur;
404                         $handle.stop().animate( o.horizontal ? { left: hPos.cur+'px' } : { top: hPos.cur+'px' }, isNumber(speed) ? speed : o.speed, o.easing );
405
406                 }
407
408                 // Pagesbar synchronization
409                 syncPages();
410
411         }
412
413
414         /**
415          * Synchronizes pagesbar
416          *
417          * @private
418          */
419         function syncPages(){
420
421                 if (!$pages.length) {
422                         return;
423                 }
424
425                 // Classes
426                 $pages.removeClass(o.activeClass).eq(rel.activePage).addClass(o.activeClass);
427
428         }
429
430
431         /**
432          * Activate previous item
433          *
434          * @public
435          */
436         this.prev = function(){
437
438                 self.activate( rel.activeItem - 1 );
439
440         };
441
442
443         /**
444          * Activate next item
445          *
446          * @public
447          */
448         this.next = function(){
449
450                 self.activate( rel.activeItem + 1 );
451
452         };
453
454
455         /**
456          * Activate previous page
457          *
458          * @public
459          */
460         this.prevPage = function(){
461
462                 self.activatePage( rel.activePage - 1 );
463
464         };
465
466
467         /**
468          * Activate next page
469          *
470          * @public
471          */
472         this.nextPage = function(){
473
474                 self.activatePage( rel.activePage + 1 );
475
476         };
477
478
479         /**
480          * Stop ongoing animations
481          *
482          * @private
483          */
484         function stop(){
485
486                 $slidee.add($handle).stop();
487
488         }
489
490
491         /**
492          * Animate element or the whole slidee to the start of the frame
493          *
494          * @public
495          *
496          * @param {Element|Int} el DOM element, or index of element in items array
497          */
498         this.toStart = function( el ){
499
500                 if( itemNav ){
501
502                         var index = getIndex( el );
503
504                         if( el === undefined ){
505
506                                 slide( pos.min, 1 );
507
508                         } else if( index !== -1 ){
509
510                                 // You can't align items to the start of the frame when centeredNav is enabled
511                                 if (centeredNav) {
512                                         return;
513                                 }
514
515                                 index !== -1 && slide( items[index].offStart );
516
517                         }
518
519                 } else {
520
521                         if( el === undefined ){
522
523                                 slide( pos.min );
524
525                         } else {
526
527                                 var $el = $slidee.find(el).eq(0);
528
529                                 if( $el.length ){
530
531                                         var offset = o.horizontal ? $el.offset().left - $slidee.offset().left : $el.offset().top - $slidee.offset().top;
532
533                                         slide( offset );
534
535                                 }
536
537                         }
538
539                 }
540
541                 syncBars();
542
543         };
544
545
546         /**
547          * Animate element or the whole slidee to the end of the frame
548          *
549          * @public
550          *
551          * @param {Element|Int} el DOM element, or index of element in items array
552          */
553         this.toEnd = function( el ){
554
555                 if( itemNav ){
556
557                         var index = getIndex( el );
558
559                         if( el === undefined ){
560
561                                 slide( pos.max, 1 );
562
563                         } else if( index !== -1 ){
564
565                                 // You can't align items to the end of the frame when centeredNav is enabled
566                                 if (centeredNav) {
567                                         return;
568                                 }
569
570                                 slide( items[index].offEnd );
571
572                         }
573
574                 } else {
575
576                         if( el === undefined ){
577
578                                 slide( pos.max );
579
580                         } else {
581
582                                 var $el = $slidee.find(el).eq(0);
583
584                                 if( $el.length ){
585
586                                         var offset = o.horizontal ? $el.offset().left - $slidee.offset().left : $el.offset().top - $slidee.offset().top;
587
588                                         slide( offset - frameSize + $el[o.horizontal ? 'outerWidth' : 'outerHeight']() );
589
590                                 }
591
592                         }
593
594                 }
595
596                 syncBars();
597
598         };
599
600
601         /**
602          * Animate element or the whole slidee to the center of the frame
603          *
604          * @public
605          *
606          * @param {Element|Int} el DOM element, or index of element in items array
607          */
608         this.toCenter = function( el ){
609
610                 if( itemNav ){
611
612                         var index = getIndex( el );
613
614                         if( el === undefined ){
615
616                                 slide( Math.round( pos.max / 2 + pos.min / 2 ), 1 );
617
618                         } else if( index !== -1 ){
619
620                                 slide( items[index].offCenter );
621                                 forceCenteredNav && self.activate( index, 1 );
622
623                         }
624
625                 } else {
626
627                         if( el === undefined ){
628
629                                 slide( Math.round( pos.max / 2 ) );
630
631                         } else {
632
633                                 var $el = $slidee.find(el).eq(0);
634
635                                 if( $el.length ){
636
637                                         var offset = o.horizontal ? $el.offset().left - $slidee.offset().left : $el.offset().top - $slidee.offset().top;
638
639                                         slide( offset - frameSize / 2 + $el[o.horizontal ? 'outerWidth' : 'outerHeight']() / 2 );
640
641                                 }
642
643                         }
644
645
646                 }
647
648                 syncBars();
649
650         };
651
652
653         /**
654          * Get an index of the element
655          *
656          * @private
657          *
658          * @param {Element|Int} el DOM element, or index of element in items array
659          */
660         function getIndex( el ){
661
662                 return isNumber(el) ? el < 0 ? 0 : el > items.length-1 ? items.length-1 : el : el === undefined ? -1 : $items.index( el );
663
664         }
665
666
667         /**
668          * Parse style to pixels
669          *
670          * @private
671          *
672          * @param {Object} $item jQuery object with element
673          * @param {Property} property Property to get the pixels from
674          */
675         function getPx( $item, property ){
676
677                 return parseInt( $item.css( property ), 10 );
678
679         }
680
681
682         /**
683          * Activates an element
684          *
685          * Element is positioned to one of the sides of the frame, based on it's current position.
686          * If the element is close to the right frame border, it will be animated to the start of the left border,
687          * and vice versa. This helps user to navigate through the elements only by clicking on them, without
688          * the need for navigation buttons, scrolling, or keyboard arrows.
689          *
690          * @public
691          *
692          * @param {Element|Int} el DOM element, or index of element in items array
693          * @param {Bool} noReposition Activate item without repositioning it
694          */
695         this.activate = function( el, noReposition ){
696
697                 if (!itemNav || el === undefined) {
698                         return;
699                 }
700
701                 var index = getIndex( el ),
702                         oldActive = rel.activeItem;
703
704                 // Update activeItem index
705                 rel.activeItem = index;
706
707                 // Add active class to the active element
708                 $items.removeClass(o.activeClass).eq(index).addClass(o.activeClass);
709
710                 // Trigget :active event if a new element is being activated
711                 index !== oldActive && $items.eq( index ).trigger( pluginName + ':active', [ $items, rel ] );
712
713                 if( !noReposition ){
714
715                         // When centeredNav is enabled, center the element
716                         if( centeredNav ){
717
718                                 self.toCenter( index );
719
720                         // Otherwise determine where to position the element
721                         } else if( smartNav ) {
722
723                                 // If activated element is currently on the far right side of the frame, assume that
724                                 // user is moving forward and animate it to the start of the visible frame, and vice versa
725                                 if (index >= rel.lastItem) {
726                                         self.toStart(index);
727                                 } else if (index <= rel.firstItem) {
728                                         self.toEnd(index);
729                                 }
730
731                         }
732
733                 }
734
735                 // Add disabled classes
736                 disableButtons();
737
738         };
739
740
741         /**
742          * Activates a page
743          *
744          * @public
745          *
746          * @param {Int} index Page index, starting from 0
747          */
748         this.activatePage = function( index ){
749
750                 // Fix overflowing
751                 index = index < 0 ? 0 : index >= pages.length ? pages.length-1 : index;
752                 slide( pages[index], itemNav );
753
754                 syncBars();
755
756         };
757
758
759         /**
760          * Return relative positions of items based on their location within visible frame
761          *
762          * @private
763          *
764          * @param {Int} sPos Position of slidee
765          */
766         function getRelatives( sPos ){
767
768                 var newRel = {},
769                         centerOffset = forceCenteredNav ? 0 : frameSize / 2;
770
771                 // Determine active page
772                 for( var p = 0; p < pages.length; p++ ){
773
774                         if( sPos >= pos.max || p === pages.length - 1 ){
775                                 newRel.activePage = pages.length - 1;
776                                 break;
777                         }
778
779                         if( sPos <= pages[p] + centerOffset ){
780                                 newRel.activePage = p;
781                                 break;
782                         }
783
784                 }
785
786                 // Relative item indexes
787                 if( itemNav ){
788
789                         var first  = false,
790                                 last   = false,
791                                 center = false;
792
793                         /* From start */
794                         for( var i=0; i < items.length; i++ ){
795
796                                 // First item
797                                 if (first === false && sPos <= items[i].offStart) {
798                                         first = i;
799                                 }
800
801                                 // Centered item
802                                 if (center === false && sPos - items[i].size / 2 <= items[i].offCenter) {
803                                         center = i;
804                                 }
805
806                                 // Last item
807                                 if (i === items.length - 1 || (last === false && sPos < items[i + 1].offEnd)) {
808                                         last = i;
809                                 }
810
811                                 // Terminate if all are assigned
812                                 if (last !== false) {
813                                         break;
814                                 }
815
816                         }
817
818                         // Safe assignment, just to be sure the false won't be returned
819                         newRel.firstItem  = isNumber( first )  ? first  : 0;
820                         newRel.centerItem = isNumber( center ) ? center : newRel.firstItem;
821                         newRel.lastItem   = isNumber( last )   ? last   : newRel.centerItem;
822
823                 }
824
825                 return newRel;
826
827         }
828
829
830         /**
831          * Assign element indexes to the relative positions
832          *
833          * @private
834          */
835         function assignRelatives(){
836
837                 $.extend( rel, getRelatives( pos.cur ) );
838
839         }
840
841
842         /**
843          * Disable buttons when needed
844          *
845          * Adds disabledClass, and when the button is <button> or <input>,
846          * activates :disabled state
847          *
848          * @private
849          */
850         function disableButtons(){
851
852                 // item navigation
853                 if( itemNav ){
854
855                         var isFirstItem = rel.activeItem === 0,
856                                 isLastItem = rel.activeItem >= items.length-1;
857
858                         if( $prevButton.is('button,input') ){
859                                 $prevButton.prop('disabled', isFirstItem);
860                         }
861
862                         if( $nextButton.is('button,input') ){
863                                 $nextButton.prop('disabled', isLastItem);
864                         }
865
866                         $prevButton[ isFirstItem ? 'removeClass' : 'addClass'](o.disabledClass);
867                         $nextButton[ isLastItem ? 'removeClass' : 'addClass'](o.disabledClass);
868
869                 }
870
871                 // pages navigation
872                 if( $pages.length ){
873
874                         var isStart = pos.cur <= pos.min,
875                                 isEnd = pos.cur >= pos.max;
876
877                         if( $prevPageButton.is('button,input') ){
878                                 $prevPageButton.prop('disabled', isStart);
879                         }
880
881                         if( $nextPageButton.is('button,input') ){
882                                 $nextPageButton.prop('disabled', isEnd);
883                         }
884
885                         $prevPageButton[ isStart ? 'removeClass' : 'addClass'](o.disabledClass);
886                         $nextPageButton[ isEnd ? 'removeClass' : 'addClass'](o.disabledClass);
887
888                 }
889
890         }
891
892
893         /**
894          * Manage cycling
895          *
896          * @public
897          *
898          * @param {Bool} pause Pass true to pause cycling
899          * @param {Bool} soft Soft pause intended for pauseOnHover - won't set cycleIsPaused variable to true
900          */
901         this.cycle = function( pause, soft ){
902
903                 if (!itemNav || !o.cycleBy) {
904                         return;
905                 }
906
907                 if( !soft ){
908                         cycleIsPaused = !!pause;
909                 }
910
911                 if( pause ){
912
913                         if( cycleIndex ){
914
915                                 cycleIndex = clearTimeout( cycleIndex );
916
917                                 // Trigger :cyclePause event
918                                 $frame.trigger( pluginName + ':cyclePause', [ pos, $items, rel ] );
919
920                         }
921
922                 } else {
923
924                         // Don't initiate more than one cycle
925                         if (cycleIndex) {
926                                 return;
927                         }
928
929                         // Trigger :cycleStart event
930                         $frame.trigger( pluginName + ':cycleStart', [ pos, $items, rel ] );
931
932                         // Cycling loop
933                         (function loop(){
934
935                                 if( o.cycleInterval === 0 ){
936                                         return;
937                                 }
938
939                                 cycleIndex = setTimeout( function(){
940
941                                         if( !isDragging ){
942                                                 switch( o.cycleBy ){
943
944                                                         case 'items':
945                                                                 var nextItem = rel.activeItem >= items.length-1 ? 0 : rel.activeItem + 1;
946                                                                 self.activate( nextItem );
947                                                         break;
948
949                                                         case 'pages':
950                                                                 var nextPage = rel.activePage >= pages.length-1 ? 0 : rel.activePage + 1;
951                                                                 self.activatePage( nextPage );
952                                                         break;
953
954                                                 }
955                                         }
956
957                                         // Trigger :cycle event
958                                         $frame.trigger( pluginName + ':cycle', [ pos, $items, rel ] );
959
960                                         // Cycle the cycle!
961                                         loop();
962
963                                 }, o.cycleInterval );
964
965                         }());
966
967                 }
968
969         };
970
971
972         /**
973          * Crossbrowser reliable way to stop default event action
974          *
975          * @private
976          *
977          * @param {Event} e Event object
978          * @param {Bool} noBubbles Cancel event bubbling
979          */
980         function stopDefault( e, noBubbles ){
981
982                 var evt = e || window.event;
983                 evt.preventDefault ? evt.preventDefault() : evt.returnValue = false;
984                 noBubbles && evt.stopPropagation ? evt.stopPropagation() : evt.cancelBubble = true;
985
986         }
987
988
989         /**
990          * Updates a signle or multiple option values
991          *
992          * @param {Mixed} property Option property name that should be updated, or object with options that will extend the current one
993          * @param {Mixed} value    Option property value
994          *
995          * @public
996          */
997         this.set = function( property, value ){
998
999                 if( $.isPlainObject(property) ){
1000
1001                         o = $.extend({}, o, property);
1002
1003                 } else if( typeof property === 'string' ) {
1004
1005                         o[property] = value;
1006
1007                 }
1008
1009         };
1010
1011
1012         /**
1013          * Destroys plugin instance and everything it created
1014          *
1015          * @public
1016          */
1017         this.destroy = function(){
1018
1019                 // Unbind all events
1020                 $frame.add(document).add($slidee).add($items).add($scrollSource).add($handle)
1021                 .add($prevButton).add($nextButton).add($prevPageButton).add($nextPageButton)
1022                 .unbind('.' + namespace);
1023
1024                 // Reset some styles
1025                 $slidee.add($handle).css( o.horizontal ? { left: 0 } : { top: 0 } );
1026
1027                 // Remove plugin classes
1028                 $prevButton.add($nextButton).removeClass(o.disabledClass);
1029
1030                 // Remove page items
1031                 $pb.empty();
1032
1033                 // Remove plugin from element data storage
1034                 $.removeData(frame, namespace);
1035
1036         };
1037
1038
1039         /**
1040          * Check if variable is a number
1041          *
1042          * @param {Mixed} n Any type of variable
1043          *
1044          * @return {Boolean}
1045          */
1046         function isNumber( n ) {
1047                 return !isNaN(parseFloat(n)) && isFinite(n);
1048         }
1049
1050
1051         /** Constructor */
1052         (function(){
1053
1054                 var doc = $(document),
1055                         dragEvents = 'mousemove.' + namespace + ' mouseup.' + namespace;
1056
1057                 // Extend options
1058                 o = $.extend( {}, $.fn[pluginName].defaults, o );
1059
1060                 // Set required styles to elements
1061                 $frame.css({ overflow: 'hidden' }).css('position') === 'static' && $frame.css({ position: 'relative' });
1062                 $sb.css('position') === 'static' && $sb.css({ position: 'relative' });
1063                 $slidee.add($handle).css( o.horizontal ? { position: 'absolute', left: 0 } : { position: 'absolute', top: 0 } );
1064
1065                 // Load
1066                 load();
1067
1068                 // Activate requested position
1069                 itemNav ? self.activate( o.startAt ) : slide( o.startAt );
1070
1071                 // Sync scrollbar & pages
1072                 syncBars();
1073
1074                 // Scrolling navigation
1075                 o.scrollBy && $scrollSource.bind('DOMMouseScroll.' + namespace + ' mousewheel.' + namespace, function(e){
1076
1077                         // If there is no scrolling to be done, leave the default event alone
1078                         if (pos.min === pos.max) {
1079                                 return;
1080                         }
1081
1082                         stopDefault( e, 1 );
1083
1084                         var orgEvent = e.originalEvent,
1085                                 delta = 0,
1086                                 isForward, nextItem;
1087
1088                         // Old school scrollwheel delta
1089                         if ( orgEvent.wheelDelta ){ delta = orgEvent.wheelDelta / 120; }
1090                         if ( orgEvent.detail     ){ delta = -orgEvent.detail / 3; }
1091
1092                         isForward = delta < 0;
1093
1094                         if( itemNav ){
1095
1096                                 nextItem = getIndex( ( centeredNav ? forceCenteredNav ? rel.activeItem : rel.centerItem : rel.firstItem ) + ( isForward ? o.scrollBy : -o.scrollBy ) );
1097
1098                                 self[centeredNav ? forceCenteredNav ? 'activate' : 'toCenter' : 'toStart']( nextItem );
1099
1100                         } else {
1101
1102                                 slide( pos.cur + ( isForward ? o.scrollBy : -o.scrollBy ) );
1103
1104                         }
1105
1106                         syncBars();
1107
1108                 });
1109
1110                 // Keyboard navigation
1111                 o.keyboardNav && doc.bind('keydown.' + namespace, function(e){
1112
1113                         switch( e.keyCode || e.which ){
1114
1115                                 // Left or Up
1116                                 case o.horizontal ? 37 : 38:
1117
1118                                         stopDefault(e);
1119                                         o.keyboardNavByPages ? self.prevPage() : self.prev();
1120
1121                                 break;
1122
1123                                 // Right or Down
1124                                 case o.horizontal ? 39 : 40:
1125
1126                                         stopDefault(e);
1127                                         o.keyboardNavByPages ? self.nextPage() : self.next();
1128
1129                                 break;
1130
1131                         }
1132
1133                 });
1134
1135                 // Navigation buttons
1136                 o.prev && $prevButton.bind('click.' + namespace, function(e){ stopDefault(e); self.prev(); });
1137                 o.next && $nextButton.bind('click.' + namespace, function(e){ stopDefault(e); self.next(); });
1138                 o.prevPage && $prevPageButton.bind('click.' + namespace, function(e){ stopDefault(e); self.prevPage(); });
1139                 o.nextPage && $nextPageButton.bind('click.' + namespace, function(e){ stopDefault(e); self.nextPage(); });
1140
1141                 // Dragging navigation
1142                 o.dragContent && $dragSource.bind('mousedown.' + namespace, function(e){
1143
1144                         // Ignore other than left mouse button
1145                         if (e.which !== 1) {
1146                                 return;
1147                         }
1148
1149                         stopDefault(e);
1150
1151                         var leftInit = e.clientX,
1152                                 topInit = e.clientY,
1153                                 posInit = pos.cur,
1154                                 start = +new Date(),
1155                                 srcEl = e.target,
1156                                 easeoff = 0,
1157                                 isInitialized = 0;
1158
1159                         // Add dragging class
1160                         $slidee.addClass(o.draggedClass);
1161
1162                         // Stop potential ongoing animations
1163                         stop();
1164
1165                         // Bind dragging events
1166                         doc.bind(dragEvents, function(e){
1167
1168                                 var released = e.type === 'mouseup',
1169                                         path = o.horizontal ? e.clientX - leftInit : e.clientY - topInit,
1170                                         newPos = posInit - path;
1171
1172                                 // Initialized logic
1173                                 if( !isInitialized && Math.abs( path ) > 10 ){
1174
1175                                         isInitialized = 1;
1176
1177                                         // Trigger :dragStart event
1178                                         $slidee.trigger( pluginName + ':dragStart', [ pos ] );
1179
1180                                 }
1181
1182                                 // Limits & Elastic bounds
1183                                 if( newPos > pos.max ){
1184                                         newPos = o.elasticBounds ? pos.max + ( newPos - pos.max ) / 6 : pos.max;
1185                                 } else if( newPos < pos.min ){
1186                                         newPos = o.elasticBounds ? pos.min + ( newPos - pos.min ) / 6 : pos.min;
1187                                 }
1188
1189                                 // Adjust newPos with easing when content has been released
1190                                 if( released ){
1191
1192                                         // Cleanup
1193                                         doc.unbind(dragEvents);
1194                                         $slidee.removeClass(o.draggedClass);
1195
1196                                         // How long was the dragging
1197                                         var time = +new Date() - start;
1198
1199                                         // Calculate swing length
1200                                         var swing = time < 300 ? Math.ceil( Math.pow( 6 / ( time / 300 ) , 2 ) * Math.abs( path ) / 120 ) : 0;
1201                                         newPos += path > 0 ? -swing : swing;
1202
1203                                 }
1204
1205                                 // Drag only when isInitialized
1206                                 if (!isInitialized) {
1207                                         return;
1208                                 }
1209
1210                                 stopDefault(e);
1211
1212                                 // Stop default click action on source element
1213                                 if( srcEl ){
1214
1215                                         $(srcEl).bind('click.' + namespace, function stopMe(e){
1216
1217                                                 stopDefault(e,true);
1218                                                 $(this).unbind('click.' + namespace, stopMe);
1219
1220                                         });
1221
1222                                         srcEl = 0;
1223
1224                                 }
1225
1226                                 // Dragging state
1227                                 isDragging = !released;
1228
1229                                 // Animage, synch bars, & align
1230                                 slide( newPos, released, released ? o.speed : 0 );
1231                                 syncBars( released ? null : 0 );
1232
1233                                 // Trigger :drag event
1234                                 if (isInitialized) {
1235                                         $slidee.trigger(pluginName + ':drag', [pos]);
1236                                 }
1237
1238                                 // Trigger :dragEnd event
1239                                 if (released) {
1240                                         $slidee.trigger(pluginName + ':dragEnd', [pos]);
1241                                 }
1242
1243                         });
1244
1245                 });
1246
1247                 // Scrollbar navigation
1248                 $handle && o.dragHandle && $handle.bind('mousedown.' + namespace, function(e){
1249
1250                         // Ignore other than left mouse button
1251                         if (e.which !== 1) {
1252                                 return;
1253                         }
1254
1255                         stopDefault(e);
1256
1257                         var leftInit = e.clientX,
1258                                 topInit = e.clientY,
1259                                 posInit = hPos.cur,
1260                                 pathMin = -hPos.cur,
1261                                 pathMax = hPos.max - hPos.cur,
1262                                 nextDrag = 0;
1263
1264                         // Add dragging class
1265                         $handle.addClass(o.draggedClass);
1266
1267                         // Stop potential ongoing animations
1268                         stop();
1269
1270                         // Bind dragging events
1271                         doc.bind(dragEvents, function(e){
1272
1273                                 stopDefault(e);
1274
1275                                 var released = e.type === 'mouseup',
1276                                         path = o.horizontal ? e.clientX - leftInit : e.clientY - topInit,
1277                                         newPos = posInit + path,
1278                                         time = +new Date();
1279
1280                                 // Dragging state
1281                                 isDragging = !released;
1282
1283                                 // Unbind events and remove classes when released
1284                                 if( released ){
1285
1286                                         doc.unbind(dragEvents);
1287                                         $handle.removeClass(o.draggedClass);
1288
1289                                 }
1290
1291                                 // Execute only moves within path limits
1292                                 if( path < pathMax+5 && path > pathMin-5 || released ){
1293
1294                                         // Fix overflows
1295                                         hPos.cur = newPos > hPos.max ? hPos.max : newPos < hPos.min ? hPos.min : newPos;
1296
1297                                         // Move handle
1298                                         $handle.stop().css( o.horizontal ? { left: hPos.cur+'px' } : { top: hPos.cur+'px' } );
1299
1300                                         // Trigger :dragStart event
1301                                         if (!nextDrag) {
1302                                                 $handle.trigger(pluginName + ':dragStart', [hPos]);
1303                                         }
1304
1305                                         // Trigger :drag event
1306                                         $handle.trigger( pluginName + ':drag', [ hPos ] );
1307
1308                                         // Trigger :dragEnd event
1309                                         if (released) {
1310                                                 $handle.trigger(pluginName + ':dragEnd', [hPos]);
1311                                         }
1312
1313                                         // Throttle sync interval -> smoother animations, lower CPU load
1314                                         if( nextDrag <= time || released || path > pathMax || path < pathMin ){
1315
1316                                                 nextDrag = time + 50;
1317
1318                                                 // Synchronize slidee position
1319                                                 slide( Math.round( hPos.cur / hPos.max * ( pos.max - pos.min ) ) + pos.min, released, released ? o.speed : 50 );
1320
1321                                         }
1322
1323                                         // Sync pagesbar
1324                                         syncPages();
1325
1326                                 }
1327
1328                         });
1329
1330                 });
1331
1332         }());
1333
1334 }
1335
1336
1337 // jQuery plugin extension
1338 $.fn[pluginName] = function( options, returnInstance ){
1339
1340         var method = false,
1341                 methodArgs,
1342                 instances = [];
1343
1344         // Basic attributes logic
1345         if( typeof options !== 'undefined' && !$.isPlainObject( options ) ){
1346                 method = options === false ? 'destroy' : options;
1347                 methodArgs = arguments;
1348                 Array.prototype.shift.call( methodArgs );
1349         }
1350
1351         // Apply requested actions on all elements
1352         this.each(function( i, element ){
1353
1354                 // Plugin call with prevention against multiple instantiations
1355                 var plugin = $.data( element, namespace );
1356
1357                 if( plugin && method ){
1358
1359                         // Call plugin method
1360                         if( plugin[method] ){
1361
1362                                 plugin[method].apply( plugin, methodArgs );
1363
1364                         }
1365
1366                 } else if( !plugin && !method ){
1367
1368                         // Create a new plugin object if it doesn't exist yet
1369                         plugin =  $.data( element, namespace, new Plugin( element, options ) );
1370
1371                 }
1372
1373                 // Push plugin to instances
1374                 instances.push( plugin );
1375
1376         });
1377
1378         // Return chainable jQuery object, or plugin instance(s)
1379         return returnInstance && !method ? instances.length > 1 ? instances : instances[0] : this;
1380
1381 };
1382
1383
1384 // Default options
1385 $.fn[pluginName].defaults = {
1386
1387         // Sly direction
1388         horizontal:      0,       // set to 1 to change the sly direction to horizontal
1389
1390         // Navigation by items; when using this, `scrollBy` option scrolls by items, not pixels
1391         itemNav:         0,       // enable type of item based navigation. when itemNav is enabled, items snap to frame edges or frame center
1392                                   // itemNav also enables "item activation" functionality and methods associated with it
1393                                   //
1394                                   // itemNav can be:
1395                                   // ------------------------------------------------------------------------------------
1396                                   // basic:         items snap to edges (ideal if you don't care about "active item" functionality)
1397                                   // smart:         same as basic, but activated item close to, or outside of the visible edge will be positioned to the opposite edge
1398                                   // centered:      activated items are positioned to the center of visible frame if possible
1399                                   // forceCentered: active items are always centered & centered items are always active (scrolling & dragging end activates centered item)
1400
1401         // Scrollbar
1402         scrollBar:       null,    // selector or DOM element for scrollbar container (scrollbar container should have one child element representing scrollbar handle)
1403           dynamicHandle: 1,       // resizes scrollbar handle to represent the relation between hidden and visible content. set to "0" to leave it as big as CSS made it
1404           dragHandle:    1,       // set to 0 to disable dragging of scrollbar handle with mouse
1405           minHandleSize: 50,      // minimal height or width (depends on sly direction) of a handle in pixels
1406
1407         // Pagesbar (when centerActive is enabled, every item is considered to be a page)
1408         pagesBar:        null,    // selector or DOM element for pages bar container
1409           pageBuilder:            // function with `index` (starting at 0) as argument that returns an HTML for one item
1410             function( index ){
1411               return '<li>'+(index+1)+'</li>';
1412             },
1413
1414         // Navigation buttons
1415         prev:            null,    // selector or DOM element for "previous item" button ; doesn't work when `itemsNav` is disabled
1416         next:            null,    // selector or DOM element for "next item" button     ; doesn't work when `itemsNav` is disabled
1417         prevPage:        null,    // selector or DOM element for "previous page" button
1418         nextPage:        null,    // selector or DOM element for "next page" button
1419
1420         // Automated cycling
1421         cycleBy:         0,       // enable automatic cycling by 'items', or 'pages'
1422           cycleInterval: 5000,    // number of milliseconds between cycles
1423           pauseOnHover:  1,       // pause cycling when mouse hovers over frame
1424           startPaused:   0,       // set to "1" to start in paused sate. cycling can be than resumed with "cycle" method
1425
1426         // Mixed options
1427         scrollBy:        0,       // how many pixels/items should one mouse scroll event go. leave "0" to disable mousewheel scrolling
1428         dragContent:     0,       // set to 1 to enable navigation by dragging the content with your mouse
1429           elasticBounds: 0,       // when dragging past limits, stretch them a little bit (like on spartphones)
1430         speed:           300,     // animations speed
1431         easing:          'swing', // animations easing. build in jQuery options are "linear" and "swing". for more, install gsgd.co.uk/sandbox/jquery/easing/
1432         scrollSource:    null,    // selector or DOM element for catching the mouse wheel event for sly scrolling. default source is the frame
1433         dragSource:      null,    // selector or DOM element for catching the mouse dragging events. default source is the frame
1434         startAt:         0,       // starting offset in pixels or items (depends on itemsNav option)
1435         keyboardNav:     0,       // whether to allow navigation by keyboard arrows (left & right for horizontal, up & down for vertical)
1436                                   // NOTE! keyboard navigation will disable page scrolling with keyboard arrows in correspondent sly direction (vertical or horizontal)
1437         keyboardNavByPages: 0,    // whether the keyboard should navigate by pages instead of items (useful when not using `itemsNav` navigation)
1438
1439         // Classes
1440         draggedClass:  'dragged', // class that will be added to scrollbar handle, or content when they are being dragged
1441         activeClass:   'active',  // class that will be added to the active item, or page
1442         disabledClass: 'disabled' // class that will be added to prev button when on start, or next button when on end
1443
1444 };
1445
1446 }(jQuery));