Skip to content

Commit 8b68234

Browse files
committed
feat: add MultipleIntersectionObserver
1 parent c3855a6 commit 8b68234

File tree

5 files changed

+247
-0
lines changed

5 files changed

+247
-0
lines changed

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,35 @@ As an alternative to binding the `intersecting` prop, you can listen to the `int
136136
</IntersectionObserver>
137137
```
138138

139+
### Multiple elements
140+
141+
For performance, use `MultipleIntersectionObserver` to observe multiple elements.
142+
143+
This avoids instantiating a new observer for every element.
144+
145+
```svelte
146+
<script>
147+
import { MultipleIntersectionObserver } from "svelte-intersection-observer";
148+
149+
let ref1;
150+
let ref2;
151+
152+
$: elements = [ref1, ref2];
153+
</script>
154+
155+
<header />
156+
157+
<MultipleIntersectionObserver {elements} let:elementIntersections>
158+
{#each elements as element, index}
159+
{@const visible = elementIntersections.get(element)}
160+
161+
<div bind:this={element} class:visible>
162+
Item {index + 1}
163+
</div>
164+
{/each}
165+
</MultipleIntersectionObserver>
166+
```
167+
139168
## API
140169

141170
### Props
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<script>
2+
// @ts-check
3+
/**
4+
* Array of HTML Elements to observe.
5+
* Use this for better performance when observing multiple elements.
6+
* @type {HTMLElement[]}
7+
*/
8+
export let elements = [];
9+
10+
/**
11+
* Set to `true` to unobserve the element
12+
* after it intersects the viewport.
13+
* @type {boolean}
14+
*/
15+
export let once = false;
16+
17+
/**
18+
* Specify the containing element.
19+
* Defaults to the browser viewport.
20+
* @type {null | HTMLElement}
21+
*/
22+
export let root = null;
23+
24+
/** Margin offset of the containing element. */
25+
export let rootMargin = "0px";
26+
27+
/**
28+
* Percentage of element visibility to trigger an event.
29+
* Value must be between 0 and 1.
30+
*/
31+
export let threshold = 0;
32+
33+
/**
34+
* Map of element to its intersection state.
35+
* @type {Map<HTMLElement, boolean>}
36+
*/
37+
export let elementIntersections = new Map();
38+
39+
/**
40+
* Map of element to its latest entry.
41+
* @type {Map<HTMLElement, IntersectionObserverEntry>}
42+
*/
43+
export let elementEntries = new Map();
44+
45+
/**
46+
* `IntersectionObserver` instance.
47+
* @type {null | IntersectionObserver}
48+
*/
49+
export let observer = null;
50+
51+
import { tick, createEventDispatcher, afterUpdate, onMount } from "svelte";
52+
53+
const dispatch = createEventDispatcher();
54+
55+
/** @type {null | string} */
56+
let prevRootMargin = null;
57+
58+
/** @type {null | HTMLElement} */
59+
let prevElement = null;
60+
61+
/** @type {HTMLElement[]} */
62+
let prevElements = [];
63+
64+
const initialize = () => {
65+
observer = new IntersectionObserver(
66+
(entries) => {
67+
entries.forEach((_entry) => {
68+
const target = /** @type {HTMLElement} */ (_entry.target);
69+
70+
elementIntersections.set(target, _entry.isIntersecting);
71+
elementEntries.set(target, _entry);
72+
73+
// Trigger reactivity.
74+
elementIntersections = new Map(elementIntersections);
75+
elementEntries = new Map(elementEntries);
76+
77+
dispatch("observe", { entry: _entry, target });
78+
79+
if (_entry.isIntersecting) {
80+
dispatch("intersect", { entry: _entry, target });
81+
if (once) observer?.unobserve(target);
82+
}
83+
});
84+
},
85+
{ root, rootMargin, threshold },
86+
);
87+
};
88+
89+
onMount(() => {
90+
initialize();
91+
92+
return () => {
93+
if (observer) {
94+
observer.disconnect();
95+
observer = null;
96+
}
97+
};
98+
});
99+
100+
afterUpdate(async () => {
101+
await tick();
102+
103+
if (elements.length > 0) {
104+
const newElements = elements.filter((el) => !prevElements.includes(el));
105+
newElements.forEach((el) => {
106+
if (el) observer?.observe(el);
107+
});
108+
109+
const removedElements = prevElements.filter(
110+
(el) => !elements.includes(el),
111+
);
112+
removedElements.forEach((el) => {
113+
if (el) observer?.unobserve(el);
114+
});
115+
116+
prevElements = [...elements];
117+
}
118+
119+
if (prevRootMargin && rootMargin !== prevRootMargin) {
120+
observer?.disconnect();
121+
prevElement = null;
122+
prevElements = [];
123+
initialize();
124+
125+
elements.forEach((el) => {
126+
if (el) observer?.observe(el);
127+
});
128+
}
129+
130+
prevRootMargin = rootMargin;
131+
});
132+
</script>
133+
134+
<slot {observer} {elementIntersections} {elementEntries} />
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import type { SvelteComponentTyped } from "svelte";
2+
3+
export default class extends SvelteComponentTyped<
4+
{
5+
/**
6+
* Array of HTML Elements to observe.
7+
* Use this for better performance when observing multiple elements.
8+
* @default []
9+
*/
10+
elements?: HTMLElement[];
11+
12+
/**
13+
* Set to `true` to unobserve the element
14+
* after it intersects the viewport.
15+
* @default false
16+
*/
17+
once?: boolean;
18+
19+
/**
20+
* Specify the containing element.
21+
* Defaults to the browser viewport.
22+
* @default null
23+
*/
24+
root?: null | HTMLElement;
25+
26+
/**
27+
* Margin offset of the containing element.
28+
* @default "0px"
29+
*/
30+
rootMargin?: string;
31+
32+
/**
33+
* Percentage of element visibility to trigger an event.
34+
* Value must be a number between 0 and 1, or an array of numbers between 0 and 1.
35+
* @default 0
36+
*/
37+
threshold?: number | number[];
38+
39+
/**
40+
* Map of element to its intersection state.
41+
* @default new Map()
42+
*/
43+
elementIntersections?: Map<HTMLElement, boolean>;
44+
45+
/**
46+
* Map of element to its latest entry.
47+
* @default new Map()
48+
*/
49+
elementEntries?: Map<HTMLElement, IntersectionObserverEntry>;
50+
51+
/**
52+
* `IntersectionObserver` instance.
53+
* @default null
54+
*/
55+
observer?: null | IntersectionObserver;
56+
},
57+
{
58+
/**
59+
* Dispatched when an element is first observed
60+
* and also whenever an intersection event occurs.
61+
*/
62+
observe: CustomEvent<{
63+
entry: IntersectionObserverEntry;
64+
target: HTMLElement;
65+
}>;
66+
67+
/**
68+
* Dispatched only when an element is intersecting the viewport.
69+
*/
70+
intersect: CustomEvent<{
71+
entry: IntersectionObserverEntry & { isIntersecting: true };
72+
target: HTMLElement;
73+
}>;
74+
},
75+
{
76+
default: {
77+
observer: IntersectionObserver;
78+
elementIntersections: Map<HTMLElement, boolean>;
79+
elementEntries: Map<HTMLElement, IntersectionObserverEntry>;
80+
};
81+
}
82+
> {}

src/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { default } from "./IntersectionObserver.svelte";
2+
export { default as MultipleIntersectionObserver } from "./MultipleIntersectionObserver.svelte";

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { default } from "./IntersectionObserver.svelte";
2+
export { default as MultipleIntersectionObserver } from "./MultipleIntersectionObserver.svelte";

0 commit comments

Comments
 (0)