1/** 2 * ModuloBox PACKAGED v1.5.0 3 * Touch & responsive multimedia Lightbox 4 * 5 * @author Themeone [https://theme-one.com/] 6 * Copyright © 2016 All Rights Reserved. 7 */ 8 9/* global navigator, location, window, document, screen, history, setTimeout, clearTimeout, requestAnimationFrame, cancelAnimationFrame, XMLHttpRequest, Image */ 10/* global define, module, require, jQuery, MediaElementPlayer */ 11 12/** 13 * requestAnimationFrame polyfill 14 * Modified version of Paul Irish (https://gist.github.com/paulirish/1579671) 15 * v1.0.0 16 */ 17( function() { 18 19 var win = window, 20 lastTime = 0; 21 22 // get rAF, prefixed, if present 23 win.requestAnimationFrame = win.requestAnimationFrame || win.webkitRequestAnimationFrame; 24 25 // fallback to setTimeout 26 if ( !win.requestAnimationFrame ) { 27 28 win.requestAnimationFrame = function( callback ) { 29 30 var currTime = new Date().getTime(), 31 timeToCall = Math.max( 0, 16 - ( currTime - lastTime ) ), 32 id = setTimeout( callback, timeToCall ); 33 34 lastTime = currTime + timeToCall; 35 36 return id; 37 38 }; 39 40 } 41 42 // fallback to setTimeout 43 if ( !win.cancelAnimationFrame ) { 44 45 win.cancelAnimationFrame = function( id ) { 46 clearTimeout( id ); 47 }; 48 49 } 50 51}()); 52 53/** 54 * Themeone Utils 55 * Utilities 56 * v1.0.0 57 */ 58( function( root, factory ) { 59 60 if ( typeof define === 'function' && define.amd ) { 61 62 // AMD 63 define( 64 'themeone-utils/utils', 65 factory 66 ); 67 68 } else if ( typeof module === 'object' && module.exports ) { 69 70 // Node, CommonJS-like 71 module.exports = factory(); 72 73 } else { 74 75 // Browser globals (root is window) 76 root.ThemeoneUtils = factory(); 77 78 } 79 80}( this, function() { 81 82 "use strict"; 83 84 var utils = {}; 85 var Console = window.console; 86 87 /** 88 * Output console error 89 * @param {string} message 90 */ 91 utils.error = function( message ) { 92 93 if ( typeof Console !== 'undefined' ) { 94 95 Console.error( message ); 96 97 } 98 99 }; 100 101 /** 102 * Extend an Object 103 * @param {Object} options 104 * @param {Object} defaults 105 * @return {Object} defaults 106 */ 107 utils.extend = function( options, defaults ) { 108 109 if ( options ) { 110 111 if ( typeof options !== 'object' ) { 112 113 this.error( 'Custom options must be an object' ); 114 115 } else { 116 117 for ( var prop in defaults ) { 118 119 if ( defaults.hasOwnProperty( prop ) && options.hasOwnProperty( prop ) ) { 120 defaults[prop] = options[prop]; 121 } 122 123 } 124 125 } 126 127 } 128 129 return defaults; 130 131 }; 132 133 /** 134 * Find style property (with vendor prefix) 135 * @param {string} prop 136 * @return {string} prefixedProp 137 */ 138 utils.prop = function( prop ) { 139 140 var el = this.createEl(), 141 prefixes = ['', 'Webkit', 'Moz', 'ms', 'O']; 142 143 for ( var p = 0, pl = prefixes.length; p < pl; p++ ) { 144 145 var prefixedProp = prefixes[p] ? prefixes[p] + prop.charAt( 0 ).toUpperCase() + prop.slice( 1 ) : prop; 146 147 if ( el.style[prefixedProp] !== undefined ) { 148 return prefixedProp; 149 } 150 151 } 152 153 return ''; 154 155 }; 156 157 /** 158 * Clone object properties 159 * @param {Object} obj 160 * @return {Object} copy 161 */ 162 utils.cloneObject = function( obj ) { 163 164 var copy = {}; 165 166 for ( var attr in obj ) { 167 168 if ( obj.hasOwnProperty( attr ) ) { 169 copy[attr] = obj[attr]; 170 } 171 172 } 173 174 return copy; 175 176 }; 177 178 /** 179 * Create DOM element 180 * @param {string} tag 181 * @param {string} classes 182 * @return {Object} el 183 */ 184 utils.createEl = function( tag, classes ) { 185 186 var el = document.createElement( tag || 'div' ); 187 188 if ( classes ) { 189 el.className = classes; 190 } 191 192 return el; 193 194 }; 195 196 /** 197 * Camel case string 198 * @param {string} string 199 * @return {string} string 200 */ 201 utils.camelize = function( string ) { 202 203 return string.replace( /-([a-z])/g, function( g ) { 204 return g[1].toUpperCase(); 205 }); 206 207 }; 208 209 /** 210 * Add/remove event listeners 211 * @param {Object} _this 212 * @param {Object} el 213 * @param {Array} event 214 * @param {string} fn 215 * @param {boolean} isBind 216 */ 217 utils.handleEvents = function( _this, el, event, fn, isBind ) { 218 219 if ( typeof this.event_handlers !== 'object' ) { 220 this.event_handlers = {}; 221 } 222 223 // register handlers for later (to remove) 224 if ( !this.event_handlers[fn] ) { 225 this.event_handlers[fn] = _this[fn].bind( _this ); 226 } 227 228 // set bind method 229 isBind = isBind === undefined ? true : !!isBind; 230 var bindMethod = isBind ? 'addEventListener' : 'removeEventListener'; 231 232 // loop through each event 233 event.forEach( function( ev ) { 234 el[bindMethod]( ev, this.event_handlers[fn], false ); 235 }.bind( this ) ); 236 237 }; 238 239 /** 240 * Emits events via EvEmitter and jQuery events 241 * @param {Object} _this 242 * @param {string} namespace - namespace of event 243 * @param {string} type - name of event 244 * @param {Event} event - original event 245 * @param {Array} args - extra arguments 246 */ 247 utils.dispatchEvent = function( _this, namespace, type, event, args ) { 248 249 // add Namespace for the event 250 type += namespace ? '.' + namespace : ''; 251 // add original event to arguments 252 var emitArgs = event ? [event].concat( args ) : [args]; 253 254 // trigger vanilla JS event 255 _this.emitEvent( type, emitArgs ); 256 257 }; 258 259 /** 260 * Function for applying a debounce effect to a function call. 261 * @function 262 * @param {Function} func - Function to be called at end of timeout. 263 * @param {number} delay - Time in ms to delay the call of `func`. 264 * @returns function 265 */ 266 utils.throttle = function( func, delay ) { 267 268 var timestamp = null, 269 limit = delay; 270 271 return function() { 272 273 var self = this, 274 args = arguments, 275 now = Date.now(); 276 277 if ( !timestamp || now - timestamp >= limit ) { 278 279 timestamp = now; 280 func.apply( self, args ); 281 282 } 283 284 }; 285 286 }; 287 288 /** 289 * Modulo calculation 290 * @param {number} length 291 * @param {number} index 292 * @return {number} 293 */ 294 utils.modulo = function( length, index ) { 295 296 return ( length + ( index % length ) ) % length; 297 298 }; 299 300 /** 301 * Regex classname from string 302 * @param {string} className 303 * @return {RegExp} 304 */ 305 utils.classReg = function( className ) { 306 307 return new RegExp( '(^|\\s+)' + className + '(\\s+|$)' ); 308 309 }; 310 311 /** 312 * Check if element has class name 313 * @param {Object} el 314 * @param {string} className 315 * @return {boolean} 316 */ 317 utils.hasClass = function( el, className ) { 318 319 return !!el.className.match( this.classReg( className ) ); 320 321 }; 322 323 /** 324 * Add a class name to an element 325 * @param {Object} el 326 * @param {string} className 327 */ 328 utils.addClass = function( el, className ) { 329 330 if ( !this.hasClass( el, className ) ) { 331 el.className += ( el.className ? ' ' : '' ) + className; 332 } 333 334 }; 335 336 /** 337 * Remove a class name to an element 338 * @param {Object} el 339 * @param {string} className 340 */ 341 utils.removeClass = function( el, className ) { 342 343 if ( this.hasClass( el, className ) ) { 344 el.className = el.className.replace( this.classReg( className ), ' ' ).replace( /\s+$/, '' ); 345 } 346 347 }; 348 349 /** 350 * Translate an element 351 * @param {Object} el 352 * @param {number} x 353 * @param {number} y 354 * @param {number} s 355 */ 356 utils.translate = function( el, x, y, s ) { 357 358 var scale = s ? ' scale(' + s + ',' + s + ')' : ''; 359 360 el.style[this.browser.trans] = ( this.browser.gpu ) ? 361 'translate3d(' + ( x || 0 ) + 'px, ' + ( y || 0 ) + 'px, 0)' + scale : 362 'translate(' + ( x || 0 ) + 'px, ' + ( y || 0 ) + 'px)' + scale; 363 364 }; 365 366 /* 367 * Browser features detection 368 */ 369 utils.browser = { 370 trans : utils.prop( 'transform' ), 371 gpu : utils.prop( 'perspective' ) ? true : false 372 }; 373 374 return utils; 375 376})); 377 378/** 379 * EvEmitter (Emit event) 380 * Modified version of David Desandro (https://github.com/desandro/EventEmitter) 381 * v1.0.0 382 */ 383( function( root, factory ) { 384 385 if ( typeof define === 'function' && define.amd ) { 386 387 // AMD 388 define( 389 'themeone-event/event', 390 factory 391 ); 392 393 } else if ( typeof module === 'object' && module.exports ) { 394 395 // Node, CommonJS-like 396 module.exports = factory(); 397 398 } else { 399 400 // Browser globals (root is window) 401 root.ThemeoneEvent = factory(); 402 403 } 404 405}( typeof window !== 'undefined' ? window : this, function() { 406 407 "use strict"; 408 409 var EvEmitter = function() {}, 410 proto = EvEmitter.prototype; 411 412 /** 413 * Bind on event 414 * @param {string} eventName 415 * @param {Object} listener 416 * @return {Object} this 417 */ 418 proto.on = function( eventName, listener ) { 419 420 if ( !eventName || !listener ) { 421 return null; 422 } 423 424 // set events hash 425 var events = this._events = this._events || {}; 426 // set listeners array 427 var listeners = events[eventName] = events[eventName] || []; 428 429 // only add once 430 if ( listeners.indexOf( listener ) === -1 ) { 431 listeners.push( listener ); 432 } 433 434 return this; 435 436 }; 437 438 /** 439 * Unbind event event 440 * @param {string} eventName 441 * @param {Object} listener 442 * @return {Object} this 443 */ 444 proto.off = function( eventName, listener ) { 445 446 var listeners = this._events && this._events[eventName]; 447 448 if ( !listeners || !listeners.length ) { 449 return null; 450 } 451 452 var index = listeners.indexOf( listener ); 453 454 if ( index !== -1 ) { 455 listeners.splice( index, 1 ); 456 } 457 458 return this; 459 460 }; 461 462 /** 463 * Emit an event 464 * @param {string} eventName 465 * @param {Object} args 466 * @return {Object} this 467 */ 468 proto.emitEvent = function( eventName, args ) { 469 470 var listeners = this._events && this._events[eventName]; 471 if ( !listeners || !listeners.length ) { 472 return null; 473 } 474 475 var i = 0, 476 listener = listeners[i];
477 args = args || []; 478 479 // once stuff 480 var onceListeners = this._onceEvents && this._onceEvents[eventName]; 481 482 while ( listener ) { 483 484 var isOnce = onceListeners && onceListeners[listener]; 485 486 if ( isOnce ) { 487 488 // remove before trigger to prevent recursion 489 this.off( eventName, listener ); 490 // unset once flag 491 delete onceListeners[listener]; 492 493 } 494 495 // trigger listener 496 listener.apply( this, args ); 497 498 // get next listener 499 i += isOnce ? 0 : 1; 500 listener = listeners[i]; 501 502 } 503 504 return this; 505 506 }; 507 508 return EvEmitter; 509 510})); 511 512/** 513 * Animate 514 * RAF animations 515 * v1.0.0 516 */ 517( function( root, factory ) { 518 519 if ( typeof define === 'function' && define.amd ) { 520 521 // AMD 522 define( 523 'themeone-animate/animate', 524 ['themeone-utils/utils', 525 'themeone-event/event'], 526 factory 527 ); 528 529 } else if ( typeof module === 'object' && module.exports ) { 530 531 // Node, CommonJS-like 532 module.exports = factory( 533 require( 'themeone-utils' ), 534 require( 'themeone-event' ) 535 ); 536 537 } else { 538 539 // Browser globals (root is window) 540 root.ThemeoneAnimate = factory( 541 root.ThemeoneUtils, 542 root.ThemeoneEvent 543 ); 544 545 } 546 547}( this, function( utils, EvEmitter ) { 548 549 'use strict'; 550 551 /** 552 * Animate 553 * @param {Object} element 554 * @param {Object} positions 555 * @param {Object} friction 556 * @param {Object} attraction 557 */ 558 var Animate = function( element, positions, friction, attraction ) { 559 560 this.element = element; 561 this.defaults = positions; 562 this.forces = { 563 friction : friction || 0.28, 564 attraction : attraction || 0.028 565 }; 566 567 this.resetAnimate(); 568 569 }; 570 571 var proto = Animate.prototype = Object.create( EvEmitter.prototype ); 572 573 /** 574 * Update animation on drag 575 * @param {Object} obj 576 */ 577 proto.updateDrag = function( obj ) { 578 579 this.move = true; 580 this.drag = obj; 581 582 }; 583 584 /** 585 * Release drag 586 */ 587 proto.releaseDrag = function() { 588 589 this.move = false; 590 591 }; 592 593 /** 594 * Animate to a specific position 595 * @param {Object} obj 596 */ 597 proto.animateTo = function( obj ) { 598 599 this.attraction = obj; 600 601 }; 602 603 /** 604 * Start render animation 605 */ 606 proto.startAnimate = function() { 607 608 this.move = true; 609 this.settle = false; 610 this.restingFrames = 0; 611 612 if ( !this.RAF ) { 613 this.animate(); 614 } 615 616 }; 617 618 /** 619 * Stop animation 620 */ 621 proto.stopAnimate = function() { 622 623 this.move = false; 624 this.restingFrames = 0; 625 626 if ( this.RAF ) { 627 cancelAnimationFrame( this.RAF ); 628 this.RAF = false; 629 } 630 631 this.start = utils.cloneObject( this.position ); 632 this.velocity = { 633 x : 0, 634 y : 0, 635 s : 0 636 }; 637 638 }; 639 640 /** 641 * Reset animation 642 */ 643 proto.resetAnimate = function() { 644 645 this.stopAnimate(); 646 647 this.settle = true; 648 this.drag = utils.cloneObject( this.defaults ); 649 this.start = utils.cloneObject( this.defaults ); 650 this.resting = utils.cloneObject( this.defaults ); 651 this.position = utils.cloneObject( this.defaults ); 652 this.attraction = utils.cloneObject( this.defaults ); 653 654 }; 655 656 /** 657 * Handle animation rendering 658 */ 659 proto.animate = function() { 660 661 // animation loop function 662 var loop = ( function() { 663 664 if ( typeof this.position !== 'undefined' ) { 665 666 // get clone previous values 667 var previous = utils.cloneObject( this.position ); 668 669 // set main forces 670 this.applyDragForce(); 671 this.applyAttractionForce(); 672 673 // dispatch render event (before physic otherwise calculation will be wrong) 674 utils.dispatchEvent( this, 'toanimate', 'render', this ); 675 676 // apply physics 677 this.integratePhysics(); 678 this.getRestingPosition(); 679 680 // render positions 681 this.render( 100 ); 682 683 // loop 684 this.RAF = requestAnimationFrame( loop ); 685 686 // cancel RAF if no animations 687 this.checkSettle( previous ); 688 689 } 690 691 }).bind( this ); 692 693 // start animation loop 694 this.RAF = requestAnimationFrame( loop ); 695 696 }; 697 698 /** 699 * Simulation physic velocity 700 */ 701 proto.integratePhysics = function() { 702 703 for ( var k in this.position ) { 704 705 if ( typeof this.position[k] !== 'undefined' ) { 706 707 this.position[k] += this.velocity[k]; 708 this.position[k] = ( k === 's' ) ? Math.max( 0.1, this.position[k] ) : this.position[k]; 709 this.velocity[k] *= this.getFrictionFactor(); 710 711 } 712 713 } 714 715 }; 716 717 /** 718 * Simulation physic friction 719 */ 720 proto.applyDragForce = function() { 721 722 if ( this.move ) { 723 724 for ( var k in this.drag ) { 725 726 if ( typeof this.drag[k] !== 'undefined' ) { 727 728 var dragVelocity = this.drag[k] - this.position[k]; 729 var dragForce = dragVelocity - this.velocity[k]; 730 this.applyForce( k, dragForce ); 731 732 } 733 734 } 735 736 } 737 738 }; 739 740 /** 741 * Simulation physic attraction 742 */ 743 proto.applyAttractionForce = function() { 744 745 if ( !this.move ) { 746 747 for ( var k in this.attraction ) { 748 749 if ( typeof this.attraction[k] !== 'undefined' ) { 750 751 var distance = this.attraction[k] - this.position[k]; 752 var force = distance * this.forces.attraction; 753 this.applyForce( k, force ); 754 755 } 756 757 } 758 759 } 760 761 }; 762 763 /** 764 * Calculate estimated resting position from physic 765 */ 766 proto.getRestingPosition = function() { 767 768 for ( var k in this.position ) { 769 770 if ( typeof this.position[k] !== 'undefined' ) { 771 this.resting[k] = this.position[k] + this.velocity[k] / ( 1 - this.getFrictionFactor() ); 772 } 773 774 } 775 776 }; 777 778 /** 779 * Apply an attraction force 780 * @param {string} direction 781 * @param {number} force 782 */ 783 proto.applyForce = function( direction, force ) { 784 785 this.velocity[direction] += force; 786 787 }; 788 789 /** 790 * Apply a friction factor 791 * @return {number} 792 */ 793 proto.getFrictionFactor = function() { 794 795 return 1 - this.forces.friction; 796 797 }; 798 799 /** 800 * Round value to correctly calculate if animate is settle or not 801 * @param {number} values 802 * @param {number} round 803 */ 804 proto.roundValues = function( values, round ) { 805 806 for ( var k in values ) { 807 808 if ( typeof values[k] !== 'undefined' ) { 809 810 round = k === 's' ? round * 100 : round; 811 values[k] = Math.round( values[k] * round ) / round; 812 813 } 814 815 } 816 817 }; 818 819 /** 820 * Check if the current animated object is settled 821 * @param {Object} previous 822 */ 823 proto.checkSettle = function( previous ) { 824 825 // keep track of frames where x hasn't moved 826 if ( !this.move ) { 827 828 var count = 0; 829 830 for ( var k in this.position ) { 831 832 if ( typeof this.position[k] !== 'undefined' ) { 833 834 var round = k === 's' ? 10000 : 100; 835 836 if ( Math.round( this.position[k] * round ) === Math.round( previous[k] * round ) ) { 837 838 count++; 839 if ( count === Object.keys( this.position ).length ) { 840 this.restingFrames++; 841 } 842 843 } 844 845 } 846 847 } 848 849 } 850 851 // stop RAF animation if position didn't change during 3 frames (60fps) 852 if ( this.restingFrames > 2 ) { 853 854 this.stopAnimate(); 855 this.render( this.position.s > 1 ? 10 : 1 ); 856 this.settle = true; 857 858 // dispatch settle only if moved 859 if ( JSON.stringify( this.start ) !== JSON.stringify( this.position ) ) { 860 utils.dispatchEvent( this, 'toanimate', 'settle', this ); 861 } 862 863 } 864 865 }; 866 867 /** 868 * Render animation 869 * @param {number} round 870 */ 871 proto.render = function( round ) { 872 873 // round new position values 874 this.roundValues( this.position, round ); 875 876 // translate 877 utils.translate( 878 this.element, 879 this.position.x, 880 this.position.y, 881 this.position.s 882 ); 883 884 }; 885 886 return Animate; 887 888})); 889 890/** 891 * ModuloBox 892 * Core Plugin 893 * v1.5.0 894 */ 895( function( root, factory ) { 896 897 if ( typeof define === 'function' && define.amd ) { 898 899 // AMD 900 define( 901 ['themeone-utils/utils', 902 'themeone-event/event', 903 'themeone-animate/animate'], 904 factory 905 ); 906 907 } else if ( typeof exports === 'object' && module.exports ) { 908 909 // Node, CommonJS-like 910 module.exports = factory( 911 require( 'themeone-utils' ), 912 require( 'themeone-event' ), 913 require( 'themeone-animate' ) 914 ); 915 916 } else { 917 918 // Browser globals (root is window) 919 root.ModuloBox = factory( 920 root.ThemeoneUtils, 921 root.ThemeoneEvent, 922 root.ThemeoneAnimate 923 ); 924 925 } 926 927}( this, function( utils, EvEmitter, Animate ) { 928 929 'use strict'; 930 931 // Modulobox version 932 var version = '1.5.0'; 933 934 // Globally unique identifiers 935 var GUID = 0; 936 937 // Internal store of all plugin intances 938 var instances = {}; 939
940 // Internal cache 941 var expando = 'mobx' + ( version + Math.random() ).replace( /\D/g, '' ); 942 var cache = { uid : 0 }; 943 944 // Default options 945 var defaults = { 946 // Setup 947 mediaSelector : '.mobx', // Media class selector used to fetch media in document 948 // Behaviour 949 threshold : 5, // Dragging doesn't start until 5px moved (not taken into account when pinched with 2 fingers) 950 attraction : { // Attracts the position of the slider to the selected cell. Higher value makes the slider move faster. Lower value makes it move slower. 951 slider : 0.055, // From 0 to 1 952 slide : 0.018, // From 0 to 1 953 thumbs : 0.016 // From 0 to 1 954 }, 955 friction : { // Friction slows the movement of slider. Higher value makes the slider feel stickier & less bouncy. Lower value makes the slider feel looser & more wobbly 956 slider : 0.62, // From 0 to 1 957 slide : 0.18, // From 0 to 1 958 thumbs : 0.22 // From 0 to 1 959 }, 960 rightToLeft : false, // Enable right to left layout 961 loop : 3, // From how much items infinite loop start (0: no loop, 1: always loop, 2: loop if 2 items at least, etc...) 962 preload : 1, // Number of media to preload. If 1 set, it will load the currently media opened and once loaded it'll preload the 2 closest media. value can be 1, 3 or 5 963 unload : false, // Allows to unload media which are out of the visible viewport to improve performance (EXPERIMENTAL - not work in Safari, can create issue on other browsers) 964 timeToIdle : 4000, // Hide controls when an idle state exceed 4000ms (0 - always keep controls visible) 965 history : false, // Enable/disable history in browser (deeplink) when opening/navigating/closing a gallery 966 mouseWheel : true, // Enable/disable mouseWheel (up/down/space keys) to scroll page 967 contextMenu : true, // Enable/disable contextmenu on right click (on image) 968 scrollBar : true, // Show/hide browser scrollbar 969 fadeIfSettle : false, // Show media only if the slider is settled (improves performance especially for iframe/html) 970 // User Interface 971 controls : ['close'], // 'zoom', 'play', 'fullScreen', 'download', 'share', 'close' 972 prevNext : true, // Show/hide prev/next navigation buttons 973 prevNextTouch : false, // Show/hide prev/next navigation buttons on small touch capable devices 974 counterMessage : '[index] / [total]', // Message used in the item counter. If empty, no counter will be displayed 975 caption : true, // Show/hide caption under media (globally) 976 autoCaption : false, // Generate captions from alt and/or title attributes if data-title and/or data-desc missing 977 captionSmallDevice : true, // Show/hide caption under media on small browser width like mobile devices 978 thumbnails : true, // Enable/disable thumbnail 979 thumbnailsNav : 'basic', // Navigation type for the thumbnail (basic or centered) 980 thumbnailSizes : { // Set thumbnails size for different browser widths (unlimited) 981 1920 : { // browser width (px) 982 width : 110, // thumbnail width (px) - 0 in width will hide thumbnails 983 height : 80, // thumbnail height(px) - 0 in height will hide thumbnails 984 gutter : 10 // gutter width (px) between thumbnails 985 }, 986 1280 : { 987 width : 90, 988 height : 65, 989 gutter : 10 990 }, 991 680 : { 992 width : 70, 993 height : 50, 994 gutter : 8 995 }, 996 480 : { 997 width : 60, 998 height : 44, 999 gutter : 5 1000 } 1001 }, 1002 spacing : 0.1, // Space in percent between each slide. For example, 0.1 will render as a 10% of sliding viewport width 1003 smartResize : true, // Allow images to overflow on top bar and/or caption on small devices only if image can fill the full screen height 1004 overflow : false, // Allow images to overflow on top bar and/or caption (if enable, smart resize will be ignored) 1005 loadError : 'Sorry, an error occured while loading the content...', 1006 noContent : 'Sorry, no content was found!', 1007 // functions 1008 prevNextKey : true, // Press keyboard left/right to navigate 1009 scrollToNav : false, // Scroll with a mousewheel to navigate next/prev (disabled if scrollToZoom set to true) 1010 scrollSensitivity : 15, // Threshold in 'px' from when a methods is called (Useful to fine tune touchpad and free mousewheel sensitivity) 1011 zoomTo : 'auto', // Zoom factor applied on button click and double tap ('auto' will scale to the natural image size, accept number value (e.g.: 2, 3, 4, etc...) 1012 minZoom : 1.2, // Min zoom factor required to enable Zoom feature on click or pinch 1013 maxZoom : 4, // Max zoom factor allowed when pinched/scrolled or if the zoomTo auto value is superior to maxZoom
1014 doubleTapToZoom : true, // double Tap/click to zoom on image 1015 scrollToZoom : false, // Scroll with a mousewheel to zoom in/out 1016 pinchToZoom : true, // Pinch in with 2 fingers to zoom on image 1017 escapeToClose : true, // Esc keyboard key to close the lightbox 1018 scrollToClose : false, // Scroll with a mousewheel to close the lightbox (disabled if scrollToNav and/or scrollToZoom set to true) 1019 pinchToClose : true, // Pinch out with 2 fingers to close the lightbox 1020 dragToClose : true, // Drag vertically to close the lightbox 1021 tapToClose : true, // Tap/click outside the image/media to close the lightbox 1022 shareButtons : ['facebook', 'googleplus', 'twitter', 'pinterest', 'linkedin', 'reddit'], // 'facebook', 'googleplus', 'twitter', 'pinterest', 'linkedin', 'reddit', 'stumbleupon', 'tumblr', 'blogger', 'buffer', 'digg', 'evernote' 1023 shareText : 'Share on', // text displayed above social sharing buttons 1024 sharedUrl : 'deeplink', // url used to share the media ('page', deeplink', 'media') 1025 slideShowInterval : 4000, // Time interval, in milliseconds, between slide changes in slideshow mode 1026 slideShowAutoPlay : false, // Automatically start slideShow mode on opening 1027 slideShowAutoStop : false, // Stop slideshow when the last item is reached (only if slider loop) 1028 countTimer : true, // Show a circular countdown timer next to the counter message when autoplay (slideshow) 1029 countTimerBg : 'rgba(255,255,255,0.25)', // Background color of the circular timer stroke 1030 countTimerColor : 'rgba(255,255,255,0.75)', // Color of the circular timer stroke 1031 mediaelement : false, // Play HTML5 videos with mediaelement 1032 videoRatio : 16 / 9, // Video aspect ratio 1033 videoMaxWidth : 1180, // Video max width allowed in a slide 1034 videoAutoPlay : false, // Autoplay video on opening 1035 videoThumbnail : false // Automatically fetches poster/thumbnails of iframe videos if missing 1036 }; 1037 1038 /** 1039 * ModuloBox 1040 * @constructor 1041 * @param {Object} options 1042 */ 1043 var ModuloBox = function( options ) { 1044 1045 var element = document.querySelector( '.mobx-holder' ); 1046 1047 if ( element && element.GUID ) { 1048 return instances[ element.GUID ]; 1049 } 1050 1051 // extend defaults with user-set options 1052 this.options = utils.extend( options, defaults ); 1053 // set main plugin variables 1054 this.setVar(); 1055 1056 }; 1057 1058 // Set plugin prototype (and add EvEmitter proto) 1059 var proto = ModuloBox.prototype = Object.create( EvEmitter.prototype ); 1060 1061 /** 1062 * Initialize ModuloBox 1063 * Create main instance & DOM 1064 */ 1065 proto.init = function() { 1066 1067 if ( this.GUID ) { 1068 return; 1069 } 1070 1071 this.createDOM(); 1072 1073 // set instance 1074 this.GUID = ++GUID; 1075 this.DOM.holder.GUID = GUID; 1076 instances[this.GUID] = this; 1077 1078 this.setAnimation(); 1079 this.getGalleries(); 1080 this.openFromQuery(); 1081 1082 }; 1083 1084 /** 1085 * Set main plugin variable 1086 */ 1087 proto.setVar = function() { 1088 1089 var win = window, 1090 doc = document, 1091 nav = navigator; 1092 1093 // prefix used for class names 1094 this.pre = 'mobx'; 1095 1096 // set main var 1097 this.gesture = {}; 1098 this.buttons = {}; 1099 this.slider = {}; 1100 this.slides = {}; 1101 this.cells = {}; 1102 this.states = {}; 1103 this.pointers = []; 1104 1105 // set internal cache 1106 this.expando = expando; 1107 this.cache = cache; 1108 1109 // drag events 1110 this.dragEvents = this.detectPointerEvents(); 1111 1112 // browser detection 1113 this.browser = { 1114 touchDevice : ( 'ontouchstart' in win ) || ( nav.maxTouchPoints > 0 ) || ( nav.msMaxTouchPoints > 0 ), 1115 pushState : 'history' in win && 'pushState' in history, 1116 fullScreen : this.detectFullScreen(),
1117 mouseWheel : 'onwheel' in doc.createElement( 'div' ) ? 1118 'wheel' : doc.onmousewheel !== undefined ? 1119 'mousewheel' : 'DOMMouseScroll' 1120 }; 1121 1122 // add social & video stream support 1123 this.iframeVideo = this.iframeVideo(); 1124 this.socialMedia = this.socialMedia(); 1125 1126 }; 1127 1128 /** 1129 * Detect pointer type browser support 1130 * @return {Object} 1131 */ 1132 proto.detectPointerEvents = function() { 1133 1134 var nav = navigator; 1135 1136 // listen for W3C Pointer Events (IE11) 1137 if ( nav.pointerEnabled ) { 1138 1139 return { 1140 start : ['pointerdown'], 1141 move : ['pointermove'], 1142 end : ['pointerup', 'pointercancel'] 1143 }; 1144 1145 } 1146 1147 // listen for IE10 Pointer Events 1148 if ( nav.msPointerEnabled ) { 1149 1150 return { 1151 start : ['MSPointerDown'], 1152 move : ['MSPointerMove'], 1153 end : ['MSPointerUp', 'MSPointerCancel'] 1154 }; 1155 1156 } 1157 1158 // listen for both Mouse & Touch Events 1159 return { 1160 start : ['mousedown', 'touchstart'], 1161 move : ['mousemove', 'touchmove'], 1162 end : ['mouseup', 'mouseleave', 'touchend', 'touchcancel'] 1163 }; 1164 1165 }; 1166 1167 /** 1168 * Detect fullScreen browser support 1169 * Remove fullScreen button if not supported 1170 * @return {Object} 1171 */ 1172 proto.detectFullScreen = function() { 1173 1174 var fullScreen = ['fullscreenEnabled', 'webkitFullscreenEnabled', 'mozFullScreenEnabled', 'msFullscreenEnabled']; 1175 1176 for ( var i = 0, l = fullScreen.length; i < l; i++ ) { 1177 1178 if ( document[fullScreen[i]] ) { 1179 1180 return { 1181 element : ['fullscreenElement', 'webkitFullscreenElement', 'mozFullScreenElement', 'msFullscreenElement'][i], 1182 request : ['requestFullscreen', 'webkitRequestFullscreen', 'mozRequestFullScreen', 'msRequestFullscreen'][i], 1183 change : ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange'][i], 1184 exit : ['exitFullscreen', 'webkitExitFullscreen', 'mozCancelFullScreen', 'msExitFullscreen'][i] 1185 }; 1186 1187 } 1188 1189 } 1190 1191 var controls = this.options.controls, 1192 index = controls.indexOf( 'fullScreen' ); 1193 1194 // if no fullscreen support remove fullscreen button if exists 1195 if ( index > -1 ) { 1196 controls.splice( index, 1 ); 1197 } 1198 1199 return null; 1200 1201 }; 1202 1203 /** 1204 * Iframe stream video 1205 * HTML5 window post message API 1206 * [ID] => iframe video ID 1207 * @return {Object} 1208 */ 1209 proto.iframeVideo = function() { 1210 1211 return { 1212 youtube : { 1213 reg : /(?:www\.)?youtu\.?be(?:\.com)?\/?.*?(?:watch|embed)?(?:.*v=|v\/|watch%3Fv%3D|\/)([\w\-_]+)&?/i, 1214 url : 'https://www.youtube.com/embed/[ID]?enablejsapi=1&rel=0&autoplay=1', 1215 share : 'https://www.youtube.com/watch?v=[ID]', 1216 poster : 'https://img.youtube.com/vi/[ID]/maxresdefault.jpg', 1217 thumb : 'https://img.youtube.com/vi/[ID]/default.jpg', 1218 play : { event : 'command', func : 'playVideo' }, 1219 pause : { event : 'command', func : 'pauseVideo' } 1220 }, 1221 vimeo : { 1222 reg : /(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^\/]*)\/videos\/|album\/(?:\d+)\/video\/|video\/)?(\d+)(?:[a-zA-Z0-9_\-]+)?/i, 1223 url : 'https://player.vimeo.com/video/[ID]?autoplay=1&api=1', 1224 share : 'https://vimeo.com/[ID]', 1225 poster : 'https://vimeo.com/api/v2/video/[ID].json', 1226 play : { event : 'command', method : 'play' }, 1227 pause : { event : 'command', method : 'pause' } 1228 }, 1229 dailymotion : { 1230 reg : /(?:www\.)?(?:dailymotion\.com(?:\/embed)(?:\/video|\/hub)|dai\.ly)\/([0-9a-z]+)(?:[\-_0-9a-zA-Z]+#video=(?:[a-zA-Z0-9_\-]+))?/i, 1231 url : 'https://dailymotion.com/embed/video/[ID]?autoplay=1&api=postMessage', 1232 share : 'https://www.dailymotion.com/video/[ID]', 1233 poster : 'https://www.dailymotion.com/thumbnail/video/[ID]', 1234 thumb : 'https://www.dailymotion.com/thumbnail/video/[ID]', 1235 play : 'play', 1236 pause : 'pause' 1237 }, 1238 wistia : { 1239 reg : /(?:www\.)?(?:wistia\.(?:com|net)|wi\.st)\/(?:(?:m|medias|projects)|embed\/(?:iframe|playlists))\/([a-zA-Z0-9_\-]+)/i, 1240 url : 'https://fast.wistia.net/embed/iframe/[ID]?version=3&enablejsapi=1&html5=1&autoplay=1', 1241 share : 'https://fast.wistia.net/embed/iframe/[ID]', 1242 poster : 'https://fast.wistia.com/oembed?url=https://home.wistia.com/medias/[ID].json', 1243 play : { event : 'cmd', method : 'play' }, 1244 pause : { event : 'cmd', method : 'pause' } 1245 } 1246 }; 1247 1248 }; 1249 1250 /** 1251 * Social media url 1252 * [url] => location deeplink or media url 1253 * [text] => text from title or null 1254 * [image] => image or video poster or null 1255 * @return {Object} 1256 */ 1257 proto.socialMedia = function() { 1258 1259 return { 1260 facebook : 'https://www.facebook.com/sharer/sharer.php?u=[url]', 1261 googleplus : 'https://plus.google.com/share?url=[url]', 1262 twitter : 'https://twitter.com/intent/tweet?text=[text]&url=[url]', 1263 pinterest : 'https://www.pinterest.com/pin/create/button/?url=[url]&media=[image]&description=[text]', 1264 linkedin : 'https://www.linkedin.com/shareArticle?url=[url]&mini=true&title=[text]', 1265 reddit : 'https://www.reddit.com/submit?url=[url]&title=[text]', 1266 stumbleupon : 'https://www.stumbleupon.com/badge?url=[url]&title=[text]', 1267 tumblr : 'https://www.tumblr.com/share?v=3&u=[url]&t=[text]', 1268 blogger : 'https://www.blogger.com/blog_this.pyra?t&u=[url]&n=[text]', 1269 buffer : 'https://bufferapp.com/add?url=[url]title=[text]', 1270 digg : 'https://digg.com/submit?url=[url]&title=[text]', 1271 evernote : 'https://www.evernote.com/clip.action?url=[url]&title=[text]' 1272 }; 1273 1274 }; 1275 1276 // ----- Create & append Lightbox in document ----- // 1277 1278 /** 1279 * Create DOM for the lightbox 1280 */ 1281 proto.createDOM = function() { 1282 1283 this.DOM = {}; 1284 1285 var elements = [ 1286 'holder', 1287 'overlay', 1288 'slider', 1289 'item', 1290 'item-inner', 1291 'ui', 1292 'top-bar', 1293 'bottom-bar', 1294 'share-tooltip', 1295 'counter', 1296 'caption', 1297 'caption-inner', 1298 'thumbs-holder', 1299 'thumbs-inner' 1300 ]; 1301 1302 for ( var i = 0; i < elements.length; i++ ) { 1303 this.DOM[utils.camelize( elements[i] )] = utils.createEl( 'div', this.pre + '-' + elements[i] ); 1304 } 1305 1306 this.appendDOM( this.DOM ); 1307 1308 }; 1309 1310 /** 1311 * Append lightbox DOM to body 1312 * @param {{holder, item, itemInner, overlay, slider, ui, topBar}} dom 1313 */ 1314 proto.appendDOM = function( dom ) { 1315 1316 var opt = this.options; 1317 1318 // append main containers 1319 dom.holder.appendChild( dom.overlay ); 1320 dom.holder.appendChild( dom.slider ); 1321 dom.holder.appendChild( dom.ui ); 1322 1323 // append slides & cells to slider 1324 for ( var i = 0; i < 5; i++ ) { 1325 1326 var slide = dom.item.cloneNode( true ); 1327 slide.appendChild( dom.itemInner.cloneNode( true ) ); 1328 dom.slider.appendChild( slide ); 1329 1330 // manually assign dom to slides object 1331 this.slides[i] = slide; 1332 1333 } 1334 1335 // set slides length 1336 this.slides.length = dom.slider.children.length; 1337 1338 // create UI elements 1339 this.createUI( dom, opt ); 1340 1341 // add lightbox attribute 1342 dom.holder.setAttribute( 'tabindex', -1 ); 1343 dom.holder.setAttribute( 'aria-hidden', true ); 1344 1345 // add plugin version in comment 1346 this.DOM.comment = document.createComment( ' ModuloBox (v' + version + ') by Themeone ' ); 1347 document.body.appendChild( this.DOM.comment ); 1348 1349 // Dispatch event before appending lightbox DOM (to add custom markup) 1350 utils.dispatchEvent( this, 'modulobox', 'beforeAppendDOM', dom ); 1351 1352 // append lightbox to body 1353 document.body.appendChild( dom.holder ); 1354 1355 // store top bar height for later 1356 dom.topBar.height = dom.topBar.clientHeight; 1357 1358 }; 1359 1360 /** 1361 * Create UI elements 1362 * @param {{holder, ui, topBar, bottomBar, thumbsHolder, thumbsInner, counter, shareTooltip, caption, captionInner}} dom 1363 * @param {Object} opt 1364 */ 1365 proto.createUI = function( dom, opt ) { 1366 1367 var shareIndex = opt.controls.indexOf( 'share' ); 1368 1369 // append share tooltip 1370 if ( shareIndex > -1 ) { 1371 1372 var buttons = opt.shareButtons, 1373 i = buttons.length; 1374 1375 // check if each social button exists 1376 while ( i-- ) { 1377 1378 if ( !this.socialMedia.hasOwnProperty( buttons[i] ) ) { 1379 buttons.splice( i, 1 ); 1380 } 1381 1382 } 1383 1384 // if there are social button(s) registered 1385 if ( buttons.length ) { 1386 1387 dom.ui.appendChild( dom.shareTooltip ); 1388 1389 // add share text 1390 if ( opt.shareText ) { 1391 dom.shareTooltip.appendChild( utils.createEl( 'span' ) ).textContent = opt.shareText; 1392 } 1393 1394 this.createButtons( buttons, dom.shareTooltip, 'shareOn' ); 1395 1396 // remove share button if no social media 1397 } else { 1398 opt.controls.splice( shareIndex, 1 ); 1399 } 1400 1401 } 1402 1403 // append top bar & buttons 1404 if ( opt.controls.length || opt.counterMessage ) { 1405 1406 var slideShow = opt.controls.indexOf( 'play' ); 1407 1408 dom.ui.appendChild( dom.topBar ); 1409 1410 // append counter message 1411 if ( opt.counterMessage ) { 1412 dom.topBar.appendChild( dom.counter ); 1413 } 1414 1415 // remove play button if slideShow interval <= 0 1416 if ( opt.slideShowInterval < 1 && slideShow > -1 ) { 1417 opt.controls.splice( slideShow, 1 ); 1418 } 1419 1420 // append countdown timer 1421 if ( opt.countTimer && slideShow > -1 ) { 1422 1423 var timer = this.DOM.timer = utils.createEl( 'canvas', this.pre + '-timer' ); 1424 timer.setAttribute( 'width', 48 ); 1425 timer.setAttribute( 'height', 48 ); 1426 dom.topBar.appendChild( timer ); 1427 1428 } 1429 1430 // append crontrol buttons 1431 if ( opt.controls.length ) { 1432 // clone array (prevent options to be reversed if destroyed and initialized again) 1433 var controls = opt.controls.slice(); 1434 this.createButtons( controls.reverse(), dom.topBar ); 1435 } 1436 1437 } 1438 1439 // append bottom bar 1440 if ( opt.caption || opt.thumbnails ) { 1441 1442 dom.ui.appendChild( dom.bottomBar ); 1443 1444 // append caption 1445 if ( opt.caption ) { 1446 1447 dom.bottomBar.appendChild( dom.caption ) 1448 .appendChild( dom.captionInner ); 1449 1450 1451 } 1452 1453 // append thumbnails 1454 if ( opt.thumbnails ) { 1455 1456 dom.bottomBar.appendChild( dom.thumbsHolder ) 1457 .appendChild( dom.thumbsInner ); 1458 1459 } 1460 1461 } 1462 1463 // append prev/next buttons 1464 if ( opt.prevNext ) { 1465 this.createButtons( ['prev', 'next'], dom.ui ); 1466 } 1467 1468 }; 1469 1470 /** 1471 * Create Buttons elements 1472 * @param {Array} buttons 1473 * @param {Object} dom 1474 * @param {string} event (event name attached to a button) 1475 */ 1476 proto.createButtons = function( buttons, dom, event ) { 1477 1478 var length = buttons.length; 1479 1480 for ( var i = 0; i < length; i++ ) { 1481 1482 var type = buttons[i]; 1483 1484 // create and append button 1485 this.buttons[type] = utils.createEl( 'BUTTON', this.pre + '-' + type.toLowerCase() ); 1486 dom.appendChild( this.buttons[type] ); 1487 1488 // attach event if button have a corresponding prototype event 1489 if ( ( type && typeof this[type] === 'function' ) || event ) { 1490 1491 this.buttons[type].event = event ? event : type; 1492 this.buttons[type].action = type; 1493 1494 if ( event === 'shareOn' ) { 1495 this.buttons[type].setAttribute( 'title', type.charAt( 0 ).toUpperCase() + type.slice( 1 ) ); 1496 } 1497 1498 } 1499 1500 } 1501 1502 }; 1503 1504 /** 1505 * Get media galleries (img, video, iframe, HTML) 1506 */ 1507 proto.getGalleries = function() { 1508 1509 // prepare galleries 1510 // reset if executed again 1511 this.galleries = {}; 1512 1513 // prepare querySelector 1514 var selectors = this.options.mediaSelector, 1515 sources = ''; 1516 1517 // if no selector set (prevent to trigger unnecessary errors) 1518 if ( !selectors ) { 1519 return false; 1520 } 1521 1522 // setup sources 1523 try { 1524 sources = document.querySelectorAll( selectors ); 1525 } catch (error) { 1526 utils.error( 'Your current mediaSelector is not a valid selector: "' + selectors + '"' ); 1527 } 1528 1529 for ( var i = 0, l = sources.length; i < l; i++ ) { 1530 1531 var source = sources[i], 1532 media = {}; 1533
1534 // get original image url/src depending of the tagName 1535 media.src = source.tagName === 'A' ? source.getAttribute( 'href' ) : null; 1536 media.src = source.tagName === 'IMG' ? source.currentSrc || source.src : media.src; 1537 // if mediaSelector have data-src attr, take this one instead of src or href attributes 1538 media.src = source.getAttribute( 'data-src' ) || media.src; 1539 1540 if ( media.src ) { 1541 1542 this.getMediaAtts( source, media ); 1543 this.setMediaType( media ); 1544 1545 // If the media have a known type 1546 if ( media.type ) { 1547 1548 this.getMediaThumb( source, media ); 1549 this.getVideoThumb( media ); 1550 this.getMediaCaption( source, media ); 1551 1552 // format caption 1553 this.setMediaCaption( media ); 1554 1555 // get the gallery name & features 1556 var gallery = this.setGalleryName( source ); 1557 this.setGalleryFeatures( gallery, media ); 1558 1559 // assign media index 1560 media.index = gallery.length; 1561 // push media data in gallery 1562 gallery.push( media ); 1563 1564 // attach click event to media 1565 this.setMediaEvent( source, gallery.name, media.index ); 1566 1567 } 1568 1569 } 1570 1571 } 1572 1573 // dispatch updateGalleries event 1574 utils.dispatchEvent( this, 'modulobox', 'updateGalleries', this.galleries ); 1575 1576 }; 1577 1578 /** 1579 * Add media in a gallery 1580 * @param {number|string} name (gallery name) 1581 * @param {Object} media (media collection) 1582 */ 1583 proto.addMedia = function( name, media ) { 1584 1585 if ( !media || typeof media !== 'object' ) { 1586 1587 utils.error( 'No media was found to addMedia() in a gallery' ); 1588 return false; 1589 1590 } 1591 1592 // set gallery 1593 name = name === '' ? 1 : name; 1594 var gallery = this.galleries[name]; 1595 gallery = !gallery ? ( this.galleries[name] = [] ) : gallery; 1596 gallery.name = name; 1597 1598 var length = media.length; 1599 for ( var i = 0; i < length; i++ ) { 1600 1601 var item = utils.cloneObject( media[i] ); 1602 1603 if ( item.src ) { 1604 1605 this.setMediaType( item ); 1606 this.getVideoThumb( item ); 1607 this.setMediaCaption( item ); 1608 this.setGalleryFeatures( gallery, item ); 1609 1610 // assign media index 1611 item.index = gallery.length; 1612 // push media data in gallery 1613 gallery.push( item ); 1614 1615 } 1616 1617 } 1618 1619 }; 1620 1621 /** 1622 * Set media type depending of its content 1623 * Image/video types only, iframe & HTML must be set manually 1624 * @param {Object} media 1625 */ 1626 proto.setMediaType = function( media ) { 1627 1628 // if the media type is already set and valid 1629 if ( ['image', 'video', 'iframe', 'HTML'].indexOf( media.type ) > -1 ) { 1630 return; 1631 } 1632 1633 // reset media type in case not valid 1634 media.type = null; 1635 1636 // get source 1637 var source = media.src ? media.src : null; 1638 1639 /* jslint bitwise: true */ 1640 // get extension without query string 1641 var extension = ( source.split( /[?#]/ )[0] || source ).substr( ( ~-source.lastIndexOf( '.' ) >>> 0 ) + 2 ); 1642 /* jslint bitwise: false */ 1643 1644 // detect image 1645 if ( /(jpg|jpeg|png|bmp|gif|tif|tiff|jfi|jfif|exif|svg)/i.test( extension ) || ['external.xx.fbcdn', 'drscdn.500px.org'].indexOf( source ) > -1 ) { 1646 1647 media.type = 'image'; 1648 media.src = this.getSrc( source ); 1649 1650 // detect HTML5 video 1651 } else if ( /(mp4|webm|ogv)/i.test( extension ) ) { 1652 1653 media.type = 'video'; 1654 media.format = 'html5'; 1655 1656 // detect iframe video 1657 } else { 1658 1659 var stream = this.iframeVideo; 1660 1661 for ( var type in stream ) { 1662 1663 if ( stream.hasOwnProperty( type ) ) { 1664 1665 var regs = source.match( stream[type].reg ); 1666 1667 if ( regs && regs[1] ) { 1668 1669 var object = stream[type]; 1670 1671 media.type = 'video'; 1672 media.format = type; 1673 media.share = object.share.replace( '[ID]', regs[1] ); 1674 media.src = object.url.replace( '[ID]', regs[1] ); 1675 media.pause = object.pause; 1676 media.play = object.play; 1677 1678 if ( this.options.videoThumbnail ) { 1679 media.poster = !media.poster && object.poster ? object.poster.replace( '[ID]', regs[1] ) : media.poster; 1680 media.thumb = !media.thumb && object.poster ? object.poster.replace( '[ID]', regs[1] ) : media.thumb; 1681 } 1682 1683 break; 1684 1685 } 1686 1687 } 1688 1689 } 1690 1691 } 1692 1693 }; 1694 1695 /** 1696 * Get image srcset custom 1697 * @param {string} source 1698 * @return {string} image url 1699 */ 1700 proto.getSrc = function( source ) { 1701 1702 var srcset = ( source || '' ).split( /,/ ), 1703 length = srcset.length, 1704 width = 0; 1705 1706 if ( length <= 1 ) { 1707 return source; 1708 } 1709 1710 for ( var i = 0; i < length; i++ ) { 1711 1712 var parts = srcset[i].replace(/\s+/g, ' ').trim().split( / / ), 1713 value = parseFloat( parts[1] ) || 0, 1714 unit = parts[1] ? parts[1].slice(-1) : null; 1715 1716 if ( ( unit === 'w' && screen.width >= value && value > width ) || !value || i === 0 ) { 1717 1718 width = value; 1719 source = parts[0]; 1720 1721 } 1722 1723 } 1724 1725 return source; 1726 1727 }; 1728 1729 /** 1730 * Get media attributes 1731 * @param {Object} source 1732 * @param {Object} media 1733 */ 1734 proto.getMediaAtts = function( source, media ) { 1735 1736 var auto = this.options.autoCaption, 1737 data = this.getAttr( source ), 1738 img = source.firstElementChild; 1739 img = source.tagName !== 'IMG' && img && img.tagName === 'IMG' ? img : source; 1740 1741 media.type = !media.type ? data.type || source.getAttribute( 'data-type' ) : media.type; 1742 media.title = data.title || source.getAttribute( 'data-title' ) || ( auto ? img.title : null ); 1743 media.desc = data.desc || source.getAttribute( 'data-desc' ) || ( auto ? img.alt : null ); 1744 media.thumb = data.thumb || source.getAttribute( 'data-thumb' );
1745 media.poster = this.getSrc( data.poster || source.getAttribute( 'data-poster' ) ); 1746 media.width = data.width || source.getAttribute( 'data-width' ); 1747 media.height = data.height || source.getAttribute( 'data-height' ); 1748 1749 // prevent duplicate content if autoCaption enabled 1750 if ( media.title === media.desc ) { 1751 media.desc = null; 1752 } 1753 1754 }; 1755 1756 /** 1757 * Get media thumbnail 1758 * @param {Object} source 1759 * @param {Object} media 1760 */ 1761 proto.getMediaThumb = function( source, media ) { 1762 1763 var thumbnail = source.getElementsByTagName( 'img' ); 1764 1765 if ( !media.thumb && thumbnail[0] ) { 1766 media.thumb = thumbnail[0].src; 1767 } 1768 1769 }; 1770 1771 /** 1772 * Get iframe video thumbnail 1773 * @param {Object} media 1774 */ 1775 proto.getVideoThumb = function( media ) { 1776 1777 if ( !this.options.videoThumbnail || media.type !== 'video' || media.format === 'html5' ) { 1778 return; 1779 } 1780 1781 var hasPoster = media.poster && media.poster.indexOf( '.json' ) > -1, 1782 hasThumb = media.thumb && media.thumb.indexOf( '.json' ) > -1; 1783 1784 if ( hasPoster || hasThumb ) { 1785 1786 var uri = hasPoster ? media.poster : media.thumb, 1787 xhr = new XMLHttpRequest(); 1788 1789 xhr.onload = function( event ) { 1790 1791 /** @type {?|{thumbnail_large: string, thumbnail_url: string, thumbnail_small: string}} */ 1792 var response = event.target.responseText; 1793 response = JSON.parse( response ); 1794 response = response.hasOwnProperty( 0 ) ? response[0] : response; 1795 1796 if ( response ) { 1797 1798 media.poster = response.thumbnail_large || response.thumbnail_url; 1799 1800 // replace background image if image DOM already built 1801 if ( media.dom ) { 1802 media.dom.style.backgroundImage = 'url("' + media.poster + '")'; 1803 } 1804 1805 if ( hasThumb ) { 1806 1807 var thumb = response.thumbnail_small || response.thumbnail_url; 1808 1809 // replace background image if thumbnail already built 1810 if ( typeof media.thumb === 'object' ) { 1811 media.thumb.style.backgroundImage = 'url("' + thumb + '")'; 1812 } else { 1813 media.thumb = thumb; 1814 } 1815 1816 } 1817 1818 } 1819 1820 }.bind( this ); 1821 1822 xhr.open( 'GET', encodeURI( uri ), true ); 1823 1824 setTimeout( function() { 1825 xhr.send(); 1826 }, 0 ); 1827 1828 } 1829 1830 }; 1831 1832 /** 1833 * Get media caption 1834 * @param {Object} source 1835 * @param {Object} media 1836 */ 1837 proto.getMediaCaption = function( source, media ) { 1838 1839 var next = source.nextElementSibling; 1840 1841 // if gallery follow schema.org markup 1842 if ( next && next.tagName === 'FIGCAPTION' ) { 1843 1844 var caption = next.innerHTML; 1845 1846 if ( ! media.title ) { 1847 media.title = caption; 1848 } else if ( ! media.desc ) { 1849 media.desc = caption; 1850 } 1851 1852 } 1853 1854 }; 1855 1856 /** 1857 * Build media caption 1858 * @param {Object} media 1859 */ 1860 proto.setMediaCaption = function( media ) { 1861 1862 media.title = media.title ? '<div class="' + this.pre + '-title">' + media.title.trim() + '</div>' : ''; 1863 media.desc = media.desc ? '<div class="' + this.pre + '-desc">' + media.desc.trim() + '</div>' : ''; 1864 media.caption = media.title + media.desc; 1865 1866 }; 1867 1868 /** 1869 * Get gallery name from DOM 1870 * @param {Object} source 1871 */ 1872 proto.getGalleryName = function( source ) { 1873 1874 var parent = source, 1875 node = 0; 1876 1877 while ( parent && node < 2 ) { 1878 1879 parent = parent.parentNode; 1880 1881 if ( parent && parent.tagName === 'FIGURE' && parent.parentNode ) { 1882 1883 return parent.parentNode.getAttribute( 'id' ); 1884 1885 } 1886 1887 node++; 1888 1889 } 1890 1891 }; 1892 1893 /** 1894 * Set gallery name 1895 * @param {Object} source 1896 */ 1897 proto.setGalleryName = function( source ) { 1898 1899 var data = this.getAttr( source ); 1900 1901 // get name 1902 var name = data.rel || source.getAttribute( 'data-rel' ); 1903 name = !name ? this.getGalleryName( source ) : name; 1904 name = !name ? Object.keys( this.galleries ).length + 1 : name; 1905 1906 // set gallery 1907 var gallery = this.galleries[name]; 1908 gallery = !gallery ? ( this.galleries[name] = [] ) : gallery; 1909 gallery.name = name; 1910 1911 return gallery; 1912 1913 }; 1914 1915 /** 1916 * Set gallery features 1917 * @param {Object} gallery 1918 * @param {Object} media 1919 */ 1920 proto.setGalleryFeatures = function( gallery, media ) { 1921 1922 if ( !gallery.zoom && media.type === 'image' ) { 1923 gallery.zoom = true; 1924 } 1925 1926 if ( !gallery.download && ( media.type === 'image' || media.format === 'html5' ) ) { 1927 gallery.download = true; 1928 } 1929 1930 }; 1931 1932 /** 1933 * Attach click event to Media DOM 1934 * @param {Object} source 1935 * @param {number} name (of gallery) 1936 * @param {number} index 1937 */ 1938 proto.setMediaEvent = function( source, name, index ) { 1939 1940 // if media already have a click event attached 1941 if ( source.mobxListener ) { 1942 source.removeEventListener( 'click', source.mobxListener, false ); 1943 } 1944 1945 // set listener function to remove later if gallery updated 1946 source.mobxListener = this.open.bind( this, name, index ); 1947 source.addEventListener( 'click', source.mobxListener, false ); 1948 1949 }; 1950 1951 /** 1952 * Open lightbox from a given Gallery name and media index 1953 * @param {string} name 1954 * @param {number} index 1955 * @param {Object} event 1956 */ 1957 proto.open = function( name, index, event ) { 1958 1959 // prevent default action and to bubbling up the DOM tree 1960 if ( event ) { 1961 1962 event.preventDefault(); 1963 event.stopPropagation(); 1964 1965 } 1966 1967 // if the instance was destroyed or does not exist 1968 if ( !this.GUID ) { 1969 return false; 1970 } 1971 1972 // check if gallery exists 1973 if ( !this.galleries.hasOwnProperty( name ) ) { 1974
1975 utils.error( 'This gallery name : "' + name + '", does not exist!' ); 1976 return false; 1977 1978 } 1979 1980 // check if gallery have media 1981 if ( !this.galleries[name].length ) { 1982 1983 utils.error( 'Sorry, no media was found for the current gallery.' ); 1984 return false; 1985 1986 } 1987 1988 // check if current media index exists (mainly to open from query param) 1989 if ( !this.galleries[name][index] ) { 1990 1991 utils.error( 'Sorry, no media was found for the current media index: ' + index ); 1992 return false; 1993 1994 } 1995 1996 // dispatch beforeOpen Event 1997 utils.dispatchEvent( this, 'modulobox', 'beforeOpen', name, index ); 1998 1999 // reset slide index 2000 this.slides.index = index; 2001 2002 // set gallery 2003 this.gallery = this.galleries[name]; 2004 this.gallery.name = name; 2005 this.gallery.index = index; 2006 this.gallery.loaded = false; 2007 2008 // prepare lightbox 2009 this.removeContent(); 2010 this.wrapAround(); 2011 this.hideScrollBar(); 2012 this.setSlider(); 2013 this.setThumbs(); 2014 this.setCaption(); 2015 this.setMedia( this.options.preload ); 2016 this.updateMediaInfo(); 2017 this.replaceState(); 2018 this.setControls(); 2019 this.bindEvents( true ); 2020 this.show(); 2021 2022 // autoplay video on opening 2023 if ( this.options.videoAutoPlay ) { 2024 this.appendVideo(); 2025 } 2026 2027 // autoplay slideshow on opening (only if video autoplay not set or not a video item opened) 2028 if ( this.options.slideShowAutoPlay && this.options.controls.indexOf( 'play' ) > -1 && 2029 ( ! this.options.videoAutoPlay || this.galleries[name][index].type !== 'video' ) ) { 2030 this.startSlideShow(); 2031 } 2032 2033 // set lightbox states 2034 this.states.zoom = false; 2035 this.states.open = true; 2036 2037 }; 2038 2039 /** 2040 * Open lightbox from query string in url 2041 */ 2042 proto.openFromQuery = function() { 2043 2044 var query = this.getQueryString( window.location.search ); 2045 2046 if ( query.hasOwnProperty( 'guid' ) && query.hasOwnProperty( 'mid' ) ) { 2047 2048 var open = this.open( 2049 decodeURIComponent( query.guid ), 2050 decodeURIComponent( query.mid ) - 1 2051 ); 2052 2053 // remove query string if failed to open 2054 if ( open === false ) { 2055 2056 this.replaceState( true ); 2057 2058 } 2059 2060 } 2061 2062 }; 2063 2064 /** 2065 * Reveal lightbox 2066 */ 2067 proto.show = function() { 2068 2069 var holder = this.DOM.holder, 2070 method = this.options.rightToLeft ? 'add' : 'remove'; 2071 2072 holder.setAttribute( 'aria-hidden', false ); 2073 utils.removeClass( holder, this.pre + '-idle' ); 2074 utils.removeClass( holder, this.pre + '-panzoom' ); 2075 utils.removeClass( holder, this.pre + '-will-close' ); 2076 utils[method + 'Class']( holder, this.pre + '-rtl' ); 2077 utils.addClass( holder, this.pre + '-open' ); 2078 2079 }; 2080 2081 /** 2082 * Close lightbox 2083 */ 2084 proto.close = function( event ) { 2085 2086 // Prevent click/touch propagation 2087 if ( event ) { 2088 event.preventDefault(); 2089 } 2090 2091 var holder = this.DOM.holder, 2092 gallery = this.gallery, 2093 index = gallery ? gallery.index : 'undefined', 2094 name = gallery ? gallery.name : 'undefined'; 2095 2096 // dispatch beforeClose Event 2097 utils.dispatchEvent( this, 'modulobox', 'beforeClose', name, index ); 2098 2099 // exist from fullscreen 2100 if ( this.states.fullScreen ) { 2101 2102 this.exitFullScreen(); 2103 utils.removeClass( holder, this.pre + '-fullscreen' ); 2104 2105 } 2106 2107 // stop all actions 2108 this.share(); 2109 this.stopSlideShow(); 2110 this.pauseVideo(); 2111 2112 this.bindEvents( false ); 2113 this.replaceState( true ); 2114 this.hideScrollBar(); 2115 2116 // hide lightbox 2117 holder.setAttribute( 'aria-hidden', true ); 2118 utils.removeClass( holder, this.pre + '-open' ); 2119 2120 this.states.open = false; 2121 2122 }; 2123 2124 /** 2125 * Set controls and buttons states 2126 */ 2127 proto.setControls = function() { 2128 2129 // handle play/prev/next buttons 2130 var gallery = this.gallery, 2131 options = this.options, 2132 buttons = this.buttons; 2133 2134 // hide counter message if one media 2135 if ( this.DOM.counter ) { 2136 this.DOM.counter.style.display = ( gallery.initialLength > 1 ) ? '' : 'none'; 2137 } 2138 2139 // hide play button if one media 2140 if ( options.controls.indexOf( 'play' ) > -1 ) { 2141 buttons.play.style.display = ( gallery.initialLength > 1 ) ? '' : 'none'; 2142 } 2143 2144 // hide zoom button if one media and not zoomable 2145 if ( options.controls.indexOf( 'zoom' ) > -1 ) { 2146 buttons.zoom.style.display = !gallery.zoom ? 'none' : ''; 2147 } 2148 2149 // hide download button if one media and not Downloadable 2150 if ( options.controls.indexOf( 'download' ) > -1 ) { 2151 buttons.download.style.display = !gallery.download ? 'none' : ''; 2152 } 2153 2154 // hide/show prev & next buttons 2155 this.setPrevNextButtons(); 2156 2157 }; 2158 2159 /** 2160 * Set responsive Prev/Next buttons 2161 */ 2162 proto.setPrevNextButtons = function() { 2163 2164 if ( this.options.prevNext ) { 2165 2166 var hide = this.slider.width < 680 && this.browser.touchDevice && !this.options.prevNextTouch; 2167 this.buttons.prev.style.display = 2168 this.buttons.next.style.display = ( this.gallery.length > 1 && !hide ) ? '' : 'none'; 2169 2170 } 2171 2172 }; 2173 2174 /** 2175 * Set caption display depending of the screen size 2176 */ 2177 proto.setCaption = function() { 2178 2179 this.states.caption = !( !this.options.captionSmallDevice && ( this.slider.width <= 480 || this.slider.height <= 480 ) ); 2180 this.DOM.caption.style.display = this.states.caption ? '' : 'none'; 2181 2182 }; 2183 2184 /** 2185 * Set buttons states 2186 */ 2187 proto.hideScrollBar = function() { 2188 2189 if ( !this.options.scrollBar ) { 2190 2191 var open = this.states.open, 2192 scrollBar = open === 'undefined' || !open ? true : false; 2193 2194 document.body.style.overflow = 2195 document.documentElement.style.overflow = scrollBar ? 'hidden' : ''; 2196 2197 } 2198 2199 }; 2200 2201 // ----- Handle events ----- // 2202 2203 /** 2204 * Bind/unbind all events attached to the lightbox 2205 * @param {boolean} bind 2206 */ 2207 proto.bindEvents = function( bind ) { 2208 2209 var win = window, 2210 doc = document, 2211 opt = this.options, 2212 holder = this.DOM.holder, 2213 buttons = this.buttons, 2214 scrollMethod; 2215 2216 // handle buttons 2217 for ( var type in buttons ) { 2218 2219 if ( buttons.hasOwnProperty( type ) ) { 2220 2221 var DOM = ( type !== 'share' ) ? buttons[type] : win; 2222 utils.handleEvents( this, DOM, ['click', 'touchend'], buttons[type].event, bind ); 2223 2224 } 2225 2226 } 2227 2228 // touch events 2229 utils.handleEvents( this, holder, this.dragEvents.start, 'touchStart', bind ); 2230 2231 // keyboard event 2232 utils.handleEvents( this, win, ['keydown'], 'keyDown', bind ); 2233 2234 // window resize event 2235 utils.handleEvents( this, win, ['resize', 'orientationchange'], 'resize', bind ); 2236 2237 // transition end event 2238 utils.handleEvents( this, holder, ['transitionend', 'webkitTransitionEnd', 'oTransitionEnd', 'otransitionend', 'MSTransitionEnd'], 'opened' ); 2239 2240 // disable double tap to zoom 2241 utils.handleEvents( this, holder, ['touchend'], 'disableZoom', bind ); 2242 2243 // handle fullscreen 2244 if ( this.browser.fullScreen ) { 2245 utils.handleEvents( this, doc, [this.browser.fullScreen.change], 'toggleFullScreen', bind ); 2246 } 2247 2248 // handle history navigation (replaceState) 2249 if ( opt.history ) { 2250 utils.handleEvents( this, win, ['mouseout'], 'mouseOut', bind ); 2251 } 2252 2253 // handle idle state 2254 if ( opt.timeToIdle > 0 ) { 2255 utils.handleEvents( this, holder, ['mousemove'], 'mouseMove', bind ); 2256 } 2257 2258 // prevent right click 2259 if ( !opt.contextMenu ) { 2260 utils.handleEvents( this, holder, ['contextmenu'], 'contextMenu', bind ); 2261 } 2262 2263 // disable scroll 2264 if ( !opt.mouseWheel ) { 2265 this.disableScroll( bind ); 2266 } 2267 2268 // scroll to zoom 2269 if ( opt.scrollToZoom ) { 2270 scrollMethod = 'scrollToZoom'; 2271 } 2272 // scroll to navigate 2273 else if ( opt.scrollToNav ) { 2274 scrollMethod = 'scrollToNav'; 2275 } 2276 // scroll to close 2277 else if ( opt.scrollToClose ) { 2278 scrollMethod = 'scrollToClose'; 2279 } 2280 2281 // handle scroll method 2282 if ( scrollMethod ) { 2283 utils.handleEvents( this, holder, [this.browser.mouseWheel], scrollMethod, bind ); 2284 } 2285 2286 }; 2287 2288 /** 2289 * Detect animation transition end 2290 * Set open/close state 2291 * Unload/remove media only when the lightbox is closed and hidden (transitionend) 2292 * @param {Object} event 2293 */ 2294 proto.opened = function( event ) { 2295 2296 if ( event.propertyName === 'visibility' && event.target === this.DOM.holder ) { 2297 2298 var name = this.gallery.name, 2299 index = this.gallery.index; 2300 2301 if ( !this.states.open ) { 2302 2303 // remove media in slide and thumbnails 2304 this.removeContent(); 2305 2306 // dispatch afterClose Event 2307 utils.dispatchEvent( this, 'modulobox', 'afterClose', name, index ); 2308 2309 } else { 2310 2311 // dispatch afterOpen Event 2312 utils.dispatchEvent( this, 'modulobox', 'afterOpen', name, index ); 2313 2314 } 2315 2316 } 2317 2318 }; 2319 2320 /** 2321 * Detect when mouse leave browser window 2322 * @param {Object} event 2323 */ 2324 proto.mouseOut = function( event ) { 2325 2326 var e = event ? event : window.event, 2327 from = e.relatedTarget || e.toElement; 2328 2329 if ( !from || from.nodeName === 'HTML' ) { 2330 2331 this.replaceState(); 2332 2333 } 2334 2335 }; 2336 2337 /** 2338 * Detect idle state from mousemove 2339 */ 2340 proto.mouseMove = function() { 2341 2342 var holder = this.DOM.holder, 2343 idleClass = this.pre + '-idle'; 2344 2345 clearTimeout( this.states.idle ); 2346 2347 this.states.idle = setTimeout( function() { 2348 2349 // prevent to hide top bar if tooltip opened 2350 if ( !utils.hasClass( holder, this.pre + '-open-tooltip' ) ) { 2351 2352 utils.addClass( holder, idleClass ); 2353 2354 } 2355 2356 }.bind( this ), this.options.timeToIdle ); 2357 2358 utils.removeClass( holder, idleClass ); 2359 2360 }; 2361 2362 /** 2363 * Disable contextmenu (right click) on image 2364 * @param {Object} event 2365 */ 2366 proto.contextMenu = function( event ) { 2367 2368 var target = event.target, 2369 tagName = target.tagName, 2370 className = target.className; 2371 2372 if ( tagName === 'IMG' || tagName === 'VIDEO' || 2373 className.indexOf( this.pre + '-video' ) > -1 || 2374 className.indexOf( this.pre + '-thumb-bg' ) > -1 || 2375 className === this.pre + '-thumb' ) { 2376 2377 event.preventDefault(); 2378 2379 } 2380 2381 }; 2382 2383 /** 2384 * Disable scroll 2385 * @param {boolean} bind 2386 */ 2387 proto.disableScroll = function( bind ) { 2388 2389 var doc = document, 2390 win = window; 2391 2392 var prevent = function( e ) { 2393 2394 if ( !this.isEl( e ) ) { 2395 return; 2396 } 2397 2398 e = e || win.event; 2399 2400 if ( e.preventDefault ) { 2401 2402 e.preventDefault(); 2403 e.returnValue = false; 2404 return false; 2405 2406 } 2407 2408 }; 2409 2410 win.onwheel = 2411 win.ontouchmove = 2412 win.onmousewheel = 2413 doc.onmousewheel = 2414 doc.onmousewheel = bind ? prevent.bind( this ) : null; 2415 2416 }; 2417 2418 /** 2419 * Scroll to zoom 2420 * @param {Object} event 2421 */ 2422 proto.scrollToZoom = function( event ) { 2423 2424 if ( !this.isEl( event ) ) { 2425 return; 2426 } 2427 2428 var data = this.normalizeWheel( event ); 2429 2430 if ( data && data.deltaY ) { 2431 2432 var cell = this.getCell(), 2433 scale = cell.attraction.s || cell.position.s; 2434 scale = Math.min( this.options.maxZoom, Math.max( 1, scale - Math.abs( data.deltaY ) / data.deltaY ) ); 2435 2436 this.stopSlideShow(); 2437 this.zoomTo( event.clientX, event.clientY, Math.round( scale * 10 ) / 10 ); 2438 2439 } 2440 2441 }; 2442 2443 /** 2444 * Scroll to navigate (next/prev slide) 2445 * @param {Object} event 2446 */ 2447 proto.scrollToNav = function( event ) { 2448 2449 if ( !this.isEl( event ) ) { 2450 return; 2451 } 2452 2453 var data = this.normalizeWheel( event ); 2454 2455 if ( data && data.delta ) { 2456 2457 this[data.delta * this.isRTL() < 0 ? 'prev' : 'next'](); 2458 2459 } 2460 2461 }; 2462 2463 /** 2464 * Scroll to close 2465 * @param {Object} event 2466 */ 2467 proto.scrollToClose = function( event ) { 2468 2469 if ( !this.isEl( event ) ) { 2470 return; 2471 } 2472 2473 event.preventDefault(); 2474 this.close(); 2475 2476 }; 2477 2478 /** 2479 * Disable double tap to zoom on touch devices 2480 * @param {Object} event 2481 */ 2482 proto.disableZoom = function( event ) { 2483 2484 var node = event.target; 2485 2486 while ( node ) { 2487 2488 // Prevent issue on Android with native HTML5 video controls 2489 // If preventDefault on Android devices, video controls will never be shown on tap 2490 // Keep click on link/input for HTML content 2491 if ( ['VIDEO', 'INPUT', 'A'].indexOf( node.tagName ) > -1 ) { 2492 return; 2493 } 2494 2495 node = node.parentElement; 2496 2497 } 2498 2499 event.preventDefault(); 2500 2501 }; 2502 2503 /** 2504 * Resize lightbox 2505 */ 2506 proto.resize = function( event ) { 2507 2508 // Set new top bar height (in case icons are replaced) 2509 this.DOM.topBar.height = this.DOM.topBar.clientHeight; 2510 2511 this.share(); 2512 this.setSlider(); 2513 this.setThumbsPosition(); 2514 this.setCaption(); 2515 this.resizeMedia();
2516 this.updateMediaInfo(); 2517 this.setPrevNextButtons(); 2518 2519 // reset current slide zoom 2520 this.states.zoom = false; 2521 utils.removeClass( this.DOM.holder, this.pre + '-panzoom' ); 2522 2523 // dispatch resize Event 2524 utils.dispatchEvent( this, 'modulobox', 'resize', event ); 2525 2526 }; 2527 2528 /** 2529 * Resize media on window resize 2530 */ 2531 proto.resizeMedia = function() { 2532 2533 var slides = this.slides; 2534 2535 for ( var i = 0; i < slides.length; i++ ) { 2536 2537 if ( !this.gallery ) { 2538 break; 2539 } 2540 2541 var media = this.gallery[slides[i].media]; 2542 2543 if ( media && ( ( media.dom && media.dom.loaded ) || ( media.dom && ['video', 'iframe', 'HTML'].indexOf( media.type ) > -1 ) ) ) { 2544 2545 this.setMediaSize( media, slides[i] ); 2546 2547 } 2548 2549 } 2550 2551 }; 2552 2553 // ----- main plugin helper functions ----- // 2554 2555 /** 2556 * Check if current element is part of Modulobox UI 2557 * @param {Object} event 2558 * @return {boolean} 2559 */ 2560 proto.isEl = function( event ) { 2561 2562 var name = event.target.className; 2563 name = typeof name === 'string' ? name : name.baseVal; 2564 2565 return name.indexOf( this.pre ) > -1; 2566 2567 }; 2568 2569 /** 2570 * Check if current media is zoomable 2571 * @return {boolean} 2572 */ 2573 proto.isZoomable = function() { 2574 2575 var media = this.getMedia(), 2576 zoom = false; 2577 2578 if ( media.type === 'image' && media.dom && media.dom.size && media.dom.size.scale > 1 ) { 2579 zoom = true; 2580 } 2581 2582 this.DOM.holder.setAttribute( 'data-zoom', zoom ); 2583 return zoom; 2584 2585 }; 2586 2587 /** 2588 * Check if current media is downloadable 2589 * @return {boolean} 2590 */ 2591 proto.isDownloadable = function() { 2592 2593 var media = this.getMedia(), 2594 download = true; 2595 2596 if ( media.type !== 'image' && media.format !== 'html5' ) { 2597 download = false; 2598 } 2599 2600 this.DOM.holder.setAttribute( 'data-download', download ); 2601 return download; 2602 2603 }; 2604 2605 /** 2606 * Check if RTL layout is enabled 2607 * @return {number} 2608 */ 2609 proto.isRTL = function() { 2610 2611 return this.options.rightToLeft ? - 1 : 1; 2612 2613 }; 2614 2615 /** 2616 * Add attribute(s) in cache 2617 * @param {Object} el 2618 * @param {Object} attrs 2619 */ 2620 proto.addAttr = function( el, attrs ) { 2621 2622 var cacheID; 2623 2624 if ( typeof el[this.expando] === 'undefined' ) { 2625 2626 cacheID = this.cache.uid++; 2627 el[this.expando] = cacheID; 2628 this.cache[cacheID] = {}; 2629 2630 } else { 2631 cacheID = el[this.expando]; 2632 } 2633 2634 for ( var attr in attrs ) { 2635 2636 if ( attrs.hasOwnProperty( attr ) ) { 2637 2638 this.cache[cacheID][attr] = attrs[attr]; 2639 2640 } 2641 2642 } 2643 2644 }; 2645 2646 /** 2647 * Get attribute(s) in cache 2648 * @param {Object} el 2649 * @return {Object} 2650 */ 2651 proto.getAttr = function( el ) { 2652 2653 return this.cache[el[this.expando]] || {}; 2654 2655 }; 2656 2657 /** 2658 * Get thumbnail height 2659 * @return {number} 2660 */ 2661 proto.getThumbHeight = function() { 2662 2663 var thumb = this.thumbs; 2664 return thumb.height > 0 && thumb.width > 0 ? thumb.height + Math.min( 10, thumb.gutter ) * 2 : 0; 2665 2666 }; 2667 2668 /** 2669 * Get current media in opened gallery 2670 * @return {Object} 2671 */ 2672 proto.getMedia = function() { 2673 2674 var gallery = this.gallery; 2675 return gallery ? gallery[gallery.index] : null; 2676 2677 }; 2678 2679 /** 2680 * Get current cell in slide 2681 * @return {Object} 2682 */ 2683 proto.getCell = function() { 2684 2685 var slides = this.slides, 2686 index = utils.modulo( slides.length, slides.index ); 2687 2688 return this.cells[index]; 2689 2690 }; 2691 2692 /** 2693 * Remove/reset media in slide and remove thumbnails 2694 */ 2695 proto.removeContent = function() { 2696 2697 // remove media in each slide 2698 for ( var i = 0; i < this.slides.length; i++ ) { 2699 2700 var slide = this.slides[i]; 2701 2702 // clean slide content 2703 this.unloadMedia( slide ); 2704 this.removeMedia( slide ); 2705 2706 // reset index and media index previously set 2707 slide.index = 2708 slide.media = null; 2709 2710 } 2711 2712 // remove all thumbnails 2713 this.removeMedia( this.DOM.thumbsHolder ); 2714 2715 }; 2716 2717 /** 2718 * Get query string paramaters from url 2719 * @param {string} search 2720 * @return {Object} 2721 */ 2722 proto.getQueryString = function( search ) { 2723 2724 var params = {}; 2725 2726 search.substr( 1 ).split( '&' ).forEach( function( param ) { 2727 2728 param = param.split( '=' ); 2729 params[decodeURIComponent( param[0] )] = param.length > 1 ? decodeURIComponent( param[1] ) : ''; 2730 2731 }); 2732 2733 return params; 2734 2735 }; 2736 2737 /** 2738 * Replace query parameter in url 2739 * @param {Object} key 2740 * @return {string} 2741 */ 2742 proto.setQueryString = function( key ) { 2743 2744 var search = window.location.search, 2745 query = this.getQueryString( search ); 2746 2747 // decode URI part 2748 search = decodeURI( search ); 2749 2750 for ( var prop in key ) { 2751 2752 if ( key.hasOwnProperty( prop ) ) { 2753 2754 // encode string component 2755 // value to replace in URI 2756 var replace = encodeURIComponent( key[prop] ); 2757 2758 if ( query.hasOwnProperty( prop ) ) { 2759 2760 // already decoded from getQueryString
2761 // value to search from decoded search URI 2762 var value = query[prop]; 2763 2764 if ( !replace ) { 2765 2766 search = search.replace( '&' + prop + '=' + value, '' ); 2767 search = search.replace( prop + '=' + value, '' ); 2768 2769 } else { 2770 2771 search = search.replace( prop + '=' + value, prop + '=' + replace ); 2772 2773 } 2774 2775 } else { 2776 2777 if ( replace ) { 2778 2779 search = search + ( !search ? '?' : '&' ) + prop + '=' + replace; 2780 2781 } else { 2782 2783 search = search.replace( prop + '=', '' ); 2784 2785 } 2786 2787 } 2788 2789 } 2790 2791 } 2792 2793 // create base url 2794 var base = [location.protocol, '//', location.host, location.pathname].join( '' ); 2795 // check if search query is empty or not 2796 search = !search.substr( 1 ) ? search.substr( 1 ) : search; 2797 2798 // encode complete URI 2799 return encodeURI( base + search ); 2800 2801 }; 2802 2803 /** 2804 * Replace url state with HTML5 API replaceState method 2805 * @param {boolean} remove 2806 */ 2807 proto.replaceState = function( remove ) { 2808 2809 if ( ( this.options.history || remove ) && this.browser.pushState && !this.states.push ) { 2810 2811 var prevData = window.history.state, 2812 data = { 2813 guid : !remove ? this.gallery.name : '', 2814 mid : !remove ? utils.modulo( this.gallery.initialLength, this.gallery.index ) + 1 : '' 2815 }; 2816 2817 if ( !prevData || prevData.mid !== data.mid ) { 2818 2819 var search = this.setQueryString( data ); 2820 2821 try { 2822 2823 window.history.replaceState( data, '', search ); 2824 2825 } catch (error) { 2826 2827 // disable history 2828 this.options.history = false; 2829 // display error message in console 2830 utils.error( 'SecurityError: A history state object with origin \'null\' cannot be created. Please run the script on a server.' ); 2831 2832 } 2833 2834 } 2835 2836 } 2837 2838 this.states.push = false; 2839 2840 }; 2841 2842 /** 2843 * Normalize mousewheel event accross browsers 2844 * @param {Object} event 2845 */ 2846 proto.normalizeWheel = function( event ) { 2847 2848 var ev = event || window.event, 2849 data = null, 2850 deltaX, 2851 deltaY, 2852 delta; 2853 2854 event.preventDefault(); 2855 2856 // old scrollwheel events 2857 if ( 'detail' in ev ) { 2858 deltaY = ev.detail * -1; 2859 } 2860 if ( 'wheelDelta' in ev ) { 2861 deltaY = ev.wheelDelta * -1; 2862 } 2863 if ( 'wheelDeltaY' in ev ) { 2864 deltaY = ev.wheelDeltaY * -1; 2865 } 2866 if ( 'wheelDeltaX' in ev ) { 2867 deltaX = ev.wheelDeltaX * -1; 2868 } 2869 2870 // W3C wheel event 2871 if ( 'deltaY' in ev ) { 2872 deltaY = ev.deltaY; 2873 } 2874 if ( 'deltaX' in ev ) { 2875 deltaX = ev.deltaX * -1; 2876 } 2877 2878 // deltaX/Y values are specified in lines 2879 if ( ev.deltaMode === 1 ) { 2880 2881 deltaX *= 40; 2882 deltaY *= 40; 2883 2884 // deltaX/Y values are specified in pages 2885 } else if ( ev.deltaMode === 2 ) { 2886 2887 deltaX *= 100; 2888 deltaY *= 100; 2889 2890 } 2891 2892 // normalize deltaX & deltaY 2893 delta = Math.abs( deltaX ) > Math.abs( deltaY ) ? deltaX : deltaY; 2894 delta = Math.min( 100, Math.max( -100, delta ) ); 2895 2896 // if no scroll happened 2897 if ( Math.abs( delta ) < this.options.scrollSensitivity ) { 2898 this.states.prevDelta = delta; 2899 return data; 2900 } 2901 2902 var time = +new Date(); 2903 // if mouse wheel velocity increases (touchpad or free wheel) 2904 // or elapsed time between mouse wheel events is superior to 60ms (user touchend touchpad or free wheel stopped) 2905 if ( Math.abs( delta ) > Math.abs( this.states.prevDelta ) || time - this.states.prevScroll > 60 ) { 2906 2907 // set data 2908 data = { 2909 'deltaX' : deltaX, 2910 'deltaY' : deltaY, 2911 'delta' : delta 2912 }; 2913 2914 } 2915 2916 // store last scroll data 2917 this.states.prevDelta = delta; 2918 this.states.prevScroll = time; 2919 2920 return data; 2921 2922 }; 2923 2924 // ----- Main function / events ----- // 2925 2926 /** 2927 * Reveal/hide share tooltip holder on click 2928 * @param {Object} event 2929 */ 2930 proto.share = function( event ) { 2931 2932 // prevent issue on Android with native HTML5 video controls 2933 if ( event && event.target.tagName === 'VIDEO' ) { 2934 return; 2935 } 2936 2937 var holder = this.DOM.holder, 2938 className = this.pre + '-open-tooltip', 2939 element = event ? event.target.className : null, 2940 method = utils.hasClass( holder, className ) ? 'remove' : 'add'; 2941 2942 if ( ( method === 'remove' && ( element !== this.pre + '-share' || !event ) ) || element === this.pre + '-share' ) { 2943 2944 if ( method === 'add' ) { 2945 this.setShareTooltip(); 2946 } 2947 2948 utils[method + 'Class']( holder, className ); 2949 2950 } 2951 2952 }; 2953 2954 /** 2955 * Share lightbox media from query url on social icon click 2956 * @param {Object} event 2957 */
2958 proto.shareOn = function( event ) { 2959 2960 var type = event.target.action, 2961 gallery = this.gallery, 2962 media = this.getMedia(), 2963 image = media.type === 'image' ? media.src : media.poster, 2964 url = this.socialMedia[type], 2965 uri; 2966 2967 if ( url ) { 2968 2969 // prepare uri from base url 2970 if ( this.options.sharedUrl === 'page' ) { 2971 2972 uri = [location.protocol, '//', location.host, location.pathname].join( '' ); 2973 2974 // prepare uri from deeplink 2975 } else if ( this.options.sharedUrl === 'deeplink' || ['iframe', 'HTML'].indexOf( media.type ) > -1 ) { 2976 2977 uri = this.setQueryString({ 2978 guid : gallery.name, 2979 mid : gallery.index + 1 2980 }); 2981 2982 // prepare uri from media file 2983 } else { 2984 2985 uri = media.src.replace( /\s/g, '' ).split( ',' )[0]; 2986 2987 if ( media.type === 'video' && media.format !== 'html5' ) { 2988 uri = media.share; 2989 } 2990 2991 } 2992 2993 // convert to absolute path if necessary 2994 var link = utils.createEl( 'a' ); 2995 link.href = image; 2996 image = link.href; 2997 link.href = uri; 2998 uri = link.href; 2999 3000 var tmp = utils.createEl( 'div' ); 3001 tmp.innerHTML = media.caption; 3002 var text = ( tmp.textContent || tmp.innerText ).replace( /\s+/g, ' ' ).trim() || ''; 3003 3004 url = url.replace( '[url]', encodeURIComponent( uri ) ) 3005 .replace( '[image]', encodeURIComponent( image ) ) 3006 .replace( '[text]', encodeURIComponent( text || document.title ) ); 3007 3008 if ( url ) { 3009 3010 var left = Math.round( window.screenX + ( window.outerWidth - 626 ) / 2 ), 3011 top = Math.round( window.screenY + ( window.outerHeight - 436 ) / 2 ); 3012 3013 window.open( url, this.pre + '_share', 'status=0,resizable=1,location=1,toolbar=0,width=626,height=436,top=' + top + ',left=' + left ); 3014 3015 } 3016 3017 } else { 3018 3019 utils.error( 'This social share media does not exist' ); 3020 3021 } 3022 3023 return false; 3024 3025 }; 3026 3027 /** 3028 * Set share tooltip position 3029 */ 3030 proto.setShareTooltip = function() { 3031 3032 if ( this.options.controls.indexOf( 'share' ) > -1 ) { 3033 3034 var attribute = 'right', 3035 tooltip = this.DOM.shareTooltip, 3036 width = tooltip.clientWidth, 3037 button = this.buttons.share.getBoundingClientRect(), 3038 position = button.left - width + button.width / 2 + 20; 3039 3040 if ( position < 0 ) { 3041 attribute = 'left'; 3042 position = button.left + button.width / 2 - 20; 3043 } 3044 3045 tooltip.setAttribute( 'data-position', attribute ); 3046 tooltip.style.top = this.DOM.topBar.height + 6 + 'px'; 3047 tooltip.style.left = position + 'px'; 3048 3049 } 3050 3051 }; 3052 3053 /** 3054 * Download a media, generate downloadable link 3055 */ 3056 proto.download = function() { 3057 3058 if ( !this.isDownloadable() ) { 3059 return false; 3060 } 3061 3062 var media = this.getMedia(), 3063 url = media.src.replace( /\s/g, '' ).split( ',' )[0], 3064 link = document.createElement( 'a' ), 3065 group = url.split( '/' ); 3066 3067 link.href = url; 3068 link.download = group.pop().split( '?' )[0]; 3069 link.setAttribute( 'target', '_blank' ); 3070 document.body.appendChild( link ); 3071 link.click(); 3072 document.body.removeChild( link ); 3073 3074 }; 3075 3076 /** 3077 * Handle click on fullScreen button 3078 */ 3079 proto.fullScreen = function() { 3080 3081 var fullScreenElement = this.browser.fullScreen.element; 3082 3083 if ( !document[fullScreenElement] ) { 3084 this.requestFullScreen(); 3085 } else { 3086 this.exitFullScreen(); 3087 } 3088 3089 }; 3090 3091 /** 3092 * Switch fullscreen state on fullScreen change 3093 */ 3094 proto.toggleFullScreen = function() { 3095 3096 var holder = this.DOM.holder, 3097 fullScreenElement = document[this.browser.fullScreen.element]; 3098 3099 if ( !fullScreenElement ) { 3100 3101 this.share(); 3102 this.states.fullScreen = false; 3103 utils.removeClass( holder, this.pre + '-fullscreen' ); 3104 3105 } else if ( fullScreenElement === holder ) { 3106 3107 // set share tooltip position (fix issue on Safari) 3108 this.setShareTooltip(); 3109 this.states.fullScreen = true; 3110 utils.addClass( holder, this.pre + '-fullscreen' ); 3111 3112 } 3113 3114 this.videoFullScreen(); 3115 3116 }; 3117 3118 /** 3119 * RequestFullScreen lightbox holder 3120 */ 3121 proto.requestFullScreen = function() { 3122 3123 var request = this.browser.fullScreen.request; 3124 3125 if ( document.documentElement[request] ) { 3126 this.DOM.holder[request](); 3127 } 3128 3129 }; 3130 3131 /** 3132 * ExitFullScreen lightbox holder 3133 */ 3134 proto.exitFullScreen = function() { 3135 3136 var exit = this.browser.fullScreen.exit; 3137 3138 if ( document[exit] ) { 3139 document[exit](); 3140 } 3141 3142 }; 3143 3144 // ----- Slideshow & count down timer ----- // 3145 3146 /**
3147 * Play slideshow event on click 3148 */ 3149 proto.play = function() { 3150 3151 if ( this.states.play ) { 3152 this.stopSlideShow(); 3153 } else { 3154 this.startSlideShow(); 3155 } 3156 3157 }; 3158 3159 /** 3160 * Start play and countdown timer 3161 */ 3162 proto.startSlideShow = function() { 3163 3164 var start = 0, 3165 gallery = this.gallery, 3166 options = this.options, 3167 loop = this.states.loop, 3168 autoStop = options.slideShowAutoStop, 3169 cycle = Math.max( 120, options.slideShowInterval ), 3170 count = options.countTimer, 3171 canvas = count && this.DOM.timer ? this.DOM.timer.getContext( '2d' ) : null; 3172 3173 var countDown = ( function( now ) { 3174 3175 now = !now ? +new Date() : now; 3176 start = !start ? now : start; 3177 3178 if ( !loop || autoStop ) { 3179 3180 if ( gallery.index === gallery.initialLength - 1 ) { 3181 3182 this.stopSlideShow(); 3183 return; 3184 3185 } 3186 3187 } 3188 3189 if ( count && canvas ) { 3190 3191 var percent = Math.min( 1, ( now - start + cycle ) / cycle - 1 ), 3192 radians = percent * 360 * ( Math.PI / 180 ); 3193 3194 canvas.clearRect( 0, 0, 48, 48 ); 3195 this.timerProgress( canvas, options.countTimerBg, 100 ); 3196 this.timerProgress( canvas, options.countTimerColor, radians ); 3197 3198 } 3199 3200 if ( now >= start + cycle ) { 3201 3202 start = now; 3203 this.slideTo( this.slides.index + 1, true ); 3204 3205 } 3206 3207 this.timer = requestAnimationFrame( countDown ); 3208 3209 }).bind( this ); 3210 3211 // reveal canvas timer 3212 utils.addClass( this.DOM.holder, this.pre + '-autoplay' ); 3213 // play state 3214 this.states.play = true; 3215 // start timer 3216 this.timer = requestAnimationFrame( countDown ); 3217 3218 }; 3219 3220 /** 3221 * Stop slideshow 3222 */ 3223 proto.stopSlideShow = function() { 3224 3225 cancelAnimationFrame( this.timer ); 3226 utils.removeClass( this.DOM.holder, this.pre + '-autoplay' ); 3227 this.states.play = false; 3228 3229 }; 3230 3231 /** 3232 * Canvas arc countdown timer progress 3233 * @param {Object} canvas 3234 * @param {string} color 3235 * @param {number} end 3236 */ 3237 proto.timerProgress = function( canvas, color, end ) { 3238 3239 var start = 1.5 * Math.PI; 3240 canvas.strokeStyle = color; 3241 canvas.lineWidth = 5; 3242 canvas.beginPath(); 3243 canvas.arc( 24, 24, 18, start, start + end, false ); 3244 canvas.stroke(); 3245 3246 }; 3247 3248 // ----- Handle videos ----- // 3249 3250 /** 3251 * Append video DOM in slide on click 3252 */ 3253 proto.appendVideo = function() { 3254 3255 var media = this.getMedia(); 3256 3257 if ( media.type !== 'video' ) { 3258 return; 3259 } 3260 3261 utils.addClass( media.dom, this.pre + '-loading' ); 3262 utils.removeClass( media.dom, this.pre + '-playing' ); 3263 3264 if ( !media.video ) { 3265 3266 // append HTML5 video 3267 if ( media.format === 'html5' ) { 3268 3269 // keep in memory video dom (for progression) 3270 media.video = utils.createEl( 'video' ); 3271 media.video.setAttribute( 'controls', '' ); 3272 media.video.setAttribute( 'autoplay', '' ); 3273 3274 var urls = media.src.replace( /\s/g, '' ).split( ',' ); 3275 3276 // loop through each video source 3277 for ( var i = 0; i < urls.length; i++ ) { 3278 3279 var frag = document.createDocumentFragment(), 3280 source = utils.createEl( 'source' ), 3281 type = ( /^.+\.([^.]+)$/ ).exec( urls[i] ); 3282 3283 // check if video type is HTML5 valid 3284 if ( type && ['mp4', 'webm', 'ogv'].indexOf( type[1] ) > -1 ) { 3285 3286 source.src = urls[i]; 3287 source.setAttribute( 'type', 'video/' + ( type[1] === 'ogv' ? 'ogg' : type[1] ) ); 3288 frag.appendChild( source ); 3289 3290 } 3291 3292 media.video.appendChild( frag ); 3293 3294 } 3295 3296 } 3297 // append iFrame video 3298 else if ( media.format ) { 3299 3300 // create iframe 3301 media.video = utils.createEl( 'iframe' ); 3302 media.video.src = media.src; 3303 media.video.setAttribute( 'frameborder', 0 ); 3304 media.video.setAttribute( 'allowfullscreen', '' ); 3305 3306 } 3307 3308 // set width/height 3309 media.video.setAttribute( 'width', '100%' ); 3310 media.video.setAttribute( 'height', '100%' ); 3311 3312 } 3313 3314 // append video if not exists 3315 if ( !media.dom.firstChild ) { 3316 3317 // append video if not exists 3318 media.dom.appendChild( media.video ); 3319 3320 // reset loaded for iframe to preload before reveal 3321 if ( media.format !== 'html5' ) { 3322 media.video.loaded = false; 3323 } 3324 3325 } 3326 3327 // play video 3328 this.playVideo( media ); 3329 3330 }; 3331 3332 /** 3333 * Play video 3334 * @param {Object} media 3335 */ 3336 proto.onVideoLoaded = function( media ) { 3337 3338 media.video.loaded = true; 3339 3340 utils.removeClass( media.dom, this.pre + '-loading' ); 3341 utils.addClass( media.dom, this.pre + '-playing' ); 3342 3343 this.cloneVideo( media ); 3344 3345 }; 3346 3347 /** 3348 * Clone video 3349 * allows to preserve video state if wrapped around (loop) 3350 * @param {Object} media 3351 */ 3352 proto.cloneVideo = function( media ) { 3353 3354 if ( this.states.loop && media.format === 'html5' ) { 3355 3356 var gallery = this.gallery, 3357 length = gallery.length, 3358 initial = gallery.initialLength, 3359 current = utils.modulo( initial, media.index ); 3360 3361 for ( var i = 0; i < length; i++ ) { 3362 3363 var index = utils.modulo( initial, gallery[i].index ); 3364 3365 if ( index === current && gallery[i].index !== media.index ) { 3366 gallery[i].video = media.video; 3367 } 3368 3369 } 3370 3371 } 3372 3373 }; 3374 3375 /** 3376 * Enable/disable iframe video fullscreen 3377 * Disable fullscreen iframe if lightbox in fullscreen 3378 * Prevent issue on webkit browsers 3379 */ 3380 proto.videoFullScreen = function() { 3381 3382 var media = this.getMedia(), 3383 fullScreen = this.states.fullScreen;
3384 3385 if ( media.type === 'video' && media.format !== 'html5' && media.video ) { 3386 3387 media.video[fullScreen ? 'removeAttribute' : 'setAttribute']( 'allowfullscreen', '' ); 3388 3389 } 3390 3391 }; 3392 3393 /** 3394 * Play video 3395 * @param {{player}} media 3396 */ 3397 proto.playVideo = function( media ) { 3398 3399 // if video already loaded 3400 if ( media.video.loaded ) { 3401 3402 // trigger reflow to apply css transition 3403 media.video.getClientRects(); 3404 3405 utils.removeClass( media.dom, this.pre + '-loading' ); 3406 utils.addClass( media.dom, this.pre + '-playing' ); 3407 3408 if ( media.format !== 'html5' ) { 3409 3410 if ( media.play ) { 3411 3412 var message = typeof media.play === 'object' ? JSON.stringify( media.play ) : String( media.play ); 3413 media.video.contentWindow.postMessage( message, '*' ); 3414 3415 } 3416 3417 } else if ( !media.video.error ) { 3418 3419 if ( typeof MediaElementPlayer === 'function' && this.options.mediaelement ) { 3420 3421 // because on IOS/Android the native HTML5 player is preserved 3422 var mejs = ( media.video.tagName === 'VIDEO' ) ? media.video : media.video.getElementsByTagName( 'video' )[0]; 3423 3424 if ( mejs.player ) { 3425 mejs.player.setControlsSize(); 3426 } 3427 3428 mejs.play(); 3429 3430 } else { 3431 3432 media.video.play(); 3433 3434 } 3435 3436 } 3437 3438 } else { 3439 3440 var _this = this; 3441 3442 // if mediaelement player 3443 if ( typeof MediaElementPlayer === 'function' && !media.play && this.options.mediaelement && !media.video.player ) { 3444 3445 new MediaElementPlayer( media.video, { 3446 features : ['playpause', 'stop', 'current', 'progress', 'duration', 'volume', 'fullscreen'], 3447 videoVolume : 'horizontal', 3448 startVolume : 0.8, 3449 classPrefix : 'mejs-', 3450 keyActions : [], 3451 enableKeyboard : false, 3452 iPadUseNativeControls : true, 3453 iPhoneUseNativeControls : true, 3454 AndroidUseNativeControls : true, 3455 success : function( mejs ) { 3456 3457 mejs.addEventListener( 'loadeddata', function() { 3458 3459 // get right DOM (because on mobile devices preserve original HTML5 player) 3460 // (iPadUseNativeControls, iPhoneUseNativeControls, AndroidUseNativeControls) 3461 media.video = media.dom.lastChild; 3462 3463 // if video is still in a slide 3464 if ( media.video ) { 3465 3466 // remove offscreen div to prevent display issue 3467 var offScreen = media.video.previousSibling; 3468 if ( offScreen && offScreen.parentNode ) { 3469 offScreen.parentNode.removeChild( offScreen ); 3470 } 3471 3472 _this.onVideoLoaded( media ); 3473 3474 } 3475 3476 }, media, false ); 3477 3478 }, 3479 error : function() { 3480 3481 _this.onVideoLoaded( media ); 3482 3483 } 3484 }); 3485 3486 } else if ( !media.video.onload ) { 3487 3488 media.video.onload = 3489 media.video.onerror = 3490 media.video.onloadedmetadata = function() { 3491 3492 // if video is still in a slide 3493 if ( media.dom.firstChild ) { 3494 3495 _this.onVideoLoaded( media ); 3496 _this.videoFullScreen(); 3497 3498 } 3499 3500 }; 3501 3502 media.video.src = media.src.replace( /\s/g, '' ).split( ',' )[0]; 3503 3504 } 3505 3506 } 3507 3508 }; 3509 3510 /** 3511 * Pause video 3512 */ 3513 proto.pauseVideo = function() { 3514 3515 var media = this.getMedia(); 3516 3517 if ( media && media.type === 'video' && media.video ) { 3518 3519 utils.removeClass( media.dom, this.pre + '-playing' ); 3520 3521 // prevent iframe to autoplay if pause while loading 3522 if ( !media.video.loaded ) { 3523 3524 media.dom.innerHTML = ''; 3525 utils.removeClass( media.dom, this.pre + '-loading' ); 3526 return; 3527 3528 } 3529 3530 if ( media.format === 'html5' ) { 3531 3532 if ( typeof MediaElementPlayer === 'function' && this.options.mediaelement ) { 3533 3534 // because on IOS/Android the native HTML5 player is preserved 3535 var mejs = ( media.video.tagName === 'VIDEO' ) ? media.video : media.video.getElementsByTagName( 'video' )[0]; 3536 mejs.pause(); 3537 3538 } else { 3539 3540 media.video.pause(); 3541 3542 } 3543 3544 } else { 3545 3546 if ( media.pause && media.format !== 'dailymotion' ) { 3547 3548 var message = typeof media.pause === 'object' ? JSON.stringify( media.pause ) : String( media.pause ); 3549 media.video.contentWindow.postMessage( message, '*' ); 3550 3551 // to handle embed videos with unknown postMessage methods 3552 } else { 3553 3554 media.dom.innerHTML = ''; 3555 media.video = null; 3556 3557 } 3558 3559 } 3560 3561 } 3562 3563 }; 3564 3565 // ----- Handle Media loading/sizing ----- // 3566 3567 /** 3568 * Append media in slider 3569 * @param {number} media_index 3570 * @param {number} slide_index 3571 */ 3572 proto.insertMedia = function( media_index, slide_index ) { 3573 3574 // get media from current gallery 3575 var media = this.gallery[media_index]; 3576 3577 if ( !media ) { 3578 return; 3579 } 3580 3581 // assign an index if not exists 3582 // necessary if manually pushed from the API 3583 if ( typeof media.index === 'undefined' ) { 3584 media.index = this.gallery.indexOf( media ); 3585 } 3586 3587 this.buildMedia( media ); 3588 this.appendMedia( media, slide_index ); 3589 this.loadMedia( media, slide_index ); 3590 3591 }; 3592 3593 /** 3594 * Build media DOM 3595 * @param {Object} media 3596 */ 3597 proto.buildMedia = function( media ) { 3598 3599 // if the media is not defined 3600 if ( typeof media.dom === 'undefined' ) { 3601 3602 // detect media type 3603 switch ( media.type ) { 3604 3605 case 'image': 3606 media.dom = utils.createEl( 'img', this.pre + '-img' ); 3607 media.dom.src = media.src; 3608 break; 3609 3610 case 'video': 3611 media.dom = utils.createEl( 'div', this.pre + '-video' ); 3612 if ( media.poster ) { 3613 media.dom.style.backgroundImage = 'url("' + media.poster + '")'; 3614 } else { 3615 media.dom.loaded = true; 3616 } 3617 break; 3618 3619 case 'iframe': 3620 media.dom = utils.createEl( 'iframe', this.pre + '-iframe' ); 3621 media.dom.setAttribute( 'allowfullscreen', '' ); 3622 media.dom.setAttribute( 'frameborder', 0 ); 3623 media.dom.src = media.src; 3624 break; 3625 3626 case 'HTML': 3627 var element = media.src; 3628 var content = document.querySelector( element ); 3629 media.dom = utils.createEl( 'div', this.pre + '-html' ); 3630 media.dom.appendChild( utils.createEl( 'div', this.pre + '-html-inner' ) ); 3631 media.dom.firstChild.innerHTML = content ? content.innerHTML : null; 3632 media.src = content ? content : ''; 3633 media.dom.loaded = true; 3634 break; 3635 3636 } 3637 3638 // handle no media type & not content found (HMTL src empty for example) 3639 if ( !media.type || !media.src ) { 3640 3641 // build no content message 3642 media.dom = utils.createEl( 'div', this.pre + '-error' ); 3643 media.dom.textContent = this.options.noContent; 3644 media.dom.loaded = true; 3645 media.dom.error = true; 3646 3647 // dispatch load error event 3648 utils.dispatchEvent( this, 'modulobox', 'noContent', this.gallery.name, parseInt( media.index, 10 ) ); 3649 3650 } 3651 3652 } 3653 3654 }; 3655 3656 /** 3657 * Append in a slide media DOM 3658 * @param {Object} media 3659 * @param {number} slide_index 3660 */ 3661 proto.appendMedia = function( media, slide_index ) { 3662 3663 // get media slide 3664 var slide = this.slides[slide_index], 3665 holder = slide.firstChild, 3666 loader; 3667 3668 // if slide empty 3669 if ( !holder.childElementCount ) { 3670 3671 var fragment = document.createDocumentFragment(); 3672 3673 loader = utils.createEl( 'div', this.pre + '-loader' ); 3674 fragment.appendChild( loader ); 3675 fragment.appendChild( media.dom ); 3676 holder.appendChild( fragment ); 3677 3678 // if a media is already present in the slide 3679 } else { 3680 3681 var oldMedia = holder.lastChild; 3682 loader = holder.firstChild; 3683 3684 loader.style.visibility = ''; 3685 3686 // prevent unnecessary DOM manipulations if media already exists in slide 3687 if ( media.dom !== oldMedia ) { 3688 3689 // prevent to remove and duplicate media from another slide (because of replaceChild) 3690 // small hack for low performance (like IE9 or too much long key frames) 3691 // slideTo() method already includes a throttle (120ms) which will also fix it more naturally 3692 var method = holder.childElementCount === 1 ? 'appendChild' : 'replaceChild'; 3693 holder[method]( media.dom, oldMedia ); 3694 3695 } 3696 3697 } 3698 3699 // Assign media index to slide 3700 slide.media = media.index; 3701 3702 }; 3703 3704 /** 3705 * Load image/iframe 3706 * @param {Object} media 3707 * @param {number} slide_index 3708 */ 3709 proto.loadMedia = function( media, slide_index ) { 3710 3711 // show immediatly media if already loaded 3712 if ( media.dom.loaded ) { 3713 3714 this.showMedia( media, slide_index ); 3715 return; 3716 3717 } 3718 3719 var _this = this, 3720 dom = media.type === 'iframe' ? media.dom : media.dom.img = new Image(); 3721 3722 // on media load complete 3723 var onComplete = function() { 3724 3725 // dispatch loaded event 3726 if ( !media.dom.error ) { 3727 utils.dispatchEvent( _this, 'modulobox', 'loadComplete', _this.gallery.name, parseInt( media.index, 10 ) ); 3728 } 3729 3730 // need to reload iframe because loaded DOM can't be stored due to cross-origin policy
3731 media.dom.loaded = media.type !== 'iframe' ? true : false; 3732 // reveal media 3733 _this.showMedia( media, slide_index ); 3734 3735 }; 3736 3737 // handle onload events 3738 dom.onload = onComplete; 3739 3740 // handle onerror event 3741 dom.onerror = function(e) { 3742 3743 // set error message 3744 // not for video because it only concerns a poster and not the media itself 3745 if ( media.type !== 'video' ) { 3746 3747 // build load error message 3748 media.dom = utils.createEl( 'p', _this.pre + '-error' ); 3749 media.dom.textContent = _this.options.loadError; 3750 media.dom.error = true; 3751 3752 // append error message 3753 _this.appendMedia( media, slide_index ); 3754 3755 } 3756 3757 // dispatch loaded error event 3758 utils.dispatchEvent( _this, 'modulobox', 'loadError', _this.gallery.name, parseInt( media.index, 10 ) ); 3759 3760 // trigger complete to reveal error 3761 onComplete(); 3762 3763 }; 3764 3765 // set src 3766 dom.src = ( media.type === 'video' ) ? media.poster : media.src; 3767 3768 }; 3769 3770 /** 3771 * Unload media src 3772 * @param {Object} slide 3773 */ 3774 proto.unloadMedia = function( slide ) { 3775 3776 if ( !this.gallery ) { 3777 return; 3778 } 3779 3780 var index = slide.media, 3781 media = this.gallery[index]; 3782 3783 if ( !media || !media.dom ) { 3784 return; 3785 } 3786 3787 // cancel old media if exists (and option enabled) 3788 if ( this.options.unload && media.type === 'image' && !media.dom.loaded && !media.dom.complete && !media.dom.naturalWidth ) { 3789 3790 // unset events and src 3791 media.dom.onload = null; 3792 media.dom.onerror = null; 3793 media.dom.src = ''; 3794 3795 if ( media.dom.img ) { 3796 3797 // unset events and src for img object 3798 media.dom.img.onload = null; 3799 media.dom.img.onerror = null; 3800 media.dom.img.src = ''; 3801 delete media.dom.img; 3802 3803 } 3804 3805 // unset dom to reset src from buildMedia method 3806 delete media.dom; 3807 3808 } 3809 // remove iframe video (prevent autoplay if loop enable) 3810 else if ( media.type === 'video' && media.format !== 'html5' && media.dom.firstChild ) { 3811 3812 media.video = null; 3813 media.dom.removeChild( media.dom.firstChild ); 3814 3815 } 3816 3817 }; 3818 3819 /** 3820 * Remove all content present in slide 3821 * @param {Object} holder 3822 */ 3823 proto.removeMedia = function( holder ) { 3824 3825 var content = holder.firstChild; 3826 3827 if ( !content ) { 3828 return; 3829 } 3830 3831 while ( content.firstChild ) { 3832 content.removeChild( content.firstChild ); 3833 } 3834 3835 }; 3836 3837 /** 3838 * Reveal media content in slide 3839 * @param {Object} media 3840 * @param {number} slide_index 3841 */ 3842 proto.showMedia = function( media, slide_index ) { 3843 3844 // get slider states 3845 var slider = this.slider; 3846 3847 // if option fade when settled, check if slider settled to reveal 3848 if ( this.options.fadeIfSettle && !slider.settle && !media.dom.revealed ) { 3849 return; 3850 } 3851 3852 // get slide DOM 3853 var slide = this.slides[slide_index], 3854 gallery = this.gallery, 3855 holder = slide.firstChild, 3856 loader = holder.firstChild, 3857 preload = this.options.preload; 3858 3859 // set media size 3860 this.setMediaSize( media, slide ); 3861 3862 // check if media is zoomable 3863 if ( media.index === gallery.index ) { 3864 this.isZoomable(); 3865 } 3866 3867 // reveal media object 3868 // even if media is not in a slide, it will be revealed in media object (stored in gallery obj) 3869 utils.addClass( media.dom, this.pre + '-media-loaded' ); 3870 media.dom.revealed = true; 3871 3872 // if current media match slide 3873 if ( slide.media === media.index ) { 3874 3875 // hide loader 3876 loader.style.visibility = 'hidden'; 3877 3878 // increment number of media loaded in the gallery 3879 gallery.loaded += 1; 3880 3881 // set closest media 3882 if ( gallery.loaded === preload && preload < 4 ) { 3883 this.setMedia( preload + 2 ); 3884 } 3885 3886 } 3887 3888 // if iframe than reset loaded 3889 // because we can't store content due to cross-origin policy 3890 if ( media.type === 'iframe' ) { 3891 media.dom.loaded = false; 3892 } 3893 3894 3895 }; 3896 3897 /** 3898 * Calculate and set media size styles 3899 * @param {Object} media 3900 * @param {Object} slide 3901 */ 3902 proto.setMediaSize = function( media, slide ) { 3903 3904 var object = media.dom, 3905 slider = this.slider, 3906 viewport = object.viewport, 3907 thumbs = this.getThumbHeight(); 3908 3909 // if error no size to calculate 3910 if ( object.error ) { 3911 return; 3912 } 3913 3914 // if the media was not already sized for the current slider viewport 3915 // prevent unnecessary calculations and reflows from caption calculations (clientHeight) 3916 if ( !viewport || 3917 viewport.width !== slider.width || 3918 viewport.height !== slider.height - thumbs ) { 3919 3920 this.getCaptionHeight( media, slide ); 3921 this.getMediaSize( media, slide ); 3922 this.fitMediaSize( media, slide ); 3923 this.setMediaOffset( media, slide ); 3924 3925 } 3926 3927 var style = object.style; 3928 3929 // media is not displayed (no reflow) 3930 style.width = object.size.width + 'px'; 3931 style.height = object.size.height + 'px'; 3932 style.left = object.offset.left + 'px'; 3933 style.top = object.offset.top + 'px'; 3934 3935 }; 3936 3937 /** 3938 * Calculate caption height & assign slide size 3939 * @param {Object} media 3940 * @param {Object} slide 3941 */ 3942 proto.getCaptionHeight = function( media, slide ) { 3943 3944 var caption = this.DOM.captionInner, 3945 topBar = this.DOM.topBar.height,
3946 content = caption.innerHTML, 3947 thumbs = this.getThumbHeight(); 3948 3949 if ( this.options.caption && this.states.caption && media.caption ) { 3950 3951 // set media caption 3952 caption.innerHTML = media.caption; 3953 // get caption height 3954 caption.height = Math.max( topBar, parseInt( caption.clientHeight, 10 ) ) || topBar; 3955 // restore current caption 3956 caption.innerHTML = content; 3957 3958 // if caption is hidden 3959 } else { 3960 3961 // set caption height to top bar 3962 // allow to center image in the viewport 3963 // if thumbnails fit to the top of the thumbs slider 3964 caption.height = thumbs ? 0 : topBar; 3965 3966 } 3967 3968 // set slide size (not real size, just an helper) 3969 slide.width = this.slider.width; 3970 slide.height = this.slider.height - topBar - caption.height - thumbs; 3971 3972 }; 3973 3974 /** 3975 * Calculate natural media size 3976 * @param {Object} media 3977 * @param {Object} slide 3978 */ 3979 proto.getMediaSize = function( media, slide ) { 3980 3981 var size = media.dom.size = {}; 3982 3983 switch ( media.type ) { 3984 3985 case 'image': 3986 size.width = media.dom.naturalWidth; 3987 size.height = media.dom.naturalHeight; 3988 break; 3989 3990 case 'video': 3991 size.width = this.options.videoMaxWidth; 3992 size.height = size.width / this.options.videoRatio; 3993 break; 3994 3995 case 'iframe': 3996 size.width = media.width ? media.width : slide.width > 680 ? slide.width * 0.8 : slide.width; 3997 size.height = media.height ? media.height : slide.height; 3998 break; 3999 4000 case 'HTML': 4001 size.width = media.width ? media.width : slide.width; 4002 size.height = media.height ? media.height : slide.height; 4003 break; 4004 4005 } 4006 4007 }; 4008 4009 /** 4010 * Fit media size to slide 4011 * @param {Object} media 4012 * @param {Object} slide 4013 */ 4014 proto.fitMediaSize = function( media, slide ) { 4015 4016 var slider = this.slider, 4017 options = this.options, 4018 zoom = options.zoomTo, 4019 size = media.dom.size, 4020 ratio = size.width / size.height, 4021 thumbs = this.getThumbHeight(), 4022 smallDevice = slider.width <= 480 || slider.height <= 680, 4023 canOverflow = ['video', 'iframe', 'HTML'].indexOf( media.type ) < 0, 4024 smartResize = options.smartResize && smallDevice, 4025 width, height; 4026 4027 // add slide height viewport 4028 var viewports = [slide.height]; 4029 4030 // add slider height viewport 4031 if ( ( smartResize || options.overflow ) && canOverflow ) { 4032 4033 viewports.unshift( slider.height - thumbs ); 4034 4035 } 4036 4037 // check if media size fit to viewports (viewport) 4038 viewports.forEach( function( viewport ) { 4039 4040 if ( !height || height < slider.height - thumbs ) { 4041 4042 width = Math.min( size.width, ratio * viewport ); 4043 width = width > slide.width ? slide.width : Math.round( width ); 4044 height = Math.ceil( 1 / ratio * width ); 4045 height = height % viewport < 2 ? viewport : height; 4046 4047 } 4048 4049 }); 4050 4051 // Calculate from how much the image can be zoomed 4052 var scale = Number( ( size.width / width ).toFixed( 3 ) ); 4053 zoom = zoom === 'auto' ? scale : zoom; 4054 4055 media.dom.size = { 4056 width : width, 4057 height : height, 4058 scale : scale >= options.minZoom ? Math.min( options.maxZoom, zoom ) : 1 4059 }; 4060 4061 }; 4062 4063 /** 4064 * Calculate media offset 4065 * @param {Object} media 4066 * @param {Object} slide 4067 */ 4068 proto.setMediaOffset = function( media, slide ) { 4069 4070 var size = media.dom.size, 4071 slider = this.slider, 4072 topBar = this.DOM.topBar.height, 4073 thumbs = this.getThumbHeight(), 4074 fromTop = 0; 4075 4076 // if the media can be centered 4077 if ( size.height <= slide.height ) { 4078 4079 // center from the available slide height 4080 fromTop = topBar + ( slide.height - size.height ) * 0.5; 4081 4082 } 4083 4084 // set media offset 4085 media.dom.offset = { 4086 top : fromTop < 0 ? 0 : Math.round( fromTop ), 4087 left : Math.round( ( slide.width - size.width ) * 0.5 ) 4088 }; 4089 4090 // set media viewport 4091 media.dom.viewport = { 4092 width : slider.width, 4093 height : slider.height - thumbs 4094 }; 4095 4096 }; 4097 4098 /** 4099 * Calculate media viewport 4100 * @param {number} scale 4101 * @return {Object} 4102 * hGap : difference between slider height and image size 4103 * vGap : difference image offset and image overflow 4104 * cGap : distance between image center and slider center 4105 * sGap : cGap scaled 4106 * these values allows to take into account the image center gap regarding the slide center 4107 */ 4108 proto.mediaViewport = function( scale ) { 4109 4110 var media = this.getMedia();
4111 4112 // if loading or not a media (error) 4113 if ( !media.dom || !media.dom.size ) { 4114 4115 return { 4116 top : 0, 4117 bottom : 0, 4118 left : 0, 4119 right : 0 4120 }; 4121 4122 } 4123 4124 // calculate media viewport 4125 var size = media.dom.size, 4126 offset = media.dom.offset, 4127 height = this.slider.height, 4128 width = this.slider.width, 4129 hGap = ( height - size.height ) * 0.5, 4130 vGap = offset.top * 2 - hGap, 4131 cGap = ( hGap - vGap ) * 0.5, 4132 sGap = cGap * scale - cGap * 2 - vGap, 4133 left = size.width / 2 * ( scale - 1 ) - offset.left, 4134 top = size.height * scale <= height ? cGap * scale : -size.height / 2 * ( scale - 1 ) + height - size.height + sGap, 4135 bottom = size.height * scale <= height ? cGap * scale : size.height / 2 * ( scale - 1 ) + sGap; 4136 4137 // set panZoom viewport 4138 return { 4139 top : scale <= 1 ? 0 : Math.round( top ), 4140 bottom : scale <= 1 ? 0 : Math.round( bottom ), 4141 left : size.width * scale < width ? 0 : Math.round( left ), 4142 right : size.width * scale < width ? 0 : Math.round( -left ) 4143 }; 4144 4145 }; 4146 4147 /** 4148 * Set/add media in slides 4149 * @param {number} number 4150 */ 4151 proto.setMedia = function( number ) { 4152 4153 var gallery = this.gallery, 4154 slides = this.slides, 4155 loop = this.states.loop, 4156 RTL = this.isRTL(), 4157 index = Math.round( - RTL * this.slider.position.x / slides.width ), 4158 length = gallery.initialLength - 1, 4159 adjust = 0, 4160 toLoad = [], 4161 i; 4162 4163 // find number of media to load 4164 if ( !number && !gallery.loaded ) { 4165 4166 number = 0; 4167 4168 for ( i = 0; i < slides.length; i++ ) { 4169 4170 if ( slides[i].firstChild.childElementCount ) { 4171 number++; 4172 } 4173 4174 } 4175 4176 number += 2; 4177 gallery.loaded = this.options.preload; 4178 4179 } 4180 4181 // prepare positions 4182 switch ( number ) { 4183 case 0: 4184 case 1: 4185 toLoad = [0]; 4186 break; 4187 case 2: 4188 case 3: 4189 toLoad = [-1, 0, 1]; 4190 break; 4191 default: 4192 number = 5; 4193 toLoad = [-2, -1, 0, 1, 2]; 4194 } 4195 4196 // adjust positions to ends if no loop 4197 if ( !loop ) { 4198 4199 var maxMedia = index + toLoad[number - 1], 4200 minMedia = index + toLoad[0]; 4201 4202 adjust = ( minMedia < 0 ) ? -minMedia : 0; 4203 adjust = ( maxMedia > length ) ? length - maxMedia : adjust; 4204 4205 } 4206 4207 // convert positions to media index 4208 toLoad = toLoad.map( function( i ) { 4209 4210 return utils.modulo( gallery.length, i + adjust + index ); 4211 4212 }); 4213 4214 // insert media in corresponding slides 4215 for ( i = 0; i < slides.length; i++ ) { 4216 4217 var slide = slides[i], 4218 media_index = utils.modulo( gallery.length, slide.index ); 4219 4220 // if no loop && media index bigger than slide index 4221 if ( !loop && slide.index > media_index ) { 4222 continue; 4223 } 4224 4225 // if the media corresponds to a slide and is not already assigned 4226 if ( toLoad.indexOf( media_index ) > -1 && slide.media !== media_index ) { 4227 4228 // unload previous media 4229 this.unloadMedia( slide ); 4230 // insert media in slide 4231 this.insertMedia( media_index, i ); 4232 4233 } 4234 4235 } 4236 4237 }; 4238 4239 /** 4240 * Update media info based on current slide index 4241 */ 4242 proto.updateMediaInfo = function() { 4243 4244 var slides = this.slides, 4245 gallery = this.gallery; 4246 4247 // update gallery index 4248 gallery.index = utils.modulo( gallery.length, slides.index ); 4249 4250 this.isZoomable(); 4251 this.isDownloadable(); 4252 this.updateCounter(); 4253 this.updateCaption(); 4254 this.updateThumbs(); 4255 4256 // dispatch update event 4257 utils.dispatchEvent( this, 'modulobox', 'updateMedia', this.getMedia() ); 4258 4259 }; 4260 4261 /** 4262 * Set thumbnails 4263 */ 4264 proto.setThumbs = function() { 4265 4266 var gallery = this.gallery, 4267 thumbs = this.thumbs, 4268 length = gallery.initialLength, 4269 thumbnails = this.options.thumbnails, 4270 thumbsHolder = this.DOM.thumbsHolder; 4271 4272 // return if no thumb opt or if only one thumbnail to display 4273 if ( !thumbnails || length < 2 ) { 4274 4275 // hide thumbs and reset caption position 4276 this.DOM.caption.style.bottom = 0; 4277 thumbsHolder.style.visibility = 'hidden'; 4278 thumbsHolder.style.height = 0; 4279 4280 // unset thumbs 4281 thumbs.height = 4282 thumbs.gutter = 0; 4283 4284 return; 4285 4286 } 4287 4288 // get max screen width available on the current device 4289 var sizes = this.options.thumbnailSizes, 4290 screenW = Math.max( window.innerWidth, Math.max( screen.width, screen.height ) ), 4291 thumbNb = 0, 4292 i; 4293 4294 // sort thumbnail browser widths 4295 var widths = Object.keys( sizes ).sort( function( a, b ) { 4296 return a - b; 4297 }); 4298
4299 // find number of thumbnails needed to build the thumbnails slider 4300 for ( i = 0; i < widths.length; i++ ) { 4301 4302 var size = widths[i], 4303 width = i === widths.length - 1 ? screenW : Math.min( screenW, size ), 4304 number = Math.ceil( width / ( sizes[size].width + sizes[size].gutter ) * 2 ); 4305 4306 if ( isFinite( number ) && number > thumbNb ) { 4307 thumbNb = number; 4308 } 4309 4310 if ( size >= screenW ) { 4311 break; 4312 } 4313 4314 } 4315 4316 // set the number of thumbnail requiered for the current gallery 4317 var fragment = document.createDocumentFragment(); 4318 length = length > 50 ? Math.min( thumbNb, length ) : length; 4319 4320 // prepare thumbnails 4321 for ( i = 0; i < length; i++ ) { 4322 4323 var thumb = utils.createEl( 'div', this.pre + '-thumb' ); 4324 fragment.appendChild( thumb ); 4325 4326 } 4327 4328 // append thumbnails 4329 this.DOM.thumbsInner.appendChild( fragment ); 4330 // set thumbnails position 4331 this.setThumbsPosition(); 4332 4333 }; 4334 4335 /** 4336 * Set thumbnails 4337 * @param {Object} event 4338 */ 4339 proto.thumbClick = function( event ) { 4340 4341 var target = event.target; 4342 4343 if ( !utils.hasClass( target, this.pre + '-thumb' ) ) { 4344 target = target.parentNode; 4345 } 4346 4347 if ( parseInt( target.index, 10 ) >= 0 ) { 4348 this.slideTo( target.index ); 4349 } 4350 4351 }; 4352 4353 /** 4354 * Set & init animation (slider & cells) 4355 * @param {Object} thumb 4356 * @param {number} index 4357 */ 4358 proto.loadThumb = function( thumb, index ) { 4359 4360 // get media 4361 var media = this.gallery[index], 4362 src; 4363 4364 // if the thumbnail was not loaded (is not DOM object) 4365 if ( !media.thumb || typeof media.thumb !== 'object' ) { 4366 4367 src = media.thumb; 4368 media.thumb = utils.createEl( 'div', this.pre + '-thumb-bg' ); 4369 media.thumb.style.backgroundImage = src && src.indexOf( '.json' ) < 0 ? 'url(' + src + ')' : null; 4370 4371 // add video icon 4372 if ( media.type === 'video' ) { 4373 4374 utils.addClass( media.thumb, this.pre + '-thumb-video' ); 4375 utils.addClass( media.thumb, this.pre + '-thumb-loaded' ); 4376 4377 } 4378 4379 } 4380 4381 // append/replace thumbnail 4382 var method = thumb.firstChild ? 'replaceChild' : 'appendChild'; 4383 thumb[method]( media.thumb, thumb.firstChild ); 4384 4385 // save media index to thumb 4386 thumb.media = index; 4387 4388 // load thumbnail 4389 if ( src ) { 4390 4391 var dom = new Image(); 4392 4393 // handle onload events (no need to handle error/abort) 4394 dom.onload = function() { 4395 4396 // reveal thumbnail 4397 utils.addClass( media.thumb, this.pre + '-thumb-loaded' ); 4398 4399 }.bind( this ); 4400 4401 // set image src 4402 dom.src = src; 4403 4404 } 4405 4406 }; 4407 4408 /** 4409 * Update thumbnail active 4410 */ 4411 proto.updateThumbs = function() { 4412 4413 var gallery = this.gallery; 4414 4415 if ( !this.options.thumbnails || gallery.initialLength < 2 ) { 4416 return; 4417 } 4418 4419 var thumbs = this.thumbs, 4420 position = this.getThumbPosition( thumbs ); 4421 4422 // stop current animation 4423 thumbs.stopAnimate(); 4424 4425 // if thumb slider already at the right position 4426 if ( position === thumbs.position.x ) { 4427 4428 this.shiftThumbs( thumbs ); 4429 return; 4430 4431 } 4432 4433 // move thumbnails without animation 4434 if ( Math.abs( position - thumbs.position.x ) > 50 * thumbs.size ) { 4435 4436 this.DOM.thumbsHolder.style.visibility = 'hidden'; 4437 thumbs.position.x = position; 4438 utils.translate( this.DOM.thumbsInner, position, 0 ); 4439 this.renderThumbs( thumbs ); 4440 this.DOM.thumbsHolder.style.visibility = ''; 4441 4442 // move thumbnails 4443 } else { 4444 4445 thumbs.startAnimate(); 4446 thumbs.releaseDrag(); 4447 thumbs.animateTo({ 4448 x : position 4449 }); 4450 4451 } 4452 4453 }; 4454 4455 /** 4456 * Set caption height 4457 */ 4458 proto.updateCaption = function() { 4459 4460 if ( this.options.caption ) { 4461 4462 // get caption content 4463 var media = this.getMedia(), 4464 content = media.caption ? media.caption : '', 4465 caption = this.DOM.captionInner; 4466 4467 // set the caption content if not already present 4468 if ( caption.innerHTML !== content ) { 4469 caption.innerHTML = content; 4470 } 4471 4472 } 4473 4474 }; 4475 4476 /** 4477 * Set counter index 4478 */ 4479 proto.updateCounter = function() { 4480 4481 if ( this.options.counterMessage ) { 4482 4483 var gallery = this.gallery, 4484 length = gallery.initialLength, 4485 index = utils.modulo( length, gallery.index ), 4486 message = this.options.counterMessage, 4487 content = message.replace( '[index]', index + 1 ).replace( '[total]', length ), 4488 counter = this.DOM.counter; 4489 4490 // set the counter if not already set 4491 if ( counter.textContent !== content ) { 4492 counter.textContent = content; 4493 } 4494 4495 } 4496 4497 }; 4498 4499 // ----- Set position and animations ----- // 4500 4501 /** 4502 * Wrap media around for infinite loop 4503 */ 4504 proto.wrapAround = function() { 4505 4506 var loop = this.options.loop, 4507 gallery = this.gallery, 4508 length = gallery.length; 4509 4510 // store initial length 4511 if ( !gallery.initialLength ) { 4512 gallery.initialLength = length; 4513 } 4514 4515 // set loop state 4516 this.states.loop = loop && loop <= length ? true : false; 4517 4518 // add fake media if infinite loop and media length inferior to slides length 4519 if ( this.states.loop && length < this.slides.length ) { 4520 4521 var add = Math.ceil( this.slides.length / length ) * length - length; 4522 4523 for ( var i = 0; i < add; i++ ) { 4524 4525 var index = length + i; 4526 gallery[index] = utils.cloneObject( gallery[utils.modulo( length, i )] ); 4527 gallery[index].index = index; 4528 4529 } 4530 4531 } 4532 4533 }; 4534 4535 /** 4536 * Set slider 4537 */ 4538 proto.setSlider = function() { 4539 4540 var slider = this.slider, 4541 slides = this.slides; 4542 4543 this.setSizes( slider, slides ); 4544 this.setSliderPosition( slider, slides ); 4545 this.setSlidesPositions( slides ); 4546 4547 // show overlay 4548 this.DOM.overlay.style.opacity = 1; 4549 4550 }; 4551 4552 /** 4553 * Set slider and slide sizes 4554 * @param {Object} slider 4555 * @param {Object} slides 4556 */ 4557 proto.setSizes = function( slider, slides ) { 4558 4559 // prevent issue on Google Chrome and mobile devices on orientation change 4560 // this.DOM.holder.clientWidth wrong on mobile devices with Chrome because fixed position 4561 slider.width = document.body.clientWidth; 4562 slider.height = window.innerHeight; // always right value on IOS devices (prevent Safari top bar issue on resize) 4563 slides.width = slider.width + Math.round( slider.width * this.options.spacing ); 4564 4565 }; 4566 4567 /** 4568 * Set slides/cells positions 4569 * @param {Object} slides 4570 */ 4571 proto.setSlidesPositions = function( slides ) { 4572 4573 for ( var i = 0; i < slides.length; i++ ) { 4574 4575 slides[i].position = null; 4576 this.setCellPosition( i ); 4577 4578 } 4579 4580 this.shiftSlides(); 4581 4582 }; 4583 4584 /** 4585 * Set thumbnails positions 4586 */ 4587 proto.setThumbsPosition = function() { 4588 4589 if ( !this.options.thumbnails || this.gallery.initialLength < 2 ) { 4590 return; 4591 } 4592 4593 var thumbs = this.thumbs, 4594 slider = this.slider, 4595 holder = this.DOM.thumbsHolder, 4596 inner = this.DOM.thumbsInner, 4597 sizes = this.options.thumbnailSizes, 4598 RTL = this.options.rightToLeft, 4599 widths = Object.keys( sizes ).sort( function( a, b ) { return b - a; }), 4600 width = Math.max.apply( null, widths ), 4601 browser = window.innerWidth, 4602 size; 4603 4604 // get current thumbnail size 4605 for ( var i = 0; i < widths.length; i++ ) { 4606 4607 size = Number( widths[i] ); 4608 4609 if ( browser <= size ) { 4610 4611 width = size; 4612 4613 } 4614 4615 } 4616 4617 // set thumbnails data 4618 thumbs.width = Number( sizes[width].width ); 4619 thumbs.gutter = Number( sizes[width].gutter ); 4620 thumbs.height = Number( sizes[width].height ); 4621 thumbs.size = thumbs.width + thumbs.gutter; 4622 thumbs.length = this.gallery.initialLength; 4623 4624 // get thumbnails slider total width 4625 var totalWidth = thumbs.length * thumbs.size; 4626 4627 // basic navigation 4628 thumbs.bound = { 4629 left: 0, 4630 right: totalWidth > slider.width ? slider.width - totalWidth : 0 4631 }; 4632 4633 if ( RTL ) { 4634 4635 thumbs.bound.right = totalWidth > slider.width ? slider.width - thumbs.size : totalWidth - thumbs.size; 4636 thumbs.bound.left = totalWidth - thumbs.size; 4637 4638 } 4639 4640 // centered navigation 4641 if ( this.options.thumbnailsNav === 'centered' ) { 4642 4643 thumbs.bound = { 4644 left : totalWidth > slider.width ? Math.floor( slider.width * 0.5 - thumbs.size * 0.5 ) : Math.floor( totalWidth * 0.5 - thumbs.size * 0.5 ), 4645 right : totalWidth > slider.width ? Math.ceil( slider.width * 0.5 - totalWidth + thumbs.size * 0.5 ) : -Math.ceil( totalWidth * 0.5 - thumbs.size * 0.5 ) 4646 }; 4647 4648 if ( RTL ) { 4649 4650 thumbs.bound.right = thumbs.bound.left; 4651 thumbs.bound.left = thumbs.bound.left + totalWidth - thumbs.size; 4652 4653 } 4654 4655 } 4656 4657 // reset thumbnail animation 4658 thumbs.resetAnimate(); 4659 4660 var position = this.getThumbPosition( thumbs ); 4661 4662 // reset thumbnails position; 4663 thumbs.position.x = position; 4664 4665 // set thumbnails inner position 4666 utils.translate( inner, position, 0 ); 4667 4668 // set holder/inner sizes 4669 var hasHeight = this.getThumbHeight(); 4670 holder.style.visibility = hasHeight ? '' : 'hidden'; 4671 holder.style.height = hasHeight ? hasHeight + 'px' : ''; 4672 inner.style.height = hasHeight ? thumbs.height + Math.min( 10, thumbs.gutter ) + 'px' : ''; 4673 inner.style.width = thumbs.length * thumbs.size + 'px'; 4674 inner.style.right = totalWidth > slider.width && RTL ? 'auto' : ''; 4675 4676 }; 4677 4678 /** 4679 * Set thumbnails positions 4680 * @param {Object} thumbs 4681 */ 4682 proto.getThumbPosition = function( thumbs ) { 4683 4684 var slider = this.slider, 4685 gallery = this.gallery, 4686 thumbNav = this.options.thumbnailsNav,
4687 RTL = this.isRTL(), 4688 left = RTL < 0 ? 'right' : 'left', 4689 index = utils.modulo( gallery.initialLength, gallery.index ), 4690 centered = slider.width * 0.5 - thumbs.size * 0.5, 4691 position = thumbs.bound[left] - index * thumbs.size * RTL; 4692 position = !thumbs.bound[left] ? position + centered : position + (RTL < 0 && thumbNav !== 'centered' ? - centered : 0); 4693 4694 return Math.max( thumbs.bound.right, Math.min( thumbs.bound.left, position ) ); 4695 4696 }; 4697 4698 /** 4699 * Set cell (inside slides) positions 4700 * @param {number} index 4701 */ 4702 proto.setCellPosition = function( index ) { 4703 4704 var cell = this.cells[index]; 4705 4706 cell.resetAnimate(); 4707 4708 utils.translate( this.slides[index].children[0], 0, 0, 1 ); 4709 4710 }; 4711 4712 /** 4713 * Set slider position 4714 * @param {Object} slider 4715 * @param {Object} slides 4716 */ 4717 proto.setSliderPosition = function( slider, slides ) { 4718 4719 var RTL = this.options.rightToLeft, 4720 posX = - slides.index * slides.width; 4721 posX = RTL ? - posX : posX; 4722 4723 slider.resetAnimate(); 4724 4725 // set slider position/attraction to animate from right position 4726 slider.position.x = 4727 slider.attraction.x = posX; 4728 4729 slider.bound = { 4730 left : 0, 4731 right : - ( this.gallery.length - 1 ) * slides.width 4732 }; 4733 4734 if ( RTL ) { 4735 slider.bound.left = - slider.bound.right; 4736 slider.bound.right = 0; 4737 } 4738 4739 utils.translate( this.DOM.slider, posX, 0 ); 4740 4741 }; 4742 4743 /** 4744 * Set & init animation (slider & cells) 4745 */ 4746 proto.setAnimation = function() { 4747 4748 // set slider animations 4749 var slider = this.DOM.slider, 4750 friction = this.options.friction, 4751 attraction = this.options.attraction; 4752 4753 this.slider = new Animate( 4754 // element to animate 4755 slider, 4756 // initial position 4757 { x : 0, y : 0 }, 4758 // friction coefficient 4759 Math.min( Math.max( friction.slider, 0 ), 1 ), 4760 // attraction coefficient 4761 Math.min( Math.max( attraction.slider, 0 ), 1 ) 4762 ); 4763 4764 this.slider.on( 'settle.toanimate', this.settleSider.bind( this ) ); 4765 this.slider.on( 'render.toanimate', this.renderSlider.bind( this ) ); 4766 4767 // set cell animations 4768 var slides = slider.children, 4769 length = slides.length; 4770 4771 for ( var i = 0; i < length; i++ ) { 4772 4773 this.cells[i] = new Animate( 4774 // element to animate 4775 slides[i].children[0], 4776 // initial position 4777 { x : 0, y : 0, s : 1 }, 4778 // friction coefficient 4779 Math.min( Math.max( friction.slide, 0 ), 1 ), 4780 // attraction coefficient 4781 Math.min( Math.max( attraction.slide, 0 ), 1 ) 4782 ); 4783 4784 this.cells[i].on( 'settle.toanimate', this.settleCell.bind( this ) ); 4785 this.cells[i].on( 'render.toanimate', this.renderCell.bind( this ) ); 4786 4787 } 4788 4789 // set thumbnail animation 4790 this.thumbs = new Animate( 4791 // element to animate 4792 this.DOM.thumbsInner, 4793 // initial position 4794 { x : 0 }, 4795 // friction coefficient 4796 Math.min( Math.max( friction.thumbs, 0 ), 1 ), 4797 // attraction coefficient 4798 Math.min( Math.max( attraction.thumbs, 0 ), 1 ) 4799 ); 4800 4801 this.thumbs.on( 'settle.toanimate', this.settleThumbs.bind( this ) ); 4802 this.thumbs.on( 'render.toanimate', this.renderThumbs.bind( this ) ); 4803 4804 }; 4805 4806 /** 4807 * Executed when the slider settle event is emitted from animate 4808 * @param {Object} slider 4809 */ 4810 proto.settleSider = function( slider ) { 4811 4812 var media; 4813 4814 utils.dispatchEvent( this, 'modulobox', 'sliderSettled', slider.position ); 4815 4816 if ( this.states.open ) { 4817 4818 this.setMedia();
4819 this.replaceState(); 4820 4821 } 4822 4823 if ( this.options.fadeIfSettle ) { 4824 4825 var slides = this.slides; 4826 4827 for ( var i = 0; i < slides.length; i++ ) { 4828 4829 var index = slides[i].media; 4830 media = this.gallery[index]; 4831 4832 if ( media.dom.loaded ) { 4833 this.showMedia( media, i ); 4834 } 4835 4836 } 4837 4838 } 4839 4840 }; 4841 4842 /** 4843 * Executed when the cell settle event is emitted from animate 4844 * @param {Object} cell 4845 */ 4846 proto.settleCell = function( cell ) { 4847 4848 var gesture = this.gesture; 4849 4850 if ( gesture.closeBy ) { 4851 utils.dispatchEvent( this, 'modulobox', 'panYSettled', null, cell.position ); 4852 } 4853 4854 if ( ( gesture.closeBy && gesture.canClose === false ) || !gesture.closeBy ) { 4855 utils.dispatchEvent( this, 'modulobox', 'panZoomSettled', null, cell.position ); 4856 } 4857 4858 }; 4859 4860 /** 4861 * Executed when the thumbs slider is settled 4862 * @param {Object} thumbs 4863 */ 4864 proto.settleThumbs = function( thumbs ) { 4865 4866 utils.dispatchEvent( this, 'modulobox', 'thumbsSettled', null, thumbs.position ); 4867 4868 }; 4869 4870 /** 4871 * Executed when the slider is animated/rendered 4872 * @param {Object} slider 4873 */ 4874 proto.renderSlider = function( slider ) { 4875 4876 // shift slide 4877 this.shiftSlides(); 4878 4879 // calculate progress in float percent 4880 var RTL = this.isRTL(), 4881 length = this.gallery.initialLength, 4882 indexPos = - RTL * slider.position.x / this.slides.width, 4883 moduloPos = utils.modulo( length, indexPos ), 4884 progress = ( moduloPos > length - 0.5 ? 0 : moduloPos ) / ( length - 1 ); 4885 4886 // dispatch sliderProgress event 4887 utils.dispatchEvent( this, 'modulobox', 'sliderProgress', null, Math.min( 1, Math.max( 0, progress ) ) ); 4888 4889 }; 4890 4891 /** 4892 * Executed when a cell is animated/rendered 4893 * @param {Object} cell 4894 */ 4895 proto.renderCell = function( cell ) { 4896 4897 this.willClose( cell ); 4898 4899 var progress; 4900 4901 // dispatch panYProgress event 4902 if ( this.gesture.type === 'panY' || this.gesture.closeBy || ( this.gesture.type === 'dragSlider' && cell.position.y !== 0 ) ) { 4903 4904 progress = 1 - Math.abs( cell.position.y ) / ( this.slider.height * 0.5 ); 4905 utils.dispatchEvent( this, 'modulobox', 'panYProgress', null, progress ); 4906 4907 } 4908 4909 // dispatch panZoomProgress event 4910 if ( this.gesture.type !== 'panY' && cell.position.s !== 1 ) { 4911 4912 progress = cell.position.s; 4913 utils.dispatchEvent( this, 'modulobox', 'panZoomProgress', null, progress ); 4914 4915 } 4916 4917 }; 4918 4919 /** 4920 * Executed when the thumbnails slider is animated/rendered 4921 * @param {Object} thumbs 4922 */ 4923 proto.renderThumbs = function( thumbs ) { 4924 4925 this.shiftThumbs( thumbs ); 4926 4927 // calculate progress in float percent 4928 var progress = thumbs.bound.left !== thumbs.bound.right ? ( thumbs.bound.left - thumbs.position.x ) / ( thumbs.bound.left - thumbs.bound.right ) : 0; 4929 4930 // dispatch thumbsProgress event 4931 utils.dispatchEvent( this, 'modulobox', 'thumbsProgress', null, progress ); 4932 4933 }; 4934 4935 // ----- gesture events ----- // 4936 4937 /** 4938 * Touch start event (mouse/touch/pointer) 4939 * @param {Object} event 4940 */ 4941 proto.touchStart = function( event ) { 4942 4943 // get current target tag name 4944 var element = event.target, 4945 tagName = element.tagName, 4946 className = element.className; 4947 4948 // stop slideshow (if not play button touched) 4949 if ( event.which !== 3 && element !== this.buttons.play ) { 4950 this.stopSlideShow(); 4951 } 4952 4953 // if right click or element touched is not part of the lightbox or a button/video/input/a 4954 if ( event.which === 3 || !this.isEl( event ) || ['BUTTON', 'VIDEO', 'INPUT', 'A'].indexOf( tagName ) > -1 ) { 4955 return; 4956 } 4957 4958 // add dragging class if an image in a gallery is touched 4959 if ( tagName === 'IMG' && this.gallery.length > 1) { 4960 utils.addClass( this.DOM.holder, this.pre + '-dragging' ); 4961 } 4962 4963 // prevent default action 4964 event.preventDefault(); 4965 4966 // if share tooltip is open 4967 // after preventDefault() to prevent flicker on touch device (prevent touch scroll) 4968 if ( utils.hasClass( this.DOM.holder, this.pre + '-open-tooltip' ) ) { 4969 return; 4970 } 4971 4972 // if no pointers available 4973 // then 1 first time pointer touches screen (without move) 4974 if ( !this.pointers.length ) { 4975 4976 // reset can close var 4977 this.gesture.canClose = undefined; 4978
4979 // bind move/end events 4980 utils.handleEvents( this, window, this.dragEvents.move, 'touchMove' ); 4981 utils.handleEvents( this, window, this.dragEvents.end, 'touchEnd' ); 4982 4983 } 4984 4985 // add pointers 4986 this.addPointer( event ); 4987 4988 // if thumbs not touched 4989 if ( className.indexOf( '-thumb' ) < 0 ) { 4990 4991 // stop slider animation 4992 this.slider.stopAnimate(); 4993 4994 // stop current cell animation 4995 var cell = this.getCell(); 4996 if ( Math.round( cell.position.s * 100 ) / 100 !== 1 || this.pointers.length === 2 || this.gesture.closeBy ) { 4997 cell.stopAnimate(); 4998 } 4999 5000 } else { 5001 5002 // stop thumbs slider animation 5003 this.thumbs.stopAnimate(); 5004 5005 } 5006 5007 // capture gestures 5008 this.gestures( 'start' ); 5009 5010 }; 5011 5012 /** 5013 * Touch move event (mouse/touch/pointer) 5014 * @param {Object} event 5015 */ 5016 proto.touchMove = function( event ) { 5017 5018 // update active pointers 5019 this.updatePointer( event ); 5020 5021 var gesture = this.gesture; 5022 // get number of actif pointers 5023 var pointerNb = this.pointers.length; 5024 // check if slider is near settled 5025 var isSettle = this.isSliderSettle(); 5026 5027 // Switch panZoom to pan gesture (2 to 1 pointers) 5028 this.switchPointers(); 5029 5030 // capture gestures 5031 this.gestures( 'move' ); 5032 5033 // if a gesture occured 5034 if ( gesture.type ) { 5035 5036 // handle/dispatch gesture Move 5037 this[gesture.type]( event ); 5038 utils.dispatchEvent( this, 'modulobox', gesture.type + 'Move', event, gesture ); 5039 5040 // set gesture movement 5041 gesture.move = true; 5042 5043 } 5044 // if dragged more than the threshold offset or pinched 5045 else if ( ( pointerNb === 2 && isSettle ) || 5046 Math.abs( gesture.dx ) > this.options.threshold || 5047 Math.abs( gesture.dy ) > this.options.threshold ) { 5048 5049 // add threshold to prevent jump when panY/dragSlider 5050 gesture.sx += gesture.dx; 5051 gesture.sy += gesture.dy; 5052 5053 // set for pinchToclose 5054 gesture.canZoom = this.isZoomable(); 5055 // reset closeBy gesture event on first move 5056 gesture.closeBy = false; 5057 5058 // set gesture type 5059 gesture.type = Math.abs( gesture.dx ) < Math.abs( gesture.dy ) / 2 ? false : 'dragSlider'; 5060 gesture.type = this.options.dragToClose && !gesture.type && isSettle ? 'panY' : gesture.type; 5061 gesture.type = ( this.options.pinchToZoom || this.states.zoom ) && gesture.canZoom && isSettle && ( pointerNb === 2 || this.states.zoom ) ? 'panZoom' : gesture.type; 5062 gesture.type = this.options.pinchToClose && gesture.scale < 1 && isSettle && pointerNb === 2 ? 'panZoom' : gesture.type; 5063 gesture.type = event.target.className.indexOf( '-thumb' ) > -1 ? 'dragThumbs' : gesture.type; 5064 5065 // update media before animation 5066 if ( gesture.type === 'dragSlider' ) { 5067 this.setMedia();
5068 } 5069 5070 // replace cell if dragSlider (slider move) 5071 if ( ['dragSlider', 'dragThumbs'].indexOf( gesture.type ) > -1 ) { 5072 5073 var cell = this.getCell(); 5074 cell.startAnimate(); 5075 cell.releaseDrag(); 5076 cell.animateTo({ 5077 x : 0, 5078 y : 0, 5079 s : 1 5080 }); 5081 5082 } 5083 5084 // replace slider if not settle and if cell move 5085 if ( gesture.type !== 'dragSlider' ) { 5086 5087 var slider = this.slider, 5088 slides = this.slides; 5089 5090 // if the slider does not snap to slides 5091 var RTL = this.isRTL(); 5092 if ( - RTL * slider.position.x !== slides.index * slides.width ) { 5093 slider.startAnimate(); 5094 slider.releaseDrag(); 5095 } 5096 5097 } 5098 5099 // if gesture occured 5100 if ( gesture.type ) { 5101 5102 this.pauseVideo(); 5103 5104 // dispatch start event 5105 utils.dispatchEvent( this, 'modulobox', gesture.type + 'Start', event, gesture ); 5106 5107 // add dragging class 5108 if ( this.gallery.length > 1 || gesture.type !== 'dragSlider' ) { 5109 utils.addClass( this.DOM.holder, this.pre + '-dragging' ); 5110 } 5111 5112 } 5113 5114 } 5115 5116 }; 5117 5118 /** 5119 * Touch end event (mouse/touch/pointer) 5120 * @param {Object} event 5121 */ 5122 proto.touchEnd = function( event ) { 5123 5124 // delete released pointers 5125 this.deletePointer( event ); 5126 5127 // if no more pointer on screen 5128 if ( !this.pointers.length ) { 5129 5130 // remove dragging class 5131 utils.removeClass( this.DOM.holder, this.pre + '-dragging' ); 5132 5133 // remove events 5134 utils.handleEvents( this, window, this.dragEvents.move, 'touchMove', false ); 5135 utils.handleEvents( this, window, this.dragEvents.end, 'touchEnd', false ); 5136 5137 // if slider settle and no gesture happened 5138 if ( this.isSliderSettle() ) { 5139 5140 var className = event.target.className; 5141 5142 // play video 5143 if ( utils.hasClass( event.target, this.pre + '-video' ) ) { 5144 this.appendVideo(); 5145 // close lightbox 5146 } else if ( this.options.tapToClose && !this.states.zoom && ( className === this.pre + '-item-inner' || className === this.pre + '-top-bar' ) && Math.abs( this.gesture.dx ) < this.options.threshold ) { 5147 5148 this.close(); 5149 return; 5150 5151 } 5152 5153 // check if double tap/click 5154 if ( event.target.tagName === 'IMG' ) { 5155 this.doubleTap( event ); 5156 } 5157 5158 } 5159 5160 // handle click on thumbnail 5161 if ( this.options.thumbnails && !this.gesture.move ) { 5162 this.thumbClick( event ); 5163 } 5164 5165 // handle/dispatch gesture End 5166 var gestureEnd = this.gesture.type + 'End'; 5167 if ( this.gesture.type && typeof this[gestureEnd] === 'function' ) { 5168 5169 this[gestureEnd]( event ); 5170 utils.dispatchEvent( this, 'modulobox', gestureEnd, event, this.gesture ); 5171 5172 } 5173 5174 // unset gesture 5175 this.gesture.type = 5176 this.gesture.move = false; 5177 5178 // return if gesture trigger a close() (prevent to animate twice) 5179 if ( !this.states.open ) { 5180 return; 5181 } 5182 5183 // make sure cell is settled 5184 var cell = this.getCell(); 5185 if ( !cell.settle ) { 5186 5187 cell.startAnimate(); 5188 cell.releaseDrag(); 5189 5190 } 5191 5192 // make sure slider is settled 5193 var slider = this.slider; 5194 if ( !slider.settle ) { 5195 5196 slider.startAnimate(); 5197 slider.releaseDrag(); 5198 5199 } 5200 5201 } 5202 5203 }; 5204 5205 /** 5206 * Switch panZoom to pan gestures 5207 */ 5208 proto.switchPointers = function() { 5209 5210 // if switch from 2 pointers to 1 pointer (reset animation) 5211 if ( this.gesture.type === 'panZoom' && this.pointers.length === 1 && this.gesture.distance !== 0 ) { 5212 5213 var moving = this.getCell(); 5214 5215 moving.stopAnimate(); 5216 moving.startAnimate(); 5217 5218 this.gesture.move = false; 5219 this.gestures( 'start' ); 5220 this.gestures( 'move' ); 5221 5222 } 5223 5224 }; 5225 5226 /** 5227 * Handle double tap 5228 * @param {Object} event 5229 */ 5230 proto.doubleTap = function( event ) { 5231 5232 event.preventDefault(); 5233 5234 // map pointers 5235 var touches = this.mapPointer( event ), 5236 clientX = touches[0].clientX, 5237 clientY = touches[0].clientY; 5238 5239 // check if double tap/click 5240 if ( typeof this.tap !== 'undefined' && // if already tap once 5241 +new Date() - this.tap.delay < 350 && // if delay between 2 taps < 350ms 5242 Math.abs( this.tap.deltaX - clientX ) < 30 && // if distanceX between 2 taps < 30px 5243 Math.abs( this.tap.deltaY - clientY ) < 30 // if distanceY between 2 taps < 30px 5244 ) { 5245 5246 // restore idle state if double tap 5247 if ( this.states.tapIdle ) { 5248 clearTimeout( this.states.tapIdle ); 5249 } 5250 5251 // zoom on image from pointer coordinates 5252 if ( this.options.doubleTapToZoom ) { 5253 this.zoomTo( clientX, clientY ); 5254 } 5255 5256 // reset double tap data 5257 this.tap = undefined; 5258 5259 // if first tap 5260 } else { 5261 5262 // check if it's a touch device only (no mouse detect thanks to states.idle) 5263 if ( this.browser.touchDevice && this.options.timeToIdle && !this.states.idle ) { 5264 5265 // wait to check if double tap will happen 5266 this.states.tapIdle = setTimeout( function() { 5267 5268 var method = !utils.hasClass( this.DOM.holder, this.pre + '-idle' ) ? 'add' : 'remove'; 5269 utils[method + 'Class']( this.DOM.holder, this.pre + '-idle' ); 5270 5271 }.bind( this ), 350 ); 5272 5273 } 5274 5275 // set tap data 5276 this.tap = { 5277 delay : +new Date(), 5278 deltaX : touches[0].clientX, 5279 deltaY : touches[0].clientY 5280 }; 5281 5282 } 5283 5284 }; 5285 5286 /** 5287 * Check if slider is near settled 5288 * @return {boolean} 5289 */ 5290 proto.isSliderSettle = function() { 5291 5292 // if a gesture occured return 5293 if ( this.gesture.type ) { 5294 return false; 5295 } 5296 5297 // calculate from how much in percent we are from a settled position of the slider (threshold for pinch/close methods) 5298 var RTL = this.isRTL(), 5299 slides = this.slides, 5300 width = slides.width, 5301 toSettle = Math.abs( RTL * this.slider.position.x + slides.index * width ) / width * 100; 5302 5303 // if slider position is a least inferior to 3% from viewport edges (treshold in percent) 5304 return toSettle <= 3 ? true : false; 5305 5306 }; 5307 5308 // ----- handle pointer event ----- // 5309 5310 /** 5311 * Map mouse and touch events 5312 * @param {Object} event 5313 * @return {Object} 5314 */ 5315 proto.mapPointer = function( event ) { 5316 5317 return event.touches ? event.changedTouches : [event]; 5318 5319 }; 5320 5321 /** 5322 * Add custom pointer for current event 5323 * Prevent multiple pointers breaking gesture 5324 * @param {Object} event 5325 */ 5326 proto.addPointer = function( event ) { 5327 5328 var pointers = this.mapPointer( event ); 5329 5330 for ( var i = 0; i < pointers.length; i++ ) { 5331 5332 if ( this.pointers.length < 2 && ['dragSlider', 'panY', 'dragThumbs'].indexOf( this.gesture.type ) === -1 ) { 5333 5334 var ev = pointers[i], 5335 // .pointerId for pointer events, .indentifier for touch events 5336 id = ev.pointerId !== undefined ? ev.pointerId : ev.identifier; 5337 5338 if ( !this.getPointer( id ) ) { 5339 5340 this.pointers[this.pointers.length] = { 5341 id : id, 5342 // clientX/clientY are rounded because "pointer" event return float values 5343 // prevent from inaccurate calculations for the direction on W3C devices capable 5344 x : Math.round( ev.clientX ), 5345 y : Math.round( ev.clientY ) 5346 }; 5347 5348 } 5349 5350 } 5351 5352 } 5353 5354 }; 5355 5356 /** 5357 * Update custom pointer for current event 5358 * @param {Object} event 5359 */ 5360 proto.updatePointer = function( event ) { 5361 5362 var pointers = this.mapPointer( event ); 5363 5364 for ( var i = 0; i < pointers.length; i++ ) { 5365 5366 var ev = pointers[i], 5367 id = ev.pointerId !== undefined ? ev.pointerId : ev.identifier, 5368 pt = this.getPointer( id ); 5369 5370 if ( pt ) { 5371 5372 pt.x = Math.round( ev.clientX ); 5373 pt.y = Math.round( ev.clientY ); 5374 5375 } 5376 5377 } 5378 5379 }; 5380 5381 /** 5382 * Delete custom pointer for current event 5383 * @param {Object} event 5384 */ 5385 proto.deletePointer = function( event ) { 5386 5387 var pointers = this.mapPointer( event ); 5388 5389 for ( var i = 0; i < pointers.length; i++ ) { 5390 5391 var ev = pointers[i], 5392 id = ev.pointerId !== undefined ? ev.pointerId : ev.identifier; 5393 5394 for ( var p = 0; p < this.pointers.length; p++ ) { 5395 5396 if ( this.pointers[p].id === id ) { 5397 this.pointers.splice( p, 1 ); 5398 } 5399 5400 } 5401 5402 } 5403 5404 }; 5405 5406 /** 5407 * Check if current pointer was record for the current gesture 5408 * @param {number} id 5409 * @return {Object} 5410 */ 5411 proto.getPointer = function( id ) { 5412 5413 for ( var k in this.pointers ) { 5414 5415 if ( this.pointers[k].id === id ) { 5416 return this.pointers[k]; 5417 } 5418 5419 } 5420 5421 return null; 5422 5423 }; 5424 5425 /** 5426 * Calculate distances from pointers 5427 * @param {string} type (start, move) 5428 */ 5429 proto.gestures = function( type ) { 5430 5431 var g = this.gesture, 5432 pointers = this.pointers, 5433 distance; 5434 5435 // if no pointer found return immediately 5436 if ( !pointers.length ) { 5437 return; 5438 } 5439 5440 // set x direction 5441 g.direction = g.x ? pointers[0].x > g.x ? 1 : -1 : 0; 5442 5443 // get current coordinates (1st pointer) 5444 g.x = pointers[0].x; 5445 g.y = pointers[0].y; 5446 5447 if ( pointers.length === 2 ) { 5448 5449 // get current coordinates (2nd pointer) 5450 var x2 = pointers[1].x, 5451 y2 = pointers[1].y; 5452 5453 // calculate current distance between 2 pointers 5454 distance = this.getDistance( [g.x, g.y], [x2, y2] ); 5455 5456 // get center coordinates (between 2 pointers) 5457 g.x = g.x - ( g.x - x2 ) / 2; 5458 g.y = g.y - ( g.y - y2 ) / 2; 5459 5460 } 5461 5462 // on touchStart only 5463 if ( type === 'start' ) { 5464 5465 // store initial coordinates 5466 g.dx = 0; 5467 g.dy = 0; 5468 g.sx = g.x; 5469 g.sy = g.y; 5470 5471 // get initial distance (between 2 pointers) 5472 g.distance = distance ? distance : 0; 5473 5474 } else { 5475 5476 // calculate distance in x & y when 1st pointer move 5477 g.dx = g.x - g.sx; 5478 g.dy = g.y - g.sy; 5479 5480 // calculate scale factor (between 2 pointers) 5481 g.scale = distance && g.distance ? distance / g.distance : 1; 5482 5483 } 5484 5485 }; 5486 5487 /** 5488 * Calculate distance between 2 pointers 5489 * @param {Array} p1 - pointer 1 5490 * @param {Array} p2 - pointer 2 5491 * @return {number} 5492 */ 5493 proto.getDistance = function( p1, p2 ) { 5494 5495 var x = p2[0] - p1[0], 5496 y = p2[1] - p1[1]; 5497 5498 return Math.sqrt( ( x * x ) + ( y * y ) ); 5499 5500 }; 5501 5502 // ----- handle gesture ----- // 5503 5504 /** 5505 * Handle drag gesture in Y axis (1 pointer) 5506 */ 5507 proto.panY = function() { 5508 5509 var moving = this.getCell(); 5510 5511 moving.startAnimate(); 5512 moving.updateDrag({ 5513 x : moving.position.x, 5514 y : moving.start.y + this.gesture.dy, 5515 s : moving.position.s 5516 }); 5517 5518 }; 5519 5520 /** 5521 * Handle drag gesture in Y axis (1 pointer) end event 5522 */ 5523 proto.panYEnd = function() { 5524 5525 var posY = 0, 5526 moving = this.getCell(), 5527 height = this.slider.height, 5528 resting = moving.resting.y; 5529 5530 if ( 1 - Math.abs( resting ) / ( height * 0.5 ) < 0.8 ) { 5531 5532 posY = Math.abs( resting ) < height * 0.5 ? Math.abs( resting ) / resting * height * 0.5 : resting; 5533 this.close(); 5534 5535 moving.animateTo({ 5536 x : 0, 5537 y : posY, 5538 s : posY ? moving.resting.s : 1 5539 }); 5540 5541 moving.startAnimate(); 5542 moving.releaseDrag(); 5543 5544 } 5545 5546 }; 5547 5548 /** 5549 * Handle panZoom gesture (2 pointers only) 5550 */ 5551 proto.panZoom = function() { 5552 5553 var moving = this.getCell(), 5554 gesture = this.gesture, 5555 bound = this.mediaViewport( moving.position.s ),
5556 minZoom = this.options.pinchToClose && gesture.canClose ? 0.1 : 0.6, 5557 scale = Math.min( this.options.maxZoom * 1.5, Math.max( minZoom, moving.start.s * gesture.scale ) ), 5558 posX = moving.start.x + gesture.dx, 5559 posY = moving.start.y + gesture.dy, 5560 centerX = gesture.sx - this.slider.width * 0.5, 5561 centerY = gesture.sy - this.slider.height * 0.5; 5562 5563 // prevent to zoom in if can't zoom but pinch to close 5564 if ( !gesture.canZoom || ( !this.options.pinchToZoom && !this.states.zoom ) ) { 5565 scale = Math.min( 1, scale ); 5566 } 5567 5568 // prevent to zoom in if disabled (pinch) and keep current scale to pan 5569 if ( !this.options.pinchToZoom && this.states.zoom ) { 5570 scale = moving.position.s; 5571 } 5572 5573 // if pan only, outside viewport, and just started animation 5574 // remove previous friction added outside viewport 5575 if ( !gesture.move && this.pointers.length === 1 ) { 5576 5577 moving.start.x += posX > bound.left ? posX - bound.left : posX < bound.right ? posX - bound.right : 0; 5578 moving.start.y += posY > bound.bottom ? posY - bound.bottom : posY < bound.top ? posY - bound.top : 0; 5579 5580 } 5581 5582 // calculate center between 1 or 2 pointers if zoom 5583 posX = gesture.dx + centerX + ( moving.start.x - centerX ) * ( scale / moving.start.s ); 5584 posY = gesture.dy + centerY + ( moving.start.y - centerY ) * ( scale / moving.start.s ); 5585 5586 // add friction if pan (1 pointer) and outside viewport 5587 if ( this.pointers.length === 1 ) { 5588 5589 posX = posX > bound.left ? ( posX + bound.left ) * 0.5 : posX < bound.right ? ( posX + bound.right ) * 0.5 : posX; 5590 posY = posY > bound.bottom ? ( posY + bound.bottom ) * 0.5 : posY < bound.top ? ( posY + bound.top ) * 0.5 : posY; 5591 5592 } 5593 5594 moving.startAnimate(); 5595 moving.updateDrag({ 5596 x : posX, 5597 y : posY, 5598 s : scale 5599 }); 5600 5601 this.updateZoom( scale ); 5602 5603 }; 5604 5605 /** 5606 * Handle panZoom gesture (2 pointers only) end event 5607 */ 5608 proto.panZoomEnd = function() { 5609 5610 // if the resting scale value is near at 1 (under 1.1) force unzoom (allow faster gesture recognition) 5611 var moving = this.getCell(), 5612 gesture = this.gesture, 5613 scale = moving.resting.s > this.options.maxZoom ? this.options.maxZoom : moving.resting.s < 1 ? 1 : moving.resting.s, 5614 bound = this.mediaViewport( scale ), 5615 posX, posY; 5616 5617 // if scaled more than authorized then recalculate position to max authorized scale 5618 if ( Math.round( moving.resting.s * 10 ) / 10 > this.options.maxZoom ) { 5619 5620 var centerX = gesture.distance ? gesture.sx - this.slider.width * 0.5 : 0; 5621 var centerY = gesture.distance ? gesture.sy - this.slider.height * 0.5 : 0; 5622 5623 posX = gesture.dx + centerX + ( moving.start.x - centerX ) * ( scale / moving.start.s ); 5624 posY = gesture.dy + centerY + ( moving.start.y - centerY ) * ( scale / moving.start.s ); 5625 posX = posX > bound.left ? bound.left : posX < bound.right ? bound.right : posX; 5626 posY = posY > bound.bottom ? bound.bottom : posY < bound.top ? bound.top : posY; 5627 5628 } else { 5629 5630 posX = moving.resting.x > bound.left ? bound.left : moving.resting.x < bound.right ? bound.right : undefined; 5631 posY = moving.resting.y > bound.bottom ? bound.bottom : moving.resting.y < bound.top ? bound.top : undefined; 5632 5633 } 5634 5635 // add resting to scale to animate when closing 5636 if ( this.options.pinchToClose && moving.resting.s < 0.8 && gesture.canClose ) { 5637 5638 scale = moving.resting.s < 0.3 ? moving.resting.s : 0.15; 5639 posX = moving.resting.x; 5640 posY = moving.resting.y; 5641 this.close(); 5642 5643 } 5644 5645 moving.animateTo({ 5646 x : posX, 5647 y : posY, 5648 s : scale !== moving.resting.s ? scale : undefined 5649 }); 5650 moving.startAnimate(); 5651 moving.releaseDrag(); 5652 5653 this.updateZoom( moving.resting.s ); 5654 5655 }; 5656 5657 /** 5658 * Handle drag gesture in X axis (1 pointer) 5659 */ 5660 proto.dragThumbs = function() { 5661 5662 var moving = this.thumbs, 5663 bound = moving.bound, 5664 posX = moving.start.x + this.gesture.dx; 5665 5666 // add resistance on ends 5667 if ( !this.gesture.move ) { 5668 5669 moving.start.x += posX > bound.left ? posX - bound.left : posX < bound.right ? posX - bound.right : 0; 5670 posX = moving.start.x + this.gesture.dx; 5671 5672 } 5673 5674 posX = posX > bound.left ? ( posX + bound.left ) * 0.5 : posX < bound.right ? ( posX + bound.right ) * 0.5 : posX; 5675 5676 moving.startAnimate(); 5677 // to add freescroll 5678 moving.attraction.x = undefined; 5679 moving.updateDrag({ 5680 x : posX 5681 }); 5682 5683 }; 5684 5685 /** 5686 * Handle drag gesture in X axis (1 pointer) 5687 */ 5688 proto.dragThumbsEnd = function() { 5689 5690 var moving = this.thumbs, 5691 bound = moving.bound, 5692 posX = moving.resting.x; 5693 5694 posX = posX > bound.left ? bound.left : posX < bound.right ? bound.right : posX; 5695 5696 // if resting reach bounds/ends 5697 if ( posX !== moving.resting.x ) { 5698 5699 moving.animateTo({ 5700 x: posX 5701 }); 5702 5703 } 5704 moving.startAnimate(); 5705 moving.releaseDrag(); 5706 5707 }; 5708 5709 /** 5710 * Handle drag gesture in X axis (1 pointer) 5711 */ 5712 proto.dragSlider = function() { 5713 5714 // prevent dragging slider if one media 5715 if ( this.gallery.length === 1 ) { 5716 return; 5717 } 5718 5719 var moving = this.slider, 5720 posX = moving.start.x + this.gesture.dx; 5721 5722 // add resistance if no loop 5723 if ( !this.states.loop ) { 5724 5725 var bound = moving.bound; 5726 5727 if ( !this.gesture.move ) { 5728 5729 moving.start.x += posX > bound.left ? posX - bound.left : posX < bound.right ? posX - bound.right : 0; 5730 posX = moving.start.x + this.gesture.dx; 5731 5732 } 5733 5734 posX = posX > bound.left ? ( posX + bound.left ) * 0.5 : posX < bound.right ? ( posX + bound.right ) * 0.5 : posX; 5735 5736 } 5737 5738 moving.startAnimate(); 5739 moving.updateDrag({ 5740 x : posX 5741 }); 5742 5743 }; 5744 5745 /** 5746 * Handle drag gesture in X axis (1 pointer) end event 5747 */ 5748 proto.dragSliderEnd = function() { 5749 5750 // prevent dragging slider if one media 5751 if ( this.gallery.length === 1 ) { 5752 return; 5753 } 5754 5755 var moving = this.slider, 5756 slides = this.slides, 5757 oldIndex = slides.index, 5758 RTL = this.isRTL(), 5759 restingX = moving.resting.x, 5760 positionX = moving.position.x; 5761 5762 this.getRestingIndex( positionX, restingX ); 5763 5764 if ( oldIndex !== slides.index ) { 5765 this.updateMediaInfo(); 5766 } 5767 5768 this.slider.animateTo({ 5769 x : - RTL * slides.index * slides.width, 5770 y : undefined, 5771 s : undefined 5772 }); 5773 5774 moving.startAnimate(); 5775 moving.releaseDrag(); 5776 5777 }; 5778 5779 /** 5780 * Calculate new index based on velocity and slider position 5781 * @param {number} positionX 5782 * @param {number} restingX 5783 */ 5784 proto.getRestingIndex = function( positionX, restingX ) { 5785 5786 var direction = this.gesture.direction, 5787 gallery = this.gallery, 5788 slides = this.slides, 5789 deltaX = this.gesture.dx, 5790 RTL = this.isRTL(), 5791 index = Math.round( - RTL * positionX / slides.width ), 5792 moved = Math.abs( restingX - positionX ); 5793 5794 if ( Math.abs( deltaX ) < slides.width * 0.5 && moved ) { 5795 5796 if ( deltaX > 0 && direction > 0 ) { 5797 index -= 1 * RTL; 5798 } else if ( deltaX < 0 && direction < 0 ) { 5799 index += 1 * RTL; 5800 } 5801 5802 } 5803 5804 // constrain to slide one slide by one slide 5805 var gap = Math.max( -1, Math.min( 1, index - slides.index ) ); 5806 5807 // prevent going outside limit if no loop 5808 if ( !this.states.loop && 5809 ( ( gallery.index + gap < 0 ) || 5810 ( gallery.index + gap > gallery.length - 1 ) ) ) { 5811 5812 return; 5813 5814 } 5815 5816 slides.index += gap; 5817 5818 }; 5819 5820 /** 5821 * Shift slide around when slider render 5822 */ 5823 proto.shiftSlides = function() { 5824 5825 var slides = this.slides, 5826 gallery = this.gallery, 5827 loop = this.states.loop, 5828 RTL = this.isRTL(), 5829 from = RTL * Math.round( - this.slider.position.x / slides.width ) - 2, 5830 to = from + 5; 5831 5832 if ( !loop && to > gallery.initialLength - 1 ) { 5833 from = gallery.initialLength - 5; 5834 to = from + 5; 5835 } 5836 5837 if ( !loop && from < 0 ) { 5838 from = 0; 5839 to = 5; 5840 } 5841 5842 for ( var i = from; i < to; i++ ) { 5843 5844 var position = RTL * i * slides.width, 5845 slide_index = utils.modulo( slides.length, i ), 5846 slide = slides[slide_index]; 5847 5848 if ( slide.index !== i || slide.position !== position ) { 5849 5850 // set slide virtual index to get the media index 5851 slide.index = i;
5852 // set position (to resize) if slide width change 5853 slide.position = position; 5854 // apply new position 5855 slide.style.left = position + 'px'; 5856 5857 } 5858 5859 } 5860 5861 // if open only because need to be set after sizing 5862 if ( this.states.open ) { 5863 5864 // load only 3 media to prevent appending one or several DOM while dragging/realasing 5865 // before a touchMove media are set (5), so no reflow/repaint happen during an animation 5866 // improve performance on touch (mobile) devices 5867 this.setMedia( 3 ); 5868 5869 } 5870 5871 }; 5872 5873 /** 5874 * Shift thumbnails and load thumbs img 5875 * @param {Object} thumbs 5876 */ 5877 proto.shiftThumbs = function( thumbs ) { 5878 5879 var child = this.DOM.thumbsInner.children, 5880 slider = this.slider, 5881 gallery = this.gallery, 5882 RTL = this.isRTL(), 5883 length = child.length, 5884 index = utils.modulo( gallery.initialLength, gallery.index ), 5885 width = thumbs.size * ( length ) * 0.5, 5886 center = Math.round( ( - RTL * thumbs.position.x + RTL * width * 0.5 ) / thumbs.size ), 5887 from = Math.max( 0, center - Math.floor( length / 2 ) ), 5888 to = from + length; 5889 5890 // current thumbnail viewport 5891 var tolerance = slider.width * 0.5, 5892 boundLeft = thumbs.position.x + tolerance, 5893 boundRight = thumbs.position.x - slider.width - tolerance; 5894 5895 if ( to > gallery.initialLength ) { 5896 5897 to = gallery.initialLength; 5898 from = to - length; 5899 5900 } 5901 5902 // make sure we are looping through all thumbs 5903 if ( to === gallery.initialLength - 1 && from - to < length ) { 5904 5905 from = gallery.initialLength - length; 5906 5907 } 5908 5909 for ( var i = from; i < to; i++ ) { 5910 5911 var thumb = child[utils.modulo( length, i )], 5912 position = RTL * i * thumbs.size + thumbs.gutter * 0.5, 5913 className = this.pre + '-active-thumb', 5914 isActive = utils.hasClass( thumb, className ); 5915 5916 // set thumbnail position 5917 if ( thumb.index !== i || thumb.position !== position ) { 5918 5919 thumb.index = i; 5920 thumb.position = position; 5921 thumb.style.left = position + 'px'; 5922 5923 } 5924 5925 // set thumbnail size 5926 this.setThumbSize( thumb, thumbs ); 5927 5928 // if thumbnail is in the viewport 5929 if ( - thumb.position <= boundLeft && - thumb.position >= boundRight && thumb.media !== i ) { 5930 this.loadThumb( thumb, i ); 5931 } 5932 5933 // add active class 5934 if ( isActive && index !== i ) { 5935 utils.removeClass( thumb, className ); 5936 } else if ( !isActive && index === i ) { 5937 utils.addClass( thumb, className ); 5938 } 5939 5940 } 5941 5942 }; 5943 5944 /** 5945 * Set thumbnail size 5946 * @param {Object} thumb 5947 * @param {Object} thumbs 5948 */ 5949 proto.setThumbSize = function( thumb, thumbs ) { 5950 5951 // set thumbnail size if not already assigned 5952 if ( thumb.width !== thumbs.width || 5953 thumb.height !== thumbs.height || 5954 thumb.gutter !== thumbs.gutter ) { 5955 5956 // store sizes 5957 thumb.width = thumbs.width; 5958 thumb.height = thumbs.height; 5959 thumb.gutter = thumbs.gutter; 5960 5961 // set thumbnail size 5962 thumb.style.width = thumbs.width + 'px'; 5963 thumb.style.height = thumbs.height + 'px'; 5964 5965 } 5966 5967 }; 5968 5969 /** 5970 * Handle close gesture 5971 * @param {Object} cell 5972 */ 5973 proto.willClose = function( cell ) { 5974 5975 var opacity = this.DOM.overlay.style.opacity, 5976 canClose = this.gesture.canClose, 5977 gestureType = this.gesture.type, 5978 gestureClose = this.gesture.closeBy, 5979 pinchToClose = gestureType === 'panZoom' || gestureClose === 'panZoom', 5980 dragYToClose = gestureType === 'panY' || gestureClose === 'panY'; 5981 5982 // check if the lightbox can be closed with a pinch 5983 if ( cell.position.s > 1.1 && typeof canClose === 'undefined' ) { 5984 this.gesture.canClose = false; 5985 } else if ( cell.position.s < 1 && typeof canClose === 'undefined' ) { 5986 this.gesture.canClose = true; 5987 } 5988 5989 // apply opacity if the user try to close the lightbox 5990 if ( this.options.pinchToClose && pinchToClose && this.gesture.canClose ) { 5991 5992 opacity = cell.position.s; 5993 this.gesture.closeBy = 'panZoom'; 5994 5995 } else if ( dragYToClose ) { 5996 5997 opacity = 1 - Math.abs( cell.position.y ) / ( this.slider.height * 0.5 ); 5998 this.gesture.closeBy = 'panY'; 5999 6000 } else if ( opacity && opacity < 1 ) { 6001 6002 opacity = 1; 6003 this.gesture.closeBy = false; 6004 6005 } 6006 6007 // normalize opacity value 6008 opacity = !opacity ? 1 : Math.max( 0, Math.min( 1, opacity ) ); 6009 6010 // hide ui if the user try to close the lightbox 6011 // only if the opacity factor is enough to close (0.8) 6012 var method = opacity <= 0.8 || !opacity ? 'add' : 'remove'; 6013 utils[method + 'Class']( this.DOM.holder, this.pre + '-will-close' ); 6014 6015 this.DOM.overlay.style.opacity = opacity; 6016 6017 }; 6018 6019 // ----- Slide Navigation ----- // 6020 6021 /** 6022 * Go to prev slide 6023 * Throttle function to reduce sliding speed 6024 */ 6025 proto.prev = utils.throttle( function() { 6026 6027 if ( !this.gesture.move ) { 6028 6029 this.slideTo( this.slides.index - 1 * this.isRTL() ); 6030 6031 } 6032 6033 }, 120 ); 6034 6035 /**
6036 * Go to next slide 6037 * Throttle function to reduce sliding speed 6038 */ 6039 proto.next = utils.throttle( function() { 6040 6041 if ( !this.gesture.move ) { 6042 6043 this.slideTo( this.slides.index + 1 * this.isRTL() ); 6044 6045 } 6046 6047 }, 120 ); 6048 6049 /** 6050 * Slide to gallery index 6051 * @param {number} to 6052 * @param {boolean} slideShow 6053 */ 6054 proto.slideTo = function( to, slideShow ) { 6055 6056 var slides = this.slides, 6057 gallery = this.gallery, 6058 holder = this.DOM.slider, 6059 RTL = this.isRTL(), 6060 length = gallery.initialLength, 6061 moduloTo = utils.modulo( length, to ), 6062 moduloFrom = utils.modulo( length, gallery.index ), 6063 slideBy = moduloTo - moduloFrom, 6064 fromEnds = length - Math.abs( slideBy ); 6065 6066 // if no loop and outside gallery ends 6067 if ( !this.states.loop && ( to < 0 || to > this.gallery.initialLength - 1 ) ) { 6068 return; 6069 } 6070 6071 // if we are closed to a slider end, then go directly 6072 if ( this.states.loop && fromEnds < 3 && fromEnds * 2 < length ) { 6073 slideBy = slideBy < 0 ? fromEnds : -fromEnds; 6074 } 6075 6076 // if same slide but different media index 6077 // necessary in loop and thumb click or slideTo triggered manually 6078 if ( moduloTo === to ) { 6079 to = slides.index + slideBy; 6080 } 6081 6082 // number of slides to animate 6083 slideBy = to - slides.index; 6084 6085 // if same slide selected 6086 if ( !slideBy ) { 6087 return; 6088 } 6089 6090 // unset zoom of current cell 6091 if ( this.states.zoom ) { 6092 this.zoom(); 6093 } 6094 6095 // pause video 6096 this.pauseVideo(); 6097 6098 // remove share tooltip 6099 this.share(); 6100 6101 // if not triggered by the slideshow 6102 if ( !slideShow ) { 6103 this.stopSlideShow(); 6104 } 6105 6106 // set new index 6107 slides.index = to; 6108 6109 // prepare slider 6110 var slider = this.slider; 6111 6112 // if the slide will move more than 2 slides 6113 // fake animation 6114 if ( Math.abs( slideBy ) > 2 ) { 6115 6116 // hide slider with css 6117 utils.addClass( holder, this.pre + '-hide' ); 6118 6119 // set slider position to selected media 6120 this.setSliderPosition( slider, slides ); 6121 this.setSlidesPositions( slides ); 6122 6123 // set slider position 2 slide before/after selected media 6124 var moveBy = RTL * slides.width * Math.min( 2, Math.abs( slideBy ) ) * Math.abs( slideBy ) / slideBy; 6125 slider.position.x = 6126 slider.attraction.x = slider.position.x + moveBy; 6127 utils.translate( holder, slider.position.x, 0 ); 6128 6129 // trigger reflow for css transitions. 6130 holder.getClientRects(); 6131 6132 } 6133 6134 // update media info with new index 6135 this.updateMediaInfo(); 6136 6137 // reveal slider with css transition 6138 utils.removeClass( holder, this.pre + '-hide' ); 6139 6140 slider.startAnimate(); 6141 slider.releaseDrag(); 6142 slider.animateTo({ 6143 x : - RTL * to * slides.width, 6144 y : 0, 6145 s : undefined 6146 }); 6147 6148 }; 6149 6150 /** 6151 * Handle keydown event 6152 * @param {Object} event 6153 */ 6154 proto.keyDown = function( event ) { 6155 6156 var key = event.keyCode,
6157 opt = this.options; 6158 6159 if ( opt.prevNextKey ) { 6160 6161 if ( key === 37 ) { 6162 this.prev( event ); 6163 } else if ( key === 39 ) { 6164 this.next( event ); 6165 } 6166 6167 } 6168 6169 if ( key === 27 && opt.escapeToClose ) { 6170 this.close(); 6171 } 6172 6173 // disable scroll on keydown 6174 // Space: 32, Page Up: 33, Page Down: 34, End: 35, Home: 36, Up arrow: 38, Down arrow: 40 6175 if ( !opt.mouseWheel && [32, 33, 34, 35, 36, 38, 40].indexOf( key ) > -1 ) { 6176 6177 event.preventDefault(); 6178 return false; 6179 6180 } 6181 6182 }; 6183 6184 // ----- Zoom functionality ----- // 6185 6186 /** 6187 * Handle zoom on icon click 6188 */ 6189 proto.zoom = function() { 6190 6191 this.zoomTo(); 6192 6193 }; 6194 6195 /** 6196 * Zoom to a position (x & y) and scale factor on image 6197 * @param {number} x 6198 * @param {number} y 6199 * @param {number} scale 6200 */ 6201 proto.zoomTo = function( x, y, scale ) { 6202 6203 if ( !this.isSliderSettle() || ( !this.isZoomable() && scale > 1 ) ) { 6204 return; 6205 } 6206 6207 this.gesture.closeBy = false; 6208 6209 var media = this.getMedia(); 6210 scale = scale ? scale : this.states.zoom ? 1 : media.dom.size.scale; 6211 6212 var cell = this.getCell(), 6213 bound = this.mediaViewport( scale ), 6214 centX = x ? x - this.slider.width * 0.5 : 0, 6215 centY = y ? y - this.slider.height * 0.5 : 0, 6216 PosX = scale > 1 ? Math.ceil( centX + ( cell.position.x - centX ) * ( scale / cell.position.s ) ) : 0, 6217 PosY = scale > 1 ? Math.ceil( centY + ( cell.position.y - centY ) * ( scale / cell.position.s ) ) : 0; 6218 6219 cell.startAnimate(); 6220 cell.releaseDrag(); 6221 cell.animateTo({ 6222 x : PosX > bound.left ? bound.left : PosX < bound.right ? bound.right : PosX, 6223 y : PosY > bound.bottom ? bound.bottom : PosY < bound.top ? bound.top : PosY, 6224 s : scale 6225 }); 6226 6227 this.updateZoom( scale ); 6228 6229 }; 6230 6231 /** 6232 * Update zoom state when zoomTo 6233 * @param {number} scale 6234 */ 6235 proto.updateZoom = function( scale ) { 6236 6237 this.states.zoom = scale > 1 ? true : false; 6238 utils[this.states.zoom ? 'addClass' : 'removeClass']( this.DOM.holder, this.pre + '-panzoom' ); 6239 6240 }; 6241 6242 /** 6243 * Destroy method 6244 */ 6245 proto.destroy = function() { 6246 6247 // if no instance 6248 if ( !this.GUID ) { 6249 return; 6250 } 6251 6252 // Close only if destroy method is called when opened 6253 if ( this.states.open ) { 6254 this.close(); 6255 } 6256 6257 // setup sources 6258 var selectors = this.options.mediaSelector, 6259 sources = ''; 6260 6261 // get all media attached to Modulobox 6262 try { 6263 sources = document.querySelectorAll( selectors ); 6264 } catch (error) {} 6265 6266 // remove all events attached to media 6267 for ( var i = 0, l = sources.length; i < l; i++ ) { 6268 6269 var source = sources[i]; 6270 6271 if ( source.mobxListener ) { 6272 source.removeEventListener( 'click', source.mobxListener, false ); 6273 } 6274 6275 } 6276 6277 // unbind all events 6278 this.bindEvents( false ); 6279 6280 // reset slider animation 6281 this.slider.resetAnimate(); 6282 6283 // reset cells animations 6284 for ( i = 0; i < this.slides.length; i++ ) { 6285 this.cells[i].resetAnimate(); 6286 } 6287 6288 // reset thumbs animation 6289 if ( this.thumbs ) { 6290 this.thumbs.resetAnimate(); 6291 } 6292 6293 // remove main holders 6294 this.DOM.holder.parentNode.removeChild( this.DOM.holder ); 6295 this.DOM.comment.parentNode.removeChild( this.DOM.comment ); 6296 6297 // delete instance; 6298 delete instances[this.GUID]; 6299 delete this.DOM.holder.GUID; 6300 delete this.GUID; 6301 6302 }; 6303 6304 /** 6305 * jQuery plugin 6306 */ 6307 if ( typeof jQuery !== 'undefined' ) { 6308 6309 ( function( $ ) { 6310 6311 $.ModuloBox = function( options ) { 6312 6313 return new ModuloBox( options ); 6314 6315 }; 6316 6317 })( jQuery ); 6318 6319 } 6320 6321 return ModuloBox; 6322 6323}));
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.