/** @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.<Stance>}
*/
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