@nikala-ui/hooks 0.8.0 → 0.9.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/README.md +15 -1
- package/package.json +1 -1
- package/src/create-audio-video.ts +444 -0
- package/src/create-battery.ts +78 -0
- package/src/create-document-title.ts +33 -0
- package/src/create-event-source.ts +126 -0
- package/src/create-favicon.ts +52 -0
- package/src/create-fetch.ts +93 -0
- package/src/create-fullscreen.ts +105 -0
- package/src/create-geolocation.ts +140 -0
- package/src/create-infinite-scroll.ts +90 -0
- package/src/create-orientation.ts +129 -0
- package/src/create-permission.ts +91 -0
- package/src/create-scroll-into-view.ts +57 -0
- package/src/create-undo-redo.ts +100 -0
- package/src/create-web-notification.ts +111 -0
- package/src/create-websocket.ts +179 -0
- package/src/index.ts +16 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { createSignal, createEffect, onCleanup, type Accessor } from "solid-js";
|
|
2
|
+
|
|
3
|
+
export type EventSourceStatus = "CONNECTING" | "OPEN" | "CLOSED";
|
|
4
|
+
|
|
5
|
+
export interface CreateEventSourceOptions {
|
|
6
|
+
/** Event names to listen to on the EventSource. Defaults to ['message']. */
|
|
7
|
+
events?: string[];
|
|
8
|
+
/** Include credentials in CORS requests. */
|
|
9
|
+
withCredentials?: boolean;
|
|
10
|
+
/** Whether to open connection immediately. Defaults to true. */
|
|
11
|
+
immediate?: boolean;
|
|
12
|
+
/** Callback fired when connection is opened. */
|
|
13
|
+
onOpen?: (event: Event) => void;
|
|
14
|
+
/** Callback fired when message event is received. */
|
|
15
|
+
onMessage?: (event: MessageEvent) => void;
|
|
16
|
+
/** Callback fired when error occurs. */
|
|
17
|
+
onError?: (event: Event) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CreateEventSourceReturn<T = unknown> {
|
|
21
|
+
/** Signal accessor containing latest received SSE event data (parsed if JSON). */
|
|
22
|
+
data: Accessor<T | null>;
|
|
23
|
+
/** Signal accessor containing current EventSource status. */
|
|
24
|
+
status: Accessor<EventSourceStatus>;
|
|
25
|
+
/** Signal accessor containing last raw MessageEvent. */
|
|
26
|
+
event: Accessor<MessageEvent | null>;
|
|
27
|
+
/** Open or reconnect EventSource stream. */
|
|
28
|
+
open: () => void;
|
|
29
|
+
/** Close active EventSource stream. */
|
|
30
|
+
close: () => void;
|
|
31
|
+
/** Signal accessor indicating whether EventSource is supported in browser environment. */
|
|
32
|
+
isSupported: Accessor<boolean>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* SolidJS reactive primitive for subscribing to Server-Sent Events (SSE) streams.
|
|
37
|
+
*/
|
|
38
|
+
export function createEventSource<T = unknown>(
|
|
39
|
+
url: string | Accessor<string>,
|
|
40
|
+
options: CreateEventSourceOptions = {}
|
|
41
|
+
): CreateEventSourceReturn<T> {
|
|
42
|
+
const [data, setData] = createSignal<T | null>(null);
|
|
43
|
+
const [event, setEvent] = createSignal<MessageEvent | null>(null);
|
|
44
|
+
const [status, setStatus] = createSignal<EventSourceStatus>("CLOSED");
|
|
45
|
+
|
|
46
|
+
const getUrl = (): string => (typeof url === "function" ? url() : url);
|
|
47
|
+
|
|
48
|
+
const isSupported = (): boolean =>
|
|
49
|
+
typeof window !== "undefined" && "EventSource" in window;
|
|
50
|
+
|
|
51
|
+
let es: EventSource | null = null;
|
|
52
|
+
|
|
53
|
+
const close = (): void => {
|
|
54
|
+
if (es) {
|
|
55
|
+
es.close();
|
|
56
|
+
es = null;
|
|
57
|
+
setStatus("CLOSED");
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const open = (): void => {
|
|
62
|
+
if (!isSupported()) return;
|
|
63
|
+
|
|
64
|
+
close();
|
|
65
|
+
|
|
66
|
+
setStatus("CONNECTING");
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const source = new EventSource(getUrl(), {
|
|
70
|
+
withCredentials: options.withCredentials,
|
|
71
|
+
});
|
|
72
|
+
es = source;
|
|
73
|
+
|
|
74
|
+
source.onopen = (e) => {
|
|
75
|
+
setStatus("OPEN");
|
|
76
|
+
options.onOpen?.(e);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
source.onerror = (e) => {
|
|
80
|
+
if (source.readyState === EventSource.CLOSED) {
|
|
81
|
+
setStatus("CLOSED");
|
|
82
|
+
} else if (source.readyState === EventSource.CONNECTING) {
|
|
83
|
+
setStatus("CONNECTING");
|
|
84
|
+
}
|
|
85
|
+
options.onError?.(e);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const eventList = options.events ?? ["message"];
|
|
89
|
+
eventList.forEach((eventName) => {
|
|
90
|
+
source.addEventListener(eventName, (e) => {
|
|
91
|
+
const msgEvt = e as MessageEvent;
|
|
92
|
+
setEvent(() => msgEvt);
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(msgEvt.data);
|
|
95
|
+
setData(() => parsed);
|
|
96
|
+
} catch {
|
|
97
|
+
setData(() => msgEvt.data as unknown as T);
|
|
98
|
+
}
|
|
99
|
+
options.onMessage?.(msgEvt);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
} catch {
|
|
103
|
+
setStatus("CLOSED");
|
|
104
|
+
es = null;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
createEffect(() => {
|
|
109
|
+
if (options.immediate ?? true) {
|
|
110
|
+
open();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
onCleanup(() => {
|
|
114
|
+
close();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
data,
|
|
120
|
+
status,
|
|
121
|
+
event,
|
|
122
|
+
open,
|
|
123
|
+
close,
|
|
124
|
+
isSupported,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createEffect, onCleanup, type Accessor } from "solid-js";
|
|
2
|
+
|
|
3
|
+
export interface CreateFaviconOptions {
|
|
4
|
+
/** Favicon rel attribute value. Defaults to 'icon'. */
|
|
5
|
+
rel?: string;
|
|
6
|
+
/** Favicon image mime-type format (e.g., 'image/x-icon', 'image/svg+xml', 'image/png'). */
|
|
7
|
+
type?: string;
|
|
8
|
+
/** Whether to restore original favicon on component unmount. Defaults to true. */
|
|
9
|
+
restoreOnUnmount?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* SolidJS reactive primitive for dynamically updating browser favicon element.
|
|
14
|
+
*/
|
|
15
|
+
export function createFavicon(
|
|
16
|
+
href: string | Accessor<string>,
|
|
17
|
+
options: CreateFaviconOptions = {}
|
|
18
|
+
): void {
|
|
19
|
+
const getHref = (): string => (typeof href === "function" ? href() : href);
|
|
20
|
+
|
|
21
|
+
createEffect(() => {
|
|
22
|
+
if (typeof document === "undefined") return;
|
|
23
|
+
|
|
24
|
+
const rel = options.rel ?? "icon";
|
|
25
|
+
let linkElement: HTMLLinkElement | null = document.querySelector(
|
|
26
|
+
`link[rel*="${rel}"]`
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const originalHref = linkElement ? linkElement.href : "";
|
|
30
|
+
|
|
31
|
+
if (!linkElement) {
|
|
32
|
+
linkElement = document.createElement("link");
|
|
33
|
+
linkElement.rel = rel;
|
|
34
|
+
if (options.type) linkElement.type = options.type;
|
|
35
|
+
document.head.appendChild(linkElement);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const newHref = getHref();
|
|
39
|
+
if (newHref) {
|
|
40
|
+
linkElement.href = newHref;
|
|
41
|
+
if (options.type) linkElement.type = options.type;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
onCleanup(() => {
|
|
45
|
+
if (typeof document !== "undefined" && (options.restoreOnUnmount ?? true) && linkElement) {
|
|
46
|
+
if (originalHref) {
|
|
47
|
+
linkElement.href = originalHref;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createSignal, createEffect, type Accessor } from "solid-js";
|
|
2
|
+
|
|
3
|
+
export interface CreateFetchOptions<T> extends RequestInit {
|
|
4
|
+
/** Whether the request should be refetched automatically on window focus. Defaults to false. */
|
|
5
|
+
refetchOnFocus?: boolean;
|
|
6
|
+
/** Custom transform function to process raw JSON / text response into required shape. */
|
|
7
|
+
transform?: (data: unknown) => T;
|
|
8
|
+
/** Whether the initial request should execute immediately. Defaults to true. */
|
|
9
|
+
immediate?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CreateFetchReturn<T> {
|
|
13
|
+
/** Signal containing fetched data. */
|
|
14
|
+
data: Accessor<T | null>;
|
|
15
|
+
/** Signal indicating whether request is loading. */
|
|
16
|
+
isLoading: Accessor<boolean>;
|
|
17
|
+
/** Signal containing request error if fetch failed. */
|
|
18
|
+
error: Accessor<Error | null>;
|
|
19
|
+
/** Imperative function to refetch data manually. */
|
|
20
|
+
refetch: () => Promise<void>;
|
|
21
|
+
/** Abort ongoing HTTP request. */
|
|
22
|
+
abort: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* SolidJS reactive primitive for handling HTTP fetch requests, loading states, errors, and manual refetching.
|
|
27
|
+
*/
|
|
28
|
+
export function createFetch<T = unknown>(
|
|
29
|
+
url: string | Accessor<string>,
|
|
30
|
+
options: CreateFetchOptions<T> = {}
|
|
31
|
+
): CreateFetchReturn<T> {
|
|
32
|
+
const [data, setData] = createSignal<T | null>(null);
|
|
33
|
+
const [isLoading, setIsLoading] = createSignal(options.immediate ?? true);
|
|
34
|
+
const [error, setError] = createSignal<Error | null>(null);
|
|
35
|
+
|
|
36
|
+
let controller: AbortController | null = null;
|
|
37
|
+
|
|
38
|
+
const getUrl = (): string => {
|
|
39
|
+
return typeof url === "function" ? url() : url;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const abort = (): void => {
|
|
43
|
+
if (controller) {
|
|
44
|
+
controller.abort();
|
|
45
|
+
controller = null;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const executeFetch = async (): Promise<void> => {
|
|
50
|
+
if (typeof window === "undefined") return;
|
|
51
|
+
|
|
52
|
+
abort();
|
|
53
|
+
controller = new AbortController();
|
|
54
|
+
|
|
55
|
+
setIsLoading(true);
|
|
56
|
+
setError(null);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch(getUrl(), {
|
|
60
|
+
...options,
|
|
61
|
+
signal: controller.signal,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
throw new Error(`HTTP error! status: ${response.status} ${response.statusText}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const raw = await response.json();
|
|
69
|
+
const result = options.transform ? options.transform(raw) : (raw as T);
|
|
70
|
+
|
|
71
|
+
setData(() => result);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
if (err instanceof Error && err.name === "AbortError") return;
|
|
74
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
75
|
+
} finally {
|
|
76
|
+
setIsLoading(false);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
createEffect(() => {
|
|
81
|
+
if (options.immediate ?? true) {
|
|
82
|
+
executeFetch();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
data,
|
|
88
|
+
isLoading,
|
|
89
|
+
error,
|
|
90
|
+
refetch: executeFetch,
|
|
91
|
+
abort,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createSignal, createEffect, onCleanup, type Accessor } from "solid-js";
|
|
2
|
+
|
|
3
|
+
export interface CreateFullscreenOptions {
|
|
4
|
+
/** Target element accessor or reference. Defaults to document.documentElement. */
|
|
5
|
+
target?: HTMLElement | Accessor<HTMLElement | undefined>;
|
|
6
|
+
/** Callback fired when entering fullscreen mode. */
|
|
7
|
+
onEnter?: () => void;
|
|
8
|
+
/** Callback fired when exiting fullscreen mode. */
|
|
9
|
+
onExit?: () => void;
|
|
10
|
+
/** Callback fired when fullscreen request fails. */
|
|
11
|
+
onError?: (err: Event) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CreateFullscreenReturn {
|
|
15
|
+
/** Signal indicating whether fullscreen mode is currently active. */
|
|
16
|
+
isFullscreen: Accessor<boolean>;
|
|
17
|
+
/** Request full screen mode for target element. */
|
|
18
|
+
enter: () => Promise<void>;
|
|
19
|
+
/** Exit full screen mode. */
|
|
20
|
+
exit: () => Promise<void>;
|
|
21
|
+
/** Toggle full screen mode. */
|
|
22
|
+
toggle: () => Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* SolidJS reactive primitive for requesting and monitoring element fullscreen status.
|
|
27
|
+
*/
|
|
28
|
+
export function createFullscreen(
|
|
29
|
+
options: CreateFullscreenOptions = {}
|
|
30
|
+
): CreateFullscreenReturn {
|
|
31
|
+
const [isFullscreen, setIsFullscreen] = createSignal(false);
|
|
32
|
+
|
|
33
|
+
const getTarget = (): HTMLElement | undefined => {
|
|
34
|
+
if (typeof document === "undefined") return undefined;
|
|
35
|
+
if (typeof options.target === "function") {
|
|
36
|
+
return options.target() ?? document.documentElement;
|
|
37
|
+
}
|
|
38
|
+
return options.target ?? document.documentElement;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const updateStatus = (): void => {
|
|
42
|
+
if (typeof document === "undefined") return;
|
|
43
|
+
const activeEl = document.fullscreenElement;
|
|
44
|
+
const isTargetFullscreen = Boolean(activeEl && activeEl === getTarget());
|
|
45
|
+
setIsFullscreen(isTargetFullscreen);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const enter = async (): Promise<void> => {
|
|
49
|
+
if (typeof window === "undefined") return;
|
|
50
|
+
const el = getTarget();
|
|
51
|
+
if (el?.requestFullscreen) {
|
|
52
|
+
await el.requestFullscreen();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const exit = async (): Promise<void> => {
|
|
57
|
+
if (typeof document === "undefined") return;
|
|
58
|
+
if (document.fullscreenElement && document.exitFullscreen) {
|
|
59
|
+
await document.exitFullscreen();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const toggle = async (): Promise<void> => {
|
|
64
|
+
if (isFullscreen()) {
|
|
65
|
+
await exit();
|
|
66
|
+
} else {
|
|
67
|
+
await enter();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
createEffect(() => {
|
|
72
|
+
if (typeof document === "undefined") return;
|
|
73
|
+
|
|
74
|
+
const handleFullscreenChange = (): void => {
|
|
75
|
+
const activeEl = document.fullscreenElement;
|
|
76
|
+
const isTarget = Boolean(activeEl && activeEl === getTarget());
|
|
77
|
+
setIsFullscreen(isTarget);
|
|
78
|
+
|
|
79
|
+
if (isTarget) {
|
|
80
|
+
options.onEnter?.();
|
|
81
|
+
} else {
|
|
82
|
+
options.onExit?.();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const handleFullscreenError = (err: Event): void => {
|
|
87
|
+
options.onError?.(err);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
|
91
|
+
document.addEventListener("fullscreenerror", handleFullscreenError);
|
|
92
|
+
|
|
93
|
+
onCleanup(() => {
|
|
94
|
+
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
|
95
|
+
document.removeEventListener("fullscreenerror", handleFullscreenError);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
isFullscreen,
|
|
101
|
+
enter,
|
|
102
|
+
exit,
|
|
103
|
+
toggle,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { createSignal, createEffect, onCleanup, type Accessor } from "solid-js";
|
|
2
|
+
|
|
3
|
+
export interface CreateGeolocationOptions extends PositionOptions {
|
|
4
|
+
/** Whether to start watching position immediately upon primitive initialization. Defaults to true. */
|
|
5
|
+
immediate?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface GeolocationState {
|
|
9
|
+
/** Current latitude coordinate in degrees. */
|
|
10
|
+
latitude: number | null;
|
|
11
|
+
/** Current longitude coordinate in degrees. */
|
|
12
|
+
longitude: number | null;
|
|
13
|
+
/** Current altitude above sea level in meters. */
|
|
14
|
+
altitude: number | null;
|
|
15
|
+
/** Position accuracy level in meters. */
|
|
16
|
+
accuracy: number | null;
|
|
17
|
+
/** Altitude accuracy in meters. */
|
|
18
|
+
altitudeAccuracy: number | null;
|
|
19
|
+
/** Current heading direction in degrees relative to true north. */
|
|
20
|
+
heading: number | null;
|
|
21
|
+
/** Current speed in meters per second. */
|
|
22
|
+
speed: number | null;
|
|
23
|
+
/** Timestamp when location was captured. */
|
|
24
|
+
timestamp: number | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CreateGeolocationReturn {
|
|
28
|
+
/** Signal accessor containing current geolocation coordinates and metrics. */
|
|
29
|
+
coords: Accessor<GeolocationState>;
|
|
30
|
+
/** Signal accessor indicating whether position retrieval is in progress. */
|
|
31
|
+
loading: Accessor<boolean>;
|
|
32
|
+
/** Signal accessor containing GeolocationPositionError if request failed. */
|
|
33
|
+
error: Accessor<GeolocationPositionError | Error | null>;
|
|
34
|
+
/** Signal accessor indicating whether Geolocation API is supported in browser environment. */
|
|
35
|
+
isSupported: Accessor<boolean>;
|
|
36
|
+
/** Imperative function to fetch current position once. */
|
|
37
|
+
getCurrentPosition: () => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const initialCoords: GeolocationState = {
|
|
41
|
+
latitude: null,
|
|
42
|
+
longitude: null,
|
|
43
|
+
altitude: null,
|
|
44
|
+
accuracy: null,
|
|
45
|
+
altitudeAccuracy: null,
|
|
46
|
+
heading: null,
|
|
47
|
+
speed: null,
|
|
48
|
+
timestamp: null,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* SolidJS reactive primitive for tracking user geographic location and GPS metrics.
|
|
53
|
+
*/
|
|
54
|
+
export function createGeolocation(
|
|
55
|
+
options: CreateGeolocationOptions = {}
|
|
56
|
+
): CreateGeolocationReturn {
|
|
57
|
+
const [coords, setCoords] = createSignal<GeolocationState>(initialCoords);
|
|
58
|
+
const [loading, setLoading] = createSignal(false);
|
|
59
|
+
const [error, setError] = createSignal<GeolocationPositionError | Error | null>(null);
|
|
60
|
+
|
|
61
|
+
const isSupported = (): boolean => {
|
|
62
|
+
if (typeof window === "undefined" || typeof navigator === "undefined") return false;
|
|
63
|
+
return "geolocation" in navigator;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const getOptions = (): PositionOptions => ({
|
|
67
|
+
timeout: options.timeout ?? 10000,
|
|
68
|
+
maximumAge: options.maximumAge ?? 5000,
|
|
69
|
+
enableHighAccuracy: options.enableHighAccuracy ?? false,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
let watchId: number | null = null;
|
|
73
|
+
|
|
74
|
+
const updatePosition = (position: GeolocationPosition): void => {
|
|
75
|
+
setCoords({
|
|
76
|
+
latitude: position.coords.latitude,
|
|
77
|
+
longitude: position.coords.longitude,
|
|
78
|
+
altitude: position.coords.altitude,
|
|
79
|
+
accuracy: position.coords.accuracy,
|
|
80
|
+
altitudeAccuracy: position.coords.altitudeAccuracy,
|
|
81
|
+
heading: position.coords.heading,
|
|
82
|
+
speed: position.coords.speed,
|
|
83
|
+
timestamp: position.timestamp,
|
|
84
|
+
});
|
|
85
|
+
setLoading(false);
|
|
86
|
+
setError(null);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const handleError = (err: GeolocationPositionError): void => {
|
|
90
|
+
setError(err);
|
|
91
|
+
setLoading(false);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const getCurrentPosition = (): void => {
|
|
95
|
+
if (!isSupported()) {
|
|
96
|
+
setError(new Error("Geolocation API is not supported in this browser environment."));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
setLoading(true);
|
|
101
|
+
setError(null);
|
|
102
|
+
|
|
103
|
+
navigator.geolocation.getCurrentPosition(
|
|
104
|
+
(pos) => {
|
|
105
|
+
updatePosition(pos);
|
|
106
|
+
},
|
|
107
|
+
(err) => {
|
|
108
|
+
handleError(err);
|
|
109
|
+
},
|
|
110
|
+
getOptions()
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
createEffect(() => {
|
|
115
|
+
if (!isSupported()) return;
|
|
116
|
+
|
|
117
|
+
if (options.immediate ?? true) {
|
|
118
|
+
setLoading(true);
|
|
119
|
+
watchId = navigator.geolocation.watchPosition(
|
|
120
|
+
updatePosition,
|
|
121
|
+
handleError,
|
|
122
|
+
getOptions()
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
onCleanup(() => {
|
|
127
|
+
if (watchId !== null && typeof window !== "undefined" && "geolocation" in navigator) {
|
|
128
|
+
navigator.geolocation.clearWatch(watchId);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
coords,
|
|
135
|
+
loading,
|
|
136
|
+
error,
|
|
137
|
+
isSupported,
|
|
138
|
+
getCurrentPosition,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createSignal, createEffect, onCleanup, type Accessor } from "solid-js";
|
|
2
|
+
|
|
3
|
+
export interface CreateInfiniteScrollOptions {
|
|
4
|
+
/** Target element to observe or trigger scroll on. Defaults to document / scroll parent. */
|
|
5
|
+
target?: HTMLElement | Accessor<HTMLElement | undefined>;
|
|
6
|
+
/** Distance threshold from bottom in pixels to trigger fetch. Defaults to 100. */
|
|
7
|
+
threshold?: number;
|
|
8
|
+
/** Whether loading is currently enabled or auto-fetching is active. Defaults to true. */
|
|
9
|
+
enabled?: boolean | Accessor<boolean>;
|
|
10
|
+
/** Callback function when scrolled near bottom to fetch next items. */
|
|
11
|
+
onLoadMore: () => Promise<void> | void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CreateInfiniteScrollReturn {
|
|
15
|
+
/** Sentinel ref function to bind to a DOM element at the bottom of the list. */
|
|
16
|
+
setSentinelRef: (el: HTMLElement | null) => void;
|
|
17
|
+
/** Signal indicating whether fetching is currently in progress. */
|
|
18
|
+
isLoading: Accessor<boolean>;
|
|
19
|
+
/** Signal indicating whether an error occurred during last fetch. */
|
|
20
|
+
error: Accessor<Error | null>;
|
|
21
|
+
/** Imperative function to manually trigger next page load. */
|
|
22
|
+
loadMore: () => Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* SolidJS reactive primitive for dynamic infinite scrolling / auto-fetching.
|
|
27
|
+
*/
|
|
28
|
+
export function createInfiniteScroll(
|
|
29
|
+
options: CreateInfiniteScrollOptions
|
|
30
|
+
): CreateInfiniteScrollReturn {
|
|
31
|
+
const [sentinelEl, setSentinelEl] = createSignal<HTMLElement | null>(null);
|
|
32
|
+
const [isLoading, setIsLoading] = createSignal(false);
|
|
33
|
+
const [error, setError] = createSignal<Error | null>(null);
|
|
34
|
+
|
|
35
|
+
const isEnabled = (): boolean => {
|
|
36
|
+
if (typeof options.enabled === "function") {
|
|
37
|
+
return options.enabled();
|
|
38
|
+
}
|
|
39
|
+
return options.enabled ?? true;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const loadMore = async (): Promise<void> => {
|
|
43
|
+
if (isLoading() || !isEnabled()) return;
|
|
44
|
+
setIsLoading(true);
|
|
45
|
+
setError(null);
|
|
46
|
+
try {
|
|
47
|
+
await options.onLoadMore();
|
|
48
|
+
} catch (err) {
|
|
49
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
50
|
+
} finally {
|
|
51
|
+
setIsLoading(false);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
createEffect(() => {
|
|
56
|
+
if (typeof window === "undefined" || !window.IntersectionObserver) return;
|
|
57
|
+
if (!isEnabled()) return;
|
|
58
|
+
|
|
59
|
+
const el = sentinelEl();
|
|
60
|
+
if (!el) return;
|
|
61
|
+
|
|
62
|
+
const rootMargin = `${options.threshold ?? 100}px`;
|
|
63
|
+
|
|
64
|
+
const observer = new IntersectionObserver(
|
|
65
|
+
(entries) => {
|
|
66
|
+
const entry = entries[0];
|
|
67
|
+
if (entry?.isIntersecting && !isLoading()) {
|
|
68
|
+
loadMore();
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
rootMargin,
|
|
73
|
+
threshold: 0,
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
observer.observe(el);
|
|
78
|
+
|
|
79
|
+
onCleanup(() => {
|
|
80
|
+
observer.disconnect();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
setSentinelRef: setSentinelEl,
|
|
86
|
+
isLoading,
|
|
87
|
+
error,
|
|
88
|
+
loadMore,
|
|
89
|
+
};
|
|
90
|
+
}
|