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
