/** @module stance/tracking */ define('stance/tracking',[ 'core/utils/array/indexOf', 'core/utils/array/some', 'core/utils/html/isVisible', 'core/utils/object/result', './utils', 'exports', ], function ( indexOf, some, isElementVisible, getResult, utils, exports ) { 'use strict'; /** * Currently registered event wrappers. * * @type {Array.} */ exports.events = []; /** * Last position passed to exports.scroll. * Useful for user-facing methods like isVisible. * * @type {?ViewportPos} */ exports.lastPos = null; /** * Clear cached measurements. * * @param {Element-like} [obj] - Element or object to clear cached * measurements for. If omitted, entire cache will be cleared. */ exports.clearCache = function (obj) { if (obj === undefined) { exports.getElementOffset.cache = {}; } else { var id = utils.getId(obj); if (id) exports.getElementOffset.cache[id] = null; } }; /** * Offset of element. * * @typedef ElementOffset * @property {number} top - Offset of element from top of document. * @property {number} height - Height of element. */ /** * Calculate the top offset and height of an element. * Returns null if element is not visible. * * @param {external:HTMLElement} el * @returns {?ElementOffset} */ exports.calculateOffset = function (el) { if (!el) return null; // Element must be in DOM to measure if (!isElementVisible(el)) return null; var docElem = el.ownerDocument.documentElement; return { height: el.offsetHeight, top: el.getBoundingClientRect().top + window.pageYOffset - (docElem.clientTop || 0), }; }; /** * Offset of element, modified by topEdgeOffset and bottomEdgeOffset. * * @typedef FullOffset * @property {number} visibleTop - First pixel from the top of the containing * document which must be above screen bottom to consider element as visible. * @property {number} visibleBottom - Last pixel from the top of the containing * document which must be below screen top to consider element as visible. * @property {number} offsetTop - Offset of element from the top of the * containing document. * @property {number} height - Height of element. */ /** * Get the modified offset of an element. * * @param {Element-like} obj * @returns {?FullOffset} */ exports._getElementOffset = function (obj) { var el = utils.getElement(obj); if (!el) return null; var offset = exports.calculateOffset(el); if (!offset) return null; return { visibleTop: offset.top + (getResult(obj, 'topEdgeOffset') || 0), visibleBottom: offset.top + offset.height - (getResult(obj, 'bottomEdgeOffset') || 0), offsetTop: offset.top, height: offset.height, }; }; /** * Get the modified offset of an element, retrieving the cached version if possible * * @function * @param {Element-like} obj * @returns {?FullOffset} */ exports.getElementOffset = (function () { // A modified _.memoize approach which: // 1. Better supports testing // 2. Does not cache when unable to generate key // 3. Does not cache when result is falsy var memoized = function (obj) { var cache = memoized.cache; var key = utils.getId(obj); if (key && cache[key]) return cache[key]; var result = exports._getElementOffset(obj); if (key && result) cache[key] = result; return result; }; memoized.cache = {}; return memoized; }()); exports.EVENT_NAMES = [ 'enter', 'exit', 'visible', 'invisible', 'all', ]; /** * Update internal tracking of element. * * Checks if there are any relevant event listeners on * the wrapper and starts or stops tracking as a result. * * @param {Stance} wrapper */ exports.updateTracking = function (wrapper) { var lastIndex; var propOf = function (obj) { if (!obj) return function () { return undefined; }; return function (key) { return obj[key]; }; }; if (some(exports.EVENT_NAMES, propOf(wrapper._events))) { // Start tracking lastIndex = indexOf(exports.events, wrapper); if (lastIndex === -1) exports.events.push(wrapper); } else { // Remove existing tracking (if any) lastIndex = indexOf(exports.events, wrapper); if (lastIndex !== -1) exports.events.splice(lastIndex, 1); } }; /** * Call any applicable event handlers. * * @param {ViewportPos} pos - Scroll information. */ exports.processEvents = function (pos) { exports.lastPos = pos; var events = exports.events; if (!events.length) return; // Iterate backwards to allow events to remove themselves // (ex. via Backbone.Events.once) // TODO: This is not fully safe as an event callback could alter // events other than itself for (var i = events.length - 1; i >= 0; --i) { var wrapper = events[i]; var visible = wrapper.isVisible(pos); if (visible === null) continue; if (visible !== wrapper.lastVisible) wrapper.trigger(visible ? 'enter' : 'exit', wrapper, pos); wrapper.trigger(visible ? 'visible' : 'invisible', wrapper, pos); wrapper.lastVisible = visible; } }; }); // https://c.disquscdn.com/next/next-core/core/stance/tracking.js