github.com/fanux/shipyard@v0.0.0-20161009071005-6515ce223235/controller/static/semantic/src/definitions/modules/sticky.js (about)

     1  /*!
     2   * # Semantic UI - 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 );