Skip to content

Instantly share code, notes, and snippets.

@aayla-secura
Last active June 14, 2024 10:15
Show Gist options
  • Save aayla-secura/fb03e3ea3e6553c5a9208d4273617c89 to your computer and use it in GitHub Desktop.
Save aayla-secura/fb03e3ea3e6553c5a9208d4273617c89 to your computer and use it in GitHub Desktop.
Javascript Resize Observer wrapper with additions
// supports:
// - throttling by calling handler only once after a given delay has passed since the last resize
// - pausing and resuming (with or without calling callback at resume)
// - observing multiple targets and ensuring the handler is called only once all are added
// - observing targets without calling the handler initially when adding, only when later resized
class ResizeObserverExt {
observer = null;
#waitingResolver = null;
#targets = new Set();
#isPaused = false;
#skip = new Set();
constructor(callback, delay = 0) {
this.#waitingResolver = new WaitingResolver(
this.#getCallbackWrapper(callback, delay),
);
this.observer = new ResizeObserver((entries) => {
this.#waitingResolver.add(...entries);
});
}
observe(...targets) {
// call handler only once all are added
this.#waitingResolver.waitForMore(targets.length);
targets.forEach((target) => {
if (this.#targets.has(target)) {
console.log("target already present", target);
} else {
console.log("adding target", target);
this.#targets.add(target);
this.observer.observe(target);
}
});
}
// does not call the handler when adding the targets
observeLater(...targets) {
targets.forEach((target) => {
this.#skip.add(target);
});
this.observe(...targets);
}
unobserve(target) {
if (this.#targets.has(target)) {
console.log("removing target", target);
this.#targets.delete(target);
this.observer.unobserve(target);
} else {
console.log("no such target", target);
}
}
disconnect() {
console.log("removing all targets");
this.#targets.clear();
return this.observer.disconnect();
}
pause() {
if (this.#isPaused) {
console.log("already paused");
return;
}
console.log("pausing");
this.#isPaused = true;
this.observer.disconnect();
}
resume(skipCallback = false) {
if (!this.#isPaused) {
console.log("not paused");
return;
}
console.log("resuming");
if (skipCallback) {
this.#skip = this.#skip.union(this.#targets);
}
this.#isPaused = false;
this.#targets.forEach((el) => {
this.observer.observe(el);
});
}
static #getFuncDelayed(f, delay) {
var timer = 0;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => f.apply(this, args), delay);
};
}
#getCallbackWrapper(callback, delay) {
const wrapped = (entries, observer) => {
var selectedEntries = entries.filter((entry) => {
if (this.#skip.has(entry.target)) {
this.#skip.delete(entry.target);
return false;
}
return true;
});
if (selectedEntries.length > 0) {
callback(selectedEntries, observer);
}
};
if (delay > 0) {
return this.constructor.#getFuncDelayed(wrapped, delay);
}
return wrapped;
}
}
class WaitingResolver {
#callback = null;
#waitFor = 0;
#buffer = [];
constructor(callback, waitFor = 0) {
if ((!callback) instanceof Function) {
throw "Callback must be a function";
}
this.#callback = callback;
this.#waitFor = waitFor;
}
waitForMore(n) {
this.#waitFor += n;
}
add(...values) {
this.#buffer.push(...values);
if (this.#buffer.length >= this.#waitFor) {
this.#callback(this.#buffer);
this.#buffer = [];
this.#waitFor = 0;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment