Facebook
From module, 2 Months ago, written in JavaScript.
Embed
Download Paste or View Raw
Hits: 138
  1. /** @module stance/tracking */
  2.  
  3. define('stance/tracking',[
  4.     'core/utils/array/indexOf',
  5.     'core/utils/array/some',
  6.     'core/utils/html/isVisible',
  7.     'core/utils/object/result',
  8.  
  9.     './utils',
  10.  
  11.     'exports',
  12. ], function (
  13.     indexOf,
  14.     some,
  15.     isElementVisible,
  16.     getResult,
  17.  
  18.     utils,
  19.  
  20.     exports
  21. ) {
  22.     'use strict';
  23.  
  24.     /**
  25.      * Currently registered event wrappers.
  26.      *
  27.      * @type {Array.<Stance>}
  28.      */
  29.     exports.events = [];
  30.  
  31.     /**
  32.      * Last position passed to exports.scroll.
  33.      * Useful for user-facing methods like isVisible.
  34.      *
  35.      * @type {?ViewportPos}
  36.      */
  37.     exports.lastPos = null;
  38.  
  39.     /**
  40.      * Clear cached measurements.
  41.      *
  42.      * @param {Element-like} [obj] - Element or object to clear cached
  43.      *   measurements for. If omitted, entire cache will be cleared.
  44.      */
  45.     exports.clearCache = function (obj) {
  46.         if (obj === undefined) {
  47.             exports.getElementOffset.cache = {};
  48.         } else {
  49.             var id = utils.getId(obj);
  50.             if (id)
  51.                 exports.getElementOffset.cache[id] = null;
  52.         }
  53.     };
  54.  
  55.     /**
  56.      * Offset of element.
  57.      *
  58.      * @typedef ElementOffset
  59.      * @property {number} top - Offset of element from top of document.
  60.      * @property {number} height - Height of element.
  61.      */
  62.  
  63.     /**
  64.      * Calculate the top offset and height of an element.
  65.      * Returns null if element is not visible.
  66.      *
  67.      * @param {external:HTMLElement} el
  68.      * @returns {?ElementOffset}
  69.      */
  70.     exports.calculateOffset = function (el) {
  71.         if (!el)
  72.             return null;
  73.  
  74.         // Element must be in DOM to measure
  75.         if (!isElementVisible(el))
  76.             return null;
  77.  
  78.         var docElem = el.ownerDocument.documentElement;
  79.         return {
  80.             height: el.offsetHeight,
  81.             top: el.getBoundingClientRect().top +
  82.                 window.pageYOffset - (docElem.clientTop || 0),
  83.         };
  84.     };
  85.  
  86.     /**
  87.      * Offset of element, modified by topEdgeOffset and bottomEdgeOffset.
  88.      *
  89.      * @typedef FullOffset
  90.      * @property {number} visibleTop - First pixel from the top of the containing
  91.      *   document which must be above screen bottom to consider element as visible.
  92.      * @property {number} visibleBottom - Last pixel from the top of the containing
  93.      *   document which must be below screen top to consider element as visible.
  94.      * @property {number} offsetTop - Offset of element from the top of the
  95.      *   containing document.
  96.      * @property {number} height - Height of element.
  97.      */
  98.  
  99.     /**
  100.      * Get the modified offset of an element.
  101.      *
  102.      * @param {Element-like} obj
  103.      * @returns {?FullOffset}
  104.      */
  105.     exports._getElementOffset = function (obj) {
  106.         var el = utils.getElement(obj);
  107.         if (!el)
  108.             return null;
  109.  
  110.         var offset = exports.calculateOffset(el);
  111.         if (!offset)
  112.             return null;
  113.  
  114.         return {
  115.             visibleTop: offset.top + (getResult(obj, 'topEdgeOffset') || 0),
  116.             visibleBottom: offset.top + offset.height - (getResult(obj, 'bottomEdgeOffset') || 0),
  117.             offsetTop: offset.top,
  118.             height: offset.height,
  119.         };
  120.     };
  121.  
  122.     /**
  123.      * Get the modified offset of an element, retrieving the cached version if possible
  124.      *
  125.      * @function
  126.      * @param {Element-like} obj
  127.      * @returns {?FullOffset}
  128.      */
  129.     exports.getElementOffset = (function () {
  130.         // A modified _.memoize approach which:
  131.         // 1. Better supports testing
  132.         // 2. Does not cache when unable to generate key
  133.         // 3. Does not cache when result is falsy
  134.         var memoized = function (obj) {
  135.             var cache = memoized.cache;
  136.             var key = utils.getId(obj);
  137.  
  138.             if (key && cache[key])
  139.                 return cache[key];
  140.  
  141.             var result = exports._getElementOffset(obj);
  142.             if (key && result)
  143.                 cache[key] = result;
  144.  
  145.             return result;
  146.         };
  147.         memoized.cache = {};
  148.         return memoized;
  149.     }());
  150.  
  151.     exports.EVENT_NAMES = [
  152.         'enter',
  153.         'exit',
  154.         'visible',
  155.         'invisible',
  156.         'all',
  157.     ];
  158.  
  159.     /**
  160.      * Update internal tracking of element.
  161.      *
  162.      * Checks if there are any relevant event listeners on
  163.      * the wrapper and starts or stops tracking as a result.
  164.      *
  165.      * @param {Stance} wrapper
  166.      */
  167.     exports.updateTracking = function (wrapper) {
  168.         var lastIndex;
  169.         var propOf = function (obj) {
  170.             if (!obj)
  171.                 return function () { return undefined; };
  172.             return function (key) {
  173.                 return obj[key];
  174.             };
  175.         };
  176.  
  177.         if (some(exports.EVENT_NAMES, propOf(wrapper._events))) {
  178.             // Start tracking
  179.             lastIndex = indexOf(exports.events, wrapper);
  180.             if (lastIndex === -1)
  181.                 exports.events.push(wrapper);
  182.         } else {
  183.             // Remove existing tracking (if any)
  184.             lastIndex = indexOf(exports.events, wrapper);
  185.             if (lastIndex !== -1)
  186.                 exports.events.splice(lastIndex, 1);
  187.         }
  188.     };
  189.  
  190.     /**
  191.      * Call any applicable event handlers.
  192.      *
  193.      * @param {ViewportPos} pos - Scroll information.
  194.      */
  195.     exports.processEvents = function (pos) {
  196.         exports.lastPos = pos;
  197.         var events = exports.events;
  198.  
  199.         if (!events.length)
  200.             return;
  201.  
  202.         // Iterate backwards to allow events to remove themselves
  203.         // (ex. via Backbone.Events.once)
  204.         // TODO: This is not fully safe as an event callback could alter
  205.         //       events other than itself
  206.         for (var i = events.length - 1; i >= 0; --i) {
  207.             var wrapper = events[i];
  208.             var visible = wrapper.isVisible(pos);
  209.  
  210.             if (visible === null)
  211.                 continue;
  212.  
  213.             if (visible !== wrapper.lastVisible)
  214.                 wrapper.trigger(visible ? 'enter' : 'exit', wrapper, pos);
  215.  
  216.             wrapper.trigger(visible ? 'visible' : 'invisible', wrapper, pos);
  217.  
  218.             wrapper.lastVisible = visible;
  219.         }
  220.     };
  221. });
  222.  
  223. // https://c.disquscdn.com/next/next-core/core/stance/tracking.js