svelte-intersection-observer 1.2.0 → 2.1.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,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
- * Set to `true` to pause observing without disconnecting the
51
- * observer or losing `entry`/`intersecting` state. Set back to
52
- * `false` to resume.
53
- * @type {boolean}
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
- let prevThreshold = threshold;
64
-
65
- /** @type {null | HTMLElement} */
66
- 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();
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,14 @@
77
45
  entry = _entry;
78
46
  intersecting = _entry.isIntersecting;
79
47
 
80
- dispatch("observe", entry);
48
+ onobserve?.(_entry);
81
49
 
82
50
  if (_entry.isIntersecting) {
83
- dispatch("intersect", entry);
51
+ onintersect?.(
52
+ /** @type {IntersectionObserverEntry & { isIntersecting: true }} */ (
53
+ _entry
54
+ ),
55
+ );
84
56
 
85
57
  if (element && once) observer?.unobserve(element);
86
58
  }
@@ -90,53 +62,34 @@
90
62
  );
91
63
  };
92
64
 
93
- onMount(() => {
65
+ $effect(() => {
66
+ prevElement = null;
94
67
  initialize();
95
68
 
96
69
  return () => {
97
- if (observer) {
98
- observer.disconnect();
99
- observer = null;
100
- }
70
+ observer?.disconnect();
71
+ observer = null;
101
72
  };
102
73
  });
103
74
 
104
- afterUpdate(async () => {
105
- await tick();
106
-
107
- if (element !== null && element !== prevElement) {
108
- if (!skip) observer?.observe(element);
75
+ $effect(() => {
76
+ const target = element;
77
+ const isSkipped = skip;
78
+ const activeObserver = observer;
109
79
 
110
- if (prevElement !== null) observer?.unobserve(prevElement);
111
- prevElement = element;
80
+ if (target !== null && target !== prevElement) {
81
+ if (!isSkipped) activeObserver?.observe(target);
82
+ if (prevElement !== null) activeObserver?.unobserve(prevElement);
83
+ prevElement = target;
112
84
  }
113
85
 
114
- if (skip !== prevSkip && element !== null) {
115
- if (skip) observer?.unobserve(element);
116
- else observer?.observe(element);
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;
86
+ if (isSkipped !== prevSkip && target !== null) {
87
+ if (isSkipped) activeObserver?.unobserve(target);
88
+ else activeObserver?.observe(target);
129
89
  }
130
90
 
131
- prevRootMargin = rootMargin;
132
- prevThreshold = threshold;
133
- prevRoot = root;
134
- prevSkip = skip;
91
+ prevSkip = isSkipped;
135
92
  });
136
93
  </script>
137
94
 
138
- <slot
139
- {intersecting}
140
- {entry}
141
- {observer}
142
- />
95
+ {@render children?.({ intersecting, entry, observer })}
@@ -1,87 +1,95 @@
1
- import type { SvelteComponentTyped } from "svelte";
1
+ import type { Component, Snippet } from "svelte";
2
2
 
3
- export default class extends SvelteComponentTyped<
4
- {
5
- /**
6
- * The HTML Element to observe.
7
- * @default null
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
- * Set to `true` to unobserve the element
13
- * after it intersects the viewport.
14
- * @default false
15
- */
16
- once?: boolean;
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
- * `true` if the observed element
20
- * is intersecting the viewport.
21
- * @default false
22
- */
23
- intersecting?: boolean;
17
+ /**
18
+ * `true` if the observed element
19
+ * is intersecting the viewport.
20
+ * @default false
21
+ */
22
+ intersecting?: boolean;
24
23
 
25
- /**
26
- * Specify the containing element.
27
- * Defaults to the browser viewport.
28
- * @default null
29
- */
30
- root?: null | HTMLElement;
24
+ /**
25
+ * Specify the containing element.
26
+ * Defaults to the browser viewport.
27
+ * @default null
28
+ */
29
+ root?: null | HTMLElement;
31
30
 
32
- /**
33
- * Margin offset of the containing element.
34
- * @default "0px"
35
- */
36
- rootMargin?: string;
31
+ /**
32
+ * Margin offset of the containing element.
33
+ * @default "0px"
34
+ */
35
+ rootMargin?: string;
37
36
 
38
- /**
39
- * Percentage of element visibility to trigger an event.
40
- * Value must be a number between 0 and 1, or an array of numbers between 0 and 1.
41
- * @default 0
42
- */
43
- threshold?: number | number[];
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
- * Observed element metadata.
47
- * @default null
48
- */
49
- entry?: null | IntersectionObserverEntry;
44
+ /**
45
+ * Observed element metadata.
46
+ * @default null
47
+ */
48
+ entry?: null | IntersectionObserverEntry;
50
49
 
51
- /**
52
- * `IntersectionObserver` instance.
53
- * @default null
54
- */
55
- observer?: null | IntersectionObserver;
50
+ /**
51
+ * `IntersectionObserver` instance.
52
+ * @default null
53
+ */
54
+ observer?: null | IntersectionObserver;
56
55
 
57
- /**
58
- * Set to `true` to pause observing without disconnecting the
59
- * observer or losing `entry`/`intersecting` state. Set back to
60
- * `false` to resume.
61
- * @default false
62
- */
63
- skip?: boolean;
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
- * Dispatched only when the element is intersecting the viewport.
74
- * `event.detail.isIntersecting` will only be `true`
75
- */
76
- intersect: CustomEvent<
77
- IntersectionObserverEntry & { isIntersecting: true }
78
- >;
79
- },
80
- {
81
- default: {
82
- intersecting: boolean;
83
- entry: null | IntersectionObserverEntry;
84
- observer: IntersectionObserver;
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
- * Percentage of element visibility to trigger an event.
28
- * Value must be between 0 and 1.
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
- * Map of element to its intersection state.
34
- * @type {Map<HTMLElement | null, boolean>}
35
- */
36
- export let elementIntersections = new Map();
37
-
38
- /**
39
- * Map of element to its latest entry.
40
- * @type {Map<HTMLElement | null, IntersectionObserverEntry>}
41
- */
42
- export let elementEntries = new Map();
43
-
44
- /**
45
- * `IntersectionObserver` instance.
46
- * @type {null | IntersectionObserver}
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,16 @@
80
47
  elementIntersections.set(target, _entry.isIntersecting);
81
48
  elementEntries.set(target, _entry);
82
49
 
83
- dispatch("observe", { entry: _entry, target });
50
+ onobserve?.({ entry: _entry, target });
84
51
 
85
52
  if (_entry.isIntersecting) {
86
- dispatch("intersect", { entry: _entry, target });
53
+ onintersect?.({
54
+ entry:
55
+ /** @type {IntersectionObserverEntry & { isIntersecting: true }} */ (
56
+ _entry
57
+ ),
58
+ target,
59
+ });
87
60
  if (once) observer?.unobserve(target);
88
61
  }
89
62
  }
@@ -96,73 +69,53 @@
96
69
  );
97
70
  };
98
71
 
99
- onMount(() => {
72
+ $effect(() => {
73
+ prevElements = [];
100
74
  initialize();
101
75
 
102
76
  return () => {
103
- if (observer) {
104
- observer.disconnect();
105
- observer = null;
106
- }
77
+ observer?.disconnect();
78
+ observer = null;
107
79
  };
108
80
  });
109
81
 
110
- afterUpdate(async () => {
111
- await tick();
82
+ $effect(() => {
83
+ const currentElements = elements;
84
+ const isSkipped = skip;
85
+ const activeObserver = observer;
112
86
 
113
- if (elements.length > 0) {
114
- const newElements = elements.filter(
87
+ if (currentElements.length > 0) {
88
+ const newElements = currentElements.filter(
115
89
  (el) => el && !prevElements.includes(el),
116
90
  );
117
- if (!skip) {
91
+
92
+ if (!isSkipped) {
118
93
  for (const el of newElements) {
119
- if (el) observer?.observe(el);
94
+ if (el) activeObserver?.observe(el);
120
95
  }
121
96
  }
122
97
 
123
98
  const removedElements = prevElements.filter(
124
- (el) => el && !elements.includes(el),
99
+ (el) => el && !currentElements.includes(el),
125
100
  );
101
+
126
102
  for (const el of removedElements) {
127
- if (el) observer?.unobserve(el);
103
+ if (el) activeObserver?.unobserve(el);
128
104
  }
129
105
 
130
- prevElements = [...elements];
106
+ prevElements = [...currentElements];
131
107
  }
132
108
 
133
- if (skip !== prevSkip) {
134
- for (const el of elements.filter((el) => el)) {
109
+ if (isSkipped !== prevSkip) {
110
+ for (const el of currentElements) {
135
111
  if (!el) continue;
136
- if (skip) observer?.unobserve(el);
137
- else observer?.observe(el);
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
- }
112
+ if (isSkipped) activeObserver?.unobserve(el);
113
+ else activeObserver?.observe(el);
154
114
  }
155
115
  }
156
116
 
157
- prevRootMargin = rootMargin;
158
- prevThreshold = threshold;
159
- prevRoot = root;
160
- prevSkip = skip;
117
+ prevSkip = isSkipped;
161
118
  });
162
119
  </script>
163
120
 
164
- <slot
165
- {observer}
166
- {elementIntersections}
167
- {elementEntries}
168
- />
121
+ {@render children?.({ observer, elementIntersections, elementEntries })}