UNPKG

@thednp/position-observer

Version:

🏯 PositionObserver is a JavaScript tool that provides a way to asynchronously observe changes in the position of a target element within its viewport.

1 lines 12.6 kB
{"version":3,"file":"index.mjs","names":["callback: PositionObserverCallback","options?: PositionObserverOptions","target: Element","updates: IntersectionObserverEntry[]"],"sources":["../package.json","../src/index.ts"],"sourcesContent":["{\n \"name\": \"@thednp/position-observer\",\n \"version\": \"1.1.0\",\n \"author\": \"thednp\",\n \"license\": \"MIT\",\n \"description\": \"🏯 PositionObserver is a JavaScript tool that provides a way to asynchronously observe changes in the position of a target element within its viewport.\",\n \"homepage\": \"https://thednp.github.io/position-observer/\",\n \"source\": \"./src/index.ts\",\n \"main\": \"./dist/index.js\",\n \"module\": \"./dist/index.mjs\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"import\": \"./dist/index.mjs\",\n \"require\": \"./dist/index.js\"\n },\n \"./package.json\": \"./package.json\"\n },\n \"scripts\": {\n \"pre-test\": \"pnpm clean-coverage\",\n \"dev\": \"vite --open ./demo/index.html --port 8577\",\n \"serve\": \"vite --open ./docs/index.html --port 8577\",\n \"test\": \"pnpm pre-test && vitest --config vitest.config.ts\",\n \"test-ui\": \"pnpm pre-test && vitest --config vitest.config-ui.ts --browser=chrome\",\n \"clean-coverage\": \"rm -rf coverage .nyc_output\",\n \"badges\": \"npx -p dependency-version-badge update-badge typescript vitest vite\",\n \"build\": \"tsdown\",\n \"build_\": \"vite build\",\n \"build:all\": \"pnpm build && pnpm build:docs\",\n \"build:docs\": \"vite build demo --config demo/vite.config.ts\",\n \"lint\": \"deno lint src && pnpm check:ts\",\n \"format\": \"deno fmt src\",\n \"check:ts\": \"tsc --noEmit\",\n \"prepublishOnly\": \"pnpm update --latest && pnpm lint && pnpm format && pnpm build:all && pnpm badges\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/thednp/position-observer/issues\"\n },\n \"publishConfig\": {\n \"access\": \"public\",\n \"registry\": \"https://registry.npmjs.org/\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/thednp/position-observer.git\"\n },\n \"keywords\": [\n \"position-observer\",\n \"observer\",\n \"intersection\",\n \"resize\",\n \"position\",\n \"typescript\"\n ],\n \"dependencies\": {\n \"@thednp/shorty\": \"^2.0.11\"\n },\n \"devDependencies\": {\n \"@types/node\": \"^24.0.0\",\n \"@vitest/browser\": \"^3.2.3\",\n \"@vitest/coverage-istanbul\": \"^3.2.3\",\n \"@vitest/ui\": \"^3.2.3\",\n \"playwright\": \"^1.53.0\",\n \"tsdown\": \"^0.12.7\",\n \"typescript\": \"5.8.3\",\n \"vite\": \"^6.3.5\",\n \"vitest\": \"^3.2.3\"\n },\n \"packageManager\": \"pnpm@8.6.12\",\n \"engines\": {\n \"node\": \">=16\",\n \"pnpm\": \">=8.6.0\"\n }\n}\n","import { isElement, isFunction } from \"@thednp/shorty\";\nimport { version } from \"../package.json\";\n\nexport type PositionObserverCallback = (\n entries: IntersectionObserverEntry[],\n observer: PositionObserver,\n) => void;\n\nconst callbackModes = [\"all\", \"intersecting\", \"update\"] as const;\ntype CallbackMode = typeof callbackModes[number];\ntype CallbackModeIndex = 0 | 1 | 2;\n\nexport type PositionObserverOptions = {\n root?: Element; // PositionObserver root only, IntersectionObserver root is always the document\n rootMargin?: IntersectionObserverInit[\"rootMargin\"];\n threshold?: IntersectionObserverInit[\"threshold\"];\n callbackMode?: CallbackMode;\n};\n\nconst errorString = \"PositionObserver Error\";\n\n/**\n * The PositionObserver class is a utility class that observes the position\n * of DOM elements and triggers a callback when their position changes.\n */\nexport default class PositionObserver {\n public entries: Map<Element, IntersectionObserverEntry>;\n public static version = version;\n /** `PositionObserver.tick` */\n protected _t: number;\n /** `PositionObserver.root` */\n protected _r: Element;\n /** `PositionObserver.callbackMode` */\n protected _cm: CallbackModeIndex;\n /** `PositionObserver.root.clientWidth` */\n protected _w: number;\n /** `PositionObserver.root.clientHeight` */\n protected _h: number;\n /** `IntersectionObserver.options.rootMargin` */\n protected _rm: string | undefined;\n /** `IntersectionObserver.options.threshold` */\n protected _th: number | number[] | undefined;\n /** `PositionObserver.callback` */\n protected _c: PositionObserverCallback;\n\n /**\n * The constructor takes two arguments, a `callback`, which is called\n * whenever the position of an observed element changes and an `options` object.\n * The callback function takes an array of `PositionObserverEntry` objects\n * as its first argument and the PositionObserver instance as its second argument.\n *\n * @param callback the callback that applies to all targets of this observer\n * @param options the options of this observer\n */\n constructor(\n callback: PositionObserverCallback,\n options?: PositionObserverOptions,\n ) {\n if (!isFunction(callback)) {\n throw new Error(`${errorString}: ${callback} is not a function.`);\n }\n this.entries = new Map<Element, IntersectionObserverEntry>();\n this._c = callback;\n this._t = 0;\n const root = isElement(options?.root)\n ? options.root\n /* istanbul ignore next @preserve */\n : document?.documentElement;\n this._r = root;\n this._rm = options?.rootMargin;\n this._th = options?.threshold;\n /* istanbul ignore next @preserve */\n this._cm = callbackModes.indexOf(\n options?.callbackMode || \"intersecting\",\n ) as CallbackModeIndex;\n this._w = root.clientWidth;\n this._h = root.clientHeight;\n }\n\n /**\n * Start observing the position of the specified element.\n * If the element is not currently attached to the DOM,\n * it will NOT be added to the entries.\n *\n * @param target an `Element` target\n */\n public observe = (target: Element) => {\n if (!isElement(target)) {\n throw new Error(\n `${errorString}: ${target} is not an instance of Element.`,\n );\n }\n\n /* istanbul ignore else @preserve - a guard must be set */\n if (!this._r.contains(target)) return;\n\n // define a new entry\n // push the entry into the queue\n this._n(target).then((ioEntry) => {\n /* istanbul ignore else @preserve - don't allow duplicate entries */\n if (ioEntry.boundingClientRect && !this.getEntry(target)) {\n this.entries.set(target, ioEntry);\n }\n\n /* istanbul ignore else @preserve */\n if (!this._t) this._t = requestAnimationFrame(this._rc);\n });\n };\n\n /**\n * Stop observing the position of the specified element.\n *\n * @param target an `Element` target\n */\n public unobserve = (target: Element) => {\n /* istanbul ignore else @preserve */\n if (this.entries.has(target)) this.entries.delete(target);\n };\n\n /**\n * Private method responsible for all the heavy duty,\n * the observer's runtime.\n * `PositionObserver.runCallback`\n */\n protected _rc = () => {\n /* istanbul ignore if @preserve - a guard must be set */\n if (!this.entries.size) {\n this._t = 0;\n return;\n }\n const { clientWidth, clientHeight } = this._r;\n\n const queue = new Promise<IntersectionObserverEntry[]>((resolve) => {\n const updates: IntersectionObserverEntry[] = [];\n this.entries.forEach(\n (\n {\n target,\n boundingClientRect: oldBoundingBox,\n isIntersecting: oldIsIntersecting,\n },\n ) => {\n /* istanbul ignore if @preserve - a guard must be set when target has been removed */\n if (!this._r.contains(target)) return;\n\n this._n(target).then((ioEntry) => {\n /* istanbul ignore if @preserve - make sure to only count visible entries */\n if (!ioEntry.isIntersecting) {\n if (this._cm === 1) { // 1 = \"intersecting\"\n return;\n } else if (this._cm === 2) { // 2 = \"update\"\n if (oldIsIntersecting) {\n this.entries.set(target, ioEntry);\n updates.push(ioEntry);\n }\n return;\n }\n }\n // 0 = \"all\"\n const { left, top } = ioEntry.boundingClientRect;\n\n /* istanbul ignore else @preserve - only schedule entries that changed position */\n if (\n oldBoundingBox.top !== top || oldBoundingBox.left !== left ||\n this._w !== clientWidth || this._h !== clientHeight\n ) {\n this.entries.set(target, ioEntry);\n updates.push(ioEntry);\n }\n });\n },\n );\n // update root client width & height\n this._w = clientWidth;\n this._h = clientHeight;\n\n resolve(updates);\n });\n\n this._t = requestAnimationFrame(async () => {\n // execute the queue\n const updates: IntersectionObserverEntry[] = await queue;\n\n // only execute the callback if position actually changed\n /* istanbul ignore else @preserve */\n if (updates.length) this._c(updates, this);\n\n this._rc();\n });\n };\n\n /**\n * Check intersection status and resolve it\n * right away.\n *\n * `PositionObserver.newEntryForTarget`\n *\n * @param target an `Element` target\n */\n protected _n = (target: Element) => {\n return new Promise<IntersectionObserverEntry>((resolve) => {\n const intersectionObserver = new IntersectionObserver(\n ([ioEntry], ob) => {\n ob.disconnect();\n resolve(ioEntry);\n },\n {\n // IntersectionObserver.root is always the document\n // root: target.ownerDocument || document,\n threshold: this._th,\n rootMargin: this._rm,\n },\n );\n\n intersectionObserver.observe(target);\n });\n };\n\n /**\n * Find the entry for a given target.\n *\n * @param target an `HTMLElement` target\n */\n public getEntry = (target: Element) => this.entries.get(target);\n\n /**\n * Immediately stop observing all elements.\n */\n public disconnect = () => {\n cancelAnimationFrame(this._t);\n this.entries.clear();\n this._t = 0;\n };\n}\n"],"mappings":"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"}