jquery.sly.js 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438
  1. /*jshint eqeqeq: true, noempty: true, strict: true, undef: true, expr: true, smarttabs: true, browser: true */
  2. /*global jQuery:false */
  3. ;(function($, undefined){
  4. 'use strict';
  5. // Plugin names
  6. var pluginName = 'sly',
  7. namespace = 'plugin_' + pluginName;
  8. /**
  9. * Plugin class
  10. *
  11. * @class
  12. * @param {Element} frame DOM element of sly container
  13. * @param {Object} o Object with plugin options
  14. */
  15. function Plugin( frame, o ){
  16. // Alias for this
  17. var self = this,
  18. // Frame variables
  19. $frame = $(frame),
  20. $slidee = $frame.children().eq(0),
  21. frameSize = 0,
  22. slideeSize = 0,
  23. pos = {
  24. cur: 0,
  25. max: 0,
  26. min: 0
  27. },
  28. // Scrollbar variables
  29. $sb = $(o.scrollBar).eq(0),
  30. $handle = $sb.length ? $sb.children().eq(0) : 0,
  31. sbSize = 0,
  32. handleSize = 0,
  33. hPos = {
  34. cur: 0,
  35. max: 0,
  36. min: 0
  37. },
  38. // Pagesbar variables
  39. $pb = $(o.pagesBar),
  40. $pages = 0,
  41. pages = [],
  42. // Navigation type booleans
  43. basicNav = o.itemNav === 'basic',
  44. smartNav = o.itemNav === 'smart',
  45. forceCenteredNav = o.itemNav === 'forceCentered',
  46. centeredNav = o.itemNav === 'centered' || forceCenteredNav,
  47. itemNav = basicNav || smartNav || centeredNav || forceCenteredNav,
  48. // Other variables
  49. $items = 0,
  50. items = [],
  51. rel = {
  52. firstItem: 0,
  53. lastItem: 1,
  54. centerItem: 1,
  55. activeItem: -1,
  56. activePage: 0,
  57. items: 0,
  58. pages: 0
  59. },
  60. $scrollSource = o.scrollSource ? $( o.scrollSource ) : $frame,
  61. $dragSource = o.dragSource ? $( o.dragSource ) : $frame,
  62. $prevButton = $(o.prev),
  63. $nextButton = $(o.next),
  64. $prevPageButton = $(o.prevPage),
  65. $nextPageButton = $(o.nextPage),
  66. cycleIndex = 0,
  67. cycleIsPaused = 0,
  68. isDragging = 0,
  69. callbacks = {};
  70. /**
  71. * (Re)Loading function
  72. *
  73. * Populates arrays, sets sizes, binds events, ...
  74. *
  75. * @public
  76. */
  77. var load = this.reload = function(){
  78. // Local variables
  79. var ignoredMargin = 0,
  80. oldPos = $.extend({}, pos);
  81. // Clear cycling timeout
  82. clearTimeout( cycleIndex );
  83. // Reset global variables
  84. frameSize = o.horizontal ? $frame.width() : $frame.height();
  85. sbSize = o.horizontal ? $sb.width() : $sb.height();
  86. slideeSize = o.horizontal ? $slidee.outerWidth() : $slidee.outerHeight();
  87. $items = $slidee.children();
  88. items = [];
  89. pages = [];
  90. // Set position limits & relatives
  91. pos.min = 0;
  92. pos.max = slideeSize > frameSize ? slideeSize - frameSize : 0;
  93. rel.items = $items.length;
  94. // Sizes & offsets logic, but only when needed
  95. if( itemNav ){
  96. var marginStart = getPx( $items, o.horizontal ? 'marginLeft' : 'marginTop' ),
  97. marginEnd = getPx( $items.slice(-1), o.horizontal ? 'marginRight' : 'marginBottom' ),
  98. centerOffset = 0,
  99. paddingStart = getPx( $slidee, o.horizontal ? 'paddingLeft' : 'paddingTop' ),
  100. paddingEnd = getPx( $slidee, o.horizontal ? 'paddingRight' : 'paddingBottom' ),
  101. areFloated = $items.css('float') !== 'none';
  102. // Update ignored margin
  103. ignoredMargin = marginStart ? 0 : marginEnd;
  104. // Reset slideeSize
  105. slideeSize = 0;
  106. // Iterate through items
  107. $items.each(function(i,e){
  108. // Item
  109. var item = $(e),
  110. itemSize = o.horizontal ? item.outerWidth(true) : item.outerHeight(true),
  111. marginTop = getPx( item, 'marginTop' ),
  112. marginBottom = getPx( item, 'marginBottom'),
  113. marginLeft = getPx( item, 'marginLeft'),
  114. marginRight = getPx( item, 'marginRight'),
  115. itemObj = {
  116. size: itemSize,
  117. offStart: slideeSize - ( !i || o.horizontal ? 0 : marginTop ),
  118. offCenter: slideeSize - Math.round( frameSize / 2 - itemSize / 2 ),
  119. offEnd: slideeSize - frameSize + itemSize - ( marginStart ? 0 : marginRight ),
  120. margins: {
  121. top: marginTop,
  122. bottom: marginBottom,
  123. left: marginLeft,
  124. right: marginRight
  125. }
  126. };
  127. // Account for centerOffset & slidee padding
  128. if( !i ){
  129. centerOffset = -( forceCenteredNav ? Math.round( frameSize / 2 - itemSize / 2 ) : 0 ) + paddingStart;
  130. slideeSize += paddingStart;
  131. }
  132. // Increment slidee size for size of the active element
  133. slideeSize += itemSize;
  134. // Try to account for vertical margin collapsing in vertical mode
  135. // It's not bulletproof, but should work in 99% of cases
  136. if( !o.horizontal && !areFloated ){
  137. // Subtract smaller margin, but only when top margin is not 0, and this is not the first element
  138. if( marginBottom && marginTop && i > 0 ){
  139. slideeSize -= marginTop < marginBottom ? marginTop : marginBottom;
  140. }
  141. }
  142. // Things to be done at last item
  143. if( i === $items.length - 1 ){
  144. slideeSize += paddingEnd;
  145. }
  146. // Add item object to items array
  147. items.push(itemObj);
  148. });
  149. // Resize slidee
  150. $slidee.css( o.horizontal ? { width: slideeSize+'px' } : { height: slideeSize+'px' } );
  151. // Adjust slidee size for last margin
  152. slideeSize -= ignoredMargin;
  153. // Set limits
  154. pos.min = centerOffset;
  155. pos.max = forceCenteredNav ? items[items.length-1].offCenter : slideeSize > frameSize ? slideeSize - frameSize : 0;
  156. // Fix overflowing activeItem
  157. rel.activeItem >= items.length && self.activate( items.length-1 );
  158. }
  159. // Assign relative position indexes
  160. assignRelatives();
  161. // Scrollbar
  162. if( $handle ){
  163. // Stretch scrollbar handle to represent the visible area
  164. handleSize = o.dynamicHandle ? Math.round( sbSize * frameSize / slideeSize ) : o.horizontal ? $handle.width() : $handle.height();
  165. handleSize = handleSize > sbSize ? sbSize : handleSize;
  166. handleSize = handleSize < o.minHandleSize ? o.minHandleSize : handleSize;
  167. hPos.max = sbSize - handleSize;
  168. // Resize handle
  169. $handle.css( o.horizontal ? { width: handleSize+'px' } : { height: handleSize+'px' } );
  170. }
  171. // Pages
  172. var tempPagePos = 0,
  173. pagesHtml = '',
  174. pageIndex = 0;
  175. // Populate pages array
  176. if( forceCenteredNav ){
  177. pages = $.map( items, function( o ){ return o.offCenter; } );
  178. } else {
  179. while( tempPagePos - frameSize < pos.max ){
  180. var pagePos = tempPagePos > pos.max ? pos.max : tempPagePos;
  181. pages.push( pagePos );
  182. tempPagePos += frameSize;
  183. // When item navigation, and last page is smaller than half of the last item size,
  184. // adjust the last page position to pos.max and break the loop
  185. if( tempPagePos > pos.max && itemNav && pos.max - pagePos < ( items[items.length-1].size - ignoredMargin ) / 2 ){
  186. pages[pages.length-1] = pos.max;
  187. break;
  188. }
  189. }
  190. }
  191. // Pages bar
  192. if( $pb.length ){
  193. for( var i = 0; i < pages.length; i++ ){
  194. pagesHtml += o.pageBuilder( pageIndex++ );
  195. }
  196. // Bind page navigation, append to pagesbar, and save to $pages variable
  197. $pages = $(pagesHtml).bind('click.' + namespace, function(){
  198. self.activatePage( $pages.index(this) );
  199. }).appendTo( $pb.empty() );
  200. }
  201. // Bind activating to items
  202. $items.unbind('.' + namespace).bind('mouseup.' + namespace, function(e){
  203. e.which === 1 && !isDragging && self.activate( this );
  204. });
  205. // Fix overflowing
  206. pos.cur < pos.min && slide( pos.min );
  207. pos.cur > pos.max && slide( pos.max );
  208. // Extend relative variables object with some useful info
  209. rel.pages = pages.length;
  210. rel.slideeSize = slideeSize;
  211. rel.frameSize = frameSize;
  212. rel.sbSize = sbSize;
  213. rel.handleSize = handleSize;
  214. // Synchronize scrollbar
  215. syncBars(0);
  216. // Disable buttons
  217. disableButtons();
  218. // Automatic cycling
  219. if( itemNav && o.cycleBy ){
  220. var pauseEvents = 'mouseenter.' + namespace + ' mouseleave.' + namespace;
  221. // Pause on hover
  222. o.pauseOnHover && $frame.unbind(pauseEvents).bind(pauseEvents, function(e){
  223. !cycleIsPaused && self.cycle( e.type === 'mouseenter', 1 );
  224. });
  225. // Initiate cycling
  226. self.cycle( o.startPaused );
  227. }
  228. // Trigger :load event
  229. $frame.trigger( pluginName + ':load', [ $.extend({}, pos, { old: oldPos }), $items, rel ] );
  230. };
  231. /**
  232. * Slide the slidee
  233. *
  234. * @private
  235. *
  236. * @param {Int} newPos New slidee position in relation to frame
  237. * @param {Bool} align Whetner to Align elements to the frame border
  238. * @param {Int} speed Animation speed in milliseconds
  239. */
  240. function slide( newPos, align, speed ){
  241. speed = isNumber( speed ) ? speed : o.speed;
  242. // Align items
  243. if( align && itemNav ){
  244. var tempRel = getRelatives( newPos );
  245. if( centeredNav ){
  246. newPos = items[tempRel.centerItem].offCenter;
  247. self[ forceCenteredNav ? 'activate' : 'toCenter']( tempRel.centerItem, 1 );
  248. } else if( newPos > pos.min && newPos < pos.max ){
  249. newPos = items[tempRel.firstItem].offStart;
  250. }
  251. }
  252. // Fix overflowing position
  253. if( !isDragging || !o.elasticBounds ){
  254. newPos = newPos < pos.min ? pos.min : newPos;
  255. newPos = newPos > pos.max ? pos.max : newPos;
  256. }
  257. // Stop if position has not changed
  258. if( newPos === pos.cur ) {
  259. return;
  260. } else {
  261. pos.cur = newPos;
  262. }
  263. // Reassign relative indexes
  264. assignRelatives();
  265. // Add disabled classes
  266. disableButtons();
  267. // halt ongoing animations
  268. stop();
  269. // Trigger :move event
  270. !isDragging && $frame.trigger( pluginName + ':move', [ pos, $items, rel ] );
  271. var newProp = o.horizontal ? { left: -pos.cur+'px' } : { top: -pos.cur+'px' };
  272. // Slidee move
  273. if( speed > 16 ){
  274. $slidee.animate( newProp, speed, isDragging ? 'swing' : o.easing, function(e){
  275. // Trigger :moveEnd event
  276. !isDragging && $frame.trigger( pluginName + ':moveEnd', [ pos, $items, rel ] );
  277. });
  278. } else {
  279. $slidee.css( newProp );
  280. // Trigger :moveEnd event
  281. !isDragging && $frame.trigger( pluginName + ':moveEnd', [ pos, $items, rel ] );
  282. }
  283. }
  284. /**
  285. * Synchronizes scrollbar & pagesbar positions with the slidee
  286. *
  287. * @private
  288. *
  289. * @param {Int} speed Animation speed for scrollbar synchronization
  290. */
  291. function syncBars( speed ){
  292. // Scrollbar synchronization
  293. if ($handle) {
  294. hPos.cur = Math.round( ( pos.cur - pos.min ) / ( pos.max - pos.min ) * hPos.max );
  295. hPos.cur = hPos.cur < hPos.min ? hPos.min : hPos.cur > hPos.max ? hPos.max : hPos.cur;
  296. $handle.stop().animate( o.horizontal ? { left: hPos.cur+'px' } : { top: hPos.cur+'px' }, isNumber(speed) ? speed : o.speed, o.easing );
  297. }
  298. // Pagesbar synchronization
  299. syncPages();
  300. }
  301. /**
  302. * Synchronizes pagesbar
  303. *
  304. * @private
  305. */
  306. function syncPages(){
  307. if (!$pages.length) {
  308. return;
  309. }
  310. // Classes
  311. $pages.removeClass(o.activeClass).eq(rel.activePage).addClass(o.activeClass);
  312. }
  313. /**
  314. * Activate previous item
  315. *
  316. * @public
  317. */
  318. this.prev = function(){
  319. self.activate( rel.activeItem - 1 );
  320. };
  321. /**
  322. * Activate next item
  323. *
  324. * @public
  325. */
  326. this.next = function(){
  327. self.activate( rel.activeItem + 1 );
  328. };
  329. /**
  330. * Activate previous page
  331. *
  332. * @public
  333. */
  334. this.prevPage = function(){
  335. self.activatePage( rel.activePage - 1 );
  336. };
  337. /**
  338. * Activate next page
  339. *
  340. * @public
  341. */
  342. this.nextPage = function(){
  343. self.activatePage( rel.activePage + 1 );
  344. };
  345. /**
  346. * Stop ongoing animations
  347. *
  348. * @private
  349. */
  350. function stop(){
  351. $slidee.add($handle).stop();
  352. }
  353. /**
  354. * Animate element or the whole slidee to the start of the frame
  355. *
  356. * @public
  357. *
  358. * @param {Element|Int} el DOM element, or index of element in items array
  359. */
  360. this.toStart = function( el ){
  361. if( itemNav ){
  362. var index = getIndex( el );
  363. if( el === undefined ){
  364. slide( pos.min, 1 );
  365. } else if( index !== -1 ){
  366. // You can't align items to the start of the frame when centeredNav is enabled
  367. if (centeredNav) {
  368. return;
  369. }
  370. index !== -1 && slide( items[index].offStart );
  371. }
  372. } else {
  373. if( el === undefined ){
  374. slide( pos.min );
  375. } else {
  376. var $el = $slidee.find(el).eq(0);
  377. if( $el.length ){
  378. var offset = o.horizontal ? $el.offset().left - $slidee.offset().left : $el.offset().top - $slidee.offset().top;
  379. slide( offset );
  380. }
  381. }
  382. }
  383. syncBars();
  384. };
  385. /**
  386. * Animate element or the whole slidee to the end of the frame
  387. *
  388. * @public
  389. *
  390. * @param {Element|Int} el DOM element, or index of element in items array
  391. */
  392. this.toEnd = function( el ){
  393. if( itemNav ){
  394. var index = getIndex( el );
  395. if( el === undefined ){
  396. slide( pos.max, 1 );
  397. } else if( index !== -1 ){
  398. // You can't align items to the end of the frame when centeredNav is enabled
  399. if (centeredNav) {
  400. return;
  401. }
  402. slide( items[index].offEnd );
  403. }
  404. } else {
  405. if( el === undefined ){
  406. slide( pos.max );
  407. } else {
  408. var $el = $slidee.find(el).eq(0);
  409. if( $el.length ){
  410. var offset = o.horizontal ? $el.offset().left - $slidee.offset().left : $el.offset().top - $slidee.offset().top;
  411. slide( offset - frameSize + $el[o.horizontal ? 'outerWidth' : 'outerHeight']() );
  412. }
  413. }
  414. }
  415. syncBars();
  416. };
  417. /**
  418. * Animate element or the whole slidee to the center of the frame
  419. *
  420. * @public
  421. *
  422. * @param {Element|Int} el DOM element, or index of element in items array
  423. */
  424. this.toCenter = function( el ){
  425. if( itemNav ){
  426. var index = getIndex( el );
  427. if( el === undefined ){
  428. slide( Math.round( pos.max / 2 + pos.min / 2 ), 1 );
  429. } else if( index !== -1 ){
  430. slide( items[index].offCenter );
  431. forceCenteredNav && self.activate( index, 1 );
  432. }
  433. } else {
  434. if( el === undefined ){
  435. slide( Math.round( pos.max / 2 ) );
  436. } else {
  437. var $el = $slidee.find(el).eq(0);
  438. if( $el.length ){
  439. var offset = o.horizontal ? $el.offset().left - $slidee.offset().left : $el.offset().top - $slidee.offset().top;
  440. slide( offset - frameSize / 2 + $el[o.horizontal ? 'outerWidth' : 'outerHeight']() / 2 );
  441. }
  442. }
  443. }
  444. syncBars();
  445. };
  446. /**
  447. * Get an index of the element
  448. *
  449. * @private
  450. *
  451. * @param {Element|Int} el DOM element, or index of element in items array
  452. */
  453. function getIndex( el ){
  454. return isNumber(el) ? el < 0 ? 0 : el > items.length-1 ? items.length-1 : el : el === undefined ? -1 : $items.index( el );
  455. }
  456. /**
  457. * Parse style to pixels
  458. *
  459. * @private
  460. *
  461. * @param {Object} $item jQuery object with element
  462. * @param {Property} property Property to get the pixels from
  463. */
  464. function getPx( $item, property ){
  465. return parseInt( $item.css( property ), 10 );
  466. }
  467. /**
  468. * Activates an element
  469. *
  470. * Element is positioned to one of the sides of the frame, based on it's current position.
  471. * If the element is close to the right frame border, it will be animated to the start of the left border,
  472. * and vice versa. This helps user to navigate through the elements only by clicking on them, without
  473. * the need for navigation buttons, scrolling, or keyboard arrows.
  474. *
  475. * @public
  476. *
  477. * @param {Element|Int} el DOM element, or index of element in items array
  478. * @param {Bool} noReposition Activate item without repositioning it
  479. */
  480. this.activate = function( el, noReposition ){
  481. if (!itemNav || el === undefined) {
  482. return;
  483. }
  484. var index = getIndex( el ),
  485. oldActive = rel.activeItem;
  486. // Update activeItem index
  487. rel.activeItem = index;
  488. // Add active class to the active element
  489. $items.removeClass(o.activeClass).eq(index).addClass(o.activeClass);
  490. // Trigget :active event if a new element is being activated
  491. index !== oldActive && $items.eq( index ).trigger( pluginName + ':active', [ $items, rel ] );
  492. if( !noReposition ){
  493. // When centeredNav is enabled, center the element
  494. if( centeredNav ){
  495. self.toCenter( index );
  496. // Otherwise determine where to position the element
  497. } else if( smartNav ) {
  498. // If activated element is currently on the far right side of the frame, assume that
  499. // user is moving forward and animate it to the start of the visible frame, and vice versa
  500. if (index >= rel.lastItem) {
  501. self.toStart(index);
  502. } else if (index <= rel.firstItem) {
  503. self.toEnd(index);
  504. }
  505. }
  506. }
  507. // Add disabled classes
  508. disableButtons();
  509. };
  510. /**
  511. * Activates a page
  512. *
  513. * @public
  514. *
  515. * @param {Int} index Page index, starting from 0
  516. */
  517. this.activatePage = function( index ){
  518. // Fix overflowing
  519. index = index < 0 ? 0 : index >= pages.length ? pages.length-1 : index;
  520. slide( pages[index], itemNav );
  521. syncBars();
  522. };
  523. /**
  524. * Return relative positions of items based on their location within visible frame
  525. *
  526. * @private
  527. *
  528. * @param {Int} sPos Position of slidee
  529. */
  530. function getRelatives( sPos ){
  531. var newRel = {},
  532. centerOffset = forceCenteredNav ? 0 : frameSize / 2;
  533. // Determine active page
  534. for( var p = 0; p < pages.length; p++ ){
  535. if( sPos >= pos.max || p === pages.length - 1 ){
  536. newRel.activePage = pages.length - 1;
  537. break;
  538. }
  539. if( sPos <= pages[p] + centerOffset ){
  540. newRel.activePage = p;
  541. break;
  542. }
  543. }
  544. // Relative item indexes
  545. if( itemNav ){
  546. var first = false,
  547. last = false,
  548. center = false;
  549. /* From start */
  550. for( var i=0; i < items.length; i++ ){
  551. // First item
  552. if (first === false && sPos <= items[i].offStart) {
  553. first = i;
  554. }
  555. // Centered item
  556. if (center === false && sPos - items[i].size / 2 <= items[i].offCenter) {
  557. center = i;
  558. }
  559. // Last item
  560. if (i === items.length - 1 || (last === false && sPos < items[i + 1].offEnd)) {
  561. last = i;
  562. }
  563. // Terminate if all are assigned
  564. if (last !== false) {
  565. break;
  566. }
  567. }
  568. // Safe assignment, just to be sure the false won't be returned
  569. newRel.firstItem = isNumber( first ) ? first : 0;
  570. newRel.centerItem = isNumber( center ) ? center : newRel.firstItem;
  571. newRel.lastItem = isNumber( last ) ? last : newRel.centerItem;
  572. }
  573. return newRel;
  574. }
  575. /**
  576. * Assign element indexes to the relative positions
  577. *
  578. * @private
  579. */
  580. function assignRelatives(){
  581. $.extend( rel, getRelatives( pos.cur ) );
  582. }
  583. /**
  584. * Disable buttons when needed
  585. *
  586. * Adds disabledClass, and when the button is <button> or <input>,
  587. * activates :disabled state
  588. *
  589. * @private
  590. */
  591. function disableButtons(){
  592. // item navigation
  593. if( itemNav ){
  594. var isFirstItem = rel.activeItem === 0,
  595. isLastItem = rel.activeItem >= items.length-1;
  596. if( $prevButton.is('button,input') ){
  597. $prevButton.prop('disabled', isFirstItem);
  598. }
  599. if( $nextButton.is('button,input') ){
  600. $nextButton.prop('disabled', isLastItem);
  601. }
  602. $prevButton[ isFirstItem ? 'removeClass' : 'addClass'](o.disabledClass);
  603. $nextButton[ isLastItem ? 'removeClass' : 'addClass'](o.disabledClass);
  604. }
  605. // pages navigation
  606. if( $pages.length ){
  607. var isStart = pos.cur <= pos.min,
  608. isEnd = pos.cur >= pos.max;
  609. if( $prevPageButton.is('button,input') ){
  610. $prevPageButton.prop('disabled', isStart);
  611. }
  612. if( $nextPageButton.is('button,input') ){
  613. $nextPageButton.prop('disabled', isEnd);
  614. }
  615. $prevPageButton[ isStart ? 'removeClass' : 'addClass'](o.disabledClass);
  616. $nextPageButton[ isEnd ? 'removeClass' : 'addClass'](o.disabledClass);
  617. }
  618. }
  619. /**
  620. * Manage cycling
  621. *
  622. * @public
  623. *
  624. * @param {Bool} pause Pass true to pause cycling
  625. * @param {Bool} soft Soft pause intended for pauseOnHover - won't set cycleIsPaused variable to true
  626. */
  627. this.cycle = function( pause, soft ){
  628. if (!itemNav || !o.cycleBy) {
  629. return;
  630. }
  631. if( !soft ){
  632. cycleIsPaused = !!pause;
  633. }
  634. if( pause ){
  635. if( cycleIndex ){
  636. cycleIndex = clearTimeout( cycleIndex );
  637. // Trigger :cyclePause event
  638. $frame.trigger( pluginName + ':cyclePause', [ pos, $items, rel ] );
  639. }
  640. } else {
  641. // Don't initiate more than one cycle
  642. if (cycleIndex) {
  643. return;
  644. }
  645. // Trigger :cycleStart event
  646. $frame.trigger( pluginName + ':cycleStart', [ pos, $items, rel ] );
  647. // Cycling loop
  648. (function loop(){
  649. if( o.cycleInterval === 0 ){
  650. return;
  651. }
  652. cycleIndex = setTimeout( function(){
  653. if( !isDragging ){
  654. switch( o.cycleBy ){
  655. case 'items':
  656. var nextItem = rel.activeItem >= items.length-1 ? 0 : rel.activeItem + 1;
  657. self.activate( nextItem );
  658. break;
  659. case 'pages':
  660. var nextPage = rel.activePage >= pages.length-1 ? 0 : rel.activePage + 1;
  661. self.activatePage( nextPage );
  662. break;
  663. }
  664. }
  665. // Trigger :cycle event
  666. $frame.trigger( pluginName + ':cycle', [ pos, $items, rel ] );
  667. // Cycle the cycle!
  668. loop();
  669. }, o.cycleInterval );
  670. }());
  671. }
  672. };
  673. /**
  674. * Crossbrowser reliable way to stop default event action
  675. *
  676. * @private
  677. *
  678. * @param {Event} e Event object
  679. * @param {Bool} noBubbles Cancel event bubbling
  680. */
  681. function stopDefault( e, noBubbles ){
  682. var evt = e || window.event;
  683. evt.preventDefault ? evt.preventDefault() : evt.returnValue = false;
  684. noBubbles && evt.stopPropagation ? evt.stopPropagation() : evt.cancelBubble = true;
  685. }
  686. /**
  687. * Updates a signle or multiple option values
  688. *
  689. * @param {Mixed} property Option property name that should be updated, or object with options that will extend the current one
  690. * @param {Mixed} value Option property value
  691. *
  692. * @public
  693. */
  694. this.set = function( property, value ){
  695. if( $.isPlainObject(property) ){
  696. o = $.extend({}, o, property);
  697. } else if( typeof property === 'string' ) {
  698. o[property] = value;
  699. }
  700. };
  701. /**
  702. * Destroys plugin instance and everything it created
  703. *
  704. * @public
  705. */
  706. this.destroy = function(){
  707. // Unbind all events
  708. $frame.add(document).add($slidee).add($items).add($scrollSource).add($handle)
  709. .add($prevButton).add($nextButton).add($prevPageButton).add($nextPageButton)
  710. .unbind('.' + namespace);
  711. // Reset some styles
  712. $slidee.add($handle).css( o.horizontal ? { left: 0 } : { top: 0 } );
  713. // Remove plugin classes
  714. $prevButton.add($nextButton).removeClass(o.disabledClass);
  715. // Remove page items
  716. $pb.empty();
  717. // Remove plugin from element data storage
  718. $.removeData(frame, namespace);
  719. };
  720. /**
  721. * Check if variable is a number
  722. *
  723. * @param {Mixed} n Any type of variable
  724. *
  725. * @return {Boolean}
  726. */
  727. function isNumber( n ) {
  728. return !isNaN(parseFloat(n)) && isFinite(n);
  729. }
  730. /** Constructor */
  731. (function(){
  732. var doc = $(document),
  733. dragEvents = 'mousemove.' + namespace + ' mouseup.' + namespace;
  734. // Extend options
  735. o = $.extend( {}, $.fn[pluginName].defaults, o );
  736. // Set required styles to elements
  737. $frame.css({ overflow: 'hidden' }).css('position') === 'static' && $frame.css({ position: 'relative' });
  738. $sb.css('position') === 'static' && $sb.css({ position: 'relative' });
  739. $slidee.add($handle).css( o.horizontal ? { position: 'absolute', left: 0 } : { position: 'absolute', top: 0 } );
  740. // Load
  741. load();
  742. // Activate requested position
  743. itemNav ? self.activate( o.startAt ) : slide( o.startAt );
  744. // Sync scrollbar & pages
  745. syncBars();
  746. // Scrolling navigation
  747. o.scrollBy && $scrollSource.bind('DOMMouseScroll.' + namespace + ' mousewheel.' + namespace, function(e){
  748. // If there is no scrolling to be done, leave the default event alone
  749. if (pos.min === pos.max) {
  750. return;
  751. }
  752. stopDefault( e, 1 );
  753. var orgEvent = e.originalEvent,
  754. delta = 0,
  755. isForward, nextItem;
  756. // Old school scrollwheel delta
  757. if ( orgEvent.wheelDelta ){ delta = orgEvent.wheelDelta / 120; }
  758. if ( orgEvent.detail ){ delta = -orgEvent.detail / 3; }
  759. isForward = delta < 0;
  760. if( itemNav ){
  761. nextItem = getIndex( ( centeredNav ? forceCenteredNav ? rel.activeItem : rel.centerItem : rel.firstItem ) + ( isForward ? o.scrollBy : -o.scrollBy ) );
  762. self[centeredNav ? forceCenteredNav ? 'activate' : 'toCenter' : 'toStart']( nextItem );
  763. } else {
  764. slide( pos.cur + ( isForward ? o.scrollBy : -o.scrollBy ) );
  765. }
  766. syncBars();
  767. });
  768. // Keyboard navigation
  769. o.keyboardNav && doc.bind('keydown.' + namespace, function(e){
  770. switch( e.keyCode || e.which ){
  771. // Left or Up
  772. case o.horizontal ? 37 : 38:
  773. stopDefault(e);
  774. o.keyboardNavByPages ? self.prevPage() : self.prev();
  775. break;
  776. // Right or Down
  777. case o.horizontal ? 39 : 40:
  778. stopDefault(e);
  779. o.keyboardNavByPages ? self.nextPage() : self.next();
  780. break;
  781. }
  782. });
  783. // Navigation buttons
  784. o.prev && $prevButton.bind('click.' + namespace, function(e){ stopDefault(e); self.prev(); });
  785. o.next && $nextButton.bind('click.' + namespace, function(e){ stopDefault(e); self.next(); });
  786. o.prevPage && $prevPageButton.bind('click.' + namespace, function(e){ stopDefault(e); self.prevPage(); });
  787. o.nextPage && $nextPageButton.bind('click.' + namespace, function(e){ stopDefault(e); self.nextPage(); });
  788. // Dragging navigation
  789. o.dragContent && $dragSource.bind('mousedown.' + namespace, function(e){
  790. // Ignore other than left mouse button
  791. if (e.which !== 1) {
  792. return;
  793. }
  794. stopDefault(e);
  795. var leftInit = e.clientX,
  796. topInit = e.clientY,
  797. posInit = pos.cur,
  798. start = +new Date(),
  799. srcEl = e.target,
  800. easeoff = 0,
  801. isInitialized = 0;
  802. // Add dragging class
  803. $slidee.addClass(o.draggedClass);
  804. // Stop potential ongoing animations
  805. stop();
  806. // Bind dragging events
  807. doc.bind(dragEvents, function(e){
  808. var released = e.type === 'mouseup',
  809. path = o.horizontal ? e.clientX - leftInit : e.clientY - topInit,
  810. newPos = posInit - path;
  811. // Initialized logic
  812. if( !isInitialized && Math.abs( path ) > 10 ){
  813. isInitialized = 1;
  814. // Trigger :dragStart event
  815. $slidee.trigger( pluginName + ':dragStart', [ pos ] );
  816. }
  817. // Limits & Elastic bounds
  818. if( newPos > pos.max ){
  819. newPos = o.elasticBounds ? pos.max + ( newPos - pos.max ) / 6 : pos.max;
  820. } else if( newPos < pos.min ){
  821. newPos = o.elasticBounds ? pos.min + ( newPos - pos.min ) / 6 : pos.min;
  822. }
  823. // Adjust newPos with easing when content has been released
  824. if( released ){
  825. // Cleanup
  826. doc.unbind(dragEvents);
  827. $slidee.removeClass(o.draggedClass);
  828. // How long was the dragging
  829. var time = +new Date() - start;
  830. // Calculate swing length
  831. var swing = time < 300 ? Math.ceil( Math.pow( 6 / ( time / 300 ) , 2 ) * Math.abs( path ) / 120 ) : 0;
  832. newPos += path > 0 ? -swing : swing;
  833. }
  834. // Drag only when isInitialized
  835. if (!isInitialized) {
  836. return;
  837. }
  838. stopDefault(e);
  839. // Stop default click action on source element
  840. if( srcEl ){
  841. $(srcEl).bind('click.' + namespace, function stopMe(e){
  842. stopDefault(e,true);
  843. $(this).unbind('click.' + namespace, stopMe);
  844. });
  845. srcEl = 0;
  846. }
  847. // Dragging state
  848. isDragging = !released;
  849. // Animage, synch bars, & align
  850. slide( newPos, released, released ? o.speed : 0 );
  851. syncBars( released ? null : 0 );
  852. // Trigger :drag event
  853. if (isInitialized) {
  854. $slidee.trigger(pluginName + ':drag', [pos]);
  855. }
  856. // Trigger :dragEnd event
  857. if (released) {
  858. $slidee.trigger(pluginName + ':dragEnd', [pos]);
  859. }
  860. });
  861. });
  862. // Scrollbar navigation
  863. $handle && o.dragHandle && $handle.bind('mousedown.' + namespace, function(e){
  864. // Ignore other than left mouse button
  865. if (e.which !== 1) {
  866. return;
  867. }
  868. stopDefault(e);
  869. var leftInit = e.clientX,
  870. topInit = e.clientY,
  871. posInit = hPos.cur,
  872. pathMin = -hPos.cur,
  873. pathMax = hPos.max - hPos.cur,
  874. nextDrag = 0;
  875. // Add dragging class
  876. $handle.addClass(o.draggedClass);
  877. // Stop potential ongoing animations
  878. stop();
  879. // Bind dragging events
  880. doc.bind(dragEvents, function(e){
  881. stopDefault(e);
  882. var released = e.type === 'mouseup',
  883. path = o.horizontal ? e.clientX - leftInit : e.clientY - topInit,
  884. newPos = posInit + path,
  885. time = +new Date();
  886. // Dragging state
  887. isDragging = !released;
  888. // Unbind events and remove classes when released
  889. if( released ){
  890. doc.unbind(dragEvents);
  891. $handle.removeClass(o.draggedClass);
  892. }
  893. // Execute only moves within path limits
  894. if( path < pathMax+5 && path > pathMin-5 || released ){
  895. // Fix overflows
  896. hPos.cur = newPos > hPos.max ? hPos.max : newPos < hPos.min ? hPos.min : newPos;
  897. // Move handle
  898. $handle.stop().css( o.horizontal ? { left: hPos.cur+'px' } : { top: hPos.cur+'px' } );
  899. // Trigger :dragStart event
  900. if (!nextDrag) {
  901. $handle.trigger(pluginName + ':dragStart', [hPos]);
  902. }
  903. // Trigger :drag event
  904. $handle.trigger( pluginName + ':drag', [ hPos ] );
  905. // Trigger :dragEnd event
  906. if (released) {
  907. $handle.trigger(pluginName + ':dragEnd', [hPos]);
  908. }
  909. // Throttle sync interval -> smoother animations, lower CPU load
  910. if( nextDrag <= time || released || path > pathMax || path < pathMin ){
  911. nextDrag = time + 50;
  912. // Synchronize slidee position
  913. slide( Math.round( hPos.cur / hPos.max * ( pos.max - pos.min ) ) + pos.min, released, released ? o.speed : 50 );
  914. }
  915. // Sync pagesbar
  916. syncPages();
  917. }
  918. });
  919. });
  920. }());
  921. }
  922. // jQuery plugin extension
  923. $.fn[pluginName] = function( options, returnInstance ){
  924. var method = false,
  925. methodArgs,
  926. instances = [];
  927. // Basic attributes logic
  928. if( typeof options !== 'undefined' && !$.isPlainObject( options ) ){
  929. method = options === false ? 'destroy' : options;
  930. methodArgs = arguments;
  931. Array.prototype.shift.call( methodArgs );
  932. }
  933. // Apply requested actions on all elements
  934. this.each(function( i, element ){
  935. // Plugin call with prevention against multiple instantiations
  936. var plugin = $.data( element, namespace );
  937. if( plugin && method ){
  938. // Call plugin method
  939. if( plugin[method] ){
  940. plugin[method].apply( plugin, methodArgs );
  941. }
  942. } else if( !plugin && !method ){
  943. // Create a new plugin object if it doesn't exist yet
  944. plugin = $.data( element, namespace, new Plugin( element, options ) );
  945. }
  946. // Push plugin to instances
  947. instances.push( plugin );
  948. });
  949. // Return chainable jQuery object, or plugin instance(s)
  950. return returnInstance && !method ? instances.length > 1 ? instances : instances[0] : this;
  951. };
  952. // Default options
  953. $.fn[pluginName].defaults = {
  954. // Sly direction
  955. horizontal: 0, // set to 1 to change the sly direction to horizontal
  956. // Navigation by items; when using this, `scrollBy` option scrolls by items, not pixels
  957. itemNav: 0, // enable type of item based navigation. when itemNav is enabled, items snap to frame edges or frame center
  958. // itemNav also enables "item activation" functionality and methods associated with it
  959. //
  960. // itemNav can be:
  961. // ------------------------------------------------------------------------------------
  962. // basic: items snap to edges (ideal if you don't care about "active item" functionality)
  963. // smart: same as basic, but activated item close to, or outside of the visible edge will be positioned to the opposite edge
  964. // centered: activated items are positioned to the center of visible frame if possible
  965. // forceCentered: active items are always centered & centered items are always active (scrolling & dragging end activates centered item)
  966. // Scrollbar
  967. scrollBar: null, // selector or DOM element for scrollbar container (scrollbar container should have one child element representing scrollbar handle)
  968. 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
  969. dragHandle: 1, // set to 0 to disable dragging of scrollbar handle with mouse
  970. minHandleSize: 50, // minimal height or width (depends on sly direction) of a handle in pixels
  971. // Pagesbar (when centerActive is enabled, every item is considered to be a page)
  972. pagesBar: null, // selector or DOM element for pages bar container
  973. pageBuilder: // function with `index` (starting at 0) as argument that returns an HTML for one item
  974. function( index ){
  975. return '<li>'+(index+1)+'</li>';
  976. },
  977. // Navigation buttons
  978. prev: null, // selector or DOM element for "previous item" button ; doesn't work when `itemsNav` is disabled
  979. next: null, // selector or DOM element for "next item" button ; doesn't work when `itemsNav` is disabled
  980. prevPage: null, // selector or DOM element for "previous page" button
  981. nextPage: null, // selector or DOM element for "next page" button
  982. // Automated cycling
  983. cycleBy: 0, // enable automatic cycling by 'items', or 'pages'
  984. cycleInterval: 5000, // number of milliseconds between cycles
  985. pauseOnHover: 1, // pause cycling when mouse hovers over frame
  986. startPaused: 0, // set to "1" to start in paused sate. cycling can be than resumed with "cycle" method
  987. // Mixed options
  988. scrollBy: 0, // how many pixels/items should one mouse scroll event go. leave "0" to disable mousewheel scrolling
  989. dragContent: 0, // set to 1 to enable navigation by dragging the content with your mouse
  990. elasticBounds: 0, // when dragging past limits, stretch them a little bit (like on spartphones)
  991. speed: 300, // animations speed
  992. easing: 'swing', // animations easing. build in jQuery options are "linear" and "swing". for more, install gsgd.co.uk/sandbox/jquery/easing/
  993. scrollSource: null, // selector or DOM element for catching the mouse wheel event for sly scrolling. default source is the frame
  994. dragSource: null, // selector or DOM element for catching the mouse dragging events. default source is the frame
  995. startAt: 0, // starting offset in pixels or items (depends on itemsNav option)
  996. keyboardNav: 0, // whether to allow navigation by keyboard arrows (left & right for horizontal, up & down for vertical)
  997. // NOTE! keyboard navigation will disable page scrolling with keyboard arrows in correspondent sly direction (vertical or horizontal)
  998. keyboardNavByPages: 0, // whether the keyboard should navigate by pages instead of items (useful when not using `itemsNav` navigation)
  999. // Classes
  1000. draggedClass: 'dragged', // class that will be added to scrollbar handle, or content when they are being dragged
  1001. activeClass: 'active', // class that will be added to the active item, or page
  1002. disabledClass: 'disabled' // class that will be added to prev button when on start, or next button when on end
  1003. };
  1004. }(jQuery));