svelte-intersection-observer 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/IntersectionObserver.svelte +49 -100
- package/IntersectionObserver.svelte.d.ts +85 -77
- package/MultipleIntersectionObserver.svelte +55 -108
- package/MultipleIntersectionObserver.svelte.d.ts +88 -80
- package/README.md +171 -132
- package/index.d.ts +2 -2
- package/index.js +1 -1
- package/{intersect.d.ts → intersect.svelte.d.ts} +3 -3
- package/{intersect.js → intersect.svelte.js} +3 -3
- package/package.json +1 -1
|
@@ -1,74 +1,42 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
|
|
3
|
-
* The HTML Element to observe.
|
|
4
|
-
* @type {null | HTMLElement}
|
|
5
|
-
*/
|
|
6
|
-
export let element = null;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Set to `true` to unobserve the element
|
|
10
|
-
* after it intersects the viewport.
|
|
11
|
-
* @type {boolean}
|
|
12
|
-
*/
|
|
13
|
-
export let once = false;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* `true` if the observed element
|
|
17
|
-
* is intersecting the viewport.
|
|
18
|
-
*/
|
|
19
|
-
export let intersecting = false;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Specify the containing element.
|
|
23
|
-
* Defaults to the browser viewport.
|
|
24
|
-
* @type {null | HTMLElement}
|
|
25
|
-
*/
|
|
26
|
-
export let root = null;
|
|
27
|
-
|
|
28
|
-
/** Margin offset of the containing element. */
|
|
29
|
-
export let rootMargin = "0px";
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Percentage of element visibility to trigger an event.
|
|
33
|
-
* Value must be between 0 and 1.
|
|
34
|
-
*/
|
|
35
|
-
export let threshold = 0;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Observed element metadata.
|
|
39
|
-
* @type {null | IntersectionObserverEntry}
|
|
40
|
-
*/
|
|
41
|
-
export let entry = null;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* `IntersectionObserver` instance.
|
|
45
|
-
* @type {null | IntersectionObserver}
|
|
46
|
-
*/
|
|
47
|
-
export let observer = null;
|
|
2
|
+
import { untrack } from "svelte";
|
|
48
3
|
|
|
49
4
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* `
|
|
53
|
-
* @
|
|
5
|
+
* @typedef {Object} Props
|
|
6
|
+
* @property {null | HTMLElement} [element] The HTML Element to observe.
|
|
7
|
+
* @property {boolean} [once] Set to `true` to unobserve the element after it intersects the viewport.
|
|
8
|
+
* @property {boolean} [intersecting] `true` if the observed element is intersecting the viewport.
|
|
9
|
+
* @property {null | HTMLElement} [root] Specify the containing element. Defaults to the browser viewport.
|
|
10
|
+
* @property {string} [rootMargin] Margin offset of the containing element.
|
|
11
|
+
* @property {number | number[]} [threshold] Percentage of element visibility to trigger an event. Value must be between 0 and 1.
|
|
12
|
+
* @property {null | IntersectionObserverEntry} [entry] Observed element metadata.
|
|
13
|
+
* @property {null | IntersectionObserver} [observer] `IntersectionObserver` instance.
|
|
14
|
+
* @property {boolean} [skip] Set to `true` to pause observing without disconnecting the observer or losing `entry`/`intersecting` state. Set back to `false` to resume.
|
|
15
|
+
* @property {(entry: IntersectionObserverEntry) => void} [onobserve] Called when the element is first observed and also whenever an intersection event occurs.
|
|
16
|
+
* @property {(entry: IntersectionObserverEntry & { isIntersecting: true }) => void} [onintersect] Called only when the observed element is intersecting the viewport.
|
|
17
|
+
* @property {import("svelte").Snippet<[{ intersecting: boolean, entry: null | IntersectionObserverEntry, observer: null | IntersectionObserver }]>} [children]
|
|
54
18
|
*/
|
|
55
|
-
export let skip = false;
|
|
56
|
-
|
|
57
|
-
import { afterUpdate, createEventDispatcher, onMount, tick } from "svelte";
|
|
58
|
-
|
|
59
|
-
const dispatch = createEventDispatcher();
|
|
60
|
-
|
|
61
|
-
let prevRootMargin = rootMargin;
|
|
62
19
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
20
|
+
/** @type {Props} */
|
|
21
|
+
let {
|
|
22
|
+
element = null,
|
|
23
|
+
once = false,
|
|
24
|
+
intersecting = $bindable(false),
|
|
25
|
+
root = null,
|
|
26
|
+
rootMargin = "0px",
|
|
27
|
+
threshold = 0,
|
|
28
|
+
entry = $bindable(null),
|
|
29
|
+
observer = $bindable(null),
|
|
30
|
+
skip = false,
|
|
31
|
+
onobserve,
|
|
32
|
+
onintersect,
|
|
33
|
+
children,
|
|
34
|
+
} = $props();
|
|
67
35
|
|
|
68
36
|
/** @type {null | HTMLElement} */
|
|
69
37
|
let prevElement = null;
|
|
70
38
|
|
|
71
|
-
let prevSkip = skip;
|
|
39
|
+
let prevSkip = untrack(() => skip);
|
|
72
40
|
|
|
73
41
|
const initialize = () => {
|
|
74
42
|
observer = new IntersectionObserver(
|
|
@@ -77,10 +45,10 @@
|
|
|
77
45
|
entry = _entry;
|
|
78
46
|
intersecting = _entry.isIntersecting;
|
|
79
47
|
|
|
80
|
-
|
|
48
|
+
onobserve?.(_entry);
|
|
81
49
|
|
|
82
50
|
if (_entry.isIntersecting) {
|
|
83
|
-
|
|
51
|
+
onintersect?.(_entry);
|
|
84
52
|
|
|
85
53
|
if (element && once) observer?.unobserve(element);
|
|
86
54
|
}
|
|
@@ -90,53 +58,34 @@
|
|
|
90
58
|
);
|
|
91
59
|
};
|
|
92
60
|
|
|
93
|
-
|
|
61
|
+
$effect(() => {
|
|
62
|
+
prevElement = null;
|
|
94
63
|
initialize();
|
|
95
64
|
|
|
96
65
|
return () => {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
observer = null;
|
|
100
|
-
}
|
|
66
|
+
observer?.disconnect();
|
|
67
|
+
observer = null;
|
|
101
68
|
};
|
|
102
69
|
});
|
|
103
70
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (!skip) observer?.observe(element);
|
|
71
|
+
$effect(() => {
|
|
72
|
+
const target = element;
|
|
73
|
+
const isSkipped = skip;
|
|
74
|
+
const activeObserver = observer;
|
|
109
75
|
|
|
110
|
-
|
|
111
|
-
|
|
76
|
+
if (target !== null && target !== prevElement) {
|
|
77
|
+
if (!isSkipped) activeObserver?.observe(target);
|
|
78
|
+
if (prevElement !== null) activeObserver?.unobserve(prevElement);
|
|
79
|
+
prevElement = target;
|
|
112
80
|
}
|
|
113
81
|
|
|
114
|
-
if (
|
|
115
|
-
if (
|
|
116
|
-
else
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
rootMargin !== prevRootMargin ||
|
|
121
|
-
threshold !== prevThreshold ||
|
|
122
|
-
root !== prevRoot
|
|
123
|
-
) {
|
|
124
|
-
observer?.disconnect();
|
|
125
|
-
initialize();
|
|
126
|
-
|
|
127
|
-
if (element !== null && !skip) observer?.observe(element);
|
|
128
|
-
prevElement = element;
|
|
82
|
+
if (isSkipped !== prevSkip && target !== null) {
|
|
83
|
+
if (isSkipped) activeObserver?.unobserve(target);
|
|
84
|
+
else activeObserver?.observe(target);
|
|
129
85
|
}
|
|
130
86
|
|
|
131
|
-
|
|
132
|
-
prevThreshold = threshold;
|
|
133
|
-
prevRoot = root;
|
|
134
|
-
prevSkip = skip;
|
|
87
|
+
prevSkip = isSkipped;
|
|
135
88
|
});
|
|
136
89
|
</script>
|
|
137
90
|
|
|
138
|
-
|
|
139
|
-
{intersecting}
|
|
140
|
-
{entry}
|
|
141
|
-
{observer}
|
|
142
|
-
/>
|
|
91
|
+
{@render children?.({ intersecting, entry, observer })}
|
|
@@ -1,87 +1,95 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Component, Snippet } from "svelte";
|
|
2
2
|
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
element?: null | HTMLElement;
|
|
3
|
+
export interface IntersectionObserverProps {
|
|
4
|
+
/**
|
|
5
|
+
* The HTML Element to observe.
|
|
6
|
+
* @default null
|
|
7
|
+
*/
|
|
8
|
+
element?: null | HTMLElement;
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Set to `true` to unobserve the element
|
|
12
|
+
* after it intersects the viewport.
|
|
13
|
+
* @default false
|
|
14
|
+
*/
|
|
15
|
+
once?: boolean;
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
/**
|
|
18
|
+
* `true` if the observed element
|
|
19
|
+
* is intersecting the viewport.
|
|
20
|
+
* @default false
|
|
21
|
+
*/
|
|
22
|
+
intersecting?: boolean;
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Specify the containing element.
|
|
26
|
+
* Defaults to the browser viewport.
|
|
27
|
+
* @default null
|
|
28
|
+
*/
|
|
29
|
+
root?: null | HTMLElement;
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Margin offset of the containing element.
|
|
33
|
+
* @default "0px"
|
|
34
|
+
*/
|
|
35
|
+
rootMargin?: string;
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Percentage of element visibility to trigger an event.
|
|
39
|
+
* Value must be a number between 0 and 1, or an array of numbers between 0 and 1.
|
|
40
|
+
* @default 0
|
|
41
|
+
*/
|
|
42
|
+
threshold?: number | number[];
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Observed element metadata.
|
|
46
|
+
* @default null
|
|
47
|
+
*/
|
|
48
|
+
entry?: null | IntersectionObserverEntry;
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
/**
|
|
51
|
+
* `IntersectionObserver` instance.
|
|
52
|
+
* @default null
|
|
53
|
+
*/
|
|
54
|
+
observer?: null | IntersectionObserver;
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
/**
|
|
67
|
-
* Dispatched when the element is first observed
|
|
68
|
-
* and also whenever an intersection event occurs.
|
|
69
|
-
*/
|
|
70
|
-
observe: CustomEvent<IntersectionObserverEntry>;
|
|
56
|
+
/**
|
|
57
|
+
* Set to `true` to pause observing without disconnecting the
|
|
58
|
+
* observer or losing `entry`/`intersecting` state. Set back to
|
|
59
|
+
* `false` to resume.
|
|
60
|
+
* @default false
|
|
61
|
+
*/
|
|
62
|
+
skip?: boolean;
|
|
71
63
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Called when the element is first observed
|
|
66
|
+
* and also whenever an intersection event occurs.
|
|
67
|
+
*/
|
|
68
|
+
onobserve?: (entry: IntersectionObserverEntry) => void;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Called only when the element is intersecting the viewport.
|
|
72
|
+
* `entry.isIntersecting` will always be `true`.
|
|
73
|
+
*/
|
|
74
|
+
onintersect?: (
|
|
75
|
+
entry: IntersectionObserverEntry & { isIntersecting: true },
|
|
76
|
+
) => void;
|
|
77
|
+
|
|
78
|
+
children?: Snippet<
|
|
79
|
+
[
|
|
80
|
+
{
|
|
81
|
+
intersecting: boolean;
|
|
82
|
+
entry: null | IntersectionObserverEntry;
|
|
83
|
+
observer: null | IntersectionObserver;
|
|
84
|
+
},
|
|
85
|
+
]
|
|
86
|
+
>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
declare const IntersectionObserverComponent: Component<
|
|
90
|
+
IntersectionObserverProps,
|
|
91
|
+
Record<string, never>,
|
|
92
|
+
"intersecting" | "entry" | "observer"
|
|
93
|
+
>;
|
|
94
|
+
|
|
95
|
+
export default IntersectionObserverComponent;
|
|
@@ -1,75 +1,42 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
|
|
3
|
-
* Array of HTML Elements to observe.
|
|
4
|
-
* Use this for better performance when observing multiple elements.
|
|
5
|
-
* @type {(HTMLElement | null)[]}
|
|
6
|
-
*/
|
|
7
|
-
export let elements = [];
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Set to `true` to unobserve the element
|
|
11
|
-
* after it intersects the viewport.
|
|
12
|
-
* @type {boolean}
|
|
13
|
-
*/
|
|
14
|
-
export let once = false;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Specify the containing element.
|
|
18
|
-
* Defaults to the browser viewport.
|
|
19
|
-
* @type {null | HTMLElement}
|
|
20
|
-
*/
|
|
21
|
-
export let root = null;
|
|
22
|
-
|
|
23
|
-
/** Margin offset of the containing element. */
|
|
24
|
-
export let rootMargin = "0px";
|
|
2
|
+
import { untrack } from "svelte";
|
|
25
3
|
|
|
26
4
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
5
|
+
* @typedef {Object} Props
|
|
6
|
+
* @property {(HTMLElement | null)[]} [elements] Array of HTML Elements to observe. Use this for better performance when observing multiple elements.
|
|
7
|
+
* @property {boolean} [once] Set to `true` to unobserve the element after it intersects the viewport.
|
|
8
|
+
* @property {null | HTMLElement} [root] Specify the containing element. Defaults to the browser viewport.
|
|
9
|
+
* @property {string} [rootMargin] Margin offset of the containing element.
|
|
10
|
+
* @property {number | number[]} [threshold] Percentage of element visibility to trigger an event. Value must be between 0 and 1.
|
|
11
|
+
* @property {Map<HTMLElement | null, boolean>} [elementIntersections] Map of element to its intersection state.
|
|
12
|
+
* @property {Map<HTMLElement | null, IntersectionObserverEntry>} [elementEntries] Map of element to its latest entry.
|
|
13
|
+
* @property {null | IntersectionObserver} [observer] `IntersectionObserver` instance.
|
|
14
|
+
* @property {boolean} [skip] Set to `true` to pause observing all elements without disconnecting the observer or losing `elementIntersections`/`elementEntries` state. Set back to `false` to resume.
|
|
15
|
+
* @property {(detail: { entry: IntersectionObserverEntry, target: HTMLElement }) => void} [onobserve] Called when an element is first observed and also whenever an intersection event occurs.
|
|
16
|
+
* @property {(detail: { entry: IntersectionObserverEntry & { isIntersecting: true }, target: HTMLElement }) => void} [onintersect] Called only when an element is intersecting the viewport.
|
|
17
|
+
* @property {import("svelte").Snippet<[{ observer: null | IntersectionObserver, elementIntersections: Map<HTMLElement | null, boolean>, elementEntries: Map<HTMLElement | null, IntersectionObserverEntry> }]>} [children]
|
|
29
18
|
*/
|
|
30
|
-
export let threshold = 0;
|
|
31
19
|
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
*/
|
|
48
|
-
export let observer = null;
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Set to `true` to pause observing all elements without disconnecting
|
|
52
|
-
* the observer or losing `elementIntersections`/`elementEntries` state.
|
|
53
|
-
* Set back to `false` to resume.
|
|
54
|
-
* @type {boolean}
|
|
55
|
-
*/
|
|
56
|
-
export let skip = false;
|
|
57
|
-
|
|
58
|
-
import { afterUpdate, createEventDispatcher, onMount, tick } from "svelte";
|
|
59
|
-
|
|
60
|
-
const dispatch = createEventDispatcher();
|
|
61
|
-
|
|
62
|
-
let prevRootMargin = rootMargin;
|
|
63
|
-
|
|
64
|
-
let prevThreshold = threshold;
|
|
65
|
-
|
|
66
|
-
/** @type {null | HTMLElement} */
|
|
67
|
-
let prevRoot = root;
|
|
20
|
+
/** @type {Props} */
|
|
21
|
+
let {
|
|
22
|
+
elements = [],
|
|
23
|
+
once = false,
|
|
24
|
+
root = null,
|
|
25
|
+
rootMargin = "0px",
|
|
26
|
+
threshold = 0,
|
|
27
|
+
elementIntersections = $bindable(new Map()),
|
|
28
|
+
elementEntries = $bindable(new Map()),
|
|
29
|
+
observer = $bindable(null),
|
|
30
|
+
skip = false,
|
|
31
|
+
onobserve,
|
|
32
|
+
onintersect,
|
|
33
|
+
children,
|
|
34
|
+
} = $props();
|
|
68
35
|
|
|
69
36
|
/** @type {(HTMLElement | null)[]} */
|
|
70
37
|
let prevElements = [];
|
|
71
38
|
|
|
72
|
-
let prevSkip = skip;
|
|
39
|
+
let prevSkip = untrack(() => skip);
|
|
73
40
|
|
|
74
41
|
const initialize = () => {
|
|
75
42
|
observer = new IntersectionObserver(
|
|
@@ -80,10 +47,10 @@
|
|
|
80
47
|
elementIntersections.set(target, _entry.isIntersecting);
|
|
81
48
|
elementEntries.set(target, _entry);
|
|
82
49
|
|
|
83
|
-
|
|
50
|
+
onobserve?.({ entry: _entry, target });
|
|
84
51
|
|
|
85
52
|
if (_entry.isIntersecting) {
|
|
86
|
-
|
|
53
|
+
onintersect?.({ entry: _entry, target });
|
|
87
54
|
if (once) observer?.unobserve(target);
|
|
88
55
|
}
|
|
89
56
|
}
|
|
@@ -96,73 +63,53 @@
|
|
|
96
63
|
);
|
|
97
64
|
};
|
|
98
65
|
|
|
99
|
-
|
|
66
|
+
$effect(() => {
|
|
67
|
+
prevElements = [];
|
|
100
68
|
initialize();
|
|
101
69
|
|
|
102
70
|
return () => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
observer = null;
|
|
106
|
-
}
|
|
71
|
+
observer?.disconnect();
|
|
72
|
+
observer = null;
|
|
107
73
|
};
|
|
108
74
|
});
|
|
109
75
|
|
|
110
|
-
|
|
111
|
-
|
|
76
|
+
$effect(() => {
|
|
77
|
+
const currentElements = elements;
|
|
78
|
+
const isSkipped = skip;
|
|
79
|
+
const activeObserver = observer;
|
|
112
80
|
|
|
113
|
-
if (
|
|
114
|
-
const newElements =
|
|
81
|
+
if (currentElements.length > 0) {
|
|
82
|
+
const newElements = currentElements.filter(
|
|
115
83
|
(el) => el && !prevElements.includes(el),
|
|
116
84
|
);
|
|
117
|
-
|
|
85
|
+
|
|
86
|
+
if (!isSkipped) {
|
|
118
87
|
for (const el of newElements) {
|
|
119
|
-
if (el)
|
|
88
|
+
if (el) activeObserver?.observe(el);
|
|
120
89
|
}
|
|
121
90
|
}
|
|
122
91
|
|
|
123
92
|
const removedElements = prevElements.filter(
|
|
124
|
-
(el) => el && !
|
|
93
|
+
(el) => el && !currentElements.includes(el),
|
|
125
94
|
);
|
|
95
|
+
|
|
126
96
|
for (const el of removedElements) {
|
|
127
|
-
if (el)
|
|
97
|
+
if (el) activeObserver?.unobserve(el);
|
|
128
98
|
}
|
|
129
99
|
|
|
130
|
-
prevElements = [...
|
|
100
|
+
prevElements = [...currentElements];
|
|
131
101
|
}
|
|
132
102
|
|
|
133
|
-
if (
|
|
134
|
-
for (const el of
|
|
103
|
+
if (isSkipped !== prevSkip) {
|
|
104
|
+
for (const el of currentElements) {
|
|
135
105
|
if (!el) continue;
|
|
136
|
-
if (
|
|
137
|
-
else
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
rootMargin !== prevRootMargin ||
|
|
143
|
-
threshold !== prevThreshold ||
|
|
144
|
-
root !== prevRoot
|
|
145
|
-
) {
|
|
146
|
-
observer?.disconnect();
|
|
147
|
-
prevElements = [];
|
|
148
|
-
initialize();
|
|
149
|
-
|
|
150
|
-
if (!skip) {
|
|
151
|
-
for (const el of elements.filter((el) => el)) {
|
|
152
|
-
if (el) observer?.observe(el);
|
|
153
|
-
}
|
|
106
|
+
if (isSkipped) activeObserver?.unobserve(el);
|
|
107
|
+
else activeObserver?.observe(el);
|
|
154
108
|
}
|
|
155
109
|
}
|
|
156
110
|
|
|
157
|
-
|
|
158
|
-
prevThreshold = threshold;
|
|
159
|
-
prevRoot = root;
|
|
160
|
-
prevSkip = skip;
|
|
111
|
+
prevSkip = isSkipped;
|
|
161
112
|
});
|
|
162
113
|
</script>
|
|
163
114
|
|
|
164
|
-
|
|
165
|
-
{observer}
|
|
166
|
-
{elementIntersections}
|
|
167
|
-
{elementEntries}
|
|
168
|
-
/>
|
|
115
|
+
{@render children?.({ observer, elementIntersections, elementEntries })}
|
|
@@ -1,90 +1,98 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Component, Snippet } from "svelte";
|
|
2
2
|
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
elements?: (HTMLElement | null)[];
|
|
3
|
+
export interface MultipleIntersectionObserverProps {
|
|
4
|
+
/**
|
|
5
|
+
* Array of HTML Elements to observe.
|
|
6
|
+
* Use this for better performance when observing multiple elements.
|
|
7
|
+
* @default []
|
|
8
|
+
*/
|
|
9
|
+
elements?: (HTMLElement | null)[];
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Set to `true` to unobserve the element
|
|
13
|
+
* after it intersects the viewport.
|
|
14
|
+
* @default false
|
|
15
|
+
*/
|
|
16
|
+
once?: boolean;
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Specify the containing element.
|
|
20
|
+
* Defaults to the browser viewport.
|
|
21
|
+
* @default null
|
|
22
|
+
*/
|
|
23
|
+
root?: null | HTMLElement;
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Margin offset of the containing element.
|
|
27
|
+
* @default "0px"
|
|
28
|
+
*/
|
|
29
|
+
rootMargin?: string;
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Percentage of element visibility to trigger an event.
|
|
33
|
+
* Value must be a number between 0 and 1, or an array of numbers between 0 and 1.
|
|
34
|
+
* @default 0
|
|
35
|
+
*/
|
|
36
|
+
threshold?: number | number[];
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Map of element to its intersection state.
|
|
40
|
+
* @default new Map()
|
|
41
|
+
*/
|
|
42
|
+
elementIntersections?: Map<HTMLElement | null, boolean>;
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Map of element to its latest entry.
|
|
46
|
+
* @default new Map()
|
|
47
|
+
*/
|
|
48
|
+
elementEntries?: Map<HTMLElement | null, IntersectionObserverEntry>;
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
/**
|
|
51
|
+
* `IntersectionObserver` instance.
|
|
52
|
+
* @default null
|
|
53
|
+
*/
|
|
54
|
+
observer?: null | IntersectionObserver;
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
/**
|
|
67
|
-
* Dispatched when an element is first observed
|
|
68
|
-
* and also whenever an intersection event occurs.
|
|
69
|
-
*/
|
|
70
|
-
observe: CustomEvent<{
|
|
71
|
-
entry: IntersectionObserverEntry;
|
|
72
|
-
target: HTMLElement;
|
|
73
|
-
}>;
|
|
56
|
+
/**
|
|
57
|
+
* Set to `true` to pause observing all elements without disconnecting
|
|
58
|
+
* the observer or losing `elementIntersections`/`elementEntries` state.
|
|
59
|
+
* Set back to `false` to resume.
|
|
60
|
+
* @default false
|
|
61
|
+
*/
|
|
62
|
+
skip?: boolean;
|
|
74
63
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Called when an element is first observed
|
|
66
|
+
* and also whenever an intersection event occurs.
|
|
67
|
+
*/
|
|
68
|
+
onobserve?: (detail: {
|
|
69
|
+
entry: IntersectionObserverEntry;
|
|
70
|
+
target: HTMLElement;
|
|
71
|
+
}) => void;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Called only when an element is intersecting the viewport.
|
|
75
|
+
*/
|
|
76
|
+
onintersect?: (detail: {
|
|
77
|
+
entry: IntersectionObserverEntry & { isIntersecting: true };
|
|
78
|
+
target: HTMLElement;
|
|
79
|
+
}) => void;
|
|
80
|
+
|
|
81
|
+
children?: Snippet<
|
|
82
|
+
[
|
|
83
|
+
{
|
|
84
|
+
observer: null | IntersectionObserver;
|
|
85
|
+
elementIntersections: Map<HTMLElement | null, boolean>;
|
|
86
|
+
elementEntries: Map<HTMLElement | null, IntersectionObserverEntry>;
|
|
87
|
+
},
|
|
88
|
+
]
|
|
89
|
+
>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
declare const MultipleIntersectionObserverComponent: Component<
|
|
93
|
+
MultipleIntersectionObserverProps,
|
|
94
|
+
Record<string, never>,
|
|
95
|
+
"elementIntersections" | "elementEntries" | "observer"
|
|
96
|
+
>;
|
|
97
|
+
|
|
98
|
+
export default MultipleIntersectionObserverComponent;
|
package/README.md
CHANGED
|
@@ -4,10 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
> Detect if an element is in the viewport using the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Use it to lazy-load images, trigger scroll animations, implement infinite scroll, autoplay video when visible, track ad or analytics impressions, or detect when a user has scrolled to the end of a list.
|
|
8
|
+
|
|
9
|
+
This library is zero-dependency and offers three ways to observe elements: the `IntersectionObserver` component for a single element, the `MultipleIntersectionObserver` component for observing many elements with one shared observer (better performance than one observer per element), and the `intersect` action for observing an element directly with `use:`.
|
|
8
10
|
|
|
9
11
|
Try it in the [Svelte REPL](https://svelte.dev/repl/8cd2327a580c4f429c71f7df999bd51d).
|
|
10
12
|
|
|
13
|
+
## Compatibility
|
|
14
|
+
|
|
15
|
+
| Package version | Svelte version | Notes |
|
|
16
|
+
| :--------------- | :----------------- | :----------------------------------------- |
|
|
17
|
+
| 1.x | 3, 4, 5 (non-runes) | Uses `export let`, slots, and `on:` events |
|
|
18
|
+
| 2.x | 5 (runes mode only) | Uses `$props()`, snippets, and callback props |
|
|
19
|
+
|
|
11
20
|
<!-- TOC -->
|
|
12
21
|
|
|
13
22
|
## Installation
|
|
@@ -39,8 +48,8 @@ Then, simply bind to the reactive `intersecting` prop to determine if the elemen
|
|
|
39
48
|
<script>
|
|
40
49
|
import IntersectionObserver from "svelte-intersection-observer";
|
|
41
50
|
|
|
42
|
-
let element;
|
|
43
|
-
let intersecting;
|
|
51
|
+
let element = $state();
|
|
52
|
+
let intersecting = $state(false);
|
|
44
53
|
</script>
|
|
45
54
|
|
|
46
55
|
<header class:intersecting>
|
|
@@ -52,6 +61,33 @@ Then, simply bind to the reactive `intersecting` prop to determine if the elemen
|
|
|
52
61
|
</IntersectionObserver>
|
|
53
62
|
```
|
|
54
63
|
|
|
64
|
+
### `children` snippet
|
|
65
|
+
|
|
66
|
+
An alternative to binding to the `intersecting` prop is to use the `children` snippet, which receives `intersecting`, `entry`, and `observer`.
|
|
67
|
+
|
|
68
|
+
In the following example, the "Hello world" element fades in when its containing element intersects the viewport.
|
|
69
|
+
|
|
70
|
+
```svelte
|
|
71
|
+
<script>
|
|
72
|
+
import IntersectionObserver from "svelte-intersection-observer";
|
|
73
|
+
import { fade } from "svelte/transition";
|
|
74
|
+
|
|
75
|
+
let node = $state();
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<header></header>
|
|
79
|
+
|
|
80
|
+
<IntersectionObserver element={node}>
|
|
81
|
+
{#snippet children({ intersecting })}
|
|
82
|
+
<div bind:this={node}>
|
|
83
|
+
{#if intersecting}
|
|
84
|
+
<div transition:fade={{ delay: 200 }}>Hello world</div>
|
|
85
|
+
{/if}
|
|
86
|
+
</div>
|
|
87
|
+
{/snippet}
|
|
88
|
+
</IntersectionObserver>
|
|
89
|
+
```
|
|
90
|
+
|
|
55
91
|
### Once
|
|
56
92
|
|
|
57
93
|
Set `once` to `true` for the intersection event to occur only once. The `element` will be unobserved after the first intersection event occurs.
|
|
@@ -60,8 +96,8 @@ Set `once` to `true` for the intersection event to occur only once. The `element
|
|
|
60
96
|
<script>
|
|
61
97
|
import IntersectionObserver from "svelte-intersection-observer";
|
|
62
98
|
|
|
63
|
-
let elementOnce;
|
|
64
|
-
let intersectOnce;
|
|
99
|
+
let elementOnce = $state();
|
|
100
|
+
let intersectOnce = $state(false);
|
|
65
101
|
</script>
|
|
66
102
|
|
|
67
103
|
<header class:intersecting={intersectOnce}>
|
|
@@ -79,78 +115,55 @@ Set `once` to `true` for the intersection event to occur only once. The `element
|
|
|
79
115
|
|
|
80
116
|
### Pausing with `skip`
|
|
81
117
|
|
|
82
|
-
Set `skip` to `true` to unobserve without disconnecting the underlying observer or losing `entry`/`intersecting` state
|
|
118
|
+
Set `skip` to `true` to unobserve without disconnecting the underlying observer or losing `entry`/`intersecting` state. This is useful for pausing tracking on an off-screen carousel panel or a closed modal. Set `skip` back to `false` to resume; unlike `once`, this can be toggled back and forth. `MultipleIntersectionObserver` and the `intersect` action support the same `skip` option.
|
|
83
119
|
|
|
84
120
|
```svelte no-eval
|
|
85
121
|
<script>
|
|
86
122
|
import IntersectionObserver from "svelte-intersection-observer";
|
|
87
123
|
|
|
88
|
-
let elementSkip;
|
|
89
|
-
let paused = false;
|
|
124
|
+
let elementSkip = $state();
|
|
125
|
+
let paused = $state(false);
|
|
90
126
|
</script>
|
|
91
127
|
|
|
92
|
-
<button
|
|
128
|
+
<button onclick={() => (paused = !paused)}>
|
|
93
129
|
{paused ? "Resume" : "Pause"}
|
|
94
130
|
</button>
|
|
95
131
|
|
|
96
|
-
<IntersectionObserver element={elementSkip} skip={paused}
|
|
97
|
-
|
|
132
|
+
<IntersectionObserver element={elementSkip} skip={paused}>
|
|
133
|
+
{#snippet children({ intersecting })}
|
|
134
|
+
<div bind:this={elementSkip}>{intersecting ? "In view" : "Not in view"}</div>
|
|
135
|
+
{/snippet}
|
|
98
136
|
</IntersectionObserver>
|
|
99
137
|
```
|
|
100
138
|
|
|
101
|
-
### `
|
|
102
|
-
|
|
103
|
-
An alternative to binding to the `intersecting` prop is to use the `let:` directive.
|
|
104
|
-
|
|
105
|
-
In the following example, the "Hello world" element will fade in when its containing element intersects the viewport.
|
|
106
|
-
|
|
107
|
-
```svelte
|
|
108
|
-
<script>
|
|
109
|
-
import IntersectionObserver from "svelte-intersection-observer";
|
|
110
|
-
import { fade } from "svelte/transition";
|
|
111
|
-
|
|
112
|
-
let node;
|
|
113
|
-
</script>
|
|
114
|
-
|
|
115
|
-
<header></header>
|
|
116
|
-
|
|
117
|
-
<IntersectionObserver element={node} let:intersecting>
|
|
118
|
-
<div bind:this={node}>
|
|
119
|
-
{#if intersecting}
|
|
120
|
-
<div transition:fade={{ delay: 200 }}>Hello world</div>
|
|
121
|
-
{/if}
|
|
122
|
-
</div>
|
|
123
|
-
</IntersectionObserver>
|
|
124
|
-
```
|
|
139
|
+
### `onobserve` callback prop
|
|
125
140
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
The `observe` event is dispatched when the element is first observed and also whenever an intersection event occurs.
|
|
141
|
+
`onobserve` is called when the element is first observed and also whenever an intersection event occurs.
|
|
129
142
|
|
|
130
143
|
```svelte no-eval
|
|
131
144
|
<IntersectionObserver
|
|
132
145
|
{element}
|
|
133
|
-
|
|
134
|
-
console.log(
|
|
135
|
-
console.log(
|
|
146
|
+
onobserve={(entry) => {
|
|
147
|
+
console.log(entry); // IntersectionObserverEntry
|
|
148
|
+
console.log(entry.isIntersecting); // true | false
|
|
136
149
|
}}
|
|
137
150
|
>
|
|
138
151
|
<div bind:this={element}>Hello world</div>
|
|
139
152
|
</IntersectionObserver>
|
|
140
153
|
```
|
|
141
154
|
|
|
142
|
-
### `
|
|
155
|
+
### `onintersect` callback prop
|
|
143
156
|
|
|
144
|
-
As an alternative to binding the `intersecting` prop, you can
|
|
157
|
+
As an alternative to binding the `intersecting` prop, you can pass an `onintersect` callback that is called if the observed element is intersecting the viewport.
|
|
145
158
|
|
|
146
|
-
**Note**: Compared to `
|
|
159
|
+
**Note**: Compared to `onobserve`, `onintersect` is called only when the element is _intersecting the viewport_. In other words, `entry.isIntersecting` will only be `true`.
|
|
147
160
|
|
|
148
161
|
```svelte no-eval
|
|
149
162
|
<IntersectionObserver
|
|
150
163
|
{element}
|
|
151
|
-
|
|
152
|
-
console.log(
|
|
153
|
-
console.log(
|
|
164
|
+
onintersect={(entry) => {
|
|
165
|
+
console.log(entry); // IntersectionObserverEntry
|
|
166
|
+
console.log(entry.isIntersecting); // true
|
|
154
167
|
}}
|
|
155
168
|
>
|
|
156
169
|
<div bind:this={element}>Hello world</div>
|
|
@@ -167,9 +180,9 @@ To detect when a user has scrolled to the end of a scrollable container, place a
|
|
|
167
180
|
<script>
|
|
168
181
|
import IntersectionObserver from "svelte-intersection-observer";
|
|
169
182
|
|
|
170
|
-
let container;
|
|
171
|
-
let sentinel;
|
|
172
|
-
let reachedEnd;
|
|
183
|
+
let container = $state();
|
|
184
|
+
let sentinel = $state();
|
|
185
|
+
let reachedEnd = $state(false);
|
|
173
186
|
</script>
|
|
174
187
|
|
|
175
188
|
<header class:intersecting={reachedEnd}>
|
|
@@ -191,15 +204,49 @@ To detect when a user has scrolled to the end of a scrollable container, place a
|
|
|
191
204
|
</div>
|
|
192
205
|
```
|
|
193
206
|
|
|
207
|
+
### Scrolling to a conditionally revealed element
|
|
208
|
+
|
|
209
|
+
Keep the `bind:this` element outside the `{#if intersecting}` block, and only gate the animated content inside it. The bound element then stays in the DOM, so you can call `scrollIntoView()` on it whenever you want, whether or not its reveal animation has played yet.
|
|
210
|
+
|
|
211
|
+
```svelte
|
|
212
|
+
<script>
|
|
213
|
+
import IntersectionObserver from "svelte-intersection-observer";
|
|
214
|
+
import { fly } from "svelte/transition";
|
|
215
|
+
|
|
216
|
+
let revealNode = $state();
|
|
217
|
+
let revealIntersecting = $state(false);
|
|
218
|
+
</script>
|
|
219
|
+
|
|
220
|
+
<header class:intersecting={revealIntersecting}>
|
|
221
|
+
<button
|
|
222
|
+
onclick={() => {
|
|
223
|
+
revealNode.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
Scroll to section
|
|
227
|
+
</button>
|
|
228
|
+
</header>
|
|
229
|
+
|
|
230
|
+
<IntersectionObserver element={revealNode} once bind:intersecting={revealIntersecting}>
|
|
231
|
+
<div bind:this={revealNode}>
|
|
232
|
+
{#if revealIntersecting}
|
|
233
|
+
<section transition:fly={{ y: 50, delay: 200, duration: 300 }}>
|
|
234
|
+
Hello world
|
|
235
|
+
</section>
|
|
236
|
+
{/if}
|
|
237
|
+
</div>
|
|
238
|
+
</IntersectionObserver>
|
|
239
|
+
```
|
|
240
|
+
|
|
194
241
|
### `use:intersect` action
|
|
195
242
|
|
|
196
|
-
As an alternative to the `IntersectionObserver` component, use the `intersect` action to observe an element directly with `use:`, without a `bind:this` reference or wrapper markup. Listen for `
|
|
243
|
+
As an alternative to the `IntersectionObserver` component, use the `intersect` action to observe an element directly with `use:`, without a `bind:this` reference or wrapper markup. Listen for `onobserve`/`onintersect` on the observed element itself.
|
|
197
244
|
|
|
198
245
|
```svelte
|
|
199
246
|
<script>
|
|
200
247
|
import { intersect } from "svelte-intersection-observer";
|
|
201
248
|
|
|
202
|
-
let actionIntersecting = false;
|
|
249
|
+
let actionIntersecting = $state(false);
|
|
203
250
|
</script>
|
|
204
251
|
|
|
205
252
|
<header class:intersecting={actionIntersecting}>
|
|
@@ -208,92 +255,92 @@ As an alternative to the `IntersectionObserver` component, use the `intersect` a
|
|
|
208
255
|
|
|
209
256
|
<div
|
|
210
257
|
use:intersect={{ once: true }}
|
|
211
|
-
|
|
258
|
+
onobserve={(e) => (actionIntersecting = e.detail.isIntersecting)}
|
|
212
259
|
>
|
|
213
260
|
Hello world
|
|
214
261
|
</div>
|
|
215
262
|
```
|
|
216
263
|
|
|
217
|
-
Options passed to `use:intersect` are reactive
|
|
264
|
+
Options passed to `use:intersect` are reactive: updating `root`, `rootMargin`, or `threshold` re-initializes the underlying observer. Updating `skip` toggles observing on the existing observer without re-initializing it.
|
|
218
265
|
|
|
219
266
|
### Multiple elements
|
|
220
267
|
|
|
221
|
-
For performance, use `MultipleIntersectionObserver` to observe multiple elements.
|
|
222
|
-
|
|
223
|
-
This avoids instantiating a new observer for every element.
|
|
268
|
+
For performance, use `MultipleIntersectionObserver` to observe multiple elements with a single shared observer instead of instantiating a new one for every element.
|
|
224
269
|
|
|
225
270
|
```svelte
|
|
226
271
|
<script>
|
|
227
272
|
import { MultipleIntersectionObserver } from "svelte-intersection-observer";
|
|
228
273
|
|
|
229
|
-
let ref1;
|
|
230
|
-
let ref2;
|
|
274
|
+
let ref1 = $state();
|
|
275
|
+
let ref2 = $state();
|
|
231
276
|
|
|
232
|
-
|
|
277
|
+
let elements = $derived([ref1, ref2]);
|
|
233
278
|
</script>
|
|
234
279
|
|
|
235
|
-
<MultipleIntersectionObserver {elements}
|
|
236
|
-
|
|
237
|
-
<
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
280
|
+
<MultipleIntersectionObserver {elements}>
|
|
281
|
+
{#snippet children({ elementIntersections })}
|
|
282
|
+
<header>
|
|
283
|
+
<div class:intersecting={elementIntersections.get(ref1)}>
|
|
284
|
+
Item 1: {elementIntersections.get(ref1) ? "✓" : "✗"}
|
|
285
|
+
</div>
|
|
286
|
+
<div class:intersecting={elementIntersections.get(ref2)}>
|
|
287
|
+
Item 2: {elementIntersections.get(ref2) ? "✓" : "✗"}
|
|
288
|
+
</div>
|
|
289
|
+
</header>
|
|
244
290
|
|
|
245
|
-
|
|
246
|
-
|
|
291
|
+
<div bind:this={ref1}>Item 1</div>
|
|
292
|
+
<div bind:this={ref2}>Item 2</div>
|
|
293
|
+
{/snippet}
|
|
247
294
|
</MultipleIntersectionObserver>
|
|
248
295
|
```
|
|
249
296
|
|
|
250
297
|
### Using with `#each`
|
|
251
298
|
|
|
252
|
-
`MultipleIntersectionObserver` also handles a dynamic, `#each`-driven list
|
|
299
|
+
`MultipleIntersectionObserver` also handles a dynamic, `#each`-driven list: give every item its own slot in an array/object instead of one shared variable.
|
|
253
300
|
|
|
254
301
|
```svelte
|
|
255
302
|
<script>
|
|
256
303
|
import { MultipleIntersectionObserver } from "svelte-intersection-observer";
|
|
257
304
|
|
|
258
|
-
let items =
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
];
|
|
305
|
+
let items = Array.from({ length: 10 }, (_, i) => ({
|
|
306
|
+
id: i + 1,
|
|
307
|
+
text: `Item ${i + 1}`,
|
|
308
|
+
}));
|
|
263
309
|
|
|
264
|
-
let refs = [];
|
|
265
|
-
let itemsContainer;
|
|
310
|
+
let refs = $state([]);
|
|
311
|
+
let itemsContainer = $state();
|
|
266
312
|
|
|
267
|
-
|
|
313
|
+
let itemElements = $derived(refs);
|
|
268
314
|
</script>
|
|
269
315
|
|
|
270
|
-
<MultipleIntersectionObserver
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
>
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
316
|
+
<MultipleIntersectionObserver elements={itemElements} root={itemsContainer}>
|
|
317
|
+
{#snippet children({ elementIntersections })}
|
|
318
|
+
<header>
|
|
319
|
+
{#each items as item, i (item.id)}
|
|
320
|
+
<div class:intersecting={elementIntersections.get(refs[i])}>
|
|
321
|
+
{item.text}: {elementIntersections.get(refs[i]) ? "✓" : "✗"}
|
|
322
|
+
</div>
|
|
323
|
+
{/each}
|
|
324
|
+
</header>
|
|
325
|
+
|
|
326
|
+
<div
|
|
327
|
+
bind:this={itemsContainer}
|
|
328
|
+
style="height: 150px; overflow-y: auto; display: flex; flex-direction: column; gap: 1rem;"
|
|
329
|
+
>
|
|
330
|
+
{#each items as item, i (item.id)}
|
|
331
|
+
<div
|
|
332
|
+
bind:this={refs[i]}
|
|
333
|
+
style="height: 100px; display: flex; align-items: center; flex-shrink: 0;"
|
|
334
|
+
>
|
|
335
|
+
{item.text}
|
|
336
|
+
</div>
|
|
337
|
+
{/each}
|
|
338
|
+
</div>
|
|
339
|
+
{/snippet}
|
|
293
340
|
</MultipleIntersectionObserver>
|
|
294
341
|
```
|
|
295
342
|
|
|
296
|
-
As with the scroll-to-end example, `root` must be an element that scrolls on its own
|
|
343
|
+
As with the scroll-to-end example, `root` must be an element that scrolls on its own; here, `itemsContainer` has an explicit `height` and `overflow-y: auto`.
|
|
297
344
|
|
|
298
345
|
**Avoid** using the single-element `IntersectionObserver` component inside an `#each` block with one variable shared across iterations (e.g. `let node;` declared outside the loop and bound via `bind:this={node}` inside it). Every iteration overwrites the same `node`, so each observer instance keeps re-observing a moving target, which can produce an infinite update loop. Use `MultipleIntersectionObserver` with a per-item ref, as shown above, instead.
|
|
299
346
|
|
|
@@ -315,16 +362,16 @@ As with the scroll-to-end example, `root` must be an element that scrolls on its
|
|
|
315
362
|
| observer | `IntersectionObserver` instance | `null` or [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) | `null` |
|
|
316
363
|
| skip | Pause observing without losing `entry`/`intersecting` state | `boolean` | `false` |
|
|
317
364
|
|
|
318
|
-
**Note**: the observed `element` must render with a non-zero width and height for `threshold` values greater than `0` to have any effect
|
|
365
|
+
**Note**: the observed `element` must render with a non-zero width and height for `threshold` values greater than `0` to have any effect. This is a constraint of the underlying [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API), not something this component controls.
|
|
319
366
|
|
|
320
|
-
####
|
|
367
|
+
#### Callback props
|
|
321
368
|
|
|
322
|
-
- **
|
|
323
|
-
- **
|
|
369
|
+
- **onobserve**: called when the element is first observed or whenever an intersection change occurs
|
|
370
|
+
- **onintersect**: called when the element is intersecting the viewport
|
|
324
371
|
|
|
325
|
-
|
|
372
|
+
Both callbacks are called with an [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry).
|
|
326
373
|
|
|
327
|
-
####
|
|
374
|
+
#### `children` snippet props
|
|
328
375
|
|
|
329
376
|
| Name | Type |
|
|
330
377
|
| :----------- | :------------------------------------------------------------------------------------------------------------------ |
|
|
@@ -348,12 +395,12 @@ The `e.detail` dispatched by the `observe` and `intersect` events is an [`Inters
|
|
|
348
395
|
| observer | `IntersectionObserver` instance | `null` or [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) | `null` |
|
|
349
396
|
| skip | Pause observing all elements without losing state | `boolean` | `false` |
|
|
350
397
|
|
|
351
|
-
####
|
|
398
|
+
#### Callback props
|
|
352
399
|
|
|
353
|
-
- **
|
|
354
|
-
- **
|
|
400
|
+
- **onobserve**: called when an element is first observed or whenever an intersection change occurs
|
|
401
|
+
- **onintersect**: called when an element is intersecting the viewport
|
|
355
402
|
|
|
356
|
-
|
|
403
|
+
Both callbacks are called with:
|
|
357
404
|
|
|
358
405
|
```ts
|
|
359
406
|
{
|
|
@@ -362,7 +409,7 @@ The `e.detail` for both events includes:
|
|
|
362
409
|
}
|
|
363
410
|
```
|
|
364
411
|
|
|
365
|
-
####
|
|
412
|
+
#### `children` snippet props
|
|
366
413
|
|
|
367
414
|
| Name | Type |
|
|
368
415
|
| :------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -374,18 +421,18 @@ The `e.detail` for both events includes:
|
|
|
374
421
|
|
|
375
422
|
#### Options
|
|
376
423
|
|
|
377
|
-
| Name | Description
|
|
378
|
-
| :--------- |
|
|
379
|
-
| root | Containing element
|
|
380
|
-
| rootMargin | Margin offset of the containing element
|
|
381
|
-
| threshold | Percentage of element visibility to trigger an event
|
|
382
|
-
| once | Unobserve the element after the first intersection event
|
|
383
|
-
| skip | Pause observing without disconnecting the observer
|
|
424
|
+
| Name | Description | Type | Default value |
|
|
425
|
+
| :--------- | :------------------------------------------------------- | :----------------------------------------------------------------- | :------------ |
|
|
426
|
+
| root | Containing element | `null` or `HTMLElement` | `null` |
|
|
427
|
+
| rootMargin | Margin offset of the containing element | `string` | `"0px"` |
|
|
428
|
+
| threshold | Percentage of element visibility to trigger an event | `number` between 0 and 1, or an array of `number`s between 0 and 1 | `0` |
|
|
429
|
+
| once | Unobserve the element after the first intersection event | `boolean` | `false` |
|
|
430
|
+
| skip | Pause observing without disconnecting the observer | `boolean` | `false` |
|
|
384
431
|
|
|
385
432
|
#### Dispatched events
|
|
386
433
|
|
|
387
|
-
- **
|
|
388
|
-
- **
|
|
434
|
+
- **onobserve**: fired on the element when it is first observed or whenever an intersection change occurs
|
|
435
|
+
- **onintersect**: fired on the element when it is intersecting the viewport
|
|
389
436
|
|
|
390
437
|
The `e.detail` dispatched by the `observe` and `intersect` events is an [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry) interface.
|
|
391
438
|
|
|
@@ -438,13 +485,5 @@ interface IntersectionObserverEntry {
|
|
|
438
485
|
|
|
439
486
|
</details>
|
|
440
487
|
|
|
441
|
-
## Changelog
|
|
442
|
-
|
|
443
|
-
[Changelog](CHANGELOG.md)
|
|
444
|
-
|
|
445
|
-
## License
|
|
446
|
-
|
|
447
|
-
[MIT](LICENSE)
|
|
448
|
-
|
|
449
488
|
[npm]: https://img.shields.io/npm/v/svelte-intersection-observer.svg?color=%23ff3e00&style=for-the-badge
|
|
450
489
|
[npm-url]: https://npmjs.com/package/svelte-intersection-observer
|
package/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { default } from "./IntersectionObserver.svelte";
|
|
2
|
-
export type { IntersectActionOptions } from "./intersect.js";
|
|
3
|
-
export { intersect } from "./intersect.js";
|
|
2
|
+
export type { IntersectActionOptions } from "./intersect.svelte.js";
|
|
3
|
+
export { intersect } from "./intersect.svelte.js";
|
|
4
4
|
export { default as MultipleIntersectionObserver } from "./MultipleIntersectionObserver.svelte";
|
package/index.js
CHANGED
|
@@ -39,14 +39,14 @@ export interface IntersectActionOptions {
|
|
|
39
39
|
/**
|
|
40
40
|
* Svelte action that observes the element with the Intersection Observer API.
|
|
41
41
|
* Dispatches `observe` (on every change) and `intersect` (on entering the
|
|
42
|
-
* viewport) `CustomEvent`s on the element — listen with `
|
|
42
|
+
* viewport) `CustomEvent`s on the element — listen with `onobserve`/`onintersect`.
|
|
43
43
|
*/
|
|
44
44
|
export const intersect: Action<
|
|
45
45
|
HTMLElement,
|
|
46
46
|
IntersectActionOptions | undefined,
|
|
47
47
|
{
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
onobserve?: (event: CustomEvent<IntersectionObserverEntry>) => void;
|
|
49
|
+
onintersect?: (
|
|
50
50
|
event: CustomEvent<IntersectionObserverEntry & { isIntersecting: true }>,
|
|
51
51
|
) => void;
|
|
52
52
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Svelte action that observes `node` with the Intersection Observer API.
|
|
3
3
|
* Dispatches `observe` (on every change) and `intersect` (on entering the
|
|
4
|
-
* viewport) `CustomEvent`s on `node` — listen with `
|
|
4
|
+
* viewport) `CustomEvent`s on `node` — listen with `onobserve`/`onintersect`.
|
|
5
5
|
* @param {HTMLElement} node
|
|
6
|
-
* @param {import("./intersect.d.ts").IntersectActionOptions} [options]
|
|
6
|
+
* @param {import("./intersect.svelte.d.ts").IntersectActionOptions} [options]
|
|
7
7
|
*/
|
|
8
8
|
export function intersect(node, options = {}) {
|
|
9
9
|
let {
|
|
@@ -36,7 +36,7 @@ export function intersect(node, options = {}) {
|
|
|
36
36
|
if (!skip) observer.observe(node);
|
|
37
37
|
|
|
38
38
|
return {
|
|
39
|
-
/** @param {import("./intersect.d.ts").IntersectActionOptions} [newOptions] */
|
|
39
|
+
/** @param {import("./intersect.svelte.d.ts").IntersectActionOptions} [newOptions] */
|
|
40
40
|
update(newOptions = {}) {
|
|
41
41
|
once = newOptions.once ?? false;
|
|
42
42
|
|
package/package.json
CHANGED