svelte-intersection-observer 1.1.2 → 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 +52 -77
- package/IntersectionObserver.svelte.d.ts +86 -70
- package/MultipleIntersectionObserver.svelte +69 -93
- package/MultipleIntersectionObserver.svelte.d.ts +89 -73
- package/README.md +256 -64
- package/index.d.ts +2 -0
- package/index.js +1 -0
- package/intersect.svelte.d.ts +53 -0
- package/intersect.svelte.js +69 -0
- package/package.json +2 -2
|
@@ -1,116 +1,91 @@
|
|
|
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;
|
|
2
|
+
import { untrack } from "svelte";
|
|
20
3
|
|
|
21
4
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* @
|
|
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]
|
|
25
18
|
*/
|
|
26
|
-
export let root = null;
|
|
27
|
-
|
|
28
|
-
/** Margin offset of the containing element. */
|
|
29
|
-
export let rootMargin = "0px";
|
|
30
19
|
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
*/
|
|
47
|
-
export let observer = null;
|
|
48
|
-
|
|
49
|
-
import { tick, createEventDispatcher, afterUpdate, onMount } from "svelte";
|
|
50
|
-
|
|
51
|
-
const dispatch = createEventDispatcher();
|
|
52
|
-
|
|
53
|
-
let prevRootMargin = rootMargin;
|
|
54
|
-
|
|
55
|
-
let prevThreshold = threshold;
|
|
56
|
-
|
|
57
|
-
/** @type {null | HTMLElement} */
|
|
58
|
-
let prevRoot = root;
|
|
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();
|
|
59
35
|
|
|
60
36
|
/** @type {null | HTMLElement} */
|
|
61
37
|
let prevElement = null;
|
|
62
38
|
|
|
39
|
+
let prevSkip = untrack(() => skip);
|
|
40
|
+
|
|
63
41
|
const initialize = () => {
|
|
64
42
|
observer = new IntersectionObserver(
|
|
65
43
|
(entries) => {
|
|
66
|
-
|
|
44
|
+
for (const _entry of entries) {
|
|
67
45
|
entry = _entry;
|
|
68
46
|
intersecting = _entry.isIntersecting;
|
|
69
47
|
|
|
70
|
-
|
|
48
|
+
onobserve?.(_entry);
|
|
71
49
|
|
|
72
50
|
if (_entry.isIntersecting) {
|
|
73
|
-
|
|
51
|
+
onintersect?.(_entry);
|
|
74
52
|
|
|
75
53
|
if (element && once) observer?.unobserve(element);
|
|
76
54
|
}
|
|
77
|
-
}
|
|
55
|
+
}
|
|
78
56
|
},
|
|
79
57
|
{ root, rootMargin, threshold },
|
|
80
58
|
);
|
|
81
59
|
};
|
|
82
60
|
|
|
83
|
-
|
|
61
|
+
$effect(() => {
|
|
62
|
+
prevElement = null;
|
|
84
63
|
initialize();
|
|
85
64
|
|
|
86
65
|
return () => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
observer = null;
|
|
90
|
-
}
|
|
66
|
+
observer?.disconnect();
|
|
67
|
+
observer = null;
|
|
91
68
|
};
|
|
92
69
|
});
|
|
93
70
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
observer?.observe(element);
|
|
71
|
+
$effect(() => {
|
|
72
|
+
const target = element;
|
|
73
|
+
const isSkipped = skip;
|
|
74
|
+
const activeObserver = observer;
|
|
99
75
|
|
|
100
|
-
|
|
101
|
-
|
|
76
|
+
if (target !== null && target !== prevElement) {
|
|
77
|
+
if (!isSkipped) activeObserver?.observe(target);
|
|
78
|
+
if (prevElement !== null) activeObserver?.unobserve(prevElement);
|
|
79
|
+
prevElement = target;
|
|
102
80
|
}
|
|
103
81
|
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
initialize();
|
|
82
|
+
if (isSkipped !== prevSkip && target !== null) {
|
|
83
|
+
if (isSkipped) activeObserver?.unobserve(target);
|
|
84
|
+
else activeObserver?.observe(target);
|
|
108
85
|
}
|
|
109
86
|
|
|
110
|
-
|
|
111
|
-
prevThreshold = threshold;
|
|
112
|
-
prevRoot = root;
|
|
87
|
+
prevSkip = isSkipped;
|
|
113
88
|
});
|
|
114
89
|
</script>
|
|
115
90
|
|
|
116
|
-
|
|
91
|
+
{@render children?.({ intersecting, entry, observer })}
|
|
@@ -1,79 +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
|
-
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
/**
|
|
59
|
-
* Dispatched when the element is first observed
|
|
60
|
-
* and also whenever an intersection event occurs.
|
|
61
|
-
*/
|
|
62
|
-
observe: CustomEvent<IntersectionObserverEntry>;
|
|
50
|
+
/**
|
|
51
|
+
* `IntersectionObserver` instance.
|
|
52
|
+
* @default null
|
|
53
|
+
*/
|
|
54
|
+
observer?: null | IntersectionObserver;
|
|
63
55
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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;
|
|
63
|
+
|
|
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,139 +1,115 @@
|
|
|
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
|
-
import { tick, createEventDispatcher, afterUpdate, onMount } from "svelte";
|
|
51
|
-
|
|
52
|
-
const dispatch = createEventDispatcher();
|
|
53
|
-
|
|
54
|
-
let prevRootMargin = rootMargin;
|
|
55
|
-
|
|
56
|
-
let prevThreshold = threshold;
|
|
57
|
-
|
|
58
|
-
/** @type {null | HTMLElement} */
|
|
59
|
-
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();
|
|
60
35
|
|
|
61
36
|
/** @type {(HTMLElement | null)[]} */
|
|
62
37
|
let prevElements = [];
|
|
63
38
|
|
|
39
|
+
let prevSkip = untrack(() => skip);
|
|
40
|
+
|
|
64
41
|
const initialize = () => {
|
|
65
42
|
observer = new IntersectionObserver(
|
|
66
43
|
(entries) => {
|
|
67
|
-
|
|
44
|
+
for (const _entry of entries) {
|
|
68
45
|
const target = /** @type {HTMLElement} */ (_entry.target);
|
|
69
46
|
|
|
70
47
|
elementIntersections.set(target, _entry.isIntersecting);
|
|
71
48
|
elementEntries.set(target, _entry);
|
|
72
49
|
|
|
73
|
-
|
|
74
|
-
elementIntersections = new Map(elementIntersections);
|
|
75
|
-
elementEntries = new Map(elementEntries);
|
|
76
|
-
|
|
77
|
-
dispatch("observe", { entry: _entry, target });
|
|
50
|
+
onobserve?.({ entry: _entry, target });
|
|
78
51
|
|
|
79
52
|
if (_entry.isIntersecting) {
|
|
80
|
-
|
|
53
|
+
onintersect?.({ entry: _entry, target });
|
|
81
54
|
if (once) observer?.unobserve(target);
|
|
82
55
|
}
|
|
83
|
-
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Trigger reactivity once per batch, not once per entry.
|
|
59
|
+
elementIntersections = new Map(elementIntersections);
|
|
60
|
+
elementEntries = new Map(elementEntries);
|
|
84
61
|
},
|
|
85
62
|
{ root, rootMargin, threshold },
|
|
86
63
|
);
|
|
87
64
|
};
|
|
88
65
|
|
|
89
|
-
|
|
66
|
+
$effect(() => {
|
|
67
|
+
prevElements = [];
|
|
90
68
|
initialize();
|
|
91
69
|
|
|
92
70
|
return () => {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
observer = null;
|
|
96
|
-
}
|
|
71
|
+
observer?.disconnect();
|
|
72
|
+
observer = null;
|
|
97
73
|
};
|
|
98
74
|
});
|
|
99
75
|
|
|
100
|
-
|
|
101
|
-
|
|
76
|
+
$effect(() => {
|
|
77
|
+
const currentElements = elements;
|
|
78
|
+
const isSkipped = skip;
|
|
79
|
+
const activeObserver = observer;
|
|
102
80
|
|
|
103
|
-
if (
|
|
104
|
-
const newElements =
|
|
81
|
+
if (currentElements.length > 0) {
|
|
82
|
+
const newElements = currentElements.filter(
|
|
105
83
|
(el) => el && !prevElements.includes(el),
|
|
106
84
|
);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
85
|
+
|
|
86
|
+
if (!isSkipped) {
|
|
87
|
+
for (const el of newElements) {
|
|
88
|
+
if (el) activeObserver?.observe(el);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
110
91
|
|
|
111
92
|
const removedElements = prevElements.filter(
|
|
112
|
-
(el) => el && !
|
|
93
|
+
(el) => el && !currentElements.includes(el),
|
|
113
94
|
);
|
|
114
|
-
removedElements.forEach((el) => {
|
|
115
|
-
if (el) observer?.unobserve(el);
|
|
116
|
-
});
|
|
117
95
|
|
|
118
|
-
|
|
96
|
+
for (const el of removedElements) {
|
|
97
|
+
if (el) activeObserver?.unobserve(el);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
prevElements = [...currentElements];
|
|
119
101
|
}
|
|
120
102
|
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
.filter((el) => el)
|
|
128
|
-
.forEach((el) => {
|
|
129
|
-
if (el) observer?.observe(el);
|
|
130
|
-
});
|
|
103
|
+
if (isSkipped !== prevSkip) {
|
|
104
|
+
for (const el of currentElements) {
|
|
105
|
+
if (!el) continue;
|
|
106
|
+
if (isSkipped) activeObserver?.unobserve(el);
|
|
107
|
+
else activeObserver?.observe(el);
|
|
108
|
+
}
|
|
131
109
|
}
|
|
132
110
|
|
|
133
|
-
|
|
134
|
-
prevThreshold = threshold;
|
|
135
|
-
prevRoot = root;
|
|
111
|
+
prevSkip = isSkipped;
|
|
136
112
|
});
|
|
137
113
|
</script>
|
|
138
114
|
|
|
139
|
-
|
|
115
|
+
{@render children?.({ observer, elementIntersections, elementEntries })}
|
|
@@ -1,82 +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
|
-
|
|
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
|
-
}>;
|
|
50
|
+
/**
|
|
51
|
+
* `IntersectionObserver` instance.
|
|
52
|
+
* @default null
|
|
53
|
+
*/
|
|
54
|
+
observer?: null | IntersectionObserver;
|
|
66
55
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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;
|
|
63
|
+
|
|
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}>
|
|
@@ -77,96 +113,237 @@ Set `once` to `true` for the intersection event to occur only once. The `element
|
|
|
77
113
|
</IntersectionObserver>
|
|
78
114
|
```
|
|
79
115
|
|
|
80
|
-
### `
|
|
81
|
-
|
|
82
|
-
An alternative to binding to the `intersecting` prop is to use the `let:` directive.
|
|
116
|
+
### Pausing with `skip`
|
|
83
117
|
|
|
84
|
-
|
|
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.
|
|
85
119
|
|
|
86
|
-
```svelte
|
|
120
|
+
```svelte no-eval
|
|
87
121
|
<script>
|
|
88
122
|
import IntersectionObserver from "svelte-intersection-observer";
|
|
89
|
-
import { fade } from "svelte/transition";
|
|
90
123
|
|
|
91
|
-
let
|
|
124
|
+
let elementSkip = $state();
|
|
125
|
+
let paused = $state(false);
|
|
92
126
|
</script>
|
|
93
127
|
|
|
94
|
-
<
|
|
128
|
+
<button onclick={() => (paused = !paused)}>
|
|
129
|
+
{paused ? "Resume" : "Pause"}
|
|
130
|
+
</button>
|
|
95
131
|
|
|
96
|
-
<IntersectionObserver element={
|
|
97
|
-
|
|
98
|
-
{
|
|
99
|
-
|
|
100
|
-
{/if}
|
|
101
|
-
</div>
|
|
132
|
+
<IntersectionObserver element={elementSkip} skip={paused}>
|
|
133
|
+
{#snippet children({ intersecting })}
|
|
134
|
+
<div bind:this={elementSkip}>{intersecting ? "In view" : "Not in view"}</div>
|
|
135
|
+
{/snippet}
|
|
102
136
|
</IntersectionObserver>
|
|
103
137
|
```
|
|
104
138
|
|
|
105
|
-
### `
|
|
139
|
+
### `onobserve` callback prop
|
|
106
140
|
|
|
107
|
-
|
|
141
|
+
`onobserve` is called when the element is first observed and also whenever an intersection event occurs.
|
|
108
142
|
|
|
109
143
|
```svelte no-eval
|
|
110
144
|
<IntersectionObserver
|
|
111
145
|
{element}
|
|
112
|
-
|
|
113
|
-
console.log(
|
|
114
|
-
console.log(
|
|
146
|
+
onobserve={(entry) => {
|
|
147
|
+
console.log(entry); // IntersectionObserverEntry
|
|
148
|
+
console.log(entry.isIntersecting); // true | false
|
|
115
149
|
}}
|
|
116
150
|
>
|
|
117
151
|
<div bind:this={element}>Hello world</div>
|
|
118
152
|
</IntersectionObserver>
|
|
119
153
|
```
|
|
120
154
|
|
|
121
|
-
### `
|
|
155
|
+
### `onintersect` callback prop
|
|
122
156
|
|
|
123
|
-
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.
|
|
124
158
|
|
|
125
|
-
**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`.
|
|
126
160
|
|
|
127
161
|
```svelte no-eval
|
|
128
162
|
<IntersectionObserver
|
|
129
163
|
{element}
|
|
130
|
-
|
|
131
|
-
console.log(
|
|
132
|
-
console.log(
|
|
164
|
+
onintersect={(entry) => {
|
|
165
|
+
console.log(entry); // IntersectionObserverEntry
|
|
166
|
+
console.log(entry.isIntersecting); // true
|
|
133
167
|
}}
|
|
134
168
|
>
|
|
135
169
|
<div bind:this={element}>Hello world</div>
|
|
136
170
|
</IntersectionObserver>
|
|
137
171
|
```
|
|
138
172
|
|
|
173
|
+
### Detecting scroll to end
|
|
174
|
+
|
|
175
|
+
To detect when a user has scrolled to the end of a scrollable container, place a sentinel element after the content and set `root` to the container. `intersecting` becomes `true` once the sentinel scrolls into view.
|
|
176
|
+
|
|
177
|
+
**Note**: `root` must be the scrollable element itself (i.e. it has its own `overflow`/fixed `height`), not just an ancestor of one. If `root` merely sits inside a scrollable ancestor, the sentinel scrolls along with `root` and never changes position relative to it, so it reports as permanently intersecting.
|
|
178
|
+
|
|
179
|
+
```svelte
|
|
180
|
+
<script>
|
|
181
|
+
import IntersectionObserver from "svelte-intersection-observer";
|
|
182
|
+
|
|
183
|
+
let container = $state();
|
|
184
|
+
let sentinel = $state();
|
|
185
|
+
let reachedEnd = $state(false);
|
|
186
|
+
</script>
|
|
187
|
+
|
|
188
|
+
<header class:intersecting={reachedEnd}>
|
|
189
|
+
{reachedEnd ? "You've reached the end" : "Keep scrolling..."}
|
|
190
|
+
</header>
|
|
191
|
+
|
|
192
|
+
<div bind:this={container} style="height: 200px; overflow-y: auto;">
|
|
193
|
+
{#each Array.from({ length: 20 }) as _, i}
|
|
194
|
+
<p>Paragraph {i + 1}</p>
|
|
195
|
+
{/each}
|
|
196
|
+
|
|
197
|
+
<IntersectionObserver
|
|
198
|
+
element={sentinel}
|
|
199
|
+
root={container}
|
|
200
|
+
bind:intersecting={reachedEnd}
|
|
201
|
+
>
|
|
202
|
+
<div bind:this={sentinel} style="height: 1px;"></div>
|
|
203
|
+
</IntersectionObserver>
|
|
204
|
+
</div>
|
|
205
|
+
```
|
|
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
|
+
|
|
241
|
+
### `use:intersect` action
|
|
242
|
+
|
|
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.
|
|
244
|
+
|
|
245
|
+
```svelte
|
|
246
|
+
<script>
|
|
247
|
+
import { intersect } from "svelte-intersection-observer";
|
|
248
|
+
|
|
249
|
+
let actionIntersecting = $state(false);
|
|
250
|
+
</script>
|
|
251
|
+
|
|
252
|
+
<header class:intersecting={actionIntersecting}>
|
|
253
|
+
{actionIntersecting ? "Element is in view" : "Element is not in view"}
|
|
254
|
+
</header>
|
|
255
|
+
|
|
256
|
+
<div
|
|
257
|
+
use:intersect={{ once: true }}
|
|
258
|
+
onobserve={(e) => (actionIntersecting = e.detail.isIntersecting)}
|
|
259
|
+
>
|
|
260
|
+
Hello world
|
|
261
|
+
</div>
|
|
262
|
+
```
|
|
263
|
+
|
|
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.
|
|
265
|
+
|
|
139
266
|
### Multiple elements
|
|
140
267
|
|
|
141
|
-
For performance, use `MultipleIntersectionObserver` to observe multiple elements.
|
|
268
|
+
For performance, use `MultipleIntersectionObserver` to observe multiple elements with a single shared observer instead of instantiating a new one for every element.
|
|
269
|
+
|
|
270
|
+
```svelte
|
|
271
|
+
<script>
|
|
272
|
+
import { MultipleIntersectionObserver } from "svelte-intersection-observer";
|
|
273
|
+
|
|
274
|
+
let ref1 = $state();
|
|
275
|
+
let ref2 = $state();
|
|
276
|
+
|
|
277
|
+
let elements = $derived([ref1, ref2]);
|
|
278
|
+
</script>
|
|
279
|
+
|
|
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>
|
|
290
|
+
|
|
291
|
+
<div bind:this={ref1}>Item 1</div>
|
|
292
|
+
<div bind:this={ref2}>Item 2</div>
|
|
293
|
+
{/snippet}
|
|
294
|
+
</MultipleIntersectionObserver>
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Using with `#each`
|
|
142
298
|
|
|
143
|
-
|
|
299
|
+
`MultipleIntersectionObserver` also handles a dynamic, `#each`-driven list: give every item its own slot in an array/object instead of one shared variable.
|
|
144
300
|
|
|
145
301
|
```svelte
|
|
146
302
|
<script>
|
|
147
303
|
import { MultipleIntersectionObserver } from "svelte-intersection-observer";
|
|
148
304
|
|
|
149
|
-
let
|
|
150
|
-
|
|
305
|
+
let items = Array.from({ length: 10 }, (_, i) => ({
|
|
306
|
+
id: i + 1,
|
|
307
|
+
text: `Item ${i + 1}`,
|
|
308
|
+
}));
|
|
151
309
|
|
|
152
|
-
|
|
310
|
+
let refs = $state([]);
|
|
311
|
+
let itemsContainer = $state();
|
|
312
|
+
|
|
313
|
+
let itemElements = $derived(refs);
|
|
153
314
|
</script>
|
|
154
315
|
|
|
155
|
-
<MultipleIntersectionObserver {
|
|
156
|
-
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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}
|
|
162
338
|
</div>
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
<div bind:this={ref1}>Item 1</div>
|
|
166
|
-
<div bind:this={ref2}>Item 2</div>
|
|
339
|
+
{/snippet}
|
|
167
340
|
</MultipleIntersectionObserver>
|
|
168
341
|
```
|
|
169
342
|
|
|
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`.
|
|
344
|
+
|
|
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.
|
|
346
|
+
|
|
170
347
|
## API
|
|
171
348
|
|
|
172
349
|
### IntersectionObserver
|
|
@@ -183,15 +360,18 @@ This avoids instantiating a new observer for every element.
|
|
|
183
360
|
| 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` |
|
|
184
361
|
| entry | Observed element metadata | `null` or [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry) | `null` |
|
|
185
362
|
| observer | `IntersectionObserver` instance | `null` or [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) | `null` |
|
|
363
|
+
| skip | Pause observing without losing `entry`/`intersecting` state | `boolean` | `false` |
|
|
186
364
|
|
|
187
|
-
|
|
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.
|
|
188
366
|
|
|
189
|
-
|
|
190
|
-
- **on:intersect**: fired when the element is intersecting the viewport
|
|
367
|
+
#### Callback props
|
|
191
368
|
|
|
192
|
-
|
|
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
|
|
371
|
+
|
|
372
|
+
Both callbacks are called with an [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry).
|
|
193
373
|
|
|
194
|
-
####
|
|
374
|
+
#### `children` snippet props
|
|
195
375
|
|
|
196
376
|
| Name | Type |
|
|
197
377
|
| :----------- | :------------------------------------------------------------------------------------------------------------------ |
|
|
@@ -213,13 +393,14 @@ The `e.detail` dispatched by the `observe` and `intersect` events is an [`Inters
|
|
|
213
393
|
| elementIntersections | Map of each element to its intersection state | `Map<HTMLElement \| null, boolean>` | `new Map()` |
|
|
214
394
|
| elementEntries | Map of each element to its latest entry | `Map<HTMLElement \| null,` [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry)`>` | `new Map()` |
|
|
215
395
|
| observer | `IntersectionObserver` instance | `null` or [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) | `null` |
|
|
396
|
+
| skip | Pause observing all elements without losing state | `boolean` | `false` |
|
|
216
397
|
|
|
217
|
-
####
|
|
398
|
+
#### Callback props
|
|
218
399
|
|
|
219
|
-
- **
|
|
220
|
-
- **
|
|
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
|
|
221
402
|
|
|
222
|
-
|
|
403
|
+
Both callbacks are called with:
|
|
223
404
|
|
|
224
405
|
```ts
|
|
225
406
|
{
|
|
@@ -228,7 +409,7 @@ The `e.detail` for both events includes:
|
|
|
228
409
|
}
|
|
229
410
|
```
|
|
230
411
|
|
|
231
|
-
####
|
|
412
|
+
#### `children` snippet props
|
|
232
413
|
|
|
233
414
|
| Name | Type |
|
|
234
415
|
| :------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -236,6 +417,25 @@ The `e.detail` for both events includes:
|
|
|
236
417
|
| elementIntersections | `Map<HTMLElement \| null, boolean>` |
|
|
237
418
|
| elementEntries | `Map<HTMLElement \| null,` [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry)`>` |
|
|
238
419
|
|
|
420
|
+
### `intersect` action
|
|
421
|
+
|
|
422
|
+
#### Options
|
|
423
|
+
|
|
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` |
|
|
431
|
+
|
|
432
|
+
#### Dispatched events
|
|
433
|
+
|
|
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
|
|
436
|
+
|
|
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.
|
|
438
|
+
|
|
239
439
|
### `IntersectionObserverEntry` interface
|
|
240
440
|
|
|
241
441
|
Note that all properties in [IntersectionObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry) are read-only.
|
|
@@ -285,13 +485,5 @@ interface IntersectionObserverEntry {
|
|
|
285
485
|
|
|
286
486
|
</details>
|
|
287
487
|
|
|
288
|
-
## Changelog
|
|
289
|
-
|
|
290
|
-
[Changelog](CHANGELOG.md)
|
|
291
|
-
|
|
292
|
-
## License
|
|
293
|
-
|
|
294
|
-
[MIT](LICENSE)
|
|
295
|
-
|
|
296
488
|
[npm]: https://img.shields.io/npm/v/svelte-intersection-observer.svg?color=%23ff3e00&style=for-the-badge
|
|
297
489
|
[npm-url]: https://npmjs.com/package/svelte-intersection-observer
|
package/index.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { default } from "./IntersectionObserver.svelte";
|
|
2
|
+
export type { IntersectActionOptions } from "./intersect.svelte.js";
|
|
3
|
+
export { intersect } from "./intersect.svelte.js";
|
|
2
4
|
export { default as MultipleIntersectionObserver } from "./MultipleIntersectionObserver.svelte";
|
package/index.js
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Action } from "svelte/action";
|
|
2
|
+
|
|
3
|
+
export interface IntersectActionOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Specify the containing element.
|
|
6
|
+
* Defaults to the browser viewport.
|
|
7
|
+
* @default null
|
|
8
|
+
*/
|
|
9
|
+
root?: null | HTMLElement;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Margin offset of the containing element.
|
|
13
|
+
* @default "0px"
|
|
14
|
+
*/
|
|
15
|
+
rootMargin?: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Percentage of element visibility to trigger an event.
|
|
19
|
+
* Value must be a number between 0 and 1, or an array of numbers between 0 and 1.
|
|
20
|
+
* @default 0
|
|
21
|
+
*/
|
|
22
|
+
threshold?: number | number[];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Set to `true` to unobserve the element
|
|
26
|
+
* after it intersects the viewport.
|
|
27
|
+
* @default false
|
|
28
|
+
*/
|
|
29
|
+
once?: boolean;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Set to `true` to pause observing without disconnecting the
|
|
33
|
+
* observer. Set back to `false` to resume.
|
|
34
|
+
* @default false
|
|
35
|
+
*/
|
|
36
|
+
skip?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Svelte action that observes the element with the Intersection Observer API.
|
|
41
|
+
* Dispatches `observe` (on every change) and `intersect` (on entering the
|
|
42
|
+
* viewport) `CustomEvent`s on the element — listen with `onobserve`/`onintersect`.
|
|
43
|
+
*/
|
|
44
|
+
export const intersect: Action<
|
|
45
|
+
HTMLElement,
|
|
46
|
+
IntersectActionOptions | undefined,
|
|
47
|
+
{
|
|
48
|
+
onobserve?: (event: CustomEvent<IntersectionObserverEntry>) => void;
|
|
49
|
+
onintersect?: (
|
|
50
|
+
event: CustomEvent<IntersectionObserverEntry & { isIntersecting: true }>,
|
|
51
|
+
) => void;
|
|
52
|
+
}
|
|
53
|
+
>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Svelte action that observes `node` with the Intersection Observer API.
|
|
3
|
+
* Dispatches `observe` (on every change) and `intersect` (on entering the
|
|
4
|
+
* viewport) `CustomEvent`s on `node` — listen with `onobserve`/`onintersect`.
|
|
5
|
+
* @param {HTMLElement} node
|
|
6
|
+
* @param {import("./intersect.svelte.d.ts").IntersectActionOptions} [options]
|
|
7
|
+
*/
|
|
8
|
+
export function intersect(node, options = {}) {
|
|
9
|
+
let {
|
|
10
|
+
root = null,
|
|
11
|
+
rootMargin = "0px",
|
|
12
|
+
threshold = 0,
|
|
13
|
+
once = false,
|
|
14
|
+
skip = false,
|
|
15
|
+
} = options;
|
|
16
|
+
|
|
17
|
+
/** @type {IntersectionObserver} */
|
|
18
|
+
let observer;
|
|
19
|
+
|
|
20
|
+
const createObserver = () =>
|
|
21
|
+
new IntersectionObserver(
|
|
22
|
+
(entries) => {
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
node.dispatchEvent(new CustomEvent("observe", { detail: entry }));
|
|
25
|
+
|
|
26
|
+
if (entry.isIntersecting) {
|
|
27
|
+
node.dispatchEvent(new CustomEvent("intersect", { detail: entry }));
|
|
28
|
+
if (once) observer.unobserve(node);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{ root, rootMargin, threshold },
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
observer = createObserver();
|
|
36
|
+
if (!skip) observer.observe(node);
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
/** @param {import("./intersect.svelte.d.ts").IntersectActionOptions} [newOptions] */
|
|
40
|
+
update(newOptions = {}) {
|
|
41
|
+
once = newOptions.once ?? false;
|
|
42
|
+
|
|
43
|
+
const newSkip = newOptions.skip ?? false;
|
|
44
|
+
|
|
45
|
+
const configChanged =
|
|
46
|
+
(newOptions.root ?? null) !== root ||
|
|
47
|
+
(newOptions.rootMargin ?? "0px") !== rootMargin ||
|
|
48
|
+
JSON.stringify(newOptions.threshold ?? 0) !== JSON.stringify(threshold);
|
|
49
|
+
|
|
50
|
+
if (configChanged) {
|
|
51
|
+
root = newOptions.root ?? null;
|
|
52
|
+
rootMargin = newOptions.rootMargin ?? "0px";
|
|
53
|
+
threshold = newOptions.threshold ?? 0;
|
|
54
|
+
|
|
55
|
+
observer.disconnect();
|
|
56
|
+
observer = createObserver();
|
|
57
|
+
if (!newSkip) observer.observe(node);
|
|
58
|
+
} else if (newSkip !== skip) {
|
|
59
|
+
if (newSkip) observer.unobserve(node);
|
|
60
|
+
else observer.observe(node);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
skip = newSkip;
|
|
64
|
+
},
|
|
65
|
+
destroy() {
|
|
66
|
+
observer.disconnect();
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-intersection-observer",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Detect if an element is in the viewport using the Intersection Observer API",
|
|
6
6
|
"author": "Eric Liu (https://github.com/metonym)",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/metonym/svelte-intersection-observer.git"
|
|
12
12
|
},
|
|
13
|
-
"homepage": "https://github.
|
|
13
|
+
"homepage": "https://metonym.github.io/svelte-intersection-observer/",
|
|
14
14
|
"bugs": "https://github.com/metonym/svelte-intersection-observer/issues",
|
|
15
15
|
"keywords": [
|
|
16
16
|
"svelte",
|