github.com/fanux/shipyard@v0.0.0-20161009071005-6515ce223235/controller/static/semantic/dist/components/sticky.js (about) 1 /*! 2 * # Semantic UI x.x - Sticky 3 * http://github.com/semantic-org/semantic-ui/ 4 * 5 * 6 * Copyright 2014 Contributorss 7 * Released under the MIT license 8 * http://opensource.org/licenses/MIT 9 * 10 */ 11 12 ;(function ( $, window, document, undefined ) { 13 14 "use strict"; 15 16 $.fn.sticky = function(parameters) { 17 var 18 $allModules = $(this), 19 moduleSelector = $allModules.selector || '', 20 21 time = new Date().getTime(), 22 performance = [], 23 24 query = arguments[0], 25 methodInvoked = (typeof query == 'string'), 26 queryArguments = [].slice.call(arguments, 1), 27 returnedValue 28 ; 29 30 $allModules 31 .each(function() { 32 var 33 settings = ( $.isPlainObject(parameters) ) 34 ? $.extend(true, {}, $.fn.sticky.settings, parameters) 35 : $.extend({}, $.fn.sticky.settings), 36 37 className = settings.className, 38 namespace = settings.namespace, 39 error = settings.error, 40 41 eventNamespace = '.' + namespace, 42 moduleNamespace = 'module-' + namespace, 43 44 $module = $(this), 45 $window = $(window), 46 $container = $module.offsetParent(), 47 $scroll = $(settings.scrollContext), 48 $context, 49 50 selector = $module.selector || '', 51 instance = $module.data(moduleNamespace), 52 53 requestAnimationFrame = window.requestAnimationFrame 54 || window.mozRequestAnimationFrame 55 || window.webkitRequestAnimationFrame 56 || window.msRequestAnimationFrame 57 || function(callback) { setTimeout(callback, 0); }, 58 59 element = this, 60 observer, 61 module 62 ; 63 64 module = { 65 66 initialize: function() { 67 68 module.determineContext(); 69 module.verbose('Initializing sticky', settings, $container); 70 71 module.save.positions(); 72 module.checkErrors(); 73 module.bind.events(); 74 75 if(settings.observeChanges) { 76 module.observeChanges(); 77 } 78 module.instantiate(); 79 }, 80 81 instantiate: function() { 82 module.verbose('Storing instance of module', module); 83 instance = module; 84 $module 85 .data(moduleNamespace, module) 86 ; 87 }, 88 89 destroy: function() { 90 module.verbose('Destroying previous module'); 91 module.reset(); 92 if(observer) { 93 observer.disconnect(); 94 } 95 $window.off('resize' + eventNamespace, module.event.resize); 96 $scroll.off('scroll' + eventNamespace, module.event.scroll); 97 $module.removeData(moduleNamespace); 98 }, 99 100 observeChanges: function() { 101 var 102 context = $context[0] 103 ; 104 if('MutationObserver' in window) { 105 observer = new MutationObserver(function(mutations) { 106 clearTimeout(module.timer); 107 module.timer = setTimeout(function() { 108 module.verbose('DOM tree modified, updating sticky menu'); 109 module.refresh(); 110 }, 20); 111 }); 112 observer.observe(element, { 113 childList : true, 114 subtree : true 115 }); 116 observer.observe(context, { 117 childList : true, 118 subtree : true 119 }); 120 module.debug('Setting up mutation observer', observer); 121 } 122 }, 123 124 determineContext: function() { 125 if(settings.context) { 126 $context = $(settings.context); 127 } 128 else { 129 $context = $container; 130 } 131 if($context.length === 0) { 132 module.error(error.invalidContext, settings.context, $module); 133 return; 134 } 135 }, 136 137 checkErrors: function() { 138 if( module.is.hidden() ) { 139 module.error(error.visible, $module); 140 } 141 if(module.cache.element.height > module.cache.context.height) { 142 module.reset(); 143 module.error(error.elementSize, $module); 144 return; 145 } 146 }, 147 148 bind: { 149 events: function() { 150 $window.on('resize' + eventNamespace, module.event.resize); 151 $scroll.on('scroll' + eventNamespace, module.event.scroll); 152 } 153 }, 154 155 event: { 156 resize: function() { 157 requestAnimationFrame(function() { 158 module.refresh(); 159 module.stick(); 160 }); 161 }, 162 scroll: function() { 163 requestAnimationFrame(function() { 164 module.stick(); 165 settings.onScroll.call(element); 166 }); 167 } 168 }, 169 170 refresh: function(hardRefresh) { 171 module.reset(); 172 if(hardRefresh) { 173 $container = $module.offsetParent(); 174 } 175 module.save.positions(); 176 module.stick(); 177 settings.onReposition.call(element); 178 }, 179 180 supports: { 181 sticky: function() { 182 var 183 $element = $('<div/>'), 184 element = $element.get() 185 ; 186 $element.addClass(className.supported); 187 return($element.css('position').match('sticky')); 188 } 189 }, 190 191 save: { 192 scroll: function(scroll) { 193 module.lastScroll = scroll; 194 }, 195 positions: function() { 196 var 197 window = { 198 height: $window.height() 199 }, 200 element = { 201 margin: { 202 top : parseInt($module.css('margin-top'), 10), 203 bottom : parseInt($module.css('margin-bottom'), 10), 204 }, 205 offset : $module.offset(), 206 width : $module.outerWidth(), 207 height : $module.outerHeight() 208 }, 209 context = { 210 offset : $context.offset(), 211 height : $context.outerHeight(), 212 bottomPadding : parseInt($context.css('padding-bottom'), 10) 213 } 214 ; 215 module.cache = { 216 fits : ( element.height < window.height ), 217 window: { 218 height: window.height 219 }, 220 element: { 221 margin : element.margin, 222 top : element.offset.top - element.margin.top, 223 left : element.offset.left, 224 width : element.width, 225 height : element.height, 226 bottom : element.offset.top + element.height 227 }, 228 context: { 229 top : context.offset.top, 230 height : context.height, 231 bottomPadding : context.bottomPadding, 232 bottom : context.offset.top + context.height - context.bottomPadding 233 } 234 }; 235 module.set.containerSize(); 236 module.set.size(); 237 module.stick(); 238 module.debug('Caching element positions', module.cache); 239 } 240 }, 241 242 get: { 243 direction: function(scroll) { 244 var 245 direction = 'down' 246 ; 247 scroll = scroll || $scroll.scrollTop(); 248 if(module.lastScroll !== undefined) { 249 if(module.lastScroll < scroll) { 250 direction = 'down'; 251 } 252 else if(module.lastScroll > scroll) { 253 direction = 'up'; 254 } 255 } 256 return direction; 257 }, 258 scrollChange: function(scroll) { 259 scroll = scroll || $scroll.scrollTop(); 260 return (module.lastScroll) 261 ? (scroll - module.lastScroll) 262 : 0 263 ; 264 }, 265 currentElementScroll: function() { 266 return ( module.is.top() ) 267 ? Math.abs(parseInt($module.css('top'), 10)) || 0 268 : Math.abs(parseInt($module.css('bottom'), 10)) || 0 269 ; 270 }, 271 elementScroll: function(scroll) { 272 scroll = scroll || $scroll.scrollTop(); 273 var 274 element = module.cache.element, 275 window = module.cache.window, 276 delta = module.get.scrollChange(scroll), 277 maxScroll = (element.height - window.height + settings.offset), 278 currentScroll = module.get.currentElementScroll(), 279 possibleScroll = (currentScroll + delta), 280 elementScroll 281 ; 282 if(module.cache.fits || possibleScroll < 0) { 283 elementScroll = 0; 284 } 285 else if (possibleScroll > maxScroll ) { 286 elementScroll = maxScroll; 287 } 288 else { 289 elementScroll = possibleScroll; 290 } 291 return elementScroll; 292 } 293 }, 294 295 remove: { 296 offset: function() { 297 $module.css('margin-top', ''); 298 } 299 }, 300 301 set: { 302 offset: function() { 303 module.verbose('Setting offset on element', settings.offset); 304 $module.css('margin-top', settings.offset); 305 }, 306 containerSize: function() { 307 var 308 tagName = $container.get(0).tagName 309 ; 310 if(tagName === 'HTML' || tagName == 'body') { 311 // this can trigger for too many reasons 312 //module.error(error.container, tagName, $module); 313 $container = $module.offsetParent(); 314 } 315 else { 316 module.debug('Settings container size', module.cache.context.height); 317 if( Math.abs($container.height() - module.cache.context.height) > 5) { 318 $container.css({ 319 height: module.cache.context.height 320 }); 321 } 322 } 323 }, 324 scroll: function(scroll) { 325 module.debug('Setting scroll on element', scroll); 326 if( module.is.top() ) { 327 $module 328 .css('bottom', '') 329 .css('top', -scroll) 330 ; 331 } 332 if( module.is.bottom() ) { 333 $module 334 .css('top', '') 335 .css('bottom', scroll) 336 ; 337 } 338 }, 339 size: function() { 340 if(module.cache.element.height !== 0 && module.cache.element.width !== 0) { 341 $module 342 .css({ 343 width : module.cache.element.width, 344 height : module.cache.element.height 345 }) 346 ; 347 } 348 } 349 }, 350 351 is: { 352 top: function() { 353 return $module.hasClass(className.top); 354 }, 355 bottom: function() { 356 return $module.hasClass(className.bottom); 357 }, 358 initialPosition: function() { 359 return (!module.is.fixed() && !module.is.bound()); 360 }, 361 hidden: function() { 362 return (!$module.is(':visible')); 363 }, 364 bound: function() { 365 return $module.hasClass(className.bound); 366 }, 367 fixed: function() { 368 return $module.hasClass(className.fixed); 369 } 370 }, 371 372 stick: function() { 373 var 374 cache = module.cache, 375 fits = cache.fits, 376 element = cache.element, 377 window = cache.window, 378 context = cache.context, 379 offset = (module.is.bottom() && settings.pushing) 380 ? settings.bottomOffset 381 : settings.offset, 382 scroll = { 383 top : $scroll.scrollTop() + offset, 384 bottom : $scroll.scrollTop() + offset + window.height 385 }, 386 direction = module.get.direction(scroll.top), 387 elementScroll = module.get.elementScroll(scroll.top), 388 389 // shorthand 390 doesntFit = !fits, 391 elementVisible = (element.height !== 0) 392 ; 393 394 // save current scroll for next run 395 module.save.scroll(scroll.top); 396 397 if(elementVisible) { 398 399 if( module.is.initialPosition() ) { 400 if(scroll.top >= context.bottom) { 401 console.log(scroll.top, context.bottom); 402 module.debug('Element bottom of container'); 403 module.bindBottom(); 404 } 405 else if(scroll.top >= element.top) { 406 module.debug('Element passed, fixing element to page'); 407 module.fixTop(); 408 } 409 } 410 else if( module.is.fixed() ) { 411 412 // currently fixed top 413 if( module.is.top() ) { 414 if( scroll.top < element.top ) { 415 module.debug('Fixed element reached top of container'); 416 module.setInitialPosition(); 417 } 418 else if( (element.height + scroll.top - elementScroll) > context.bottom ) { 419 module.debug('Fixed element reached bottom of container'); 420 module.bindBottom(); 421 } 422 // scroll element if larger than screen 423 else if(doesntFit) { 424 module.set.scroll(elementScroll); 425 } 426 } 427 428 // currently fixed bottom 429 else if(module.is.bottom() ) { 430 431 // top edge 432 if( (scroll.bottom - element.height) < element.top) { 433 module.debug('Bottom fixed rail has reached top of container'); 434 module.setInitialPosition(); 435 } 436 // bottom edge 437 else if(scroll.bottom > context.bottom) { 438 module.debug('Bottom fixed rail has reached bottom of container'); 439 module.bindBottom(); 440 } 441 // scroll element if larger than screen 442 else if(doesntFit) { 443 module.set.scroll(elementScroll); 444 } 445 446 } 447 } 448 else if( module.is.bottom() ) { 449 if(settings.pushing) { 450 if(module.is.bound() && scroll.bottom < context.bottom ) { 451 module.debug('Fixing bottom attached element to bottom of browser.'); 452 module.fixBottom(); 453 } 454 } 455 else { 456 if(module.is.bound() && (scroll.top < context.bottom - element.height) ) { 457 module.debug('Fixing bottom attached element to top of browser.'); 458 module.fixTop(); 459 } 460 } 461 } 462 } 463 }, 464 465 bindTop: function() { 466 module.debug('Binding element to top of parent container'); 467 module.remove.offset(); 468 $module 469 .css('left' , '') 470 .css('top' , '') 471 .css('margin-bottom' , '') 472 .removeClass(className.fixed) 473 .removeClass(className.bottom) 474 .addClass(className.bound) 475 .addClass(className.top) 476 ; 477 settings.onTop.call(element); 478 settings.onUnstick.call(element); 479 }, 480 bindBottom: function() { 481 module.debug('Binding element to bottom of parent container'); 482 module.remove.offset(); 483 $module 484 .css('left' , '') 485 .css('top' , '') 486 .css('margin-bottom' , module.cache.context.bottomPadding) 487 .removeClass(className.fixed) 488 .removeClass(className.top) 489 .addClass(className.bound) 490 .addClass(className.bottom) 491 ; 492 settings.onBottom.call(element); 493 settings.onUnstick.call(element); 494 }, 495 496 setInitialPosition: function() { 497 module.unfix(); 498 module.unbind(); 499 }, 500 501 502 fixTop: function() { 503 module.debug('Fixing element to top of page'); 504 module.set.offset(); 505 $module 506 .css('left', module.cache.element.left) 507 .css('bottom' , '') 508 .removeClass(className.bound) 509 .removeClass(className.bottom) 510 .addClass(className.fixed) 511 .addClass(className.top) 512 ; 513 settings.onStick.call(element); 514 }, 515 516 fixBottom: function() { 517 module.debug('Sticking element to bottom of page'); 518 module.set.offset(); 519 $module 520 .css('left', module.cache.element.left) 521 .css('bottom' , '') 522 .removeClass(className.bound) 523 .removeClass(className.top) 524 .addClass(className.fixed) 525 .addClass(className.bottom) 526 ; 527 settings.onStick.call(element); 528 }, 529 530 unbind: function() { 531 module.debug('Removing absolute position on element'); 532 module.remove.offset(); 533 $module 534 .removeClass(className.bound) 535 .removeClass(className.top) 536 .removeClass(className.bottom) 537 ; 538 }, 539 540 unfix: function() { 541 module.debug('Removing fixed position on element'); 542 module.remove.offset(); 543 $module 544 .removeClass(className.fixed) 545 .removeClass(className.top) 546 .removeClass(className.bottom) 547 ; 548 settings.onUnstick.call(element); 549 }, 550 551 reset: function() { 552 module.debug('Reseting elements position'); 553 module.unbind(); 554 module.unfix(); 555 module.resetCSS(); 556 }, 557 558 resetCSS: function() { 559 $module 560 .css({ 561 top : '', 562 bottom : '', 563 width : '', 564 height : '' 565 }) 566 ; 567 $container 568 .css({ 569 height: '' 570 }) 571 ; 572 }, 573 574 setting: function(name, value) { 575 if( $.isPlainObject(name) ) { 576 $.extend(true, settings, name); 577 } 578 else if(value !== undefined) { 579 settings[name] = value; 580 } 581 else { 582 return settings[name]; 583 } 584 }, 585 internal: function(name, value) { 586 if( $.isPlainObject(name) ) { 587 $.extend(true, module, name); 588 } 589 else if(value !== undefined) { 590 module[name] = value; 591 } 592 else { 593 return module[name]; 594 } 595 }, 596 debug: function() { 597 if(settings.debug) { 598 if(settings.performance) { 599 module.performance.log(arguments); 600 } 601 else { 602 module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); 603 module.debug.apply(console, arguments); 604 } 605 } 606 }, 607 verbose: function() { 608 if(settings.verbose && settings.debug) { 609 if(settings.performance) { 610 module.performance.log(arguments); 611 } 612 else { 613 module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); 614 module.verbose.apply(console, arguments); 615 } 616 } 617 }, 618 error: function() { 619 module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); 620 module.error.apply(console, arguments); 621 }, 622 performance: { 623 log: function(message) { 624 var 625 currentTime, 626 executionTime, 627 previousTime 628 ; 629 if(settings.performance) { 630 currentTime = new Date().getTime(); 631 previousTime = time || currentTime; 632 executionTime = currentTime - previousTime; 633 time = currentTime; 634 performance.push({ 635 'Name' : message[0], 636 'Arguments' : [].slice.call(message, 1) || '', 637 'Element' : element, 638 'Execution Time' : executionTime 639 }); 640 } 641 clearTimeout(module.performance.timer); 642 module.performance.timer = setTimeout(module.performance.display, 0); 643 }, 644 display: function() { 645 var 646 title = settings.name + ':', 647 totalTime = 0 648 ; 649 time = false; 650 clearTimeout(module.performance.timer); 651 $.each(performance, function(index, data) { 652 totalTime += data['Execution Time']; 653 }); 654 title += ' ' + totalTime + 'ms'; 655 if(moduleSelector) { 656 title += ' \'' + moduleSelector + '\''; 657 } 658 if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { 659 console.groupCollapsed(title); 660 if(console.table) { 661 console.table(performance); 662 } 663 else { 664 $.each(performance, function(index, data) { 665 console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); 666 }); 667 } 668 console.groupEnd(); 669 } 670 performance = []; 671 } 672 }, 673 invoke: function(query, passedArguments, context) { 674 var 675 object = instance, 676 maxDepth, 677 found, 678 response 679 ; 680 passedArguments = passedArguments || queryArguments; 681 context = element || context; 682 if(typeof query == 'string' && object !== undefined) { 683 query = query.split(/[\. ]/); 684 maxDepth = query.length - 1; 685 $.each(query, function(depth, value) { 686 var camelCaseValue = (depth != maxDepth) 687 ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) 688 : query 689 ; 690 if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { 691 object = object[camelCaseValue]; 692 } 693 else if( object[camelCaseValue] !== undefined ) { 694 found = object[camelCaseValue]; 695 return false; 696 } 697 else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { 698 object = object[value]; 699 } 700 else if( object[value] !== undefined ) { 701 found = object[value]; 702 return false; 703 } 704 else { 705 return false; 706 } 707 }); 708 } 709 if ( $.isFunction( found ) ) { 710 response = found.apply(context, passedArguments); 711 } 712 else if(found !== undefined) { 713 response = found; 714 } 715 if($.isArray(returnedValue)) { 716 returnedValue.push(response); 717 } 718 else if(returnedValue !== undefined) { 719 returnedValue = [returnedValue, response]; 720 } 721 else if(response !== undefined) { 722 returnedValue = response; 723 } 724 return found; 725 } 726 }; 727 728 if(methodInvoked) { 729 if(instance === undefined) { 730 module.initialize(); 731 } 732 module.invoke(query); 733 } 734 else { 735 if(instance !== undefined) { 736 instance.invoke('destroy'); 737 } 738 module.initialize(); 739 } 740 }) 741 ; 742 743 return (returnedValue !== undefined) 744 ? returnedValue 745 : this 746 ; 747 }; 748 749 $.fn.sticky.settings = { 750 751 name : 'Sticky', 752 namespace : 'sticky', 753 754 debug : false, 755 verbose : false, 756 performance : false, 757 758 pushing : false, 759 context : false, 760 scrollContext : window, 761 762 offset : 0, 763 bottomOffset : 0, 764 765 observeChanges : true, 766 767 onReposition : function(){}, 768 onScroll : function(){}, 769 onStick : function(){}, 770 onUnstick : function(){}, 771 onTop : function(){}, 772 onBottom : function(){}, 773 774 error : { 775 container : 'Sticky element must be inside a relative container', 776 visible : 'Element is hidden, you must call refresh after element becomes visible', 777 method : 'The method you called is not defined.', 778 invalidContext : 'Context specified does not exist', 779 elementSize : 'Sticky element is larger than its container, cannot create sticky.' 780 }, 781 782 className : { 783 bound : 'bound', 784 fixed : 'fixed', 785 supported : 'native', 786 top : 'top', 787 bottom : 'bottom' 788 } 789 790 }; 791 792 })( jQuery, window , document );