svelte-intersection-observer 2.0.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.
@@ -0,0 +1,22 @@
1
+ import type { Attachment } from "svelte/attachments";
2
+ import type { IntersectActionOptions } from "./intersect.svelte.js";
3
+
4
+ export interface IntersectionObserverState {
5
+ /** `true` if the observed element is intersecting the viewport. */
6
+ readonly intersecting: boolean;
7
+
8
+ /** Observed element metadata. */
9
+ readonly entry: null | IntersectionObserverEntry;
10
+
11
+ /** Attachment to apply to the observed element via `{@attach}`. */
12
+ attach: Attachment<HTMLElement>;
13
+ }
14
+
15
+ /**
16
+ * Rune-based composable that observes the attached element with the
17
+ * Intersection Observer API, without a wrapper component or `use:`/`{@attach}`
18
+ * directive of your own.
19
+ */
20
+ export function createIntersectionObserver(
21
+ getOptions?: () => IntersectActionOptions,
22
+ ): IntersectionObserverState;
@@ -0,0 +1,36 @@
1
+ import { intersectAttachment } from "./intersect.svelte.js";
2
+
3
+ export function createIntersectionObserver(getOptions = () => ({})) {
4
+ let intersecting = $state(false);
5
+ let entry = $state( (null));
6
+
7
+ const attachment = intersectAttachment(getOptions);
8
+
9
+ function attach(node) {
10
+ const onObserve = (event) => {
11
+ const detail = (
12
+ event
13
+ ).detail;
14
+ entry = detail;
15
+ intersecting = detail.isIntersecting;
16
+ };
17
+
18
+ node.addEventListener("observe", onObserve);
19
+ const cleanup = attachment(node);
20
+
21
+ return () => {
22
+ node.removeEventListener("observe", onObserve);
23
+ if (typeof cleanup === "function") cleanup();
24
+ };
25
+ }
26
+
27
+ return {
28
+ get intersecting() {
29
+ return intersecting;
30
+ },
31
+ get entry() {
32
+ return entry;
33
+ },
34
+ attach,
35
+ };
36
+ }
package/index.d.ts CHANGED
@@ -1,4 +1,15 @@
1
+ export type { IntersectionObserverState } from "./create-intersection-observer.svelte.js";
2
+ export { createIntersectionObserver } from "./create-intersection-observer.svelte.js";
1
3
  export { default } from "./IntersectionObserver.svelte";
2
- export type { IntersectActionOptions } from "./intersect.svelte.js";
3
- export { intersect } from "./intersect.svelte.js";
4
+ export type {
5
+ IntersectActionOptions,
6
+ IntersectGroupNodeOptions,
7
+ IntersectGroupSharedOptions,
8
+ IntersectionGroup,
9
+ } from "./intersect.svelte.js";
10
+ export {
11
+ createIntersectionGroup,
12
+ intersect,
13
+ intersectAttachment,
14
+ } from "./intersect.svelte.js";
4
15
  export { default as MultipleIntersectionObserver } from "./MultipleIntersectionObserver.svelte";
package/index.js CHANGED
@@ -1,3 +1,8 @@
1
+ export { createIntersectionObserver } from "./create-intersection-observer.svelte.js";
1
2
  export { default } from "./IntersectionObserver.svelte";
2
- export { intersect } from "./intersect.svelte.js";
3
+ export {
4
+ createIntersectionGroup,
5
+ intersect,
6
+ intersectAttachment,
7
+ } from "./intersect.svelte.js";
3
8
  export { default as MultipleIntersectionObserver } from "./MultipleIntersectionObserver.svelte";
@@ -1,4 +1,5 @@
1
1
  import type { Action } from "svelte/action";
2
+ import type { Attachment } from "svelte/attachments";
2
3
 
3
4
  export interface IntersectActionOptions {
4
5
  /**
@@ -51,3 +52,92 @@ export const intersect: Action<
51
52
  ) => void;
52
53
  }
53
54
  >;
55
+
56
+ /**
57
+ * Svelte attachment that observes the element with the Intersection Observer
58
+ * API. Equivalent to `intersect`, for use with `{@attach}` instead of `use:`.
59
+ */
60
+ export function intersectAttachment(
61
+ getOptions?: () => IntersectActionOptions,
62
+ ): Attachment<HTMLElement>;
63
+
64
+ declare module "svelte/elements" {
65
+ export interface HTMLAttributes<T> {
66
+ onobserve?: (event: CustomEvent<IntersectionObserverEntry>) => void;
67
+ onintersect?: (
68
+ event: CustomEvent<IntersectionObserverEntry & { isIntersecting: true }>,
69
+ ) => void;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Options shared by every node in an `createIntersectionGroup` group. These
75
+ * configure the one underlying `IntersectionObserver` instance, so they
76
+ * can't vary per node.
77
+ */
78
+ export interface IntersectGroupSharedOptions {
79
+ /**
80
+ * Specify the containing element.
81
+ * Defaults to the browser viewport.
82
+ * @default null
83
+ */
84
+ root?: null | HTMLElement;
85
+
86
+ /**
87
+ * Margin offset of the containing element.
88
+ * @default "0px"
89
+ */
90
+ rootMargin?: string;
91
+
92
+ /**
93
+ * Percentage of element visibility to trigger an event.
94
+ * Value must be a number between 0 and 1, or an array of numbers between 0 and 1.
95
+ * @default 0
96
+ */
97
+ threshold?: number | number[];
98
+ }
99
+
100
+ /**
101
+ * Per-node options for a single element within an `createIntersectionGroup` group.
102
+ */
103
+ export interface IntersectGroupNodeOptions {
104
+ /**
105
+ * Set to `true` to unobserve the element
106
+ * after it intersects the viewport.
107
+ * @default false
108
+ */
109
+ once?: boolean;
110
+
111
+ /**
112
+ * Set to `true` to skip observing this element without
113
+ * affecting the rest of the group.
114
+ * @default false
115
+ */
116
+ skip?: boolean;
117
+
118
+ /** Called when the element is first observed and also whenever an intersection change occurs. */
119
+ onobserve?: (entry: IntersectionObserverEntry) => void;
120
+
121
+ /** Called only when the element is intersecting the viewport. */
122
+ onintersect?: (
123
+ entry: IntersectionObserverEntry & { isIntersecting: true },
124
+ ) => void;
125
+ }
126
+
127
+ export interface IntersectionGroup {
128
+ /**
129
+ * Returns an attachment for a single node in the group. Call once per
130
+ * element in a loop, e.g. `{@attach group.attach({ onobserve })}`.
131
+ */
132
+ attach(options?: IntersectGroupNodeOptions): Attachment<HTMLElement>;
133
+ }
134
+
135
+ /**
136
+ * Creates a group of elements backed by a single shared `IntersectionObserver`
137
+ * instance, for use with `{@attach}` inside an `#each` block. Brings
138
+ * `MultipleIntersectionObserver`'s single-observer performance benefit to the
139
+ * action/attachment API.
140
+ */
141
+ export function createIntersectionGroup(
142
+ getSharedOptions?: () => IntersectGroupSharedOptions,
143
+ ): IntersectionGroup;
@@ -1,10 +1,5 @@
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
- */
1
+ import { fromAction } from "svelte/attachments";
2
+
8
3
  export function intersect(node, options = {}) {
9
4
  let {
10
5
  root = null,
@@ -14,29 +9,30 @@ export function intersect(node, options = {}) {
14
9
  skip = false,
15
10
  } = options;
16
11
 
17
- /** @type {IntersectionObserver} */
18
12
  let observer;
19
13
 
20
- const createObserver = () =>
21
- new IntersectionObserver(
14
+ const createObserver = () => {
15
+ if (typeof IntersectionObserver === "undefined") return null;
16
+
17
+ return new IntersectionObserver(
22
18
  (entries) => {
23
19
  for (const entry of entries) {
24
20
  node.dispatchEvent(new CustomEvent("observe", { detail: entry }));
25
21
 
26
22
  if (entry.isIntersecting) {
27
23
  node.dispatchEvent(new CustomEvent("intersect", { detail: entry }));
28
- if (once) observer.unobserve(node);
24
+ if (once) observer?.unobserve(node);
29
25
  }
30
26
  }
31
27
  },
32
28
  { root, rootMargin, threshold },
33
29
  );
30
+ };
34
31
 
35
32
  observer = createObserver();
36
- if (!skip) observer.observe(node);
33
+ if (!skip) observer?.observe(node);
37
34
 
38
35
  return {
39
- /** @param {import("./intersect.svelte.d.ts").IntersectActionOptions} [newOptions] */
40
36
  update(newOptions = {}) {
41
37
  once = newOptions.once ?? false;
42
38
 
@@ -52,18 +48,80 @@ export function intersect(node, options = {}) {
52
48
  rootMargin = newOptions.rootMargin ?? "0px";
53
49
  threshold = newOptions.threshold ?? 0;
54
50
 
55
- observer.disconnect();
51
+ observer?.disconnect();
56
52
  observer = createObserver();
57
- if (!newSkip) observer.observe(node);
53
+ if (!newSkip) observer?.observe(node);
58
54
  } else if (newSkip !== skip) {
59
- if (newSkip) observer.unobserve(node);
60
- else observer.observe(node);
55
+ if (newSkip) observer?.unobserve(node);
56
+ else observer?.observe(node);
61
57
  }
62
58
 
63
59
  skip = newSkip;
64
60
  },
65
61
  destroy() {
66
- observer.disconnect();
62
+ observer?.disconnect();
67
63
  },
68
64
  };
69
65
  }
66
+
67
+ export function intersectAttachment(getOptions = () => ({})) {
68
+ return fromAction(intersect, getOptions);
69
+ }
70
+
71
+ export function createIntersectionGroup(getSharedOptions = () => ({})) {
72
+ let observer;
73
+
74
+ const callbacks = new Map();
75
+
76
+ const handleEntries = (
77
+ entries,
78
+ ) => {
79
+ for (const entry of entries) {
80
+ const target = entry.target;
81
+ const nodeOptions = callbacks.get(target);
82
+
83
+ nodeOptions?.onobserve?.(entry);
84
+
85
+ if (entry.isIntersecting) {
86
+ nodeOptions?.onintersect?.(
87
+ (
88
+ entry
89
+ ),
90
+ );
91
+ if (nodeOptions?.once) observer?.unobserve(target);
92
+ }
93
+ }
94
+ };
95
+
96
+ function attach(nodeOptions = {}) {
97
+ return ( node) => {
98
+ if (!observer && typeof IntersectionObserver !== "undefined") {
99
+ const {
100
+ root = null,
101
+ rootMargin = "0px",
102
+ threshold = 0,
103
+ } = getSharedOptions();
104
+ observer = new IntersectionObserver(handleEntries, {
105
+ root,
106
+ rootMargin,
107
+ threshold,
108
+ });
109
+ }
110
+
111
+ callbacks.set(node, nodeOptions);
112
+ if (!nodeOptions.skip) observer?.observe(node);
113
+
114
+ return () => {
115
+ observer?.unobserve(node);
116
+ callbacks.delete(node);
117
+
118
+ if (callbacks.size === 0) {
119
+ observer?.disconnect();
120
+ observer = undefined;
121
+ }
122
+ };
123
+ };
124
+ }
125
+
126
+ return { attach };
127
+ }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "svelte-intersection-observer",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "license": "MIT",
5
- "description": "Detect if an element is in the viewport using the Intersection Observer API",
5
+ "description": "Detects when an element enters or exits the viewport using the Intersection Observer API.",
6
6
  "author": "Eric Liu (https://github.com/metonym)",
7
7
  "type": "module",
8
8
  "svelte": "./index.js",