this repo has no description
at main 428 lines 16 kB view raw
1var Registry = /** @class */ (function () { 2 function Registry() { 3 this.registry = new WeakMap(); 4 } 5 Registry.prototype.elementExists = function (elem) { 6 return this.registry.has(elem); 7 }; 8 Registry.prototype.getElement = function (elem) { 9 return this.registry.get(elem); 10 }; 11 /** 12 * administrator for lookup in the future 13 * 14 * @method add 15 * @param {HTMLElement | Window} element - the item to add to root element registry 16 * @param {IOption} options 17 * @param {IOption.root} [root] - contains optional root e.g. window, container div, etc 18 * @param {IOption.watcher} [observer] - optional 19 * @public 20 */ 21 Registry.prototype.addElement = function (element, options) { 22 if (!element) { 23 return; 24 } 25 this.registry.set(element, options || {}); 26 }; 27 /** 28 * @method remove 29 * @param {HTMLElement|Window} target 30 * @public 31 */ 32 Registry.prototype.removeElement = function (target) { 33 this.registry.delete(target); 34 }; 35 /** 36 * reset weak map 37 * 38 * @method destroy 39 * @public 40 */ 41 Registry.prototype.destroyRegistry = function () { 42 this.registry = new WeakMap(); 43 }; 44 return Registry; 45}()); 46 47var noop = function () { }; 48var CallbackType; 49(function (CallbackType) { 50 CallbackType["enter"] = "enter"; 51 CallbackType["exit"] = "exit"; 52})(CallbackType || (CallbackType = {})); 53var Notifications = /** @class */ (function () { 54 function Notifications() { 55 this.registry = new Registry(); 56 } 57 /** 58 * Adds an EventListener as a callback for an event key. 59 * @param type 'enter' or 'exit' 60 * @param key The key of the event 61 * @param callback The callback function to invoke when the event occurs 62 */ 63 Notifications.prototype.addCallback = function (type, element, callback) { 64 var _a, _b; 65 var entry; 66 if (type === CallbackType.enter) { 67 entry = (_a = {}, _a[CallbackType.enter] = callback, _a); 68 } 69 else { 70 entry = (_b = {}, _b[CallbackType.exit] = callback, _b); 71 } 72 this.registry.addElement(element, Object.assign({}, this.registry.getElement(element), entry)); 73 }; 74 /** 75 * @hidden 76 * Executes registered callbacks for key. 77 * @param type 78 * @param element 79 * @param data 80 */ 81 Notifications.prototype.dispatchCallback = function (type, element, data) { 82 if (type === CallbackType.enter) { 83 var _a = this.registry.getElement(element).enter, enter = _a === void 0 ? noop : _a; 84 enter(data); 85 } 86 else { 87 // no element in WeakMap possible because element may be removed from DOM by the time we get here 88 var found = this.registry.getElement(element); 89 if (found && found.exit) { 90 found.exit(data); 91 } 92 } 93 }; 94 return Notifications; 95}()); 96 97var __extends = (undefined && undefined.__extends) || (function () { 98 var extendStatics = function (d, b) { 99 extendStatics = Object.setPrototypeOf || 100 ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 101 function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; 102 return extendStatics(d, b); 103 }; 104 return function (d, b) { 105 if (typeof b !== "function" && b !== null) 106 throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); 107 extendStatics(d, b); 108 function __() { this.constructor = d; } 109 d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 110 }; 111})(); 112var __assign = (undefined && undefined.__assign) || function () { 113 __assign = Object.assign || function(t) { 114 for (var s, i = 1, n = arguments.length; i < n; i++) { 115 s = arguments[i]; 116 for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 117 t[p] = s[p]; 118 } 119 return t; 120 }; 121 return __assign.apply(this, arguments); 122}; 123var IntersectionObserverAdmin = /** @class */ (function (_super) { 124 __extends(IntersectionObserverAdmin, _super); 125 function IntersectionObserverAdmin() { 126 var _this = _super.call(this) || this; 127 _this.elementRegistry = new Registry(); 128 return _this; 129 } 130 /** 131 * Adds element to observe via IntersectionObserver and stores element + relevant callbacks and observer options in static 132 * administrator for lookup in the future 133 * 134 * @method observe 135 * @param {HTMLElement | Window} element 136 * @param {Object} options 137 * @public 138 */ 139 IntersectionObserverAdmin.prototype.observe = function (element, options) { 140 if (options === void 0) { options = {}; } 141 if (!element) { 142 return; 143 } 144 this.elementRegistry.addElement(element, __assign({}, options)); 145 this.setupObserver(element, __assign({}, options)); 146 }; 147 /** 148 * Unobserve target element and remove element from static admin 149 * 150 * @method unobserve 151 * @param {HTMLElement|Window} target 152 * @param {Object} options 153 * @public 154 */ 155 IntersectionObserverAdmin.prototype.unobserve = function (target, options) { 156 var matchingRootEntry = this.findMatchingRootEntry(options); 157 if (matchingRootEntry) { 158 var intersectionObserver = matchingRootEntry.intersectionObserver; 159 intersectionObserver.unobserve(target); 160 } 161 }; 162 /** 163 * register event to handle when intersection observer detects enter 164 * 165 * @method addEnterCallback 166 * @public 167 */ 168 IntersectionObserverAdmin.prototype.addEnterCallback = function (element, callback) { 169 this.addCallback(CallbackType.enter, element, callback); 170 }; 171 /** 172 * register event to handle when intersection observer detects exit 173 * 174 * @method addExitCallback 175 * @public 176 */ 177 IntersectionObserverAdmin.prototype.addExitCallback = function (element, callback) { 178 this.addCallback(CallbackType.exit, element, callback); 179 }; 180 /** 181 * retrieve registered callback and call with data 182 * 183 * @method dispatchEnterCallback 184 * @public 185 */ 186 IntersectionObserverAdmin.prototype.dispatchEnterCallback = function (element, entry) { 187 this.dispatchCallback(CallbackType.enter, element, entry); 188 }; 189 /** 190 * retrieve registered callback and call with data on exit 191 * 192 * @method dispatchExitCallback 193 * @public 194 */ 195 IntersectionObserverAdmin.prototype.dispatchExitCallback = function (element, entry) { 196 this.dispatchCallback(CallbackType.exit, element, entry); 197 }; 198 /** 199 * cleanup data structures and unobserve elements 200 * 201 * @method destroy 202 * @public 203 */ 204 IntersectionObserverAdmin.prototype.destroy = function () { 205 this.elementRegistry.destroyRegistry(); 206 }; 207 /** 208 * use function composition to curry options 209 * 210 * @method setupOnIntersection 211 * @param {Object} options 212 */ 213 IntersectionObserverAdmin.prototype.setupOnIntersection = function (options) { 214 var _this = this; 215 return function (ioEntries) { 216 return _this.onIntersection(options, ioEntries); 217 }; 218 }; 219 IntersectionObserverAdmin.prototype.setupObserver = function (element, options) { 220 var _a; 221 var _b = options.root, root = _b === void 0 ? window : _b; 222 // First - find shared root element (window or target HTMLElement) 223 // this root is responsible for coordinating it's set of elements 224 var potentialRootMatch = this.findRootFromRegistry(root); 225 // Second - if there is a matching root, see if an existing entry with the same options 226 // regardless of sort order. This is a bit of work 227 var matchingEntryForRoot; 228 if (potentialRootMatch) { 229 matchingEntryForRoot = this.determineMatchingElements(options, potentialRootMatch); 230 } 231 // next add found entry to elements and call observer if applicable 232 if (matchingEntryForRoot) { 233 var elements = matchingEntryForRoot.elements, intersectionObserver = matchingEntryForRoot.intersectionObserver; 234 elements.push(element); 235 if (intersectionObserver) { 236 intersectionObserver.observe(element); 237 } 238 } 239 else { 240 // otherwise start observing this element if applicable 241 // watcher is an instance that has an observe method 242 var intersectionObserver = this.newObserver(element, options); 243 var observerEntry = { 244 elements: [element], 245 intersectionObserver: intersectionObserver, 246 options: options 247 }; 248 // and add entry to WeakMap under a root element 249 // with watcher so we can use it later on 250 var stringifiedOptions = this.stringifyOptions(options); 251 if (potentialRootMatch) { 252 // if share same root and need to add new entry to root match 253 // not functional but :shrug 254 potentialRootMatch[stringifiedOptions] = observerEntry; 255 } 256 else { 257 // no root exists, so add to WeakMap 258 this.elementRegistry.addElement(root, (_a = {}, 259 _a[stringifiedOptions] = observerEntry, 260 _a)); 261 } 262 } 263 }; 264 IntersectionObserverAdmin.prototype.newObserver = function (element, options) { 265 // No matching entry for root in static admin, thus create new IntersectionObserver instance 266 var root = options.root, rootMargin = options.rootMargin, threshold = options.threshold; 267 var newIO = new IntersectionObserver(this.setupOnIntersection(options).bind(this), { root: root, rootMargin: rootMargin, threshold: threshold }); 268 newIO.observe(element); 269 return newIO; 270 }; 271 /** 272 * IntersectionObserver callback when element is intersecting viewport 273 * either when `isIntersecting` changes or `intersectionRadio` crosses on of the 274 * configured `threshold`s. 275 * Exit callback occurs eagerly (when element is initially out of scope) 276 * See https://stackoverflow.com/questions/53214116/intersectionobserver-callback-firing-immediately-on-page-load/53385264#53385264 277 * 278 * @method onIntersection 279 * @param {Object} options 280 * @param {Array} ioEntries 281 * @private 282 */ 283 IntersectionObserverAdmin.prototype.onIntersection = function (options, ioEntries) { 284 var _this = this; 285 ioEntries.forEach(function (entry) { 286 var isIntersecting = entry.isIntersecting, intersectionRatio = entry.intersectionRatio; 287 var threshold = options.threshold || 0; 288 if (Array.isArray(threshold)) { 289 threshold = threshold[threshold.length - 1]; 290 } 291 // then find entry's callback in static administration 292 var matchingRootEntry = _this.findMatchingRootEntry(options); 293 // first determine if entry intersecting 294 if (isIntersecting || intersectionRatio > threshold) { 295 if (matchingRootEntry) { 296 matchingRootEntry.elements.some(function (element) { 297 if (element && element === entry.target) { 298 _this.dispatchEnterCallback(element, entry); 299 return true; 300 } 301 return false; 302 }); 303 } 304 } 305 else { 306 if (matchingRootEntry) { 307 matchingRootEntry.elements.some(function (element) { 308 if (element && element === entry.target) { 309 _this.dispatchExitCallback(element, entry); 310 return true; 311 } 312 return false; 313 }); 314 } 315 } 316 }); 317 }; 318 /** 319 * { root: { stringifiedOptions: { observer, elements: []...] } } 320 * @method findRootFromRegistry 321 * @param {HTMLElement|Window} root 322 * @private 323 * @return {Object} of elements that share same root 324 */ 325 IntersectionObserverAdmin.prototype.findRootFromRegistry = function (root) { 326 if (this.elementRegistry) { 327 return this.elementRegistry.getElement(root); 328 } 329 }; 330 /** 331 * We don't care about options key order because we already added 332 * to the static administrator 333 * 334 * @method findMatchingRootEntry 335 * @param {Object} options 336 * @return {Object} entry with elements and other options 337 */ 338 IntersectionObserverAdmin.prototype.findMatchingRootEntry = function (options) { 339 var _a = options.root, root = _a === void 0 ? window : _a; 340 var matchingRoot = this.findRootFromRegistry(root); 341 if (matchingRoot) { 342 var stringifiedOptions = this.stringifyOptions(options); 343 return matchingRoot[stringifiedOptions]; 344 } 345 }; 346 /** 347 * Determine if existing elements for a given root based on passed in options 348 * regardless of sort order of keys 349 * 350 * @method determineMatchingElements 351 * @param {Object} options 352 * @param {Object} potentialRootMatch e.g. { stringifiedOptions: { elements: [], ... }, stringifiedOptions: { elements: [], ... }} 353 * @private 354 * @return {Object} containing array of elements and other meta 355 */ 356 IntersectionObserverAdmin.prototype.determineMatchingElements = function (options, potentialRootMatch) { 357 var _this = this; 358 var matchingStringifiedOptions = Object.keys(potentialRootMatch).filter(function (key) { 359 var comparableOptions = potentialRootMatch[key].options; 360 return _this.areOptionsSame(options, comparableOptions); 361 })[0]; 362 return potentialRootMatch[matchingStringifiedOptions]; 363 }; 364 /** 365 * recursive method to test primitive string, number, null, etc and complex 366 * object equality. 367 * 368 * @method areOptionsSame 369 * @param {any} a 370 * @param {any} b 371 * @private 372 * @return {boolean} 373 */ 374 IntersectionObserverAdmin.prototype.areOptionsSame = function (a, b) { 375 if (a === b) { 376 return true; 377 } 378 // simple comparison 379 var type1 = Object.prototype.toString.call(a); 380 var type2 = Object.prototype.toString.call(b); 381 if (type1 !== type2) { 382 return false; 383 } 384 else if (type1 !== '[object Object]' && type2 !== '[object Object]') { 385 return a === b; 386 } 387 if (a && b && typeof a === 'object' && typeof b === 'object') { 388 // complex comparison for only type of [object Object] 389 for (var key in a) { 390 if (Object.prototype.hasOwnProperty.call(a, key)) { 391 // recursion to check nested 392 if (this.areOptionsSame(a[key], b[key]) === false) { 393 return false; 394 } 395 } 396 } 397 } 398 // if nothing failed 399 return true; 400 }; 401 /** 402 * Stringify options for use as a key. 403 * Excludes options.root so that the resulting key is stable 404 * 405 * @param {Object} options 406 * @private 407 * @return {String} 408 */ 409 IntersectionObserverAdmin.prototype.stringifyOptions = function (options) { 410 var root = options.root; 411 var replacer = function (key, value) { 412 if (key === 'root' && root) { 413 var classList = Array.prototype.slice.call(root.classList); 414 var classToken = classList.reduce(function (acc, item) { 415 return (acc += item); 416 }, ''); 417 var id = root.id; 418 return "".concat(id, "-").concat(classToken); 419 } 420 return value; 421 }; 422 return JSON.stringify(options, replacer); 423 }; 424 return IntersectionObserverAdmin; 425}(Notifications)); 426 427export default IntersectionObserverAdmin; 428//# sourceMappingURL=intersection-observer-admin.es5.js.map