1/** 2 * Fluxus Lightbox 2.0 3 * 4 * Dependencies: 5 * jQuery 6 * underscore debounce 7 */ 8(function ($, debounce) { 9 var TOUCH_DEVICE = false; 10 var FADE_OUT_DURATION = 300; 11 var DEFAULT_OPTIONS = { 12 error: 'Unable to load', 13 mode: 'fit', // fit or full, 14 margin: 15, // Margin around content when in fit mode. 15 loadingIndicatorDelay: 800, 16 idleTimeout: 5000, 17 onHide: function () {}, 18 // Called on link click. Can return false to cancel 19 // opening of the Lightbox. 20 onClick: undefined 21 }; 22 var $window = $(window); 23 var $html = $('html'); 24 25 $window.on('touchstart.fluxus-lightbox', function () { 26 TOUCH_DEVICE = true; 27 $window.trigger('is-touch-device').off('touchstart.fluxus-lightbox'); 28 }); 29 30 $.FluxusLightbox = function ($links, options) { 31 this.$links = $links; 32 if (0 === this.$links.length) { 33 return false; 34 } 35 36 this.options = $.extend({}, DEFAULT_OPTIONS, options); 37 this._isModeFullWidth = this.options.mode == 'full'; 38 this.hdImage = new Image(); 39 40 this.render(); 41 this.bindOnClickListener(); 42 43 // Detects touch device and enables gestures. 44 if (TOUCH_DEVICE) { 45 this.setupGestures(); 46 } else { 47 $window.on( 48 'is-touch-device.fluxus-lightbox', 49 this.setupGestures.bind(this) 50 ); 51 } 52 }; 53 54 $.FluxusLightbox.prototype = { 55 open: function (event, callback) { 56 // Render Lightbox offscreen, prevent overflow. 57 $html.addClass('fluxus-lightbox-visible'); 58 59 // Store current window size. 60 $window.on( 61 'resize.fluxus-lightbox', 62 debounce(this.storeWindowSize.bind(this)) 63 ); 64 this.storeWindowSize(); 65 66 // Calculate scroll position and start tracking mouse. 67 this._scrollPosition = { 68 left: $window.scrollLeft(), 69 top: $window.scrollTop() 70 }; 71 event && this.onMouseMove(event); 72 $window.on('mousemove.fluxus-lightbox', this.onMouseMove.bind(this)); 73 74 if (this._isModeFullWidth) { 75 this.toggleFullWidthMode(true); 76 } 77 78 // Transition into view. 79 onTransitionEnd( 80 this.$lightbox, 81 function () { 82 this.$lightbox.addClass('fluxus-lightbox--transitioned'); 83 }.bind(this), 84 { propertyName: 'transform' } 85 ); 86 this.$lightbox.addClass('fluxus-lightbox--slide-in'); 87 88 $window.on( 89 'keydown.fluxus-lightbox', 90 this.handleKeyboardEvent.bind(this) 91 ); 92 93 callback && callback.call(this); 94 }, 95 96 close: function (options) { 97 $window.off('.fluxus-lightbox'); 98 this.$content.css({ transition: '' }); 99 100 options = $.extend({}, { animation: true }, options); 101 102 if (!options.animation) { 103 this.$lightbox 104 .css({ display: 'none', transition: 'none' }) 105 .removeClass('fluxus-lightbox--transitioned') 106 .removeClass('fluxus-lightbox--slide-in'); 107 this.$lightbox.width(); 108 $html.removeClass('fluxus-lightbox-visible'); 109 this.$lightbox.css({ display: '', transition: '' }); 110 return; 111 } 112 113 onTransitionEnd( 114 this.$lightbox, 115 function () { 116 $html.removeClass('fluxus-lightbox-visible'); 117 var $iframe = this.$media.find('iframe'); 118 if ($iframe.length) { 119 this.$media.html(''); 120 } 121 this.options.onHide && this.options.onHide.call(this); 122 }.bind(this), 123 { propertyName: 'transform' } 124 ); 125 126 this.$lightbox 127 .removeClass('fluxus-lightbox--transitioned') 128 .removeClass('fluxus-lightbox--slide-in'); 129 }, 130 131 _startCloseAnimation: function (callback) { 132 onTransitionEnd(this.$lightbox, callback.bind(this), { 133 propertyName: 'transform' 134 }); 135 136 this.$lightbox 137 .removeClass('fluxus-lightbox--transitioned') 138 .removeClass('fluxus-lightbox--slide-in'); 139 }, 140 141 idleOn: function () { 142 this.isIdle = true; 143 this.$lightbox.addClass('fluxus-lightbox--idle'); 144 }, 145 146 idleOff: function () { 147 this.isIdle = false; 148 this.$lightbox.removeClass('fluxus-lightbox--idle'); 149 }, 150 151 onMouseMove: function (event) { 152 this.stickTipToCursorOnMouseMove(event); 153 this.triggerActivityEvent(); 154 }, 155 156 triggerActivityEvent: function () { 157 this.idleOff(); 158 clearTimeout(this._idleTimeout); 159 this._idleTimeout = setTimeout( 160 this.idleOn.bind(this), 161 this.options.idleTimeout 162 ); 163 }, 164 165 stickTipToCursorOnMouseMove: function (event) { 166 this._mousePosition = { 167 x: event.pageX, 168 y: event.pageY 169 }; 170 this.stickTipToCursor(); 171 }, 172 173 stickTipToCursor: function () { 174 var x = this._mousePosition.x - this._scrollPosition.left, 175 y = this._mousePosition.y - this._scrollPosition.top; 176 177 var middlePoint = this._windowSize.width / 2; 178 var rotate; 179 if (x > middlePoint) { 180 rotate = ' rotate(0)'; 181 } else { 182 rotate = ' rotate(180deg)'; 183 } 184 185 this.$tip.css('transform', 'translate3d(' + x + 'px, ' + y + 'px, 0)'); 186 this.$tipIcon.css('transform', rotate); 187 }, 188 189 handleKeyboardEvent: function (event) { 190 if (39 == event.which) { 191 event.preventDefault(); 192 this.next(); 193 } 194 195 if (37 == event.which) { 196 event.preventDefault(); 197 this.previous(); 198 } 199 200 if (27 == event.which) { 201 event.preventDefault(); 202 203 if (this._isModeFullWidth) { 204 this.toggleFullWidthMode(false); 205 } else { 206 this.close(); 207 } 208 } 209 }, 210 211 loadingStarted: function () { 212 this._loadingMessageTimeout = setTimeout( 213 function () { 214 this.$lightbox.addClass('fluxus-lightbox--long-loading'); 215 }.bind(this), 216 this.options.loadingIndicatorDelay 217 ); 218 }, 219 220 loadingFinished: function () { 221 clearTimeout(this._loadingMessageTimeout); 222 this.$lightbox.removeClass('fluxus-lightbox--long-loading'); 223 }, 224 225 showIframe: function ($iframe, options) { 226 this.$lightbox.addClass('fluxus-lightbox--iframe'); 227 this.$lightbox.removeClass('fluxus-lightbox--error'); 228 229 // Show loading. 230 this.$media.removeClass('fluxus-lightbox__media--loaded'); 231 this.loadingStarted(); 232 233 // Mark active element. 234 this.$links.removeClass('lightbox-active'); 235 $iframe.addClass('lightbox-active'); 236 237 this.$iframe = $iframe.clone().attr('style', ''); 238 239 this.$iframe 240 .on( 241 'error', 242 function () { 243 this.loadingFinished(); 244 this.$lightbox.addClass('fluxus-lightbox--error'); 245 }.bind(this) 246 ) 247 .on( 248 'load', 249 function () { 250 // put iframe in position 251 this.resizeIframe(); 252 this.$media.addClass('fluxus-lightbox__media--loaded'); 253 254 $window 255 .off('resize.media.fluxus-lightbox') 256 .on( 257 'resize.media.fluxus-lightbox', 258 debounce(this.resizeIframe.bind(this)) 259 ); 260 261 this.loadingFinished(); 262 }.bind(this) 263 ); 264 265 // Append contents with iFrame 266 this.$iframeWrap = $('<div class="fluxus-lightbox__iframe-wrap" />') 267 .on('mousemove.fluxus-lightbox', this.onMouseMoveOverIframe.bind(this)) 268 .prepend(this.$iframe); 269 270 this.$media.html(this.$iframeWrap); 271 }, 272 273 /** 274 * iFrame captures mouseover events. As a workaround we create an overlay, 275 * it captures mouse move and hides itself and navigation tip. When mouse 276 * event is captured again we show the overlay. 277 * 278 * Timeout is needed because window.mousemove event will fire straight away. 279 */ 280 onMouseMoveOverIframe: function () { 281 this.$lightbox.addClass('fluxus-lightbox--mouse-over-iframe'); 282 setTimeout( 283 function () { 284 $window.one( 285 'mousemove.iframe.fluxus-lightbox', 286 this.onMouseOutOfIframe.bind(this) 287 ); 288 }.bind(this), 289 50 290 ); 291 }, 292 293 onMouseOutOfIframe: function () { 294 this.$lightbox.removeClass('fluxus-lightbox--mouse-over-iframe'); 295 }, 296 297 resizeIframe: function () { 298 var CONTROLS_HEIGHT = 160, 299 DEFAULT_RATIO = 16 / 9, 300 parentWidth = this._windowSize.width - this.options.margin * 2, 301 parentHeight = 302 this._windowSize.height - CONTROLS_HEIGHT - this.options.margin * 2, 303 height, 304 width; 305 306 var parentRatio = 307 parentHeight > 0 ? parentWidth / parentHeight : DEFAULT_RATIO, 308 iframeRatio; 309 310 if (this.$iframe.attr('width') && this.$iframe.attr('height')) { 311 iframeRatio = this.$iframe.attr('width') / this.$iframe.attr('height'); 312 } else { 313 iframeRatio = DEFAULT_RATIO; 314 } 315 316 if (iframeRatio > parentRatio) { 317 // Fill axis: x 318 this.$iframeWrap.css({ 319 width: parentWidth, 320 height: parentWidth / iframeRatio 321 }); 322 } else { 323 this.$iframeWrap.css({ 324 width: parentHeight * iframeRatio, 325 height: parentHeight 326 }); 327 } 328 }, 329 330 showImage: function ($image, options) { 331 options = $.extend( 332 { 333 delay: FADE_OUT_DURATION 334 }, 335 options 336 ); 337 338 this.$lightbox.removeClass( 339 'fluxus-lightbox--iframe fluxus-lightbox--error' 340 ); 341 this.$media.removeClass('fluxus-lightbox__media--loaded'); 342 this.$links.removeClass('lightbox-active'); 343 $image.addClass('lightbox-active'); 344 345 this._userTransform = { x: 0, y: 0, scale: 1 }; 346 this.loadingStarted(); 347 348 var srcset = $image.attr('srcset'); 349 if (srcset) { 350 srcset = srcset.split(','); 351 srcset = srcset[srcset.length - 1]; // For now simply assume last src to have highest resolution. 352 srcset = srcset.split(' '); 353 this.hdSource = srcset[0]; 354 } else { 355 this.hdSource = null; 356 } 357 358 var source = 359 $image.attr('href') || $image.attr('src') || $image.data('src'); 360 var fadeOutAnimation = createTimeoutPromise(options.delay); 361 362 return $.when(this.loadImage(source), fadeOutAnimation) 363 .then( 364 function (image) { 365 this.loadingFinished(); 366 $window 367 .off('resize.media.fluxus-lightbox') 368 .on( 369 'resize.media.fluxus-lightbox', 370 debounce(this.resizeImage.bind(this)) 371 ); 372 373 this.$image = $('<img />').attr({ 374 src: image.src, 375 width: image.width, 376 height: image.height 377 }); 378 this.resizeImage(); 379 this.$media.html(this.$image); 380 this.$image[0].offsetHeight; 381 this.$media.addClass('fluxus-lightbox__media--loaded'); 382 383 // Load HD 384 var disableHd = TOUCH_DEVICE; 385 if (!disableHd && this.hdSource) { 386 // Cancel previous HD load. 387 this.hdImage.src = 388 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEAAAAALAAAAAABAAEAAAI=;'; 389 this.hdImage.onload = function () { 390 this.hdImageLoaded(new String(this.hdSource)); 391 }.bind(this); 392 this.hdImage.src = this.hdSource; 393 } 394 }.bind(this) 395 ) 396 .fail( 397 function () { 398 this.loadingFinished(); 399 this.$lightbox.addClass('fluxus-lightbox--error'); 400 }.bind(this) 401 ); 402 }, 403 404 hdImageLoaded: function (originalSource) { 405 // Only update image if it has not changed. 406 if (this.hdSource == originalSource) { 407 this.$image.attr('src', this.hdImage.src); 408 } 409 }, 410 411 /** 412 * Calculates images transform and scale values. 413 * We only operate on translate values for position. 414 * Combining other properties appeared problematic when trying to transition them. 415 */ 416 resizeImage: function () { 417 var width = this.$image.attr('width'), 418 height = this.$image.attr('height'), 419 ratio = width / height, 420 parentHeight = this.$media.height(), 421 parentWidth = this.$media.width(), 422 parentRatio = parentWidth / parentHeight, 423 data = { 424 height: parseInt(height, 10), 425 width: parseInt(width, 10), 426 // Which axis fills whole area 427 fillAxis: ratio > parentRatio ? 'x' : 'y' 428 }, 429 scaledWidth, 430 scaledHeight, 431 invertedScale; 432 433 if (!this._isModeFullWidth) { 434 // Fit mode has margins. Remove margins from available height. 435 parentWidth = parentWidth - this.options.margin * 2; 436 parentHeight = parentHeight - this.options.margin * 2; 437 } 438
439 if (data.fillAxis == 'x') { 440 // Full width, center vertically. 441 if (this._isModeFullWidth) { 442 data.scale = parentHeight / height; 443 data.x = (width * data.scale - parentWidth) / -2; 444 data.y = 0; 445 } else { 446 data.scale = parentWidth / width; 447 } 448 } else { 449 // Full height, center horizontally. 450 if (this._isModeFullWidth) { 451 data.scale = parentWidth / width; 452 data.x = 0; 453 data.y = (height * data.scale - parentHeight) / -2; 454 } else { 455 data.scale = parentHeight / height; 456 } 457 } 458 459 /** 460 * "(parentWidth - scaledWidth) / 2" = move by half the difference between image and container. 461 * "+ this.options.margin" = add margin. 462 */ 463 if (!this._isModeFullWidth) { 464 scaledWidth = width * data.scale; 465 scaledHeight = height * data.scale; 466 data.x = (parentWidth - scaledWidth) / 2 + this.options.margin; 467 data.y = (parentHeight - scaledHeight) / 2 + this.options.margin; 468 } 469 470 this.setImageStyle(data); 471 }, 472 473 setImageStyle: function (sugarStyle) { 474 this._imageStyle = $.extend({}, sugarStyle); 475 this.$image.scss(sugarStyle); 476 }, 477 478 loadImage: function (src) { 479 var dfd = new $.Deferred(), 480 image = new Image(); 481 482 image.onload = function () { 483 dfd.resolve(image); 484 }.bind(this); 485 486 image.onerror = dfd.reject; 487 image.src = src; 488 489 return dfd; 490 }, 491 492 next: function (options) { 493 var activeIndex = this.$links.index( 494 this.$links.filter('.lightbox-active') 495 ), 496 newIndex = 0; 497 498 if (activeIndex != -1 && activeIndex + 1 != this.$links.length) { 499 newIndex = activeIndex + 1; 500 } 501 502 return this.show(this.$links.eq(newIndex), options); 503 }, 504 505 previous: function (options) { 506 var activeIndex = this.$links.index( 507 this.$links.filter('.lightbox-active') 508 ), 509 newIndex = this.$links.length - 1; 510 511 if (activeIndex !== -1 && activeIndex !== 0) { 512 newIndex = activeIndex - 1; 513 } 514 515 return this.show(this.$links.eq(newIndex), options); 516 }, 517 518 show: function ($item, options) { 519 var method = $item.is('iframe') ? 'showIframe' : 'showImage'; 520 return this[method]($item, options); 521 }, 522 523 bindOnClickListener: function () { 524 this.$links.filter(':not(iframe)').on( 525 'click.fluxus-lightbox', 526 function (event) { 527 event.preventDefault(); 528 if (this.options.onClick && this.options.onClick(event) === false) { 529 return; 530 } 531 this.showImage($(event.currentTarget)); 532 this.open(event, this.options.onShow); 533 }.bind(this) 534 ); 535 }, 536 537 render: function () { 538 var that = this, 539 template = 540 '' + 541 '<div class="fluxus-lightbox">' + 542 '<div class="fluxus-lightbox__content">' + 543 '<div class="fluxus-lightbox__media"></div>' + 544 '</div>' + 545 '<div class="fluxus-lightbox__tip"><div class="fluxus-lightbox__tip-icon"></div></div>' + 546 '<div class="fluxus-lightbox__top-right">' + 547 '<span class="fluxus-lightbox__btn fluxus-lightbox__btn--close icon-close-slim"></span>' + 548 '</div>' + 549 '<div class="fluxus-lightbox__bottom-right">' + 550 '<span class="fluxus-lightbox__btn fluxus-lightbox__btn--resize">' + 551 '<span class="icon-zoom-in-slim"></span>' + 552 '<span class="icon-zoom-out-slim"></span>' + 553 '</span>' + 554 '<span class="fluxus-lightbox__btn fluxus-lightbox__btn--previous icon-previous-slim-arrow"></span>' + 555 '<span class="fluxus-lightbox__btn fluxus-lightbox__btn--next icon-next-slim-arrow"></span>' + 556 '</div>' + 557 '<div class="fluxus-lightbox__status"></div>' + 558 '<div class="fluxus-lightbox__error">' + 559 this.options.error + 560 '</div>' + 561 '</div>'; 562 563 this.$lightbox = $(template); 564 if (this.$links.length == 1) { 565 this.$lightbox.addClass('fluxus-lightbox--single'); 566 } 567 this.$content = this.$lightbox.find('.fluxus-lightbox__content'); 568 this.$media = this.$lightbox.find('.fluxus-lightbox__media'); 569 this.$tip = this.$lightbox.find('.fluxus-lightbox__tip'); 570 this.$tipIcon = this.$lightbox.find('.fluxus-lightbox__tip-icon'); 571 572 this.$lightbox 573 .find('.fluxus-lightbox__top-right, .fluxus-lightbox__bottom-right') 574 .on('mouseenter mouseleave', this.onMouseHoverControls.bind(this)); 575 576 /** 577 * Assign DOM events 578 */ 579 this.$next = this.$lightbox 580 .on('click', '.fluxus-lightbox__btn--next', function (e) { 581 that.next.call(that); 582 e && e.preventDefault(); 583 }) 584 .find('.fluxus-lightbox__btn--next'); 585 586 this.$previous = this.$lightbox 587 .on('click', '.fluxus-lightbox__btn--previous', function (e) { 588 that.previous.call(that); 589 e && e.preventDefault(); 590 }) 591 .find('.fluxus-lightbox__btn--previous'); 592 593 this.$close = this.$lightbox 594 .on('click', '.fluxus-lightbox__btn--close', function (e) { 595 that.close.call(that); 596 e && e.preventDefault(); 597 }) 598 .find('.fluxus-lightbox__btn--close'); 599 600 this.$resize = this.$lightbox 601 .on('click', '.fluxus-lightbox__btn--resize', function (event) { 602 event && event.preventDefault(); 603 that.toggleFullWidthMode(!that._isModeFullWidth); 604 }) 605 .find('.fluxus-lightbox__btn--resize'); 606 this.$lightbox.toggleClass( 607 'fluxus-lightbox--full-width', 608 this.options.mode === 'fill' 609 ); 610 611 if (this.$links.length > 1) { 612 this.$content.on('click', this.onAreaClick.bind(this)); 613 } 614 615 $('body').append(this.$lightbox); 616 }, 617 618 toggleFullWidthMode: function (on) { 619 this._isModeFullWidth = on; 620 // resizeImage calculates width that depends on .fluxus-lightbox--full-width 621 // therefore order of execution is important. 622 this.$lightbox.toggleClass('fluxus-lightbox--full-width', on); 623 this.resizeImage(); 624 if (on) { 625 $window 626 .off('mousemove.pan.fluxus-lightbox') 627 .on( 628 'mousemove.pan.fluxus-lightbox', 629 this.panImageOnMouseMove.bind(this) 630 ); 631 } else { 632 $window.off('mousemove.pan.fluxus-lightbox'); 633 } 634 }, 635 636 onAreaClick: function (event) { 637 if (!TOUCH_DEVICE) { 638 var x = event.pageX - this._scrollPosition.left, 639 y = event.pageY - this._scrollPosition.top, 640 middlePoint = this._windowSize.width / 2; 641 642 if (x > middlePoint) { 643 this.next(); 644 } else { 645 this.previous(); 646 } 647 } 648 }, 649 650 panImageOnMouseMove: function (event) { 651 var previousImageStyle = this._imageStyle; 652
653 if (previousImageStyle.fillAxis == 'x') { 654 var offscreenWidth = 655 previousImageStyle.width * previousImageStyle.scale - 656 this._windowSize.width, 657 panRatio = offscreenWidth / this._windowSize.width; 658 659 previousImageStyle.x = 660 -(event.pageX - this.$media.offset().left) * panRatio; 661 } else { 662 var offscreenHeight = 663 previousImageStyle.height * previousImageStyle.scale - 664 this._windowSize.height, 665 panRatio = offscreenHeight / this._windowSize.height; 666 667 previousImageStyle.y = 668 -(event.pageY - this.$media.offset().top) * panRatio; 669 } 670 671 this.setImageStyle(previousImageStyle); 672 }, 673 674 onMouseHoverControls: function (event) { 675 this.$lightbox.toggleClass( 676 'fluxus-lightbox--over-controls', 677 event.type == 'mouseenter' 678 ); 679 }, 680 681 storeWindowSize: function () { 682 this._windowSize = { 683 width: $window.width(), 684 height: $window.height() 685 }; 686 }, 687 688 setupGestures: function () { 689 this.$lightbox.addClass('fluxus-lightbox--touch'); 690 var enableIfNotPinchingAndNotZoomedIn = function () { 691 return ( 692 !this._isPinching && 693 (!this._userTransform || this._userTransform.scale == 1) 694 ); 695 }.bind(this); 696 697 var horizontalPan = new Hammer.Pan({ 698 event: 'horizontalPan', 699 direction: Hammer.DIRECTION_HORIZONTAL, 700 enable: enableIfNotPinchingAndNotZoomedIn 701 }); 702 var verticalPan = new Hammer.Pan({ 703 event: 'verticalPan', 704 direction: Hammer.DIRECTION_VERTICAL, 705 enable: enableIfNotPinchingAndNotZoomedIn 706 }); 707 var pan = new Hammer.Pan({ 708 enable: function () { 709 return ( 710 !this._isPinching && 711 this._userTransform && 712 this._userTransform.scale > 1 713 ); 714 }.bind(this) 715 }); 716 var pinch = new Hammer.Pinch(); 717 var singleTap = new Hammer.Tap({ event: 'singleTap' }); 718 var doubleTap = new Hammer.Tap({ event: 'doubleTap', taps: 2 }); 719 720 var manager = new Hammer.Manager(this.$lightbox[0]); 721 manager.add([ 722 horizontalPan, 723 verticalPan, 724 pan, 725 pinch, 726 doubleTap, 727 singleTap 728 ]); 729 730 // See http://hammerjs.github.io/require-failure/ 731 doubleTap.recognizeWith(singleTap); 732 singleTap.requireFailure(doubleTap); 733 734 manager.on('horizontalPanstart', this.onHorizontalPanStart.bind(this)); 735 manager.on('horizontalPanend', this.onHorizontalPanEnd.bind(this)); 736 manager.on('horizontalPan', this.onHorizontalPan.bind(this)); 737 738 manager.on('verticalPanstart', this.onVerticalPanStart.bind(this)); 739 manager.on('verticalPanend', this.onVerticalPanEnd.bind(this)); 740 manager.on('verticalPan', this.onVerticalPan.bind(this)); 741 742 manager.on('pan', this.onPan.bind(this)); 743 manager.on('panend', this.onPanEnd.bind(this)); 744 745 manager.on('pinchstart', this.onPinchStart.bind(this)); 746 manager.on('pinch pinchend pinchstart', this.onPinching.bind(this)); 747 manager.on('pinchend', this.onPinchEnd.bind(this)); 748 749 manager.on('singleTap', this.onSingleTap.bind(this)); 750 manager.on('doubleTap', this.onDoubleTap.bind(this)); 751 }, 752 753 onVerticalPanStart: function () { 754 this.$content.css('transition', 'none'); 755 }, 756 757 onVerticalPanEnd: function (event) { 758 this.$lightbox.addClass('fluxus-lightbox--bounce-back-transition'); 759 this.$lightbox.css({ transition: '' }); 760 this.$lightbox.width(); 761 762 var MIN_DISTANCE = 200; 763 764 // Come back to place. 765 if (event.distance < MIN_DISTANCE) { 766 onTransitionEnd( 767 this.$lightbox, 768 function () { 769 this.$lightbox.removeClass( 770 'fluxus-lightbox--bounce-back-transition' 771 ); 772 }.bind(this) 773 ); 774 this.$lightbox.scss({ y: 0, opacity: 1 }); 775 return; 776 } 777 778 var direction = event.deltaY > 0 ? 1 : -1; 779 var endY = screen.height * direction; 780 onTransitionEnd( 781 this.$lightbox, 782 function () { 783 this.close({ animation: false }); 784 this.$lightbox 785 .scss({ x: '', y: '' }) 786 .removeClass('fluxus-lightbox--bounce-back-transition'); 787 }.bind(this), 788 { propertyName: 'transform' } 789 ); 790 this.$lightbox.scss({ y: endY }); 791 }, 792 793 onVerticalPan: function (event) { 794 this.$lightbox.scss({ 795 y: event.deltaY, 796 transition: 'none' 797 }); 798 }, 799 800 animateUserTransform: function (destination) { 801 var delta = { 802 x: destination.x - this._userTransform.x, 803 y: destination.y - this._userTransform.y, 804 scale: destination.scale - this._userTransform.scale 805 }; 806 807 if (!delta.x && !delta.y && !delta.scale) { 808 return; 809 } 810 811 var initialTransform = $.extend({}, this._userTransform); 812 var time = { 813 start: performance.now(), 814 total: 300 815 }; 816 var animate = function (now) {
817 time.elapsed = now - time.start; 818 var progress = time.elapsed / time.total; 819 progress = progress > 1 ? 1 : progress; 820 821 this._userTransform.scale = 822 initialTransform.scale + progress * delta.scale; 823 this._userTransform.x = initialTransform.x + progress * delta.x; 824 this._userTransform.y = initialTransform.y + progress * delta.y; 825 this.$media.scss(this._userTransform); 826 827 progress < 1 && requestAnimationFrame(animate); 828 }.bind(this); 829 830 requestAnimationFrame(animate); 831 }, 832 833 startTransformConstraintAnimation: function () { 834 // Scale 835 var MAX_SCALE = 3; 836 var MIN_SCALE = 1; 837 var normalizedTransform = $.extend({}, this._userTransform); 838 839 if (normalizedTransform.scale > MAX_SCALE) { 840 normalizedTransform.scale = MAX_SCALE; 841 } else if (normalizedTransform.scale < MIN_SCALE) { 842 normalizedTransform.scale = MIN_SCALE; 843 } 844 845 // X 846 var mediaWidth = this._imageStyle.width * this._imageStyle.scale; 847 // When scale is 1 we use fit mode with margin, thus mediaWidth 848 // when scale is zoomed in we use whole viewport what is this.$media.width() 849 var viewportWidth = 850 normalizedTransform.scale == 1 ? mediaWidth : this.$media.width(); 851 var scaledMediaWidth = mediaWidth * normalizedTransform.scale; 852 var boundingX = Math.abs(scaledMediaWidth - viewportWidth) / 2; 853 854 if (normalizedTransform.x < boundingX * -1) { 855 normalizedTransform.x = boundingX * -1; 856 } else if (normalizedTransform.x > boundingX) { 857 normalizedTransform.x = boundingX; 858 } 859 860 // Y 861 var mediaHeight = this._imageStyle.height * this._imageStyle.scale; 862 // When scale is 1 we use fit mode with margin, thus mediaHeight 863 // when scale is zoomed in we use whole viewport what is this.$media.height() 864 var viewportHeight = 865 normalizedTransform.scale == 1 ? mediaHeight : this.$media.height(); 866 var scaledMediaHeight = mediaHeight * normalizedTransform.scale; 867 var boundingY = Math.abs(scaledMediaHeight - viewportHeight) / 2; 868 869 if (normalizedTransform.y < boundingY * -1) { 870 normalizedTransform.y = boundingY * -1; 871 } else if (normalizedTransform.y > boundingY) { 872 normalizedTransform.y = boundingY; 873 } 874 875 this.animateUserTransform(normalizedTransform); 876 }, 877 878 onHorizontalPanStart: function (event) { 879 this.$content.css('transition', 'none'); 880 }, 881 882 onHorizontalPanEnd: function (event) { 883 var $content = this.$content; 884 var MIN_PAN_DISTANCE = 100; 885 var limitAnimationDuration = function (duration) { 886 if (duration < 100) { 887 return 100; 888 } 889 890 if (duration > 600) { 891 return 600; 892 } 893 894 return duration; 895 }; 896 var pxPerMillisecond = Math.abs(event.overallVelocityX); 897 var LINEAR_TRANSITION = 'transform %dms linear'; 898 var EASE_OUT_TRANSITION = 899 'transform %dms cubic-bezier(.165, .84, .44, 1)'; 900 901 // Come back to place. 902 if (event.distance < MIN_PAN_DISTANCE) { 903 var comebackDuration = Math.abs(event.distance) / pxPerMillisecond; 904 comebackDuration = limitAnimationDuration(comebackDuration); 905 $content.css({ 906 transition: EASE_OUT_TRANSITION.replace('%d', comebackDuration) 907 }); 908 $content.scss({ x: 0 }); 909 return; 910 } 911 912 var distanceToGo = $content.width() - Math.abs(event.distance); 913 var swipeoutDuration = distanceToGo / pxPerMillisecond; 914 swipeoutDuration = limitAnimationDuration(swipeoutDuration); 915 $content.css( 916 'transition', 917 LINEAR_TRANSITION.replace('%d', swipeoutDuration) 918 ); 919 920 var panDirection = event.deltaX > 0 ? 'previous' : 'next'; 921 var panDirectionModifier = panDirection == 'previous' ? 1 : -1; 922 923 $content.scss({ x: toCssPercent(100 * panDirectionModifier) }); 924 925 setTimeout( 926 function () { 927 this[panDirection]({ delay: 0 }).then(function () { 928 $content.css('transition', 'none'); 929 $content 930 .scss({ x: toCssPercent(-100 * panDirectionModifier) }) 931 .width(); 932 var duration = $content.width() / pxPerMillisecond; 933 duration = limitAnimationDuration(duration); 934 $content.css({ 935 transition: EASE_OUT_TRANSITION.replace('%d', duration) 936 }); 937 $content.scss({ x: 0 }); 938 }); 939 }.bind(this), 940 swipeoutDuration 941 ); 942 }, 943 944 onHorizontalPan: function (event) { 945 this.$content.scss({ x: event.deltaX }); 946 }, 947 948 onPan: function (event) { 949 var currentTransform = $.extend({}, this._userTransform, { 950 x: this._userTransform.x + event.deltaX, 951 y: this._userTransform.y + event.deltaY 952 }); 953 this.$media.scss(currentTransform); 954 }, 955 956 onPanEnd: function (event) { 957 this._userTransform.x = this._userTransform.x + event.deltaX; 958 this._userTransform.y = this._userTransform.y + event.deltaY; 959 this.startTransformConstraintAnimation(); 960 }, 961 962 onSingleTap: function (event) { 963 this.$lightbox.addClass('fluxus-lightbox--after-tap'); 964 if (this.isIdle) { 965 this.triggerActivityEvent(); 966 } else { 967 this.idleOn(); 968 } 969 setTimeout( 970 function () { 971 this.$lightbox.removeClass('fluxus-lightbox--after-tap'); 972 }.bind(this), 973 1000 974 ); 975 }, 976 977 onDoubleTap: function () { 978 var zoomIn = this._userTransform.scale == 1; 979 var transform = $.extend({}, this._userTransform, { 980 scale: this._userTransform.scale == 1 ? 2 : 1 981 }); 982 if (!zoomIn) { 983 transform.x = 0; 984 transform.y = 0; 985 } 986 this.animateUserTransform(transform); 987 }, 988 989 onPinchStart: function (event) { 990 this._isPinching = true; 991 }, 992 993 onPinchEnd: function (event) { 994 this._userTransform.scale = this._userTransform.scale * event.scale; 995 this.startTransformConstraintAnimation(); 996 setTimeout( 997 function () { 998 this._isPinching = false;
999 }.bind(this), 1000 300 1001 ); 1002 }, 1003 1004 onPinching: function (event) { 1005 event.preventDefault(); 1006 var currentTransform = $.extend({}, this._userTransform, { 1007 scale: this._userTransform.scale * event.scale 1008 }); 1009 this.$media.scss(currentTransform); 1010 } 1011 }; 1012 1013 $.fn.fluxusLightbox = function (options) { 1014 return this.data('fluxus-lightbox', new $.FluxusLightbox(this, options)); 1015 }; 1016 1017 /** 1018 * Utils 1019 */ 1020 var cssFromSugar = function (sugar) { 1021 sugar = $.extend({}, sugar); 1022 1023 var transform = [], 1024 numberWithotSuffix = /^\-?\d+(\.\d+)?$/; 1025 1026 if (sugar.x == '' && sugar.y == '') { 1027 // Explicitly remove transform. 1028 } else if (sugar.x !== void 0 || sugar.y !== void 0) { 1029 sugar.x = sugar.x ? sugar.x : 0; 1030 sugar.x = String(sugar.x).match(numberWithotSuffix) 1031 ? sugar.x + 'px' 1032 : sugar.x; 1033 sugar.y = sugar.y ? sugar.y : 0; 1034 sugar.y = String(sugar.y).match(numberWithotSuffix) 1035 ? sugar.y + 'px' 1036 : sugar.y; 1037 1038 transform.push('translate3d(' + sugar.x + ', ' + sugar.y + ', 0)'); 1039 } else { 1040 transform.push('translate3d(0, 0, 0)'); 1041 } 1042 1043 if (sugar.scale !== void 0) { 1044 transform.push('scale(' + sugar.scale + ')'); 1045 } 1046 1047 sugar['transform'] = transform.join(' '); 1048 sugar['-webkit-transform'] = sugar['transform']; 1049 1050 // Remove blacklisted props. 1051 delete sugar.x; 1052 delete sugar.y; 1053 delete sugar.scale; 1054 delete sugar.fillAxis; 1055 return sugar; 1056 }; 1057 1058 var createTimeoutPromise = function (delay) { 1059 var dfd = new $.Deferred(); 1060 setTimeout(dfd.resolve.bind(this), delay); 1061 return dfd; 1062 }; 1063 1064 var toCssPercent = function (numer) { 1065 return String(numer) + '%'; 1066 }; 1067 1068 /** 1069 * Utility for tracking transitionend event. 1070 * once: Boolean - will unbind after first event. 1071 * propertyName: String - will trigger only on specific properties. 1072 */ 1073 var onTransitionEnd = (function () { 1074 var index = 0; 1075 var uuid = function () { 1076 return 'lightbox-event-' + index++; 1077 }; 1078 1079 return function ($el, callback, options) { 1080 options = $.extend( 1081 {}, 1082 { 1083 once: true, 1084 propertyName: undefined, 1085 captureBubbledEvents: false 1086 }, 1087 options 1088 ); 1089 1090 var namespace = uuid(); 1091 var events = 1092 'transitionend.' + namespace + ' webkitTransitionEnd.' + namespace; 1093 1094 $el.on(events, function (event) { 1095 var matchesPropertyName = 1096 options.propertyName && 1097 event.originalEvent.propertyName == options.propertyName; 1098 1099 var matchesTarget = 1100 !options.captureBubbledEvents && event.originalEvent.target == $el[0]; 1101 1102 if (matchesPropertyName && matchesTarget) { 1103 callback.call(this, event.originalEvent.srcElement); 1104 options && options.once && $el.off(events); 1105 } 1106 }); 1107 }; 1108 })(); 1109 1110 $.fn.scss = function (sugar) { 1111 return this.css(cssFromSugar(sugar)); 1112 }; 1113})(jQuery, _.debounce);
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.