Open izderadicka opened 2 years ago
Hey @izderadicka thanks for that insight. I'll take a look and get back to you :)
Quick and dirty fix.
export const inview = (node: HTMLElement, options = {}) => {
//adapted from https://github.com/maciekgrzybek/svelte-inview
let defaultOptions = {
root: null,
rootMargin: '0px',
threshold: 0,
unobserveOnEnter: false,
}
const { root, rootMargin, threshold, unobserveOnEnter } = {
...defaultOptions,
...options,
};
let prevPos = {
x: undefined,
y: undefined,
};
let scrollDirection: ScrollDirection = {
vertical: undefined,
horizontal: undefined,
};
if (typeof IntersectionObserver !== 'undefined' && node) {
// Global variable to hold observers;
window._inview_observers = window._inview_observers || {};
// Unique id for each observer (defaults to root for viewport)
let observerid = "root";
if (root != null) {
observerid = root.getAttribute("data-svelte-inview-id");
if (!observerid) {
observerid = "_" + Math.floor(Math.random() * 100000);
root.setAttribute("data-svelte-inview-id", observerid);
}
}
window._inview_observers[observerid] = window._inview_observers[observerid] || new IntersectionObserver(
(entries, _observer) => {
entries.forEach((singleEntry) => {
if (prevPos.y > singleEntry.boundingClientRect.y) {
scrollDirection.vertical = 'up';
} else {
scrollDirection.vertical = 'down';
}
if (prevPos.x > singleEntry.boundingClientRect.x) {
scrollDirection.horizontal = 'left';
} else {
scrollDirection.horizontal = 'right';
}
prevPos = {
y: singleEntry.boundingClientRect.y,
x: singleEntry.boundingClientRect.x,
};
const detail = {
inView: singleEntry.isIntersecting,
entry: singleEntry,
scrollDirection,
node: singleEntry.target,
observer: _observer,
};
singleEntry.target.dispatchEvent(new CustomEvent('change', { detail }));
if (singleEntry.isIntersecting) {
singleEntry.target.dispatchEvent(new CustomEvent('enter', { detail }));
unobserveOnEnter && _observer.unobserve(node);
} else {
singleEntry.target.dispatchEvent(new CustomEvent('leave', { detail }));
}
});
},
{
root,
rootMargin,
threshold,
}
);
// This dispatcher has to be wrapped in setTimeout, as it won't work otherwise.
// Not sure why is it happening, maybe a callstack has to pass between the listeners?
// Definitely something to investigate to understand better.
setTimeout(() => {
node.dispatchEvent(
new CustomEvent('init', { detail: { observer: window._inview_observers[observerid], node } })
);
}, 0);
window._inview_observers[observerid].observe(node);
return {
destroy() {
window._inview_observers[observerid].unobserve(node);
},
};
}
}
@brahma-dev it is A solution, but definitely not THE solution :) Keeping things in a global variable doesn't sound like good idea to me, although it might be a solution for a quick fix :) I thought about this whole thing @izderadicka and I'd like to go with the svelte store, but I won't really have time soon to do this. If you want to give it a shot, please, be my guest :) I'll let you know if I'll have time earlier to work on this.
@maciekgrzybek here's an improved version of @brahma-dev 's code that creates a single store that holds all observers. It appears to behave properly.
import { tick } from 'svelte';
import { writable, get, type Readable } from 'svelte/store';
import type {
ObserverEventDetails,
Options,
Position,
ScrollDirection,
Event,
LifecycleEventDetails,
} from './types';
export function createMapStore<T>(initialValue: Record<string, T> = {}) : Readable<Record<string, T>> & {
get: (k:string) => T;
set: (key:string, value:T) => void;
remove: (k:string) => void;
update: (fn: (m:Record<string, T>) => Record<string, T>) => void;
} {
const store = writable(initialValue);
return {
subscribe: store.subscribe,
update: store.update,
get: (k:string) => get(store)[k],
set: (key:string, value:T) => store.update(m => Object.assign({}, m, {[key]: value})),
remove(k:string) {
store.update(s => {
delete s[k];
return s;
});
},
}
}
// Store to hold 'global' observers
export const inview_observers = createMapStore<IntersectionObserver>({});
export default function inview (node: HTMLElement, options:Options = {}){
//adapted from https://github.com/maciekgrzybek/svelte-inview
const { root, rootMargin, threshold, unobserveOnEnter } = {
...defaultOptions,
...options,
};
let prevPos: Position = {
x: undefined,
y: undefined,
};
let scrollDirection: ScrollDirection = {
vertical: undefined,
horizontal: undefined,
};
// This ensures it's running in the browser, so we're safe to modify the shared store
if (typeof IntersectionObserver !== 'undefined' && node) {
let observerid:string = "root";
// Unique id for each observer (defaults to root for viewport)
if (root != null) {
if (!root.getAttribute("data-svelte-inview-id")) {
observerid = "_" + Math.floor(Math.random() * 100000);
root.setAttribute("data-svelte-inview-id", observerid);
}
}
if (!inview_observers.get(observerid)) inview_observers.set(observerid, new IntersectionObserver(
(entries, _observer) => {
entries.forEach((singleEntry) => {
if ((prevPos.y ?? 0) > singleEntry.boundingClientRect.y) {
scrollDirection.vertical = 'up';
} else {
scrollDirection.vertical = 'down';
}
if ((prevPos.x ?? 0) > singleEntry.boundingClientRect.x) {
scrollDirection.horizontal = 'left';
} else {
scrollDirection.horizontal = 'right';
}
prevPos = {
y: singleEntry.boundingClientRect.y,
x: singleEntry.boundingClientRect.x,
};
const detail = {
inView: singleEntry.isIntersecting,
entry: singleEntry,
scrollDirection,
node: singleEntry.target,
observer: _observer,
};
singleEntry.target.dispatchEvent(createEvent('inview_change', detail));
if (singleEntry.isIntersecting) {
singleEntry.target.dispatchEvent(createEvent('inview_enter', detail));
unobserveOnEnter && _observer.unobserve(node);
} else {
singleEntry.target.dispatchEvent(createEvent('inview_leave', detail));
}
});
},
{
root,
rootMargin,
threshold,
}
))
inview_observers.get(observerid).observe(node);
tick().then(() => {
node.dispatchEvent(
createEvent('inview_init', { detail: { observer: inview_observers.get(observerid), node } })
);
});
return {
destroy() {
if (inview_observers.get(observerid)) {
inview_observers.get(observerid)?.unobserve(node);
inview_observers.remove(observerid);
}
},
}
} else {
return {
destroy() {},
}
}
}
@rgon that looks great, can you prepare a PR for that?
I have been looking into code and if I understand code correctly new
IntersectObserver
is created for every component, that has inview action.My scenario has one root element which has many (thousands) of child elements/components , which lazy load an image, when they get into view.
As per this article: https://www.bennadel.com/blog/3954-intersectionobserver-api-performance-many-vs-shared-in-angular-11-0-5.htm it looks that there is notable performance advantage for single
IntersectObserver
having many observed elements, comparing to manyIntersectObservers
, each one just with one observed element.What do you think?