1/* 2 * Remodal - v1.1.1 3 * Responsive, lightweight, fast, synchronized with CSS animations, fully customizable modal window plugin with declarative configuration and hash tracking. 4 * http://vodkabears.github.io/remodal/ 5 * 6 * Made by Ilya Makarov 7 * Under MIT License 8 */ 9 10!(function(root, factory) { 11 if (typeof define === 'function' && define.amd) { 12 define(['jquery'], function($) { 13 return factory(root, $); 14 }); 15 } else if (typeof exports === 'object') { 16 factory(root, require('jquery')); 17 } else { 18 factory(root, root.jQuery || root.Zepto); 19 } 20})(this, function(global, $) { 21 22 'use strict'; 23 24 /** 25 * Name of the plugin 26 * @private 27 * @const 28 * @type {String} 29 */ 30 var PLUGIN_NAME = 'remodal'; 31 32 /** 33 * Namespace for CSS and events 34 * @private 35 * @const 36 * @type {String} 37 */ 38 var NAMESPACE = global.REMODAL_GLOBALS && global.REMODAL_GLOBALS.NAMESPACE || PLUGIN_NAME; 39 40 /** 41 * Animationstart event with vendor prefixes 42 * @private 43 * @const 44 * @type {String} 45 */ 46 var ANIMATIONSTART_EVENTS = $.map( 47 ['animationstart', 'webkitAnimationStart', 'MSAnimationStart', 'oAnimationStart'], 48 49 function(eventName) { 50 return eventName + '.' + NAMESPACE; 51 } 52 53 ).join(' '); 54 55 /** 56 * Animationend event with vendor prefixes 57 * @private 58 * @const 59 * @type {String} 60 */ 61 var ANIMATIONEND_EVENTS = $.map( 62 ['animationend', 'webkitAnimationEnd', 'MSAnimationEnd', 'oAnimationEnd'], 63 64 function(eventName) { 65 return eventName + '.' + NAMESPACE; 66 } 67 68 ).join(' '); 69 70 /** 71 * Default settings 72 * @private 73 * @const 74 * @type {Object} 75 */ 76 var DEFAULTS = $.extend({ 77 hashTracking: true, 78 closeOnConfirm: true, 79 closeOnCancel: true, 80 closeOnEscape: true, 81 closeOnOutsideClick: true, 82 modifier: '', 83 appendTo: null 84 }, global.REMODAL_GLOBALS && global.REMODAL_GLOBALS.DEFAULTS); 85 86 /** 87 * States of the Remodal 88 * @private 89 * @const 90 * @enum {String} 91 */ 92 var STATES = { 93 CLOSING: 'closing', 94 CLOSED: 'closed', 95 OPENING: 'opening', 96 OPENED: 'opened' 97 }; 98 99 /** 100 * Reasons of the state change. 101 * @private 102 * @const 103 * @enum {String} 104 */ 105 var STATE_CHANGE_REASONS = { 106 CONFIRMATION: 'confirmation', 107 CANCELLATION: 'cancellation' 108 }; 109 110 /** 111 * Is animation supported? 112 * @private 113 * @const 114 * @type {Boolean} 115 */ 116 var IS_ANIMATION = (function() { 117 var style = document.createElement('div').style; 118 119 return style.animationName !== undefined || 120 style.WebkitAnimationName !== undefined || 121 style.MozAnimationName !== undefined || 122 style.msAnimationName !== undefined || 123 style.OAnimationName !== undefined; 124 })(); 125 126 /** 127 * Is iOS? 128 * @private 129 * @const 130 * @type {Boolean} 131 */ 132 var IS_IOS = /iPad|iPhone|iPod/.test(navigator.platform); 133 134 /** 135 * Current modal 136 * @private 137 * @type {Remodal} 138 */ 139 var current; 140 141 /** 142 * Scrollbar position 143 * @private 144 * @type {Number} 145 */ 146 var scrollTop; 147 148 /** 149 * Returns an animation duration 150 * @private 151 * @param {jQuery} $elem 152 * @returns {Number} 153 */ 154 function getAnimationDuration($elem) { 155 if ( 156 IS_ANIMATION && 157 $elem.css('animation-name') === 'none' && 158 $elem.css('-webkit-animation-name') === 'none' && 159 $elem.css('-moz-animation-name') === 'none' && 160 $elem.css('-o-animation-name') === 'none' && 161 $elem.css('-ms-animation-name') === 'none' 162 ) { 163 return 0; 164 } 165 166 var duration = $elem.css('animation-duration') || 167 $elem.css('-webkit-animation-duration') || 168 $elem.css('-moz-animation-duration') || 169 $elem.css('-o-animation-duration') || 170 $elem.css('-ms-animation-duration') || 171 '0s'; 172 173 var delay = $elem.css('animation-delay') || 174 $elem.css('-webkit-animation-delay') || 175 $elem.css('-moz-animation-delay') || 176 $elem.css('-o-animation-delay') || 177 $elem.css('-ms-animation-delay') || 178 '0s'; 179 180 var iterationCount = $elem.css('animation-iteration-count') || 181 $elem.css('-webkit-animation-iteration-count') || 182 $elem.css('-moz-animation-iteration-count') || 183 $elem.css('-o-animation-iteration-count') || 184 $elem.css('-ms-animation-iteration-count') || 185 '1'; 186 187 var max; 188 var len; 189 var num; 190 var i; 191 192 duration = duration.split(', ');
193 delay = delay.split(', '); 194 iterationCount = iterationCount.split(', '); 195 196 // The 'duration' size is the same as the 'delay' size 197 for (i = 0, len = duration.length, max = Number.NEGATIVE_INFINITY; i < len; i++) { 198 num = parseFloat(duration[i]) * parseInt(iterationCount[i], 10) + parseFloat(delay[i]); 199 200 if (num > max) { 201 max = num; 202 } 203 } 204 205 return max; 206 } 207 208 /** 209 * Returns a scrollbar width 210 * @private 211 * @returns {Number} 212 */ 213 function getScrollbarWidth() { 214 if ($(document).height() <= $(window).height()) { 215 return 0; 216 } 217 218 var outer = document.createElement('div'); 219 var inner = document.createElement('div'); 220 var widthNoScroll; 221 var widthWithScroll; 222 223 outer.style.visibility = 'hidden'; 224 outer.style.width = '100px'; 225 document.body.appendChild(outer); 226 227 widthNoScroll = outer.offsetWidth; 228 229 // Force scrollbars 230 outer.style.overflow = 'scroll'; 231 232 // Add inner div 233 inner.style.width = '100%'; 234 outer.appendChild(inner); 235 236 widthWithScroll = inner.offsetWidth; 237 238 // Remove divs 239 outer.parentNode.removeChild(outer); 240 241 return widthNoScroll - widthWithScroll; 242 } 243 244 /** 245 * Locks the screen 246 * @private 247 */ 248 function lockScreen() { 249 if (IS_IOS) { 250 return; 251 } 252 253 var $html = $('html'); 254 var lockedClass = namespacify('is-locked'); 255 var paddingRight; 256 var $body; 257 258 if (!$html.hasClass(lockedClass)) { 259 $body = $(document.body); 260 261 // Zepto does not support '-=', '+=' in the `css` method 262 paddingRight = parseInt($body.css('padding-right'), 10) + getScrollbarWidth(); 263 264 $body.css('padding-right', paddingRight + 'px'); 265 $html.addClass(lockedClass); 266 } 267 } 268 269 /** 270 * Unlocks the screen 271 * @private 272 */ 273 function unlockScreen() { 274 if (IS_IOS) { 275 return; 276 } 277 278 var $html = $('html'); 279 var lockedClass = namespacify('is-locked'); 280 var paddingRight; 281 var $body; 282 283 if ($html.hasClass(lockedClass)) { 284 $body = $(document.body); 285 286 // Zepto does not support '-=', '+=' in the `css` method 287 paddingRight = parseInt($body.css('padding-right'), 10) - getScrollbarWidth(); 288 289 $body.css('padding-right', paddingRight + 'px'); 290 $html.removeClass(lockedClass); 291 } 292 } 293 294 /** 295 * Sets a state for an instance 296 * @private 297 * @param {Remodal} instance 298 * @param {STATES} state 299 * @param {Boolean} isSilent If true, Remodal does not trigger events 300 * @param {String} Reason of a state change. 301 */ 302 function setState(instance, state, isSilent, reason) { 303 304 var newState = namespacify('is', state); 305 var allStates = [namespacify('is', STATES.CLOSING), 306 namespacify('is', STATES.OPENING), 307 namespacify('is', STATES.CLOSED), 308 namespacify('is', STATES.OPENED)].join(' '); 309 310 instance.$bg 311 .removeClass(allStates) 312 .addClass(newState); 313 314 instance.$overlay 315 .removeClass(allStates) 316 .addClass(newState); 317 318 instance.$wrapper 319 .removeClass(allStates) 320 .addClass(newState); 321 322 instance.$modal 323 .removeClass(allStates) 324 .addClass(newState); 325 326 instance.state = state; 327 !isSilent && instance.$modal.trigger({ 328 type: state, 329 reason: reason 330 }, [{ reason: reason }]); 331 } 332 333 /** 334 * Synchronizes with the animation 335 * @param {Function} doBeforeAnimation 336 * @param {Function} doAfterAnimation 337 * @param {Remodal} instance 338 */ 339 function syncWithAnimation(doBeforeAnimation, doAfterAnimation, instance) { 340 var runningAnimationsCount = 0; 341 342 var handleAnimationStart = function(e) { 343 if (e.target !== this) { 344 return; 345 } 346 347 runningAnimationsCount++; 348 }; 349 350 var handleAnimationEnd = function(e) { 351 if (e.target !== this) { 352 return; 353 } 354 355 if (--runningAnimationsCount === 0) { 356 357 // Remove event listeners 358 $.each(['$bg', '$overlay', '$wrapper', '$modal'], function(index, elemName) { 359 instance[elemName].off(ANIMATIONSTART_EVENTS + ' ' + ANIMATIONEND_EVENTS); 360 }); 361 362 doAfterAnimation(); 363 } 364 }; 365 366 $.each(['$bg', '$overlay', '$wrapper', '$modal'], function(index, elemName) { 367 instance[elemName] 368 .on(ANIMATIONSTART_EVENTS, handleAnimationStart) 369 .on(ANIMATIONEND_EVENTS, handleAnimationEnd); 370 }); 371 372 doBeforeAnimation(); 373 374 // If the animation is not supported by a browser or its duration is 0 375 if (
376 getAnimationDuration(instance.$bg) === 0 && 377 getAnimationDuration(instance.$overlay) === 0 && 378 getAnimationDuration(instance.$wrapper) === 0 && 379 getAnimationDuration(instance.$modal) === 0 380 ) { 381 382 // Remove event listeners 383 $.each(['$bg', '$overlay', '$wrapper', '$modal'], function(index, elemName) { 384 instance[elemName].off(ANIMATIONSTART_EVENTS + ' ' + ANIMATIONEND_EVENTS); 385 }); 386 387 doAfterAnimation(); 388 } 389 } 390 391 /** 392 * Closes immediately 393 * @private 394 * @param {Remodal} instance 395 */ 396 function halt(instance) { 397 if (instance.state === STATES.CLOSED) { 398 return; 399 } 400 401 $.each(['$bg', '$overlay', '$wrapper', '$modal'], function(index, elemName) { 402 instance[elemName].off(ANIMATIONSTART_EVENTS + ' ' + ANIMATIONEND_EVENTS); 403 }); 404 405 instance.$bg.removeClass(instance.settings.modifier); 406 instance.$overlay.removeClass(instance.settings.modifier).hide(); 407 instance.$wrapper.hide(); 408 unlockScreen(); 409 setState(instance, STATES.CLOSED, true); 410 } 411 412 /** 413 * Parses a string with options 414 * @private 415 * @param str 416 * @returns {Object} 417 */ 418 function parseOptions(str) { 419 var obj = {}; 420 var arr; 421 var len; 422 var val; 423 var i; 424 425 // Remove spaces before and after delimiters 426 str = str.replace(/\s*:\s*/g, ':').replace(/\s*,\s*/g, ','); 427 428 // Parse a string 429 arr = str.split(','); 430 for (i = 0, len = arr.length; i < len; i++) { 431 arr[i] = arr[i].split(':'); 432 val = arr[i][1]; 433 434 // Convert a string value if it is like a boolean 435 if (typeof val === 'string' || val instanceof String) { 436 val = val === 'true' || (val === 'false' ? false : val); 437 } 438 439 // Convert a string value if it is like a number 440 if (typeof val === 'string' || val instanceof String) { 441 val = !isNaN(val) ? +val : val; 442 } 443 444 obj[arr[i][0]] = val; 445 } 446 447 return obj; 448 } 449 450 /** 451 * Generates a string separated by dashes and prefixed with NAMESPACE 452 * @private 453 * @param {...String} 454 * @returns {String} 455 */ 456 function namespacify() { 457 var result = NAMESPACE; 458 459 for (var i = 0; i < arguments.length; ++i) { 460 result += '-' + arguments[i]; 461 } 462 463 return result; 464 } 465 466 /** 467 * Handles the hashchange event 468 * @private 469 * @listens hashchange 470 */ 471 function handleHashChangeEvent() { 472 var id = location.hash.replace('#', ''); 473 var instance; 474 var $elem; 475 476 if (!id) { 477 478 // Check if we have currently opened modal and animation was completed 479 if (current && current.state === STATES.OPENED && current.settings.hashTracking) { 480 current.close(); 481 } 482 } else { 483 484 // Catch syntax error if your hash is bad 485 try { 486 $elem = $( 487 '[data-' + PLUGIN_NAME + '-id="' + id + '"]' 488 ); 489 } catch (err) {} 490 491 if ($elem && $elem.length) { 492 instance = $[PLUGIN_NAME].lookup[$elem.data(PLUGIN_NAME)]; 493 494 if (instance && instance.settings.hashTracking) { 495 instance.open(); 496 } 497 } 498 499 } 500 } 501 502 /** 503 * Remodal constructor 504 * @constructor 505 * @param {jQuery} $modal 506 * @param {Object} options 507 */ 508 function Remodal($modal, options) { 509 var $body = $(document.body); 510 var $appendTo = $body; 511 var remodal = this; 512 513 remodal.settings = $.extend({}, DEFAULTS, options); 514 remodal.index = $[PLUGIN_NAME].lookup.push(remodal) - 1; 515 remodal.state = STATES.CLOSED; 516 517 remodal.$overlay = $('.' + namespacify('overlay')); 518 519 if (remodal.settings.appendTo !== null && remodal.settings.appendTo.length) { 520 $appendTo = $(remodal.settings.appendTo); 521 } 522 523 if (!remodal.$overlay.length) { 524 remodal.$overlay = $('<div>').addClass(namespacify('overlay') + ' ' + namespacify('is', STATES.CLOSED)).hide(); 525 $appendTo.append(remodal.$overlay); 526 } 527 528 remodal.$bg = $('.' + namespacify('bg')).addClass(namespacify('is', STATES.CLOSED)); 529 530 remodal.$modal = $modal 531 .addClass( 532 NAMESPACE + ' ' + 533 namespacify('is-initialized') + ' ' + 534 remodal.settings.modifier + ' ' + 535 namespacify('is', STATES.CLOSED)) 536 .attr('tabindex', '-1'); 537
538 remodal.$wrapper = $('<div>') 539 .addClass( 540 namespacify('wrapper') + ' ' + 541 remodal.settings.modifier + ' ' + 542 namespacify('is', STATES.CLOSED)) 543 .hide() 544 .append(remodal.$modal); 545 $appendTo.append(remodal.$wrapper); 546 547 // Add the event listener for the close button 548 remodal.$wrapper.on('click.' + NAMESPACE, '[data-' + PLUGIN_NAME + '-action="close"]', function(e) { 549 e.preventDefault(); 550 551 remodal.close(); 552 }); 553 554 // Add the event listener for the cancel button 555 remodal.$wrapper.on('click.' + NAMESPACE, '[data-' + PLUGIN_NAME + '-action="cancel"]', function(e) { 556 e.preventDefault(); 557 558 remodal.$modal.trigger(STATE_CHANGE_REASONS.CANCELLATION); 559 560 if (remodal.settings.closeOnCancel) { 561 remodal.close(STATE_CHANGE_REASONS.CANCELLATION); 562 } 563 }); 564 565 // Add the event listener for the confirm button 566 remodal.$wrapper.on('click.' + NAMESPACE, '[data-' + PLUGIN_NAME + '-action="confirm"]', function(e) { 567 e.preventDefault(); 568 569 remodal.$modal.trigger(STATE_CHANGE_REASONS.CONFIRMATION); 570 571 if (remodal.settings.closeOnConfirm) { 572 remodal.close(STATE_CHANGE_REASONS.CONFIRMATION); 573 } 574 }); 575 576 // Add the event listener for the overlay 577 remodal.$wrapper.on('click.' + NAMESPACE, function(e) { 578 var $target = $(e.target); 579 580 if (!$target.hasClass(namespacify('wrapper'))) { 581 return; 582 } 583 584 if (remodal.settings.closeOnOutsideClick) { 585 remodal.close(); 586 } 587 }); 588 } 589 590 /** 591 * Opens a modal window 592 * @public 593 */ 594 Remodal.prototype.open = function() { 595 var remodal = this; 596 var id; 597 598 // Check if the animation was completed 599 if (remodal.state === STATES.OPENING || remodal.state === STATES.CLOSING) { 600 return; 601 } 602 603 id = remodal.$modal.attr('data-' + PLUGIN_NAME + '-id'); 604 605 if (id && remodal.settings.hashTracking) { 606 scrollTop = $(window).scrollTop(); 607 location.hash = id; 608 } 609 610 if (current && current !== remodal) { 611 halt(current); 612 } 613 614 current = remodal; 615 lockScreen(); 616 remodal.$bg.addClass(remodal.settings.modifier); 617 remodal.$overlay.addClass(remodal.settings.modifier).show(); 618 remodal.$wrapper.show().scrollTop(0); 619 remodal.$modal.focus(); 620 621 syncWithAnimation( 622 function() { 623 setState(remodal, STATES.OPENING); 624 }, 625 626 function() { 627 setState(remodal, STATES.OPENED); 628 }, 629 630 remodal); 631 }; 632 633 /** 634 * Closes a modal window 635 * @public 636 * @param {String} reason 637 */ 638 Remodal.prototype.close = function(reason) { 639 var remodal = this; 640 641 // Check if the animation was completed 642 if (remodal.state === STATES.OPENING || remodal.state === STATES.CLOSING || remodal.state === STATES.CLOSED) { 643 return; 644 } 645 646 if ( 647 remodal.settings.hashTracking && 648 remodal.$modal.attr('data-' + PLUGIN_NAME + '-id') === location.hash.substr(1) 649 ) { 650 location.hash = ''; 651 $(window).scrollTop(scrollTop); 652 } 653 654 syncWithAnimation( 655 function() { 656 setState(remodal, STATES.CLOSING, false, reason); 657 }, 658 659 function() { 660 remodal.$bg.removeClass(remodal.settings.modifier); 661 remodal.$overlay.removeClass(remodal.settings.modifier).hide(); 662 remodal.$wrapper.hide(); 663 unlockScreen(); 664 665 setState(remodal, STATES.CLOSED, false, reason); 666 }, 667 668 remodal); 669 }; 670 671 /** 672 * Returns a current state of a modal 673 * @public 674 * @returns {STATES} 675 */ 676 Remodal.prototype.getState = function() { 677 return this.state; 678 }; 679 680 /** 681 * Destroys a modal 682 * @public 683 */ 684 Remodal.prototype.destroy = function() { 685 var lookup = $[PLUGIN_NAME].lookup; 686 var instanceCount; 687 688 halt(this); 689 this.$wrapper.remove(); 690 691 delete lookup[this.index]; 692 instanceCount = $.grep(lookup, function(instance) { 693 return !!instance; 694 }).length; 695 696 if (instanceCount === 0) { 697 this.$overlay.remove(); 698 this.$bg.removeClass( 699 namespacify('is', STATES.CLOSING) + ' ' + 700 namespacify('is', STATES.OPENING) + ' ' + 701 namespacify('is', STATES.CLOSED) + ' ' + 702 namespacify('is', STATES.OPENED)); 703 } 704 }; 705 706 /** 707 * Special plugin object for instances 708 * @public 709 * @type {Object} 710 */ 711 $[PLUGIN_NAME] = { 712 lookup: [] 713 }; 714 715 /** 716 * Plugin constructor 717 * @constructor 718 * @param {Object} options 719 * @returns {JQuery} 720 */ 721 $.fn[PLUGIN_NAME] = function(opts) { 722 var instance; 723 var $elem; 724 725 this.each(function(index, elem) { 726 $elem = $(elem); 727 728 if ($elem.data(PLUGIN_NAME) == null) { 729 instance = new Remodal($elem, opts); 730 $elem.data(PLUGIN_NAME, instance.index); 731 732 if ( 733 instance.settings.hashTracking && 734 $elem.attr('data-' + PLUGIN_NAME + '-id') === location.hash.substr(1) 735 ) { 736 instance.open(); 737 } 738 } else { 739 instance = $[PLUGIN_NAME].lookup[$elem.data(PLUGIN_NAME)]; 740 } 741 }); 742 743 return instance; 744 }; 745 746 $(document).ready(function() { 747 748 // data-remodal-target opens a modal window with the special Id 749 $(document).on('click', '[data-' + PLUGIN_NAME + '-target]', function(e) { 750 e.preventDefault(); 751 752 var elem = e.currentTarget; 753 var id = elem.getAttribute('data-' + PLUGIN_NAME + '-target'); 754 var $target = $('[data-' + PLUGIN_NAME + '-id="' + id + '"]'); 755 756 $[PLUGIN_NAME].lookup[$target.data(PLUGIN_NAME)].open(); 757 }); 758 759 // Auto initialization of modal windows 760 // They should have the 'remodal' class attribute 761 // Also you can write the `data-remodal-options` attribute to pass params into the modal 762 $(document).find('.' + NAMESPACE).each(function(i, container) { 763 var $container = $(container);
764 var options = $container.data(PLUGIN_NAME + '-options'); 765 766 if (!options) { 767 options = {}; 768 } else if (typeof options === 'string' || options instanceof String) { 769 options = parseOptions(options); 770 } 771 772 $container[PLUGIN_NAME](options); 773 }); 774 775 // Handles the keydown event 776 $(document).on('keydown.' + NAMESPACE, function(e) { 777 if (current && current.settings.closeOnEscape && current.state === STATES.OPENED && e.keyCode === 27) { 778 current.close(); 779 } 780 }); 781 782 // Handles the hashchange event 783 $(window).on('hashchange.' + NAMESPACE, handleHashChangeEvent); 784 }); 785});
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.