svelte-intersection-observer 2.1.0 → 2.2.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.
@@ -1,23 +1,6 @@
1
1
  <script>
2
2
  import { untrack } from "svelte";
3
3
 
4
- /**
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]
18
- */
19
-
20
- /** @type {Props} */
21
4
  let {
22
5
  element = null,
23
6
  once = false,
@@ -33,12 +16,20 @@
33
16
  children,
34
17
  } = $props();
35
18
 
36
- /** @type {null | HTMLElement} */
37
19
  let prevElement = null;
38
20
 
39
21
  let prevSkip = untrack(() => skip);
40
22
 
23
+ const configKey = $derived(
24
+ `${root}|${rootMargin}|${JSON.stringify(threshold)}`,
25
+ );
26
+
41
27
  const initialize = () => {
28
+ if (typeof IntersectionObserver === "undefined") {
29
+ observer = null;
30
+ return;
31
+ }
32
+
42
33
  observer = new IntersectionObserver(
43
34
  (entries) => {
44
35
  for (const _entry of entries) {
@@ -49,7 +40,7 @@
49
40
 
50
41
  if (_entry.isIntersecting) {
51
42
  onintersect?.(
52
- /** @type {IntersectionObserverEntry & { isIntersecting: true }} */ (
43
+ (
53
44
  _entry
54
45
  ),
55
46
  );
@@ -63,8 +54,12 @@
63
54
  };
64
55
 
65
56
  $effect(() => {
66
- prevElement = null;
67
- initialize();
57
+ configKey;
58
+
59
+ untrack(() => {
60
+ prevElement = null;
61
+ initialize();
62
+ });
68
63
 
69
64
  return () => {
70
65
  observer?.disconnect();
@@ -77,10 +72,15 @@
77
72
  const isSkipped = skip;
78
73
  const activeObserver = observer;
79
74
 
80
- if (target !== null && target !== prevElement) {
81
- if (!isSkipped) activeObserver?.observe(target);
75
+ if (target !== prevElement) {
82
76
  if (prevElement !== null) activeObserver?.unobserve(prevElement);
77
+ if (target !== null && !isSkipped) activeObserver?.observe(target);
83
78
  prevElement = target;
79
+
80
+ if (target === null) {
81
+ intersecting = false;
82
+ entry = null;
83
+ }
84
84
  }
85
85
 
86
86
  if (isSkipped !== prevSkip && target !== null) {
@@ -1,23 +1,6 @@
1
1
  <script>
2
2
  import { untrack } from "svelte";
3
3
 
4
- /**
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]
18
- */
19
-
20
- /** @type {Props} */
21
4
  let {
22
5
  elements = [],
23
6
  once = false,
@@ -33,16 +16,24 @@
33
16
  children,
34
17
  } = $props();
35
18
 
36
- /** @type {(HTMLElement | null)[]} */
37
- let prevElements = [];
19
+ let prevElements = new Set();
38
20
 
39
21
  let prevSkip = untrack(() => skip);
40
22
 
23
+ const configKey = $derived(
24
+ `${root}|${rootMargin}|${JSON.stringify(threshold)}`,
25
+ );
26
+
41
27
  const initialize = () => {
28
+ if (typeof IntersectionObserver === "undefined") {
29
+ observer = null;
30
+ return;
31
+ }
32
+
42
33
  observer = new IntersectionObserver(
43
34
  (entries) => {
44
35
  for (const _entry of entries) {
45
- const target = /** @type {HTMLElement} */ (_entry.target);
36
+ const target = (_entry.target);
46
37
 
47
38
  elementIntersections.set(target, _entry.isIntersecting);
48
39
  elementEntries.set(target, _entry);
@@ -52,7 +43,7 @@
52
43
  if (_entry.isIntersecting) {
53
44
  onintersect?.({
54
45
  entry:
55
- /** @type {IntersectionObserverEntry & { isIntersecting: true }} */ (
46
+ (
56
47
  _entry
57
48
  ),
58
49
  target,
@@ -61,7 +52,6 @@
61
52
  }
62
53
  }
63
54
 
64
- // Trigger reactivity once per batch, not once per entry.
65
55
  elementIntersections = new Map(elementIntersections);
66
56
  elementEntries = new Map(elementEntries);
67
57
  },
@@ -70,8 +60,12 @@
70
60
  };
71
61
 
72
62
  $effect(() => {
73
- prevElements = [];
74
- initialize();
63
+ configKey;
64
+
65
+ untrack(() => {
66
+ prevElements = new Set();
67
+ initialize();
68
+ });
75
69
 
76
70
  return () => {
77
71
  observer?.disconnect();
@@ -84,28 +78,35 @@
84
78
  const isSkipped = skip;
85
79
  const activeObserver = observer;
86
80
 
87
- if (currentElements.length > 0) {
88
- const newElements = currentElements.filter(
89
- (el) => el && !prevElements.includes(el),
90
- );
91
-
92
- if (!isSkipped) {
93
- for (const el of newElements) {
94
- if (el) activeObserver?.observe(el);
95
- }
96
- }
81
+ const currentSet = new Set(currentElements);
97
82
 
98
- const removedElements = prevElements.filter(
99
- (el) => el && !currentElements.includes(el),
100
- );
83
+ const newElements = currentElements.filter(
84
+ (el) => el && !prevElements.has(el),
85
+ );
86
+ const removedElements = [...prevElements].filter(
87
+ (el) => el && !currentSet.has(el),
88
+ );
101
89
 
102
- for (const el of removedElements) {
103
- if (el) activeObserver?.unobserve(el);
90
+ if (!isSkipped) {
91
+ for (const el of newElements) {
92
+ if (el) activeObserver?.observe(el);
104
93
  }
94
+ }
105
95
 
106
- prevElements = [...currentElements];
96
+ for (const el of removedElements) {
97
+ if (!el) continue;
98
+ activeObserver?.unobserve(el);
99
+ elementIntersections.delete(el);
100
+ elementEntries.delete(el);
107
101
  }
108
102
 
103
+ if (removedElements.length > 0) {
104
+ elementIntersections = new Map(elementIntersections);
105
+ elementEntries = new Map(elementEntries);
106
+ }
107
+
108
+ prevElements = currentSet;
109
+
109
110
  if (isSkipped !== prevSkip) {
110
111
  for (const el of currentElements) {
111
112
  if (!el) continue;