var Registry = /** @class */ (function () { function Registry() { this.registry = new WeakMap(); } Registry.prototype.elementExists = function (elem) { return this.registry.has(elem); }; Registry.prototype.getElement = function (elem) { return this.registry.get(elem); }; /** * administrator for lookup in the future * * @method add * @param {HTMLElement | Window} element - the item to add to root element registry * @param {IOption} options * @param {IOption.root} [root] - contains optional root e.g. window, container div, etc * @param {IOption.watcher} [observer] - optional * @public */ Registry.prototype.addElement = function (element, options) { if (!element) { return; } this.registry.set(element, options || {}); }; /** * @method remove * @param {HTMLElement|Window} target * @public */ Registry.prototype.removeElement = function (target) { this.registry.delete(target); }; /** * reset weak map * * @method destroy * @public */ Registry.prototype.destroyRegistry = function () { this.registry = new WeakMap(); }; return Registry; }()); var noop = function () { }; var CallbackType; (function (CallbackType) { CallbackType["enter"] = "enter"; CallbackType["exit"] = "exit"; })(CallbackType || (CallbackType = {})); var Notifications = /** @class */ (function () { function Notifications() { this.registry = new Registry(); } /** * Adds an EventListener as a callback for an event key. * @param type 'enter' or 'exit' * @param key The key of the event * @param callback The callback function to invoke when the event occurs */ Notifications.prototype.addCallback = function (type, element, callback) { var _a, _b; var entry; if (type === CallbackType.enter) { entry = (_a = {}, _a[CallbackType.enter] = callback, _a); } else { entry = (_b = {}, _b[CallbackType.exit] = callback, _b); } this.registry.addElement(element, Object.assign({}, this.registry.getElement(element), entry)); }; /** * @hidden * Executes registered callbacks for key. * @param type * @param element * @param data */ Notifications.prototype.dispatchCallback = function (type, element, data) { if (type === CallbackType.enter) { var _a = this.registry.getElement(element).enter, enter = _a === void 0 ? noop : _a; enter(data); } else { // no element in WeakMap possible because element may be removed from DOM by the time we get here var found = this.registry.getElement(element); if (found && found.exit) { found.exit(data); } } }; return Notifications; }()); var __extends = (undefined && undefined.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __assign = (undefined && undefined.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var IntersectionObserverAdmin = /** @class */ (function (_super) { __extends(IntersectionObserverAdmin, _super); function IntersectionObserverAdmin() { var _this = _super.call(this) || this; _this.elementRegistry = new Registry(); return _this; } /** * Adds element to observe via IntersectionObserver and stores element + relevant callbacks and observer options in static * administrator for lookup in the future * * @method observe * @param {HTMLElement | Window} element * @param {Object} options * @public */ IntersectionObserverAdmin.prototype.observe = function (element, options) { if (options === void 0) { options = {}; } if (!element) { return; } this.elementRegistry.addElement(element, __assign({}, options)); this.setupObserver(element, __assign({}, options)); }; /** * Unobserve target element and remove element from static admin * * @method unobserve * @param {HTMLElement|Window} target * @param {Object} options * @public */ IntersectionObserverAdmin.prototype.unobserve = function (target, options) { var matchingRootEntry = this.findMatchingRootEntry(options); if (matchingRootEntry) { var intersectionObserver = matchingRootEntry.intersectionObserver; intersectionObserver.unobserve(target); } }; /** * register event to handle when intersection observer detects enter * * @method addEnterCallback * @public */ IntersectionObserverAdmin.prototype.addEnterCallback = function (element, callback) { this.addCallback(CallbackType.enter, element, callback); }; /** * register event to handle when intersection observer detects exit * * @method addExitCallback * @public */ IntersectionObserverAdmin.prototype.addExitCallback = function (element, callback) { this.addCallback(CallbackType.exit, element, callback); }; /** * retrieve registered callback and call with data * * @method dispatchEnterCallback * @public */ IntersectionObserverAdmin.prototype.dispatchEnterCallback = function (element, entry) { this.dispatchCallback(CallbackType.enter, element, entry); }; /** * retrieve registered callback and call with data on exit * * @method dispatchExitCallback * @public */ IntersectionObserverAdmin.prototype.dispatchExitCallback = function (element, entry) { this.dispatchCallback(CallbackType.exit, element, entry); }; /** * cleanup data structures and unobserve elements * * @method destroy * @public */ IntersectionObserverAdmin.prototype.destroy = function () { this.elementRegistry.destroyRegistry(); }; /** * use function composition to curry options * * @method setupOnIntersection * @param {Object} options */ IntersectionObserverAdmin.prototype.setupOnIntersection = function (options) { var _this = this; return function (ioEntries) { return _this.onIntersection(options, ioEntries); }; }; IntersectionObserverAdmin.prototype.setupObserver = function (element, options) { var _a; var _b = options.root, root = _b === void 0 ? window : _b; // First - find shared root element (window or target HTMLElement) // this root is responsible for coordinating it's set of elements var potentialRootMatch = this.findRootFromRegistry(root); // Second - if there is a matching root, see if an existing entry with the same options // regardless of sort order. This is a bit of work var matchingEntryForRoot; if (potentialRootMatch) { matchingEntryForRoot = this.determineMatchingElements(options, potentialRootMatch); } // next add found entry to elements and call observer if applicable if (matchingEntryForRoot) { var elements = matchingEntryForRoot.elements, intersectionObserver = matchingEntryForRoot.intersectionObserver; elements.push(element); if (intersectionObserver) { intersectionObserver.observe(element); } } else { // otherwise start observing this element if applicable // watcher is an instance that has an observe method var intersectionObserver = this.newObserver(element, options); var observerEntry = { elements: [element], intersectionObserver: intersectionObserver, options: options }; // and add entry to WeakMap under a root element // with watcher so we can use it later on var stringifiedOptions = this.stringifyOptions(options); if (potentialRootMatch) { // if share same root and need to add new entry to root match // not functional but :shrug potentialRootMatch[stringifiedOptions] = observerEntry; } else { // no root exists, so add to WeakMap this.elementRegistry.addElement(root, (_a = {}, _a[stringifiedOptions] = observerEntry, _a)); } } }; IntersectionObserverAdmin.prototype.newObserver = function (element, options) { // No matching entry for root in static admin, thus create new IntersectionObserver instance var root = options.root, rootMargin = options.rootMargin, threshold = options.threshold; var newIO = new IntersectionObserver(this.setupOnIntersection(options).bind(this), { root: root, rootMargin: rootMargin, threshold: threshold }); newIO.observe(element); return newIO; }; /** * IntersectionObserver callback when element is intersecting viewport * either when `isIntersecting` changes or `intersectionRadio` crosses on of the * configured `threshold`s. * Exit callback occurs eagerly (when element is initially out of scope) * See https://stackoverflow.com/questions/53214116/intersectionobserver-callback-firing-immediately-on-page-load/53385264#53385264 * * @method onIntersection * @param {Object} options * @param {Array} ioEntries * @private */ IntersectionObserverAdmin.prototype.onIntersection = function (options, ioEntries) { var _this = this; ioEntries.forEach(function (entry) { var isIntersecting = entry.isIntersecting, intersectionRatio = entry.intersectionRatio; var threshold = options.threshold || 0; if (Array.isArray(threshold)) { threshold = threshold[threshold.length - 1]; } // then find entry's callback in static administration var matchingRootEntry = _this.findMatchingRootEntry(options); // first determine if entry intersecting if (isIntersecting || intersectionRatio > threshold) { if (matchingRootEntry) { matchingRootEntry.elements.some(function (element) { if (element && element === entry.target) { _this.dispatchEnterCallback(element, entry); return true; } return false; }); } } else { if (matchingRootEntry) { matchingRootEntry.elements.some(function (element) { if (element && element === entry.target) { _this.dispatchExitCallback(element, entry); return true; } return false; }); } } }); }; /** * { root: { stringifiedOptions: { observer, elements: []...] } } * @method findRootFromRegistry * @param {HTMLElement|Window} root * @private * @return {Object} of elements that share same root */ IntersectionObserverAdmin.prototype.findRootFromRegistry = function (root) { if (this.elementRegistry) { return this.elementRegistry.getElement(root); } }; /** * We don't care about options key order because we already added * to the static administrator * * @method findMatchingRootEntry * @param {Object} options * @return {Object} entry with elements and other options */ IntersectionObserverAdmin.prototype.findMatchingRootEntry = function (options) { var _a = options.root, root = _a === void 0 ? window : _a; var matchingRoot = this.findRootFromRegistry(root); if (matchingRoot) { var stringifiedOptions = this.stringifyOptions(options); return matchingRoot[stringifiedOptions]; } }; /** * Determine if existing elements for a given root based on passed in options * regardless of sort order of keys * * @method determineMatchingElements * @param {Object} options * @param {Object} potentialRootMatch e.g. { stringifiedOptions: { elements: [], ... }, stringifiedOptions: { elements: [], ... }} * @private * @return {Object} containing array of elements and other meta */ IntersectionObserverAdmin.prototype.determineMatchingElements = function (options, potentialRootMatch) { var _this = this; var matchingStringifiedOptions = Object.keys(potentialRootMatch).filter(function (key) { var comparableOptions = potentialRootMatch[key].options; return _this.areOptionsSame(options, comparableOptions); })[0]; return potentialRootMatch[matchingStringifiedOptions]; }; /** * recursive method to test primitive string, number, null, etc and complex * object equality. * * @method areOptionsSame * @param {any} a * @param {any} b * @private * @return {boolean} */ IntersectionObserverAdmin.prototype.areOptionsSame = function (a, b) { if (a === b) { return true; } // simple comparison var type1 = Object.prototype.toString.call(a); var type2 = Object.prototype.toString.call(b); if (type1 !== type2) { return false; } else if (type1 !== '[object Object]' && type2 !== '[object Object]') { return a === b; } if (a && b && typeof a === 'object' && typeof b === 'object') { // complex comparison for only type of [object Object] for (var key in a) { if (Object.prototype.hasOwnProperty.call(a, key)) { // recursion to check nested if (this.areOptionsSame(a[key], b[key]) === false) { return false; } } } } // if nothing failed return true; }; /** * Stringify options for use as a key. * Excludes options.root so that the resulting key is stable * * @param {Object} options * @private * @return {String} */ IntersectionObserverAdmin.prototype.stringifyOptions = function (options) { var root = options.root; var replacer = function (key, value) { if (key === 'root' && root) { var classList = Array.prototype.slice.call(root.classList); var classToken = classList.reduce(function (acc, item) { return (acc += item); }, ''); var id = root.id; return "".concat(id, "-").concat(classToken); } return value; }; return JSON.stringify(options, replacer); }; return IntersectionObserverAdmin; }(Notifications)); export default IntersectionObserverAdmin; //# sourceMappingURL=intersection-observer-admin.es5.js.map