Last active
June 27, 2025 07:29
-
-
Save benkitzelman/68abeb28f14ed7bd5467b7533a428ea9 to your computer and use it in GitHub Desktop.
Sticky Position Manager
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* StickyPositionManager - allows registration of a Top sticky element, | |
* under which all other stickies will stack. Parses existing dom els, and | |
* monitors the dom for added 'managed' els | |
* | |
* -- Assumes all sticky elements have style 'position: sticky; top: 0' --- | |
* | |
* Usage: | |
* To apply to child sticky elements - ensure el has attr 'data-sticky-position-managed' | |
* Then: | |
* ```StickyPositionManager.registerAsTop('.some-top-sticky-el')``` | |
* | |
* And all child sticky elements will stick under .some-top-sticky-el | |
*/ | |
export default class StickyPositionManager { | |
public static registrationAttribute = 'data-sticky-position-managed'; | |
private static instance: StickyPositionManager; | |
private topElSelector?: string; | |
private observer: MutationObserver; | |
static registerAsTop(cssSelector: string) { | |
this.instance ||= new StickyPositionManager(); | |
return this.instance.setTopEl(cssSelector); | |
} | |
constructor() { | |
this.observer = this.createObserver(); | |
} | |
monitorDOM() { | |
this.observer.observe(document.body, { childList: true, subtree: true }); | |
} | |
unmonitorDOM() { | |
this.observer.disconnect(); | |
} | |
onStickyElAdded(el: HTMLElement): void { | |
this.stackUnderTopEl(el); | |
} | |
private topEl() { | |
const topEl = this.topElSelector ? $(this.topElSelector) : undefined; | |
if (topEl?.length !== 0) return topEl; | |
} | |
private setTopEl(cssSelector: string) { | |
this.unmonitorDOM(); | |
this.topElSelector = cssSelector; | |
$(`[${StickyPositionManager.registrationAttribute}]`).each((_idx, el) => { | |
this.stackUnderTopEl(el); | |
}); | |
this.monitorDOM(); | |
return this; | |
} | |
private stackUnderTopEl(el: HTMLElement) { | |
const height = this.topEl()?.outerHeight(); | |
if (!height) return; | |
const currentTop = $(el).css("top")?.match(/^(\d+)/)?.at(1) || 0; | |
$(el).css({ top: `${Number(currentTop) + height}px` }); | |
} | |
private createObserver() { | |
return new MutationObserver((mutations) => { | |
const attribute = StickyPositionManager.registrationAttribute; | |
for (const mutation of mutations) { | |
for (const node of Array.from(mutation.addedNodes)) { | |
if (node instanceof HTMLElement) { | |
// Check if the node itself has the attribute | |
if (node.hasAttribute(attribute)) { | |
this.onStickyElAdded(node); | |
} | |
// Check all descendants for the attribute | |
const elements = node.querySelectorAll(`[${attribute}]`); | |
for (const el of Array.from(elements)) { | |
this.onStickyElAdded(el as HTMLElement); | |
} | |
} | |
} | |
} | |
}); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment