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