1/*! 2 * sly 1.6.1 - 8th Aug 2015 3 * https://github.com/darsain/sly 4 * 5 * Licensed under the MIT license. 6 * http://opensource.org/licenses/MIT 7 */ 8 9;(function ($, w, undefined) { 10 'use strict'; 11 12 var pluginName = 'sly'; 13 var className = 'Sly'; 14 var namespace = pluginName; 15 16 // Local WindowAnimationTiming interface 17 var cAF = w.cancelAnimationFrame || w.cancelRequestAnimationFrame; 18 var rAF = w.requestAnimationFrame; 19 20 // Support indicators 21 var transform, gpuAcceleration; 22 23 // Other global values 24 var $doc = $(document); 25 var dragInitEvents = 'touchstart.' + namespace + ' mousedown.' + namespace; 26 var dragMouseEvents = 'mousemove.' + namespace + ' mouseup.' + namespace; 27 var dragTouchEvents = 'touchmove.' + namespace + ' touchend.' + namespace; 28 var wheelEvent = (document.implementation.hasFeature('Event.wheel', '3.0') ? 'wheel.' : 'mousewheel.') + namespace; 29 var clickEvent = 'click.' + namespace; 30 var mouseDownEvent = 'mousedown.' + namespace; 31 var interactiveElements = ['INPUT', 'SELECT', 'BUTTON', 'TEXTAREA']; 32 var tmpArray = []; 33 var time; 34 35 // Math shorthands 36 var abs = Math.abs; 37 var sqrt = Math.sqrt; 38 var pow = Math.pow; 39 var round = Math.round; 40 var max = Math.max; 41 var min = Math.min; 42 43 // Keep track of last fired global wheel event 44 var lastGlobalWheel = 0; 45 $doc.on(wheelEvent, function (event) { 46 var sly = event.originalEvent[namespace]; 47 var time = +new Date(); 48 // Update last global wheel time, but only when event didn't originate 49 // in Sly frame, or the origin was less than scrollHijack time ago 50 if (!sly || sly.options.scrollHijack < time - lastGlobalWheel) lastGlobalWheel = time; 51 }); 52 53 /** 54 * Sly. 55 * 56 * @class 57 * 58 * @param {Element} frame DOM element of sly container. 59 * @param {Object} options Object with options. 60 * @param {Object} callbackMap Callbacks map. 61 */ 62 function Sly(frame, options, callbackMap) { 63 if (!(this instanceof Sly)) return new Sly(frame, options, callbackMap); 64 65 // Extend options 66 var o = $.extend({}, Sly.defaults, options); 67 68 // Private variables 69 var self = this; 70 var parallax = isNumber(frame); 71 72 // Frame 73 var $frame = $(frame); 74 var $slidee = o.slidee ? $(o.slidee).eq(0) : $frame.children().eq(0); 75 var frameSize = 0; 76 var slideeSize = 0; 77 var pos = { 78 start: 0, 79 center: 0, 80 end: 0, 81 cur: 0, 82 dest: 0 83 }; 84 85 // Scrollbar 86 var $sb = $(o.scrollBar).eq(0); 87 var $handle = $sb.children().eq(0); 88 var sbSize = 0; 89 var handleSize = 0; 90 var hPos = { 91 start: 0, 92 end: 0, 93 cur: 0 94 }; 95 96 // Pagesbar 97 var $pb = $(o.pagesBar); 98 var $pages = 0; 99 var pages = []; 100 101 // Items 102 var $items = 0; 103 var items = []; 104 var rel = { 105 firstItem: 0, 106 lastItem: 0, 107 centerItem: 0, 108 activeItem: null, 109 activePage: 0 110 }; 111 112 // Styles 113 var frameStyles = new StyleRestorer($frame[0]); 114 var slideeStyles = new StyleRestorer($slidee[0]); 115 var sbStyles = new StyleRestorer($sb[0]); 116 var handleStyles = new StyleRestorer($handle[0]); 117 118 // Navigation type booleans 119 var basicNav = o.itemNav === 'basic'; 120 var forceCenteredNav = o.itemNav === 'forceCentered'; 121 var centeredNav = o.itemNav === 'centered' || forceCenteredNav; 122 var itemNav = !parallax && (basicNav || centeredNav || forceCenteredNav); 123 124 // Miscellaneous 125 var $scrollSource = o.scrollSource ? $(o.scrollSource) : $frame; 126 var $dragSource = o.dragSource ? $(o.dragSource) : $frame; 127 var $forwardButton = $(o.forward); 128 var $backwardButton = $(o.backward); 129 var $prevButton = $(o.prev); 130 var $nextButton = $(o.next); 131 var $prevPageButton = $(o.prevPage); 132 var $nextPageButton = $(o.nextPage); 133 var callbacks = {}; 134 var last = {}; 135 var animation = {}; 136 var move = {}; 137 var dragging = { 138 released: 1 139 }; 140 var scrolling = { 141 last: 0, 142 delta: 0, 143 resetTime: 200 144 }; 145 var renderID = 0; 146 var historyID = 0; 147 var cycleID = 0; 148 var continuousID = 0; 149 var i, l; 150 151 // Normalizing frame 152 if (!parallax) { 153 frame = $frame[0]; 154 } 155 156 // Expose properties 157 self.initialized = 0; 158 self.frame = frame; 159 self.slidee = $slidee[0];
160 self.pos = pos; 161 self.rel = rel; 162 self.items = items; 163 self.pages = pages; 164 self.isPaused = 0; 165 self.options = o; 166 self.dragging = dragging; 167 168 /** 169 * Loading function. 170 * 171 * Populate arrays, set sizes, bind events, ... 172 * 173 * @param {Boolean} [isInit] Whether load is called from within self.init(). 174 * @return {Void} 175 */ 176 function load(isInit) { 177 // Local variables 178 var lastItemsCount = 0; 179 var lastPagesCount = pages.length; 180 181 // Save old position 182 pos.old = $.extend({}, pos); 183 184 // Reset global variables 185 frameSize = parallax ? 0 : $frame[o.horizontal ? 'width' : 'height'](); 186 sbSize = $sb[o.horizontal ? 'width' : 'height'](); 187 slideeSize = parallax ? frame : $slidee[o.horizontal ? 'outerWidth' : 'outerHeight'](); 188 pages.length = 0; 189 190 // Set position limits & relatives 191 pos.start = 0; 192 pos.end = max(slideeSize - frameSize, 0); 193 194 // Sizes & offsets for item based navigations 195 if (itemNav) { 196 // Save the number of current items 197 lastItemsCount = items.length; 198 199 // Reset itemNav related variables 200 $items = $slidee.children(o.itemSelector); 201 items.length = 0; 202 203 // Needed variables 204 var paddingStart = getPx($slidee, o.horizontal ? 'paddingLeft' : 'paddingTop'); 205 var paddingEnd = getPx($slidee, o.horizontal ? 'paddingRight' : 'paddingBottom'); 206 var borderBox = $($items).css('boxSizing') === 'border-box'; 207 var areFloated = $items.css('float') !== 'none'; 208 var ignoredMargin = 0; 209 var lastItemIndex = $items.length - 1; 210 var lastItem; 211 212 // Reset slideeSize 213 slideeSize = 0; 214 215 // Iterate through items 216 $items.each(function (i, element) { 217 // Item 218 var $item = $(element); 219 var rect = element.getBoundingClientRect(); 220 var itemSize = round(o.horizontal ? rect.width || rect.right - rect.left : rect.height || rect.bottom - rect.top); 221 var itemMarginStart = getPx($item, o.horizontal ? 'marginLeft' : 'marginTop'); 222 var itemMarginEnd = getPx($item, o.horizontal ? 'marginRight' : 'marginBottom'); 223 var itemSizeFull = itemSize + itemMarginStart + itemMarginEnd; 224 var singleSpaced = !itemMarginStart || !itemMarginEnd; 225 var item = {}; 226 item.el = element; 227 item.size = singleSpaced ? itemSize : itemSizeFull; 228 item.half = item.size / 2; 229 item.start = slideeSize + (singleSpaced ? itemMarginStart : 0); 230 item.center = item.start - round(frameSize / 2 - item.size / 2); 231 item.end = item.start - frameSize + item.size; 232 233 // Account for slidee padding 234 if (!i) { 235 slideeSize += paddingStart; 236 } 237 238 // Increment slidee size for size of the active element 239 slideeSize += itemSizeFull; 240 241 // Try to account for vertical margin collapsing in vertical mode 242 // It's not bulletproof, but should work in 99% of cases 243 if (!o.horizontal && !areFloated) { 244 // Subtract smaller margin, but only when top margin is not 0, and this is not the first element 245 if (itemMarginEnd && itemMarginStart && i > 0) { 246 slideeSize -= min(itemMarginStart, itemMarginEnd); 247 } 248 } 249 250 // Things to be done on last item 251 if (i === lastItemIndex) { 252 item.end += paddingEnd; 253 slideeSize += paddingEnd; 254 ignoredMargin = singleSpaced ? itemMarginEnd : 0; 255 } 256 257 // Add item object to items array 258 items.push(item); 259 lastItem = item; 260 }); 261 262 // Resize SLIDEE to fit all items 263 $slidee[0].style[o.horizontal ? 'width' : 'height'] = (borderBox ? slideeSize: slideeSize - paddingStart - paddingEnd) + 'px'; 264 265 // Adjust internal SLIDEE size for last margin 266 slideeSize -= ignoredMargin; 267 268 // Set limits 269 if (items.length) { 270 pos.start = items[0][forceCenteredNav ? 'center' : 'start']; 271 pos.end = forceCenteredNav ? lastItem.center : frameSize < slideeSize ? lastItem.end : pos.start; 272 } else { 273 pos.start = pos.end = 0; 274 } 275 } 276 277 // Calculate SLIDEE center position 278 pos.center = round(pos.end / 2 + pos.start / 2); 279 280 // Update relative positions 281 updateRelatives(); 282 283 // Scrollbar 284 if ($handle.length && sbSize > 0) { 285 // Stretch scrollbar handle to represent the visible area 286 if (o.dynamicHandle) { 287 handleSize = pos.start === pos.end ? sbSize : round(sbSize * frameSize / slideeSize); 288 handleSize = within(handleSize, o.minHandleSize, sbSize); 289 $handle[0].style[o.horizontal ? 'width' : 'height'] = handleSize + 'px'; 290 } else { 291 handleSize = $handle[o.horizontal ? 'outerWidth' : 'outerHeight'](); 292 } 293 294 hPos.end = sbSize - handleSize; 295 296 if (!renderID) { 297 syncScrollbar(); 298 } 299 } 300 301 // Pages 302 if (!parallax && frameSize > 0) { 303 var tempPagePos = pos.start; 304 var pagesHtml = ''; 305 306 // Populate pages array 307 if (itemNav) { 308 $.each(items, function (i, item) { 309 if (forceCenteredNav) { 310 pages.push(item.center); 311 } else if (item.start + item.size > tempPagePos && tempPagePos <= pos.end) { 312 tempPagePos = item.start; 313 pages.push(tempPagePos); 314 tempPagePos += frameSize; 315 if (tempPagePos > pos.end && tempPagePos < pos.end + frameSize) { 316 pages.push(pos.end); 317 } 318 } 319 });
320 } else { 321 while (tempPagePos - frameSize < pos.end) { 322 pages.push(tempPagePos); 323 tempPagePos += frameSize; 324 } 325 } 326 327 // Pages bar 328 if ($pb[0] && lastPagesCount !== pages.length) { 329 for (var i = 0; i < pages.length; i++) { 330 pagesHtml += o.pageBuilder.call(self, i); 331 } 332 $pages = $pb.html(pagesHtml).children(); 333 $pages.eq(rel.activePage).addClass(o.activeClass); 334 } 335 } 336 337 // Extend relative variables object with some useful info 338 rel.slideeSize = slideeSize; 339 rel.frameSize = frameSize; 340 rel.sbSize = sbSize; 341 rel.handleSize = handleSize; 342 343 // Activate requested position 344 if (itemNav) { 345 if (isInit && o.startAt != null) { 346 activate(o.startAt); 347 self[centeredNav ? 'toCenter' : 'toStart'](o.startAt); 348 } 349 // Fix possible overflowing 350 var activeItem = items[rel.activeItem]; 351 slideTo(centeredNav && activeItem ? activeItem.center : within(pos.dest, pos.start, pos.end)); 352 } else { 353 if (isInit) { 354 if (o.startAt != null) slideTo(o.startAt, 1); 355 } else { 356 // Fix possible overflowing 357 slideTo(within(pos.dest, pos.start, pos.end)); 358 } 359 } 360 361 // Trigger load event 362 trigger('load'); 363 } 364 self.reload = function () { load(); }; 365 366 /** 367 * Animate to a position. 368 * 369 * @param {Int} newPos New position. 370 * @param {Bool} immediate Reposition immediately without an animation. 371 * @param {Bool} dontAlign Do not align items, use the raw position passed in first argument. 372 * 373 * @return {Void} 374 */ 375 function slideTo(newPos, immediate, dontAlign) { 376 // Align items 377 if (itemNav && dragging.released && !dontAlign) { 378 var tempRel = getRelatives(newPos); 379 var isNotBordering = newPos > pos.start && newPos < pos.end; 380 381 if (centeredNav) { 382 if (isNotBordering) { 383 newPos = items[tempRel.centerItem].center; 384 } 385 if (forceCenteredNav && o.activateMiddle) { 386 activate(tempRel.centerItem); 387 } 388 } else if (isNotBordering) { 389 newPos = items[tempRel.firstItem].start; 390 } 391 } 392 393 // Handle overflowing position limits 394 if (dragging.init && dragging.slidee && o.elasticBounds) { 395 if (newPos > pos.end) { 396 newPos = pos.end + (newPos - pos.end) / 6; 397 } else if (newPos < pos.start) { 398 newPos = pos.start + (newPos - pos.start) / 6; 399 } 400 } else { 401 newPos = within(newPos, pos.start, pos.end); 402 } 403 404 // Update the animation object 405 animation.start = +new Date(); 406 animation.time = 0; 407 animation.from = pos.cur; 408 animation.to = newPos; 409 animation.delta = newPos - pos.cur; 410 animation.tweesing = dragging.tweese || dragging.init && !dragging.slidee; 411 animation.immediate = !animation.tweesing && (immediate || dragging.init && dragging.slidee || !o.speed); 412 413 // Reset dragging tweesing request 414 dragging.tweese = 0; 415 416 // Start animation rendering 417 if (newPos !== pos.dest) { 418 pos.dest = newPos; 419 trigger('change'); 420 if (!renderID) { 421 render(); 422 } 423 } 424 425 // Reset next cycle timeout 426 resetCycle(); 427 428 // Synchronize states 429 updateRelatives(); 430 updateButtonsState(); 431 syncPagesbar(); 432 } 433 434 /** 435 * Render animation frame. 436 * 437 * @return {Void} 438 */ 439 function render() { 440 if (!self.initialized) { 441 return; 442 } 443 444 // If first render call, wait for next animationFrame 445 if (!renderID) { 446 renderID = rAF(render); 447 if (dragging.released) { 448 trigger('moveStart'); 449 } 450 return; 451 } 452 453 // If immediate repositioning is requested, don't animate. 454 if (animation.immediate) { 455 pos.cur = animation.to; 456 } 457 // Use tweesing for animations without known end point 458 else if (animation.tweesing) { 459 animation.tweeseDelta = animation.to - pos.cur; 460 // Fuck Zeno's paradox 461 if (abs(animation.tweeseDelta) < 0.1) { 462 pos.cur = animation.to; 463 } else { 464 pos.cur += animation.tweeseDelta * (dragging.released ? o.swingSpeed : o.syncSpeed); 465 } 466 } 467 // Use tweening for basic animations with known end point 468 else { 469 animation.time = min(+new Date() - animation.start, o.speed); 470 pos.cur = animation.from + animation.delta * $.easing[o.easing](animation.time/o.speed, animation.time, 0, 1, o.speed); 471 } 472 473 // If there is nothing more to render break the rendering loop, otherwise request new animation frame. 474 if (animation.to === pos.cur) { 475 pos.cur = animation.to; 476 dragging.tweese = renderID = 0; 477 } else { 478 renderID = rAF(render); 479 } 480 481 trigger('move'); 482 483 // Update SLIDEE position 484 if (!parallax) { 485 if (transform) { 486 $slidee[0].style[transform] = gpuAcceleration + (o.horizontal ? 'translateX' : 'translateY') + '(' + (-pos.cur) + 'px)'; 487 } else { 488 $slidee[0].style[o.horizontal ? 'left' : 'top'] = -round(pos.cur) + 'px'; 489 } 490 } 491 492 // When animation reached the end, and dragging is not active, trigger moveEnd 493 if (!renderID && dragging.released) { 494 trigger('moveEnd'); 495 } 496 497 syncScrollbar(); 498 } 499 500 /** 501 * Synchronizes scrollbar with the SLIDEE. 502 * 503 * @return {Void} 504 */ 505 function syncScrollbar() { 506 if ($handle.length) { 507 hPos.cur = pos.start === pos.end ? 0 : (((dragging.init && !dragging.slidee) ? pos.dest : pos.cur) - pos.start) / (pos.end - pos.start) * hPos.end; 508 hPos.cur = within(round(hPos.cur), hPos.start, hPos.end); 509 if (last.hPos !== hPos.cur) { 510 last.hPos = hPos.cur; 511 if (transform) { 512 $handle[0].style[transform] = gpuAcceleration + (o.horizontal ? 'translateX' : 'translateY') + '(' + hPos.cur + 'px)'; 513 } else { 514 $handle[0].style[o.horizontal ? 'left' : 'top'] = hPos.cur + 'px'; 515 } 516 } 517 } 518 } 519 520 /** 521 * Synchronizes pagesbar with SLIDEE. 522 * 523 * @return {Void} 524 */ 525 function syncPagesbar() { 526 if ($pages[0] && last.page !== rel.activePage) { 527 last.page = rel.activePage; 528 $pages.removeClass(o.activeClass).eq(rel.activePage).addClass(o.activeClass); 529 trigger('activePage', last.page); 530 } 531 } 532 533 /** 534 * Returns the position object. 535 * 536 * @param {Mixed} item 537 * 538 * @return {Object} 539 */ 540 self.getPos = function (item) { 541 if (itemNav) { 542 var index = getIndex(item); 543 return index !== -1 ? items[index] : false; 544 } else { 545 var $item = $slidee.find(item).eq(0); 546 547 if ($item[0]) { 548 var offset = o.horizontal ? $item.offset().left - $slidee.offset().left : $item.offset().top - $slidee.offset().top; 549 var size = $item[o.horizontal ? 'outerWidth' : 'outerHeight'](); 550 551 return { 552 start: offset, 553 center: offset - frameSize / 2 + size / 2, 554 end: offset - frameSize + size, 555 size: size 556 }; 557 } else { 558 return false; 559 } 560 } 561 }; 562 563 /** 564 * Continuous move in a specified direction. 565 * 566 * @param {Bool} forward True for forward movement, otherwise it'll go backwards. 567 * @param {Int} speed Movement speed in pixels per frame. Overrides options.moveBy value. 568 * 569 * @return {Void} 570 */ 571 self.moveBy = function (speed) {
572 move.speed = speed; 573 // If already initiated, or there is nowhere to move, abort 574 if (dragging.init || !move.speed || pos.cur === (move.speed > 0 ? pos.end : pos.start)) { 575 return; 576 } 577 // Initiate move object 578 move.lastTime = +new Date(); 579 move.startPos = pos.cur; 580 // Set dragging as initiated 581 continuousInit('button'); 582 dragging.init = 1; 583 // Start movement 584 trigger('moveStart'); 585 cAF(continuousID); 586 moveLoop(); 587 }; 588 589 /** 590 * Continuous movement loop. 591 * 592 * @return {Void} 593 */ 594 function moveLoop() { 595 // If there is nowhere to move anymore, stop 596 if (!move.speed || pos.cur === (move.speed > 0 ? pos.end : pos.start)) { 597 self.stop(); 598 } 599 // Request new move loop if it hasn't been stopped 600 continuousID = dragging.init ? rAF(moveLoop) : 0; 601 // Update move object 602 move.now = +new Date(); 603 move.pos = pos.cur + (move.now - move.lastTime) / 1000 * move.speed; 604 // Slide 605 slideTo(dragging.init ? move.pos : round(move.pos)); 606 // Normally, this is triggered in render(), but if there 607 // is nothing to render, we have to do it manually here. 608 if (!dragging.init && pos.cur === pos.dest) { 609 trigger('moveEnd'); 610 } 611 // Update times for future iteration 612 move.lastTime = move.now; 613 } 614 615 /** 616 * Stops continuous movement. 617 * 618 * @return {Void} 619 */ 620 self.stop = function () { 621 if (dragging.source === 'button') { 622 dragging.init = 0; 623 dragging.released = 1; 624 } 625 }; 626 627 /** 628 * Activate previous item. 629 * 630 * @return {Void} 631 */ 632 self.prev = function () { 633 self.activate(rel.activeItem == null ? 0 : rel.activeItem - 1); 634 }; 635 636 /** 637 * Activate next item. 638 * 639 * @return {Void} 640 */ 641 self.next = function () { 642 self.activate(rel.activeItem == null ? 0 : rel.activeItem + 1); 643 }; 644 645 /** 646 * Activate previous page. 647 * 648 * @return {Void} 649 */ 650 self.prevPage = function () { 651 self.activatePage(rel.activePage - 1); 652 }; 653 654 /** 655 * Activate next page. 656 * 657 * @return {Void} 658 */ 659 self.nextPage = function () {
660 self.activatePage(rel.activePage + 1); 661 }; 662 663 /** 664 * Slide SLIDEE by amount of pixels. 665 * 666 * @param {Int} delta Pixels/Items. Positive means forward, negative means backward. 667 * @param {Bool} immediate Reposition immediately without an animation. 668 * 669 * @return {Void} 670 */ 671 self.slideBy = function (delta, immediate) { 672 if (!delta) { 673 return; 674 } 675 if (itemNav) { 676 self[centeredNav ? 'toCenter' : 'toStart']( 677 within((centeredNav ? rel.centerItem : rel.firstItem) + o.scrollBy * delta, 0, items.length) 678 ); 679 } else { 680 slideTo(pos.dest + delta, immediate); 681 } 682 }; 683 684 /** 685 * Animate SLIDEE to a specific position. 686 * 687 * @param {Int} pos New position. 688 * @param {Bool} immediate Reposition immediately without an animation. 689 * 690 * @return {Void} 691 */ 692 self.slideTo = function (pos, immediate) { 693 slideTo(pos, immediate); 694 }; 695 696 /** 697 * Core method for handling `toLocation` methods. 698 * 699 * @param {String} location 700 * @param {Mixed} item 701 * @param {Bool} immediate 702 * 703 * @return {Void} 704 */ 705 function to(location, item, immediate) { 706 // Optional arguments logic 707 if (type(item) === 'boolean') { 708 immediate = item; 709 item = undefined; 710 } 711 712 if (item === undefined) { 713 slideTo(pos[location], immediate); 714 } else { 715 // You can't align items to sides of the frame 716 // when centered navigation type is enabled 717 if (centeredNav && location !== 'center') { 718 return; 719 } 720 721 var itemPos = self.getPos(item); 722 if (itemPos) { 723 slideTo(itemPos[location], immediate, !centeredNav); 724 } 725 } 726 } 727 728 /** 729 * Animate element or the whole SLIDEE to the start of the frame. 730 * 731 * @param {Mixed} item Item DOM element, or index starting at 0. Omitting will animate SLIDEE. 732 * @param {Bool} immediate Reposition immediately without an animation. 733 * 734 * @return {Void} 735 */ 736 self.toStart = function (item, immediate) { 737 to('start', item, immediate); 738 }; 739 740 /** 741 * Animate element or the whole SLIDEE to the end of the frame. 742 * 743 * @param {Mixed} item Item DOM element, or index starting at 0. Omitting will animate SLIDEE. 744 * @param {Bool} immediate Reposition immediately without an animation. 745 * 746 * @return {Void} 747 */ 748 self.toEnd = function (item, immediate) { 749 to('end', item, immediate); 750 }; 751 752 /** 753 * Animate element or the whole SLIDEE to the center of the frame. 754 * 755 * @param {Mixed} item Item DOM element, or index starting at 0. Omitting will animate SLIDEE. 756 * @param {Bool} immediate Reposition immediately without an animation. 757 * 758 * @return {Void} 759 */ 760 self.toCenter = function (item, immediate) { 761 to('center', item, immediate); 762 }; 763 764 /** 765 * Get the index of an item in SLIDEE. 766 * 767 * @param {Mixed} item Item DOM element. 768 * 769 * @return {Int} Item index, or -1 if not found. 770 */ 771 function getIndex(item) { 772 return item != null ? 773 isNumber(item) ? 774 item >= 0 && item < items.length ? item : -1 : 775 $items.index(item) : 776 -1; 777 } 778 // Expose getIndex without lowering the compressibility of it, 779 // as it is used quite often throughout Sly. 780 self.getIndex = getIndex; 781 782 /** 783 * Get index of an item in SLIDEE based on a variety of input types. 784 * 785 * @param {Mixed} item DOM element, positive or negative integer. 786 * 787 * @return {Int} Item index, or -1 if not found. 788 */ 789 function getRelativeIndex(item) { 790 return getIndex(isNumber(item) && item < 0 ? item + items.length : item); 791 } 792 793 /** 794 * Activates an item. 795 * 796 * @param {Mixed} item Item DOM element, or index starting at 0. 797 * 798 * @return {Mixed} Activated item index or false on fail. 799 */ 800 function activate(item, force) { 801 var index = getIndex(item); 802 803 if (!itemNav || index < 0) { 804 return false; 805 } 806 807 // Update classes, last active index, and trigger active event only when there 808 // has been a change. Otherwise just return the current active index. 809 if (last.active !== index || force) { 810 // Update classes 811 $items.eq(rel.activeItem).removeClass(o.activeClass); 812 $items.eq(index).addClass(o.activeClass); 813 814 last.active = rel.activeItem = index; 815 816 updateButtonsState(); 817 trigger('active', index); 818 } 819 820 return index; 821 } 822 823 /** 824 * Activates an item and helps with further navigation when o.smart is enabled. 825 * 826 * @param {Mixed} item Item DOM element, or index starting at 0. 827 * @param {Bool} immediate Whether to reposition immediately in smart navigation. 828 * 829 * @return {Void} 830 */ 831 self.activate = function (item, immediate) { 832 var index = activate(item); 833 834 // Smart navigation 835 if (o.smart && index !== false) { 836 // When centeredNav is enabled, center the element. 837 // Otherwise, determine where to position the element based on its current position. 838 // If the element is currently on the far end side of the frame, assume that user is 839 // moving forward and animate it to the start of the visible frame, and vice versa. 840 if (centeredNav) { 841 self.toCenter(index, immediate); 842 } else if (index >= rel.lastItem) { 843 self.toStart(index, immediate); 844 } else if (index <= rel.firstItem) { 845 self.toEnd(index, immediate); 846 } else { 847 resetCycle(); 848 } 849 } 850 }; 851 852 /** 853 * Activates a page. 854 * 855 * @param {Int} index Page index, starting from 0. 856 * @param {Bool} immediate Whether to reposition immediately without animation. 857 * 858 * @return {Void} 859 */ 860 self.activatePage = function (index, immediate) { 861 if (isNumber(index)) { 862 slideTo(pages[within(index, 0, pages.length - 1)], immediate); 863 } 864 }; 865 866 /** 867 * Return relative positions of items based on their visibility within FRAME. 868 * 869 * @param {Int} slideePos Position of SLIDEE. 870 * 871 * @return {Void} 872 */ 873 function getRelatives(slideePos) {
874 slideePos = within(isNumber(slideePos) ? slideePos : pos.dest, pos.start, pos.end); 875 876 var relatives = {}; 877 var centerOffset = forceCenteredNav ? 0 : frameSize / 2; 878 879 // Determine active page 880 if (!parallax) { 881 for (var p = 0, pl = pages.length; p < pl; p++) { 882 if (slideePos >= pos.end || p === pages.length - 1) { 883 relatives.activePage = pages.length - 1; 884 break; 885 } 886 887 if (slideePos <= pages[p] + centerOffset) { 888 relatives.activePage = p; 889 break; 890 } 891 } 892 } 893 894 // Relative item indexes 895 if (itemNav) { 896 var first = false; 897 var last = false; 898 var center = false; 899 900 // From start 901 for (var i = 0, il = items.length; i < il; i++) { 902 // First item 903 if (first === false && slideePos <= items[i].start + items[i].half) { 904 first = i; 905 } 906 907 // Center item 908 if (center === false && slideePos <= items[i].center + items[i].half) { 909 center = i; 910 } 911 912 // Last item 913 if (i === il - 1 || slideePos <= items[i].end + items[i].half) { 914 last = i; 915 break; 916 } 917 } 918 919 // Safe assignment, just to be sure the false won't be returned 920 relatives.firstItem = isNumber(first) ? first : 0; 921 relatives.centerItem = isNumber(center) ? center : relatives.firstItem; 922 relatives.lastItem = isNumber(last) ? last : relatives.centerItem; 923 } 924 925 return relatives; 926 } 927 928 /** 929 * Update object with relative positions. 930 * 931 * @param {Int} newPos 932 * 933 * @return {Void} 934 */ 935 function updateRelatives(newPos) { 936 $.extend(rel, getRelatives(newPos)); 937 } 938 939 /** 940 * Disable navigation buttons when needed. 941 * 942 * Adds disabledClass, and when the button is <button> or <input>, activates :disabled state. 943 * 944 * @return {Void} 945 */ 946 function updateButtonsState() { 947 var isStart = pos.dest <= pos.start; 948 var isEnd = pos.dest >= pos.end; 949 var slideePosState = (isStart ? 1 : 0) | (isEnd ? 2 : 0); 950 951 // Update paging buttons only if there has been a change in SLIDEE position 952 if (last.slideePosState !== slideePosState) { 953 last.slideePosState = slideePosState; 954 955 if ($prevPageButton.is('button,input')) { 956 $prevPageButton.prop('disabled', isStart); 957 } 958 959 if ($nextPageButton.is('button,input')) { 960 $nextPageButton.prop('disabled', isEnd); 961 } 962 963 $prevPageButton.add($backwardButton)[isStart ? 'addClass' : 'removeClass'](o.disabledClass); 964 $nextPageButton.add($forwardButton)[isEnd ? 'addClass' : 'removeClass'](o.disabledClass); 965 } 966 967 // Forward & Backward buttons need a separate state caching because we cannot "property disable" 968 // them while they are being used, as disabled buttons stop emitting mouse events. 969 if (last.fwdbwdState !== slideePosState && dragging.released) { 970 last.fwdbwdState = slideePosState; 971 972 if ($backwardButton.is('button,input')) { 973 $backwardButton.prop('disabled', isStart); 974 } 975 976 if ($forwardButton.is('button,input')) { 977 $forwardButton.prop('disabled', isEnd); 978 } 979 } 980 981 // Item navigation 982 if (itemNav && rel.activeItem != null) { 983 var isFirst = rel.activeItem === 0; 984 var isLast = rel.activeItem >= items.length - 1; 985 var itemsButtonState = (isFirst ? 1 : 0) | (isLast ? 2 : 0); 986 987 if (last.itemsButtonState !== itemsButtonState) { 988 last.itemsButtonState = itemsButtonState; 989 990 if ($prevButton.is('button,input')) { 991 $prevButton.prop('disabled', isFirst); 992 } 993 994 if ($nextButton.is('button,input')) { 995 $nextButton.prop('disabled', isLast); 996 } 997 998 $prevButton[isFirst ? 'addClass' : 'removeClass'](o.disabledClass); 999 $nextButton[isLast ? 'addClass' : 'removeClass'](o.disabledClass); 1000 } 1001 } 1002 } 1003 1004 /** 1005 * Resume cycling. 1006 * 1007 * @param {Int} priority Resume pause with priority lower or equal than this. Used internally for pauseOnHover. 1008 * 1009 * @return {Void} 1010 */ 1011 self.resume = function (priority) { 1012 if (!o.cycleBy || !o.cycleInterval || o.cycleBy === 'items' && (!items[0] || rel.activeItem == null) || priority < self.isPaused) { 1013 return; 1014 } 1015 1016 self.isPaused = 0; 1017 1018 if (cycleID) { 1019 cycleID = clearTimeout(cycleID); 1020 } else { 1021 trigger('resume'); 1022 } 1023 1024 cycleID = setTimeout(function () { 1025 trigger('cycle'); 1026 switch (o.cycleBy) { 1027 case 'items': 1028 self.activate(rel.activeItem >= items.length - 1 ? 0 : rel.activeItem + 1); 1029 break; 1030 1031 case 'pages': 1032 self.activatePage(rel.activePage >
1032= pages.length - 1 ? 0 : rel.activePage + 1); 1033 break; 1034 } 1035 }, o.cycleInterval); 1036 }; 1037 1038 /** 1039 * Pause cycling. 1040 * 1041 * @param {Int} priority Pause priority. 100 is default. Used internally for pauseOnHover. 1042 * 1043 * @return {Void} 1044 */ 1045 self.pause = function (priority) { 1046 if (priority < self.isPaused) { 1047 return; 1048 } 1049 1050 self.isPaused = priority || 100; 1051 1052 if (cycleID) { 1053 cycleID = clearTimeout(cycleID); 1054 trigger('pause'); 1055 } 1056 }; 1057 1058 /** 1059 * Toggle cycling. 1060 * 1061 * @return {Void} 1062 */ 1063 self.toggle = function () { 1064 self[cycleID ? 'pause' : 'resume'](); 1065 }; 1066 1067 /** 1068 * Updates a signle or multiple option values. 1069 * 1070 * @param {Mixed} name Name of the option that should be updated, or object that will extend the options. 1071 * @param {Mixed} value New option value. 1072 * 1073 * @return {Void} 1074 */ 1075 self.set = function (name, value) { 1076 if ($.isPlainObject(name)) { 1077 $.extend(o, name); 1078 } else if (o.hasOwnProperty(name)) { 1079 o[name] = value; 1080 } 1081 }; 1082 1083 /** 1084 * Add one or multiple items to the SLIDEE end, or a specified position index. 1085 * 1086 * @param {Mixed} element Node element, or HTML string. 1087 * @param {Int} index Index of a new item position. By default item is appended at the end. 1088 * 1089 * @return {Void} 1090 */ 1091 self.add = function (element, index) { 1092 var $element = $(element); 1093 1094 if (itemNav) { 1095 // Insert the element(s) 1096 if (index == null || !items[0] || index >= items.length) { 1097 $element.appendTo($slidee); 1098 } else if (items.length) { 1099 $element.insertBefore(items[index].el); 1100 } 1101 1102 // Adjust the activeItem index 1103 if (rel.activeItem != null && index <= rel.activeItem) { 1104 last.active = rel.activeItem += $element.length; 1105 } 1106 } else { 1107 $slidee.append($element); 1108 } 1109 1110 // Reload 1111 load(); 1112 }; 1113 1114 /** 1115 * Remove an item from SLIDEE. 1116 * 1117 * @param {Mixed} element Item index, or DOM element. 1118 * @param {Int} index Index of a new item position. By default item is appended at the end. 1119 * 1120 * @return {Void} 1121 */ 1122 self.remove = function (element) { 1123 if (itemNav) { 1124 var index = getRelativeIndex(element); 1125 1126 if (index > -1) { 1127 // Remove the element 1128 $items.eq(index).remove(); 1129 1130 // If the current item is being removed, activate new one after reload 1131 var reactivate = index === rel.activeItem; 1132 1133 // Adjust the activeItem index 1134 if (rel.activeItem != null && index < rel.activeItem) { 1135 last.active = --rel.activeItem; 1136 } 1137 1138 // Reload 1139 load(); 1140 1141 // Activate new item at the removed position 1142 if (reactivate) { 1143 last.active = null; 1144 self.activate(rel.activeItem); 1145 } 1146 } 1147 } else { 1148 $(element).remove(); 1149 load(); 1150 } 1151 }; 1152 1153 /** 1154 * Helps re-arranging items. 1155 * 1156 * @param {Mixed} item Item DOM element, or index starting at 0. Use negative numbers to select items from the end. 1157 * @param {Mixed} position Item insertion anchor. Accepts same input types as item argument. 1158 * @param {Bool} after Insert after instead of before the anchor. 1159 * 1160 * @return {Void} 1161 */ 1162 function moveItem(item, position, after) { 1163 item = getRelativeIndex(item); 1164 position = getRelativeIndex(position); 1165 1166 // Move only if there is an actual change requested 1167 if (item > -1 && position > -1 && item !== position && (!after || position !== item - 1) && (after || position !== item + 1)) { 1168 $items.eq(item)[after ? 'insertAfter' : 'insertBefore'](items[position].el); 1169 1170 var shiftStart = item < position ? item : (after ? position : position - 1); 1171 var shiftEnd = item > position ? item : (after ? position + 1 : position); 1172 var shiftsUp = item > position; 1173 1174 // Update activeItem index 1175 if (rel.activeItem != null) { 1176 if (item === rel.activeItem) { 1177 last.active = rel.activeItem = after ? (shiftsUp ? position + 1 : position) : (shiftsUp ? position : position - 1); 1178 } else if (rel.activeItem > shiftStart && rel.activeItem < shiftEnd) { 1179 last.active = rel.activeItem += shiftsUp ? 1 : -1; 1180 } 1181 } 1182 1183 // Reload 1184 load(); 1185 } 1186 } 1187 1188 /** 1189 * Move item after the target anchor. 1190 * 1191 * @param {Mixed} item Item to be moved. Can be DOM element or item index. 1192 * @param {Mixed} position Target position anchor. Can be DOM element or item index. 1193 * 1194 * @return {Void} 1195 */ 1196 self.moveAfter = function (item, position) { 1197 moveItem(item, position, 1); 1198 }; 1199 1200 /** 1201 * Move item before the target anchor. 1202 * 1203 * @param {Mixed} item Item to be moved. Can be DOM element or item index. 1204 * @param {Mixed} position Target position anchor. Can be DOM element or item index. 1205 * 1206 * @return {Void} 1207 */ 1208 self.moveBefore = function (item, position) { 1209 moveItem(item, position); 1210 }; 1211 1212 /** 1213 * Registers callbacks. 1214 * 1215 * @param {Mixed}
1215 name Event name, or callbacks map. 1216 * @param {Mixed} fn Callback, or an array of callback functions. 1217 * 1218 * @return {Void} 1219 */ 1220 self.on = function (name, fn) { 1221 // Callbacks map 1222 if (type(name) === 'object') { 1223 for (var key in name) { 1224 if (name.hasOwnProperty(key)) { 1225 self.on(key, name[key]); 1226 } 1227 } 1228 // Callback 1229 } else if (type(fn) === 'function') { 1230 var names = name.split(' '); 1231 for (var n = 0, nl = names.length; n < nl; n++) { 1232 callbacks[names[n]] = callbacks[names[n]] || []; 1233 if (callbackIndex(names[n], fn) === -1) { 1234 callbacks[names[n]].push(fn); 1235 } 1236 } 1237 // Callbacks array 1238 } else if (type(fn) === 'array') { 1239 for (var f = 0, fl = fn.length; f < fl; f++) { 1240 self.on(name, fn[f]); 1241 } 1242 } 1243 }; 1244 1245 /** 1246 * Registers callbacks to be executed only once. 1247 * 1248 * @param {Mixed} name Event name, or callbacks map. 1249 * @param {Mixed} fn Callback, or an array of callback functions. 1250 * 1251 * @return {Void} 1252 */ 1253 self.one = function (name, fn) { 1254 function proxy() { 1255 fn.apply(self, arguments); 1256 self.off(name, proxy); 1257 } 1258 self.on(name, proxy); 1259 }; 1260 1261 /** 1262 * Remove one or all callbacks. 1263 * 1264 * @param {String} name Event name. 1265 * @param {Mixed} fn Callback, or an array of callback functions. Omit to remove all callbacks. 1266 * 1267 * @return {Void} 1268 */ 1269 self.off = function (name, fn) { 1270 if (fn instanceof Array) { 1271 for (var f = 0, fl = fn.length; f < fl; f++) { 1272 self.off(name, fn[f]); 1273 } 1274 } else { 1275 var names = name.split(' '); 1276 for (var n = 0, nl = names.length; n < nl; n++) { 1277 callbacks[names[n]] = callbacks[names[n]] || []; 1278 if (fn == null) { 1279 callbacks[names[n]].length = 0; 1280 } else { 1281 var index = callbackIndex(names[n], fn); 1282 if (index !== -1) { 1283 callbacks[names[n]].splice(index, 1); 1284 } 1285 } 1286 } 1287 } 1288 }; 1289 1290 /** 1291 * Returns callback array index. 1292 * 1293 * @param {String} name Event name. 1294 * @param {Function} fn Function 1295 * 1296 * @return {Int} Callback array index, or -1 if isn't registered. 1297 */ 1298 function callbackIndex(name, fn) { 1299 for (var i = 0, l = callbacks[name].length; i < l; i++) { 1300 if (callbacks[name][i] === fn) { 1301 return i; 1302 } 1303 } 1304 return -1; 1305 } 1306 1307 /** 1308 * Reset next cycle timeout. 1309 * 1310 * @return {Void} 1311 */ 1312 function resetCycle() { 1313 if (dragging.released && !self.isPaused) { 1314 self.resume(); 1315 } 1316 } 1317 1318 /** 1319 * Calculate SLIDEE representation of handle position. 1320 * 1321 * @param {Int} handlePos 1322 * 1323 * @return {Int} 1324 */ 1325 function handleToSlidee(handlePos) { 1326 return round(within(handlePos, hPos.start, hPos.end) / hPos.end * (pos.end - pos.start)) + pos.start; 1327 } 1328 1329 /** 1330 * Keeps track of a dragging delta history. 1331 * 1332 * @return {Void} 1333 */ 1334 function draggingHistoryTick() { 1335 // Looking at this, I know what you're thinking :) But as we need only 4 history states, doing it this way 1336 // as opposed to a proper loop is ~25 bytes smaller (when minified with GCC), a lot faster, and doesn't 1337 // generate garbage. The loop version would create 2 new variables on every tick. Unexaptable! 1338 dragging.history[0] = dragging.history[1]; 1339 dragging.history[1] = dragging.history[2]; 1340 dragging.history[2] = dragging.history[3]; 1341 dragging.history[3] = dragging.delta; 1342 } 1343 1344 /** 1345 * Initialize continuous movement. 1346 * 1347 * @return {Void} 1348 */ 1349 function continuousInit(source) { 1350 dragging.released = 0; 1351 dragging.source = source; 1352 dragging.slidee = source === 'slidee'; 1353 } 1354 1355 /** 1356 * Dragging initiator. 1357 * 1358 * @param {Event} event 1359 * 1360 * @return {Void} 1361 */ 1362 function dragInit(event) { 1363 var isTouch = event.type === 'touchstart'; 1364 var source = event.data.source; 1365 var isSlidee = source === 'slidee'; 1366 1367 // Ignore when already in progress, or interactive element in non-touch navivagion 1368 if (dragging.init || !isTouch && isInteractive(event.target)) { 1369 return; 1370 } 1371 1372 // Handle dragging conditions 1373 if (source === 'handle' && (!o.dragHandle || hPos.start === hPos.end)) { 1374 return; 1375 } 1376 1377 // SLIDEE dragging conditions 1378 if (isSlidee && !(isTouch ? o.touchDragging : o.mouseDragging && event.which < 2)) { 1379 return; 1380 } 1381 1382 if (!isTouch) { 1383 // prevents native image dragging in Firefox 1384 stopDefault(event); 1385 } 1386 1387 // Reset dragging object 1388 continuousInit(source); 1389 1390 // Properties used in dragHandler 1391 dragging.init = 0; 1392 dragging.$source = $(event.target); 1393 dragging.touch = isTouch; 1394 dragging.pointer = isTouch ? event.originalEvent.touches[0] : event; 1395 dragging.initX = dragging.pointer.pageX; 1396 dragging.initY = dragging.pointer.pageY;
1397 dragging.initPos = isSlidee ? pos.cur : hPos.cur; 1398 dragging.start = +new Date(); 1399 dragging.time = 0; 1400 dragging.path = 0; 1401 dragging.delta = 0; 1402 dragging.locked = 0; 1403 dragging.history = [0, 0, 0, 0]; 1404 dragging.pathToLock = isSlidee ? isTouch ? 30 : 10 : 0; 1405 1406 // Bind dragging events 1407 $doc.on(isTouch ? dragTouchEvents : dragMouseEvents, dragHandler); 1408 1409 // Pause ongoing cycle 1410 self.pause(1); 1411 1412 // Add dragging class 1413 (isSlidee ? $slidee : $handle).addClass(o.draggedClass); 1414 1415 // Trigger moveStart event 1416 trigger('moveStart'); 1417 1418 // Keep track of a dragging path history. This is later used in the 1419 // dragging release swing calculation when dragging SLIDEE. 1420 if (isSlidee) { 1421 historyID = setInterval(draggingHistoryTick, 10); 1422 } 1423 } 1424 1425 /** 1426 * Handler for dragging scrollbar handle or SLIDEE. 1427 * 1428 * @param {Event} event 1429 * 1430 * @return {Void} 1431 */ 1432 function dragHandler(event) { 1433 dragging.released = event.type === 'mouseup' || event.type === 'touchend'; 1434 dragging.pointer = dragging.touch ? event.originalEvent[dragging.released ? 'changedTouches' : 'touches'][0] : event; 1435 dragging.pathX = dragging.pointer.pageX - dragging.initX; 1436 dragging.pathY = dragging.pointer.pageY - dragging.initY; 1437 dragging.path = sqrt(pow(dragging.pathX, 2) + pow(dragging.pathY, 2)); 1438 dragging.delta = o.horizontal ? dragging.pathX : dragging.pathY; 1439 1440 if (!dragging.released && dragging.path < 1) return; 1441 1442 // We haven't decided whether this is a drag or not... 1443 if (!dragging.init) { 1444 // If the drag path was very short, maybe it's not a drag? 1445 if (dragging.path < o.dragThreshold) { 1446 // If the pointer was released, the path will not become longer and it's 1447 // definitely not a drag. If not released yet, decide on next iteration 1448 return dragging.released ? dragEnd() : undefined; 1449 } 1450 else { 1451 // If dragging path is sufficiently long we can confidently start a drag 1452 // if drag is in different direction than scroll, ignore it 1453 if (o.horizontal ? abs(dragging.pathX) > abs(dragging.pathY) : abs(dragging.pathX) < abs(dragging.pathY)) { 1454 dragging.init = 1; 1455 } else { 1456 return dragEnd(); 1457 } 1458 } 1459 } 1460 1461 stopDefault(event); 1462 1463 // Disable click on a source element, as it is unwelcome when dragging 1464 if (!dragging.locked && dragging.path > dragging.pathToLock && dragging.slidee) { 1465 dragging.locked = 1; 1466 dragging.$source.on(clickEvent, disableOneEvent); 1467 } 1468 1469 // Cancel dragging on release 1470 if (dragging.released) { 1471 dragEnd(); 1472 1473 // Adjust path with a swing on mouse release 1474 if (o.releaseSwing && dragging.slidee) { 1475 dragging.swing = (dragging.delta - dragging.history[0]) / 40 * 300; 1476 dragging.delta += dragging.swing; 1477 dragging.tweese = abs(dragging.swing) > 10; 1478 } 1479 } 1480 1481 slideTo(dragging.slidee ? round(dragging.initPos - dragging.delta) : handleToSlidee(dragging.initPos + dragging.delta)); 1482 } 1483 1484 /** 1485 * Stops dragging and cleans up after it. 1486 * 1487 * @return {Void} 1488 */ 1489 function dragEnd() { 1490 clearInterval(historyID); 1491 dragging.released = true; 1492 $doc.off(dragging.touch ? dragTouchEvents : dragMouseEvents, dragHandler); 1493 (dragging.slidee ? $slidee : $handle).removeClass(o.draggedClass); 1494 1495 // Make sure that disableOneEvent is not active in next tick. 1496 setTimeout(function () { 1497 dragging.$source.off(clickEvent, disableOneEvent); 1498 }); 1499 1500 // Normally, this is triggered in render(), but if there 1501 // is nothing to render, we have to do it manually here. 1502 if (pos.cur === pos.dest && dragging.init) { 1503 trigger('moveEnd'); 1504 } 1505 1506 // Resume ongoing cycle 1507 self.resume(1); 1508 1509 dragging.init = 0; 1510 } 1511 1512 /** 1513 * Check whether element is interactive. 1514 * 1515 * @return {Boolean} 1516 */ 1517 function isInteractive(element) { 1518 return ~$.inArray(element.nodeName, interactiveElements) || $(element).is(o.interactive); 1519 } 1520 1521 /** 1522 * Continuous movement cleanup on mouseup. 1523 * 1524 * @return {Void} 1525 */ 1526 function movementReleaseHandler() { 1527 self.stop(); 1528 $doc.off('mouseup', movementReleaseHandler); 1529 } 1530 1531 /** 1532 * Buttons navigation handler. 1533 * 1534 * @param {Event} event 1535 * 1536 * @return {Void} 1537 */ 1538 function buttonsHandler(event) { 1539 /*jshint validthis:true */ 1540 stopDefault(event); 1541 switch (this) { 1542 case $forwardButton[0]: 1543 case $backwardButton[0]: 1544 self.moveBy($forwardButton.is(this) ? o.moveBy : -o.moveBy); 1545 $doc.on('mouseup', movementReleaseHandler); 1546 break; 1547 1548 case $prevButton[0]: 1549 self.prev(); 1550 break; 1551 1552 case $nextButton[0]: 1553 self.next(); 1554 break; 1555 1556 case $prevPageButton[0]: 1557 self.prevPage(); 1558 break; 1559 1560 case $nextPageButton[0]: 1561 self.nextPage(); 1562 break; 1563 } 1564 } 1565 1566 /** 1567 * Mouse wheel delta normalization. 1568 * 1569 * @param {Event} event 1570 * 1571 * @return {Int} 1572 */ 1573 function normalizeWheelDelta(event) { 1574 // wheelDelta needed only for IE8- 1575 scrolling.curDelta = ((o.horizontal ? event.deltaY || event.deltaX : event.deltaY) || -event.wheelDelta); 1576 scrolling.curDelta /= event.deltaMode === 1 ? 3 : 100; 1577 if (!itemNav) { 1578 return scrolling.curDelta; 1579 } 1580 time = +new Date(); 1581 if (scrolling.last < time - scrolling.resetTime) { 1582 scrolling.delta = 0; 1583 } 1584 scrolling.last = time; 1585 scrolling.delta += scrolling.curDelta; 1586 if (abs(scrolling.delta) < 1) { 1587 scrolling.finalDelta = 0; 1588 } else { 1589 scrolling.finalDelta = round(scrolling.delta / 1); 1590 scrolling.delta %= 1;
1591 } 1592 return scrolling.finalDelta; 1593 } 1594 1595 /** 1596 * Mouse scrolling handler. 1597 * 1598 * @param {Event} event 1599 * 1600 * @return {Void} 1601 */ 1602 function scrollHandler(event) { 1603 // Mark event as originating in a Sly instance 1604 event.originalEvent[namespace] = self; 1605 // Don't hijack global scrolling 1606 var time = +new Date(); 1607 if (lastGlobalWheel + o.scrollHijack > time && $scrollSource[0] !== document && $scrollSource[0] !== window) { 1608 lastGlobalWheel = time; 1609 return; 1610 } 1611 // Ignore if there is no scrolling to be done 1612 if (!o.scrollBy || pos.start === pos.end) { 1613 return; 1614 } 1615 var delta = normalizeWheelDelta(event.originalEvent); 1616 // Trap scrolling only when necessary and/or requested 1617 if (o.scrollTrap || delta > 0 && pos.dest < pos.end || delta < 0 && pos.dest > pos.start) { 1618 stopDefault(event, 1); 1619 } 1620 self.slideBy(o.scrollBy * delta); 1621 } 1622 1623 /** 1624 * Scrollbar click handler. 1625 * 1626 * @param {Event} event 1627 * 1628 * @return {Void} 1629 */ 1630 function scrollbarHandler(event) { 1631 // Only clicks on scroll bar. Ignore the handle. 1632 if (o.clickBar && event.target === $sb[0]) { 1633 stopDefault(event); 1634 // Calculate new handle position and sync SLIDEE to it 1635 slideTo(handleToSlidee((o.horizontal ? event.pageX - $sb.offset().left : event.pageY - $sb.offset().top) - handleSize / 2)); 1636 } 1637 } 1638 1639 /** 1640 * Keyboard input handler. 1641 * 1642 * @param {Event} event 1643 * 1644 * @return {Void} 1645 */ 1646 function keyboardHandler(event) { 1647 if (!o.keyboardNavBy) { 1648 return; 1649 } 1650 1651 switch (event.which) { 1652 // Left or Up 1653 case o.horizontal ? 37 : 38: 1654 stopDefault(event); 1655 self[o.keyboardNavBy === 'pages' ? 'prevPage' : 'prev'](); 1656 break; 1657 1658 // Right or Down 1659 case o.horizontal ? 39 : 40: 1660 stopDefault(event); 1661 self[o.keyboardNavBy === 'pages' ? 'nextPage' : 'next'](); 1662 break; 1663 } 1664 } 1665 1666 /** 1667 * Click on item activation handler. 1668 * 1669 * @param {Event} event 1670 * 1671 * @return {Void} 1672 */ 1673 function activateHandler(event) { 1674 /*jshint validthis:true */ 1675 1676 // Ignore clicks on interactive elements. 1677 if (isInteractive(this)) { 1678 event.originalEvent[namespace + 'ignore'] = true; 1679 return; 1680 } 1681 1682 // Ignore events that: 1683 // - are not originating from direct SLIDEE children 1684 // - originated from interactive elements 1685 if (this.parentNode !== $slidee[0] || event.originalEvent[namespace + 'ignore']) return; 1686 1687 self.activate(this); 1688 } 1689 1690 /** 1691 * Click on page button handler. 1692 * 1693 * @param {Event} event 1694 * 1695 * @return {Void} 1696 */ 1697 function activatePageHandler() { 1698 /*jshint validthis:true */ 1699 // Accept only events from direct pages bar children. 1700 if (this.parentNode === $pb[0]) { 1701 self.activatePage($pages.index(this)); 1702 } 1703 } 1704 1705 /** 1706 * Pause on hover handler. 1707 * 1708 * @param {Event} event 1709 * 1710 * @return {Void} 1711 */ 1712 function pauseOnHoverHandler(event) { 1713 if (o.pauseOnHover) { 1714 self[event.type === 'mouseenter' ? 'pause' : 'resume'](2); 1715 } 1716 } 1717 1718 /** 1719 * Trigger callbacks for event. 1720 * 1721 * @param {String} name Event name. 1722 * @param {Mixed} argX Arguments passed to callbacks. 1723 * 1724 * @return {Void} 1725 */ 1726 function trigger(name, arg1) { 1727 if (callbacks[name]) { 1728 l = callbacks[name].length; 1729 // Callbacks will be stored and executed from a temporary array to not 1730 // break the execution queue when one of the callbacks unbinds itself. 1731 tmpArray.length = 0; 1732 for (i = 0; i < l; i++) { 1733 tmpArray.push(callbacks[name][i]); 1734 } 1735 // Execute the callbacks 1736 for (i = 0; i < l; i++) { 1737 tmpArray[i].call(self, name, arg1); 1738 } 1739 } 1740 } 1741 1742 /** 1743 * Destroys instance and everything it created. 1744 * 1745 * @return {Void} 1746 */ 1747 self.destroy = function () { 1748 // Remove the reference to itself 1749 Sly.removeInstance(frame); 1750 1751 // Unbind all events 1752 $scrollSource 1753 .add($handle) 1754 .add($sb) 1755 .add($pb) 1756 .add($forwardButton) 1757 .add($backwardButton) 1758 .add($prevButton) 1759 .add($nextButton) 1760 .add($prevPageButton) 1761 .add($nextPageButton) 1762 .off('.' + namespace); 1763 1764 // Unbinding specifically as to not nuke out other instances 1765 $doc.off('keydown', keyboardHandler); 1766 1767 // Remove classes 1768 $prevButton 1769 .add($nextButton) 1770 .add($prevPageButton) 1771 .add($nextPageButton) 1772 .removeClass(o.disabledClass); 1773 1774 if ($items && rel.activeItem != null) { 1775 $items.eq(rel.activeItem).removeClass(o.activeClass); 1776 } 1777 1778 // Remove page items 1779 $pb.empty(); 1780 1781 if (!parallax) { 1782 // Unbind events from frame 1783 $frame.off('.' + namespace); 1784 // Restore original styles
1785 frameStyles.restore(); 1786 slideeStyles.restore(); 1787 sbStyles.restore(); 1788 handleStyles.restore(); 1789 // Remove the instance from element data storage 1790 $.removeData(frame, namespace); 1791 } 1792 1793 // Clean up collections 1794 items.length = pages.length = 0; 1795 last = {}; 1796 1797 // Reset initialized status and return the instance 1798 self.initialized = 0; 1799 return self; 1800 }; 1801 1802 /** 1803 * Initialize. 1804 * 1805 * @return {Object} 1806 */ 1807 self.init = function () { 1808 if (self.initialized) { 1809 return; 1810 } 1811 1812 // Disallow multiple instances on the same element 1813 if (Sly.getInstance(frame)) throw new Error('There is already a Sly instance on this element'); 1814 1815 // Store the reference to itself 1816 Sly.storeInstance(frame, self); 1817 1818 // Register callbacks map 1819 self.on(callbackMap); 1820 1821 // Save styles 1822 var holderProps = ['overflow', 'position']; 1823 var movableProps = ['position', 'webkitTransform', 'msTransform', 'transform', 'left', 'top', 'width', 'height']; 1824 frameStyles.save.apply(frameStyles, holderProps); 1825 sbStyles.save.apply(sbStyles, holderProps); 1826 slideeStyles.save.apply(slideeStyles, movableProps); 1827 handleStyles.save.apply(handleStyles, movableProps); 1828 1829 // Set required styles 1830 var $movables = $handle; 1831 if (!parallax) { 1832 $movables = $movables.add($slidee); 1833 $frame.css('overflow', 'hidden'); 1834 if (!transform && $frame.css('position') === 'static') { 1835 $frame.css('position', 'relative'); 1836 } 1837 } 1838 if (transform) { 1839 if (gpuAcceleration) { 1840 $movables.css(transform, gpuAcceleration); 1841 } 1842 } else { 1843 if ($sb.css('position') === 'static') { 1844 $sb.css('position', 'relative'); 1845 } 1846 $movables.css({ position: 'absolute' }); 1847 } 1848 1849 // Navigation buttons 1850 if (o.forward) { 1851 $forwardButton.on(mouseDownEvent, buttonsHandler); 1852 } 1853 if (o.backward) { 1854 $backwardButton.on(mouseDownEvent, buttonsHandler); 1855 } 1856 if (o.prev) { 1857 $prevButton.on(clickEvent, buttonsHandler); 1858 } 1859 if (o.next) { 1860 $nextButton.on(clickEvent, buttonsHandler); 1861 } 1862 if (o.prevPage) { 1863 $prevPageButton.on(clickEvent, buttonsHandler); 1864 } 1865 if (o.nextPage) { 1866 $nextPageButton.on(clickEvent, buttonsHandler); 1867 } 1868 1869 // Scrolling navigation 1870 $scrollSource.on(wheelEvent, scrollHandler); 1871 1872 // Clicking on scrollbar navigation 1873 if ($sb[0]) { 1874 $sb.on(clickEvent, scrollbarHandler); 1875 } 1876 1877 // Click on items navigation 1878 if (itemNav && o.activateOn) { 1879 $frame.on(o.activateOn + '.' + namespace, '*', activateHandler); 1880 } 1881 1882 // Pages navigation 1883 if ($pb[0] && o.activatePageOn) { 1884 $pb.on(o.activatePageOn + '.' + namespace, '*', activatePageHandler); 1885 } 1886 1887 // Dragging navigation 1888 $dragSource.on(dragInitEvents, { source: 'slidee' }, dragInit); 1889 1890 // Scrollbar dragging navigation 1891 if ($handle) { 1892 $handle.on(dragInitEvents, { source: 'handle' }, dragInit); 1893 } 1894 1895 // Keyboard navigation 1896 $doc.on('keydown', keyboardHandler); 1897 1898 if (!parallax) { 1899 // Pause on hover 1900 $frame.on('mouseenter.' + namespace + ' mouseleave.' + namespace, pauseOnHoverHandler); 1901 // Reset native FRAME element scroll 1902 $frame.on('scroll.' + namespace, resetScroll); 1903 } 1904 1905 // Mark instance as initialized 1906 self.initialized = 1; 1907 1908 // Load 1909 load(true); 1910 1911 // Initiate automatic cycling 1912 if (o.cycleBy && !parallax) { 1913 self[o.startPaused ? 'pause' : 'resume'](); 1914 } 1915 1916 // Return instance 1917 return self; 1918 }; 1919 } 1920 1921 Sly.getInstance = function (element) { 1922 return $.data(element, namespace); 1923 }; 1924 1925 Sly.storeInstance = function (element, sly) { 1926 return $.data(element, namespace, sly); 1927 }; 1928 1929 Sly.removeInstance = function (element) { 1930 return $.removeData(element, namespace); 1931 }; 1932 1933 /** 1934 * Return type of the value. 1935 * 1936 * @param {Mixed} value 1937 * 1938 * @return {String} 1939 */ 1940 function type(value) { 1941 if (value == null) { 1942 return String(value); 1943 } 1944 1945 if (typeof value === 'object' || typeof value === 'function') { 1946 return Object.prototype.toString.call(value).match(/\s([a-z]+)/i)[1].toLowerCase() || 'object'; 1947 } 1948 1949 return typeof value; 1950 } 1951 1952 /** 1953 * Event preventDefault & stopPropagation helper. 1954 * 1955 * @param {Event} event Event object. 1956 * @param {Bool} noBubbles Cancel event bubbling. 1957 * 1958 * @return {Void} 1959 */ 1960 function stopDefault(event, noBubbles) { 1961 event.preventDefault(); 1962 if (noBubbles) { 1963 event.stopPropagation(); 1964 } 1965 } 1966 1967 /** 1968 * Disables an event it was triggered on and unbinds itself. 1969 * 1970 * @param {Event} event 1971 * 1972 * @return {Void} 1973 */ 1974 function disableOneEvent(event) { 1975 /*jshint validthis:true */ 1976 stopDefault(event, 1); 1977 $(this).off(event.type, disableOneEvent); 1978 } 1979 1980 /** 1981 * Resets native element scroll values to 0. 1982 * 1983 * @return {Void} 1984 */ 1985 function resetScroll() { 1986 /*jshint validthis:true */ 1987 this.scrollLeft = 0; 1988 this.scrollTop = 0; 1989 } 1990 1991 /** 1992 * Check if variable is a number. 1993 * 1994 * @param {Mixed} value 1995 * 1996 * @return {Boolean} 1997 */ 1998 function isNumber(value) { 1999 return !isNaN(parseFloat(value)) && isFinite(value); 2000 } 2001 2002 /** 2003 * Parse style to pixels. 2004 * 2005 * @param {Object} $item jQuery object with element. 2006 * @param {Property} property CSS property to get the pixels from. 2007 * 2008 * @return {Int} 2009 */ 2010 function getPx($item, property) {
2011 return 0 | round(String($item.css(property)).replace(/[^\-0-9.]/g, '')); 2012 } 2013 2014 /** 2015 * Make sure that number is within the limits. 2016 * 2017 * @param {Number} number 2018 * @param {Number} min 2019 * @param {Number} max 2020 * 2021 * @return {Number} 2022 */ 2023 function within(number, min, max) { 2024 return number < min ? min : number > max ? max : number; 2025 } 2026 2027 /** 2028 * Saves element styles for later restoration. 2029 * 2030 * Example: 2031 * var styles = new StyleRestorer(frame); 2032 * styles.save('position'); 2033 * element.style.position = 'absolute'; 2034 * styles.restore(); // restores to state before the assignment above 2035 * 2036 * @param {Element} element 2037 */ 2038 function StyleRestorer(element) { 2039 var self = {}; 2040 self.style = {}; 2041 self.save = function () { 2042 if (!element || !element.nodeType) return; 2043 for (var i = 0; i < arguments.length; i++) { 2044 self.style[arguments[i]] = element.style[arguments[i]]; 2045 } 2046 return self; 2047 }; 2048 self.restore = function () { 2049 if (!element || !element.nodeType) return; 2050 for (var prop in self.style) { 2051 if (self.style.hasOwnProperty(prop)) element.style[prop] = self.style[prop]; 2052 } 2053 return self; 2054 }; 2055 return self; 2056 } 2057 2058 // Local WindowAnimationTiming interface polyfill 2059 (function (w) { 2060 rAF = w.requestAnimationFrame 2061 || w.webkitRequestAnimationFrame 2062 || fallback; 2063 2064 /** 2065 * Fallback implementation. 2066 */ 2067 var prev = new Date().getTime(); 2068 function fallback(fn) { 2069 var curr = new Date().getTime(); 2070 var ms = Math.max(0, 16 - (curr - prev)); 2071 var req = setTimeout(fn, ms); 2072 prev = curr; 2073 return req; 2074 } 2075 2076 /** 2077 * Cancel. 2078 */ 2079 var cancel = w.cancelAnimationFrame 2080 || w.webkitCancelAnimationFrame 2081 || w.clearTimeout; 2082 2083 cAF = function(id){ 2084 cancel.call(w, id); 2085 }; 2086 }(window)); 2087 2088 // Feature detects 2089 (function () { 2090 var prefixes = ['', 'Webkit', 'Moz', 'ms', 'O']; 2091 var el = document.createElement('div'); 2092 2093 function testProp(prop) { 2094 for (var p = 0, pl = prefixes.length; p < pl; p++) { 2095 var prefixedProp = prefixes[p] ? prefixes[p] + prop.charAt(0).toUpperCase() + prop.slice(1) : prop; 2096 if (el.style[prefixedProp] != null) { 2097 return prefixedProp; 2098 } 2099 } 2100 } 2101 2102 // Global support indicators 2103 transform = testProp('transform'); 2104 gpuAcceleration = testProp('perspective') ? 'translateZ(0) ' : ''; 2105 }()); 2106 2107 // Expose class globally 2108 w[className] = Sly; 2109 2110 // jQuery proxy 2111 $.fn[pluginName] = function (options, callbackMap) { 2112 var method, methodArgs; 2113 2114 // Attributes logic 2115 if (!$.isPlainObject(options)) { 2116 if (type(options) === 'string' || options === false) { 2117 method = options === false ? 'destroy' : options; 2118 methodArgs = Array.prototype.slice.call(arguments, 1); 2119 } 2120 options = {}; 2121 } 2122 2123 // Apply to all elements 2124 return this.each(function (i, element) { 2125 // Call with prevention against multiple instantiations 2126 var plugin = Sly.getInstance(element); 2127 2128 if (!plugin && !method) { 2129 // Create a new object if it doesn't exist yet 2130 plugin = new Sly(element, options, callbackMap).init(); 2131 } else if (plugin && method) { 2132 // Call method 2133 if (plugin[method]) { 2134 plugin[method].apply(plugin, methodArgs); 2135 } 2136 } 2137 }); 2138 }; 2139 2140 // Default options 2141 Sly.defaults = { 2142 slidee: null, // Selector, DOM element, or jQuery object with DOM element representing SLIDEE. 2143 horizontal: false, // Switch to horizontal mode. 2144 2145 // Item based navigation 2146 itemNav: null, // Item navigation type. Can be: 'basic', 'centered', 'forceCentered'. 2147 itemSelector: null, // Select only items that match this selector. 2148 smart: false, // Repositions the activated item to help with further navigation. 2149 activateOn: null, // Activate an item on this event. Can be: 'click', 'mouseenter', ... 2150 activateMiddle: false, // Always activate the item in the middle of the FRAME. forceCentered only. 2151 2152 // Scrolling 2153 scrollSource: null, // Element for catching the mouse wheel scrolling. Default is FRAME. 2154 scrollBy: 0, // Pixels or items to move per one mouse scroll. 0 to disable scrolling. 2155 scrollHijack: 300, // Milliseconds since last wheel event after which it is acceptable to hijack global scroll. 2156 scrollTrap: false, // Don't bubble scrolling when hitting scrolling limits. 2157 2158 // Dragging 2159 dragSource: null, // Selector or DOM element for catching dragging events. Default is FRAME. 2160 mouseDragging: false, // Enable navigation by dragging the SLIDEE with mouse cursor. 2161 touchDragging: false, // Enable navigation by dragging the SLIDEE with touch events. 2162 releaseSwing: false, // Ease out on dragging swing release. 2163 swingSpeed: 0.2, // Swing synchronization speed, where: 1 = instant, 0 = infinite. 2164 elasticBounds: false, // Stretch SLIDEE position limits when dragging past FRAME boundaries. 2165 dragThreshold: 3, // Distance in pixels before Sly recognizes dragging. 2166 interactive: null, // Selector for special interactive elements. 2167 2168 // Scrollbar 2169 scrollBar: null, // Selector or DOM element for scrollbar container. 2170 dragHandle: false, // Whether the scrollbar handle should be draggable. 2171 dynamicHandle: false, // Scrollbar handle represents the ratio between hidden and visible content. 2172 minHandleSize: 50, // Minimal height or width (depends on sly direction) of a handle in pixels. 2173 clickBar: false, // Enable navigation by clicking on scrollbar. 2174 syncSpeed: 0.5, // Handle =>
2174 SLIDEE synchronization speed, where: 1 = instant, 0 = infinite. 2175 2176 // Pagesbar 2177 pagesBar: null, // Selector or DOM element for pages bar container. 2178 activatePageOn: null, // Event used to activate page. Can be: click, mouseenter, ... 2179 pageBuilder: // Page item generator. 2180 function (index) { 2181 return '<li>' + (index + 1) + '</li>'; 2182 }, 2183 2184 // Navigation buttons 2185 forward: null, // Selector or DOM element for "forward movement" button. 2186 backward: null, // Selector or DOM element for "backward movement" button. 2187 prev: null, // Selector or DOM element for "previous item" button. 2188 next: null, // Selector or DOM element for "next item" button. 2189 prevPage: null, // Selector or DOM element for "previous page" button. 2190 nextPage: null, // Selector or DOM element for "next page" button. 2191 2192 // Automated cycling 2193 cycleBy: null, // Enable automatic cycling by 'items' or 'pages'. 2194 cycleInterval: 5000, // Delay between cycles in milliseconds. 2195 pauseOnHover: false, // Pause cycling when mouse hovers over the FRAME. 2196 startPaused: false, // Whether to start in paused sate. 2197 2198 // Mixed options 2199 moveBy: 300, // Speed in pixels per second used by forward and backward buttons. 2200 speed: 0, // Animations speed in milliseconds. 0 to disable animations. 2201 easing: 'swing', // Easing for duration based (tweening) animations. 2202 startAt: null, // Starting offset in pixels or items. 2203 keyboardNavBy: null, // Enable keyboard navigation by 'items' or 'pages'. 2204 2205 // Classes 2206 draggedClass: 'dragged', // Class for dragged elements (like SLIDEE or scrollbar handle). 2207 activeClass: 'active', // Class for active items and pages. 2208 disabledClass: 'disabled' // Class for disabled navigation elements. 2209 }; 2210}(jQuery, window));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.