@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,129 @@
|
|
|
1
|
+
import { createSignal, createEffect, onCleanup, type Accessor } from "solid-js";
|
|
2
|
+
|
|
3
|
+
export type ScreenOrientationType =
|
|
4
|
+
| "portrait-primary"
|
|
5
|
+
| "portrait-secondary"
|
|
6
|
+
| "landscape-primary"
|
|
7
|
+
| "landscape-secondary"
|
|
8
|
+
| "portrait"
|
|
9
|
+
| "landscape"
|
|
10
|
+
| "unknown";
|
|
11
|
+
|
|
12
|
+
export interface CreateOrientationOptions {
|
|
13
|
+
/** Callback fired when screen orientation changes. */
|
|
14
|
+
onChange?: (orientation: ScreenOrientationType, angle: number) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CreateOrientationReturn {
|
|
18
|
+
/** Signal indicating current orientation type. */
|
|
19
|
+
type: Accessor<ScreenOrientationType>;
|
|
20
|
+
/** Signal indicating current orientation angle in degrees (0, 90, 180, 270). */
|
|
21
|
+
angle: Accessor<number>;
|
|
22
|
+
/** Signal indicating if device screen is in portrait mode. */
|
|
23
|
+
isPortrait: Accessor<boolean>;
|
|
24
|
+
/** Signal indicating if device screen is in landscape mode. */
|
|
25
|
+
isLandscape: Accessor<boolean>;
|
|
26
|
+
/** Lock screen orientation if supported by device/browser. */
|
|
27
|
+
lock: (orientation: string) => Promise<void>;
|
|
28
|
+
/** Unlock screen orientation. */
|
|
29
|
+
unlock: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* SolidJS reactive primitive for observing mobile/desktop screen orientation and angle.
|
|
34
|
+
*/
|
|
35
|
+
export function createOrientation(
|
|
36
|
+
options: CreateOrientationOptions = {}
|
|
37
|
+
): CreateOrientationReturn {
|
|
38
|
+
const [type, setType] = createSignal<ScreenOrientationType>("unknown");
|
|
39
|
+
const [angle, setAngle] = createSignal<number>(0);
|
|
40
|
+
|
|
41
|
+
const getOrientationState = (): { type: ScreenOrientationType; angle: number } => {
|
|
42
|
+
if (typeof window === "undefined") {
|
|
43
|
+
return { type: "unknown", angle: 0 };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (window.screen?.orientation) {
|
|
47
|
+
return {
|
|
48
|
+
type: window.screen.orientation.type as ScreenOrientationType,
|
|
49
|
+
angle: window.screen.orientation.angle || 0,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Fallback for older browsers using window.orientation */
|
|
54
|
+
const legacyAngle = (window as unknown as { orientation?: number }).orientation ?? 0;
|
|
55
|
+
const isPortraitMode = Math.abs(legacyAngle) !== 90;
|
|
56
|
+
return {
|
|
57
|
+
type: isPortraitMode ? "portrait" : "landscape",
|
|
58
|
+
angle: Number(legacyAngle),
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
createEffect(() => {
|
|
63
|
+
if (typeof window === "undefined") return;
|
|
64
|
+
|
|
65
|
+
const updateState = (): void => {
|
|
66
|
+
const state = getOrientationState();
|
|
67
|
+
setType(state.type);
|
|
68
|
+
setAngle(state.angle);
|
|
69
|
+
options.onChange?.(state.type, state.angle);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
updateState();
|
|
73
|
+
|
|
74
|
+
if (window.screen?.orientation) {
|
|
75
|
+
window.screen.orientation.addEventListener("change", updateState);
|
|
76
|
+
onCleanup(() => {
|
|
77
|
+
window.screen.orientation.removeEventListener("change", updateState);
|
|
78
|
+
});
|
|
79
|
+
} else {
|
|
80
|
+
window.addEventListener("orientationchange", updateState);
|
|
81
|
+
window.addEventListener("resize", updateState);
|
|
82
|
+
onCleanup(() => {
|
|
83
|
+
window.removeEventListener("orientationchange", updateState);
|
|
84
|
+
window.removeEventListener("resize", updateState);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const isPortrait = (): boolean => {
|
|
90
|
+
const currentType = type();
|
|
91
|
+
return currentType.startsWith("portrait");
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const isLandscape = (): boolean => {
|
|
95
|
+
const currentType = type();
|
|
96
|
+
return currentType.startsWith("landscape");
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const lock = async (orientation: string): Promise<void> => {
|
|
100
|
+
if (typeof window !== "undefined" && window.screen?.orientation) {
|
|
101
|
+
const orientationApi = window.screen.orientation as unknown as {
|
|
102
|
+
lock?: (orient: string) => Promise<void>;
|
|
103
|
+
};
|
|
104
|
+
if (typeof orientationApi.lock === "function") {
|
|
105
|
+
await orientationApi.lock(orientation);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const unlock = (): void => {
|
|
111
|
+
if (typeof window !== "undefined" && window.screen?.orientation) {
|
|
112
|
+
const orientationApi = window.screen.orientation as unknown as {
|
|
113
|
+
unlock?: () => void;
|
|
114
|
+
};
|
|
115
|
+
if (typeof orientationApi.unlock === "function") {
|
|
116
|
+
orientationApi.unlock();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
type,
|
|
123
|
+
angle,
|
|
124
|
+
isPortrait,
|
|
125
|
+
isLandscape,
|
|
126
|
+
lock,
|
|
127
|
+
unlock,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createSignal, createEffect, onCleanup, type Accessor } from "solid-js";
|
|
2
|
+
|
|
3
|
+
export type PermissionNameType =
|
|
4
|
+
| "geolocation"
|
|
5
|
+
| "notifications"
|
|
6
|
+
| "persistent-storage"
|
|
7
|
+
| "push"
|
|
8
|
+
| "screen-wake-lock"
|
|
9
|
+
| "clipboard-read"
|
|
10
|
+
| "clipboard-write"
|
|
11
|
+
| "camera"
|
|
12
|
+
| "microphone"
|
|
13
|
+
| (string & {});
|
|
14
|
+
|
|
15
|
+
export type PermissionStatusState = "granted" | "denied" | "prompt" | "unknown";
|
|
16
|
+
|
|
17
|
+
export interface CreatePermissionOptions {
|
|
18
|
+
/** Permission descriptor or name to query. */
|
|
19
|
+
name: PermissionNameType;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CreatePermissionReturn {
|
|
23
|
+
/** Signal accessor containing current permission status ('granted', 'denied', 'prompt', 'unknown'). */
|
|
24
|
+
state: Accessor<PermissionStatusState>;
|
|
25
|
+
/** Signal accessor indicating whether Permissions API is supported in browser environment. */
|
|
26
|
+
isSupported: Accessor<boolean>;
|
|
27
|
+
/** Imperative function to re-query permission status manually. */
|
|
28
|
+
query: () => Promise<PermissionStatusState>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* SolidJS reactive primitive for querying and observing browser permission status changes.
|
|
33
|
+
*/
|
|
34
|
+
export function createPermission(
|
|
35
|
+
options: PermissionNameType | CreatePermissionOptions
|
|
36
|
+
): CreatePermissionReturn {
|
|
37
|
+
const [state, setState] = createSignal<PermissionStatusState>("unknown");
|
|
38
|
+
|
|
39
|
+
const permissionName = typeof options === "string" ? options : options.name;
|
|
40
|
+
|
|
41
|
+
const isSupported = (): boolean =>
|
|
42
|
+
typeof window !== "undefined" &&
|
|
43
|
+
typeof navigator !== "undefined" &&
|
|
44
|
+
"permissions" in navigator;
|
|
45
|
+
|
|
46
|
+
let permissionStatus: PermissionStatus | null = null;
|
|
47
|
+
|
|
48
|
+
const query = async (): Promise<PermissionStatusState> => {
|
|
49
|
+
if (!isSupported()) {
|
|
50
|
+
setState("unknown");
|
|
51
|
+
return "unknown";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const status = await navigator.permissions.query({
|
|
56
|
+
name: permissionName as PermissionName,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
permissionStatus = status;
|
|
60
|
+
const currentState = status.state as PermissionStatusState;
|
|
61
|
+
setState(currentState);
|
|
62
|
+
|
|
63
|
+
status.onchange = () => {
|
|
64
|
+
setState(status.state as PermissionStatusState);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return currentState;
|
|
68
|
+
} catch {
|
|
69
|
+
setState("unknown");
|
|
70
|
+
return "unknown";
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
createEffect(() => {
|
|
75
|
+
if (!isSupported()) return;
|
|
76
|
+
|
|
77
|
+
query();
|
|
78
|
+
|
|
79
|
+
onCleanup(() => {
|
|
80
|
+
if (permissionStatus) {
|
|
81
|
+
permissionStatus.onchange = null;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
state,
|
|
88
|
+
isSupported,
|
|
89
|
+
query,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createEffect, onCleanup, type Accessor } from "solid-js";
|
|
2
|
+
|
|
3
|
+
export interface CreateScrollIntoViewOptions extends ScrollIntoViewOptions {
|
|
4
|
+
/** Whether the element should scroll into view automatically. Defaults to true. */
|
|
5
|
+
enabled?: boolean | Accessor<boolean>;
|
|
6
|
+
/** Delay in milliseconds before executing scrollIntoView. Defaults to 0. */
|
|
7
|
+
delay?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* SolidJS reactive primitive for scrolling a target element into view smooth or auto behavior.
|
|
12
|
+
*/
|
|
13
|
+
export function createScrollIntoView(
|
|
14
|
+
target: HTMLElement | Accessor<HTMLElement | null | undefined> | null | undefined,
|
|
15
|
+
options: CreateScrollIntoViewOptions = {}
|
|
16
|
+
): void {
|
|
17
|
+
const getTarget = (): HTMLElement | null | undefined => {
|
|
18
|
+
if (typeof target === "function") {
|
|
19
|
+
return target();
|
|
20
|
+
}
|
|
21
|
+
return target;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const isEnabled = (): boolean => {
|
|
25
|
+
if (typeof options.enabled === "function") {
|
|
26
|
+
return options.enabled();
|
|
27
|
+
}
|
|
28
|
+
return options.enabled ?? true;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
createEffect(() => {
|
|
32
|
+
if (!isEnabled()) return;
|
|
33
|
+
|
|
34
|
+
const el = getTarget();
|
|
35
|
+
if (!el || typeof window === "undefined") return;
|
|
36
|
+
|
|
37
|
+
const scrollOptions: ScrollIntoViewOptions = {
|
|
38
|
+
behavior: options.behavior ?? "smooth",
|
|
39
|
+
block: options.block ?? "nearest",
|
|
40
|
+
inline: options.inline ?? "nearest",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
44
|
+
|
|
45
|
+
if (options.delay && options.delay > 0) {
|
|
46
|
+
timer = setTimeout(() => {
|
|
47
|
+
el.scrollIntoView(scrollOptions);
|
|
48
|
+
}, options.delay);
|
|
49
|
+
} else {
|
|
50
|
+
el.scrollIntoView(scrollOptions);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
onCleanup(() => {
|
|
54
|
+
if (timer) clearTimeout(timer);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createSignal, type Accessor } from "solid-js";
|
|
2
|
+
|
|
3
|
+
export interface CreateUndoRedoOptions<T> {
|
|
4
|
+
/** Maximum number of history states to retain. Defaults to 50. */
|
|
5
|
+
maxHistory?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface CreateUndoRedoReturn<T> {
|
|
9
|
+
/** Signal containing the current state value. */
|
|
10
|
+
state: Accessor<T>;
|
|
11
|
+
/** Update state value and append to history. */
|
|
12
|
+
set: (nextState: T | ((prev: T) => T)) => void;
|
|
13
|
+
/** Revert to previous state history entry. */
|
|
14
|
+
undo: () => void;
|
|
15
|
+
/** Advance to next state history entry. */
|
|
16
|
+
redo: () => void;
|
|
17
|
+
/** Signal indicating whether undo is available. */
|
|
18
|
+
canUndo: Accessor<boolean>;
|
|
19
|
+
/** Signal indicating whether redo is available. */
|
|
20
|
+
canRedo: Accessor<boolean>;
|
|
21
|
+
/** History stack of past states. */
|
|
22
|
+
history: Accessor<T[]>;
|
|
23
|
+
/** Reset history to initial state value. */
|
|
24
|
+
reset: (initialState?: T) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* SolidJS reactive primitive for undo/redo state history management.
|
|
29
|
+
*/
|
|
30
|
+
export function createUndoRedo<T>(
|
|
31
|
+
initialValue: T | Accessor<T>,
|
|
32
|
+
options: CreateUndoRedoOptions<T> = {}
|
|
33
|
+
): CreateUndoRedoReturn<T> {
|
|
34
|
+
const getInitial = (): T => {
|
|
35
|
+
return typeof initialValue === "function"
|
|
36
|
+
? (initialValue as Accessor<T>)()
|
|
37
|
+
: initialValue;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const maxHistory = options.maxHistory ?? 50;
|
|
41
|
+
|
|
42
|
+
const [history, setHistory] = createSignal<T[]>([getInitial()]);
|
|
43
|
+
const [pointer, setPointer] = createSignal<number>(0);
|
|
44
|
+
|
|
45
|
+
const state = (): T => history()[pointer()] ?? getInitial();
|
|
46
|
+
|
|
47
|
+
const canUndo = (): boolean => pointer() > 0;
|
|
48
|
+
const canRedo = (): boolean => pointer() < history().length - 1;
|
|
49
|
+
|
|
50
|
+
const set = (nextState: T | ((prev: T) => T)): void => {
|
|
51
|
+
const current = state();
|
|
52
|
+
const resolved =
|
|
53
|
+
typeof nextState === "function"
|
|
54
|
+
? (nextState as (prev: T) => T)(current)
|
|
55
|
+
: nextState;
|
|
56
|
+
|
|
57
|
+
if (Object.is(resolved, current)) return;
|
|
58
|
+
|
|
59
|
+
/* Slice history up to current pointer and push new state */
|
|
60
|
+
const sliced = history().slice(0, pointer() + 1);
|
|
61
|
+
const updated = [...sliced, resolved];
|
|
62
|
+
|
|
63
|
+
/* Trim to maxHistory limit */
|
|
64
|
+
if (updated.length > maxHistory) {
|
|
65
|
+
updated.shift();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setHistory(updated);
|
|
69
|
+
setPointer(updated.length - 1);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const undo = (): void => {
|
|
73
|
+
if (canUndo()) {
|
|
74
|
+
setPointer((p) => p - 1);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const redo = (): void => {
|
|
79
|
+
if (canRedo()) {
|
|
80
|
+
setPointer((p) => p + 1);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const reset = (newInitial?: T): void => {
|
|
85
|
+
const init = newInitial !== undefined ? newInitial : getInitial();
|
|
86
|
+
setHistory([init]);
|
|
87
|
+
setPointer(0);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
state,
|
|
92
|
+
set,
|
|
93
|
+
undo,
|
|
94
|
+
redo,
|
|
95
|
+
canUndo,
|
|
96
|
+
canRedo,
|
|
97
|
+
history,
|
|
98
|
+
reset,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createSignal, createEffect, type Accessor } from "solid-js";
|
|
2
|
+
|
|
3
|
+
export interface CreateWebNotificationOptions extends NotificationOptions {
|
|
4
|
+
/** Title of the notification. */
|
|
5
|
+
title?: string;
|
|
6
|
+
/** Callback fired when notification is clicked. */
|
|
7
|
+
onClick?: (event: Event) => void;
|
|
8
|
+
/** Callback fired when notification is closed. */
|
|
9
|
+
onClose?: (event: Event) => void;
|
|
10
|
+
/** Callback fired when notification error occurs. */
|
|
11
|
+
onError?: (event: Event) => void;
|
|
12
|
+
/** Callback fired when notification is shown. */
|
|
13
|
+
onShow?: (event: Event) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CreateWebNotificationReturn {
|
|
17
|
+
/** Signal accessor containing current Notification permission state. */
|
|
18
|
+
permission: Accessor<NotificationPermission>;
|
|
19
|
+
/** Signal accessor indicating whether Web Notifications API is supported in browser environment. */
|
|
20
|
+
isSupported: Accessor<boolean>;
|
|
21
|
+
/** Function to show a web notification. */
|
|
22
|
+
show: (overrideTitle?: string, overrideOptions?: NotificationOptions) => Notification | null;
|
|
23
|
+
/** Request notification permission from browser. */
|
|
24
|
+
requestPermission: () => Promise<NotificationPermission>;
|
|
25
|
+
/** Close active notification. */
|
|
26
|
+
close: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* SolidJS reactive primitive for sending browser desktop notifications and managing notification permissions.
|
|
31
|
+
*/
|
|
32
|
+
export function createWebNotification(
|
|
33
|
+
defaultOptions: CreateWebNotificationOptions = {}
|
|
34
|
+
): CreateWebNotificationReturn {
|
|
35
|
+
const [permission, setPermission] = createSignal<NotificationPermission>("default");
|
|
36
|
+
|
|
37
|
+
const isSupported = (): boolean =>
|
|
38
|
+
typeof window !== "undefined" &&
|
|
39
|
+
"Notification" in window;
|
|
40
|
+
|
|
41
|
+
let activeNotification: Notification | null = null;
|
|
42
|
+
|
|
43
|
+
const updatePermission = (): void => {
|
|
44
|
+
if (isSupported()) {
|
|
45
|
+
setPermission(Notification.permission);
|
|
46
|
+
} else {
|
|
47
|
+
setPermission("denied");
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const requestPermission = async (): Promise<NotificationPermission> => {
|
|
52
|
+
if (!isSupported()) return "denied";
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const result = await Notification.requestPermission();
|
|
56
|
+
setPermission(result);
|
|
57
|
+
return result;
|
|
58
|
+
} catch {
|
|
59
|
+
updatePermission();
|
|
60
|
+
return permission();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const close = (): void => {
|
|
65
|
+
if (activeNotification) {
|
|
66
|
+
activeNotification.close();
|
|
67
|
+
activeNotification = null;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const show = (
|
|
72
|
+
overrideTitle?: string,
|
|
73
|
+
overrideOptions?: NotificationOptions
|
|
74
|
+
): Notification | null => {
|
|
75
|
+
if (!isSupported() || permission() !== "granted") return null;
|
|
76
|
+
|
|
77
|
+
const title = overrideTitle ?? defaultOptions.title ?? "Notification";
|
|
78
|
+
const options: NotificationOptions = {
|
|
79
|
+
...defaultOptions,
|
|
80
|
+
...overrideOptions,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
close();
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const notification = new Notification(title, options);
|
|
87
|
+
activeNotification = notification;
|
|
88
|
+
|
|
89
|
+
if (defaultOptions.onClick) notification.onclick = (e) => defaultOptions.onClick?.(e);
|
|
90
|
+
if (defaultOptions.onClose) notification.onclose = (e) => defaultOptions.onClose?.(e);
|
|
91
|
+
if (defaultOptions.onError) notification.onerror = (e) => defaultOptions.onError?.(e);
|
|
92
|
+
if (defaultOptions.onShow) notification.onshow = (e) => defaultOptions.onShow?.(e);
|
|
93
|
+
|
|
94
|
+
return notification;
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
createEffect(() => {
|
|
101
|
+
updatePermission();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
permission,
|
|
106
|
+
isSupported,
|
|
107
|
+
show,
|
|
108
|
+
requestPermission,
|
|
109
|
+
close,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { createSignal, createEffect, onCleanup, type Accessor } from "solid-js";
|
|
2
|
+
|
|
3
|
+
export type WebSocketReadyState = "CONNECTING" | "OPEN" | "CLOSING" | "CLOSED";
|
|
4
|
+
|
|
5
|
+
export interface CreateWebSocketOptions {
|
|
6
|
+
/** Subprotocol or list of subprotocols. */
|
|
7
|
+
protocols?: string | string[];
|
|
8
|
+
/** Whether to automatically reconnect upon disconnect. Defaults to true. */
|
|
9
|
+
autoReconnect?: boolean;
|
|
10
|
+
/** Reconnection delay in milliseconds. Defaults to 3000ms. */
|
|
11
|
+
reconnectInterval?: number;
|
|
12
|
+
/** Maximum reconnection attempts. Defaults to 5. */
|
|
13
|
+
maxReconnectAttempts?: number;
|
|
14
|
+
/** Whether to open connection immediately upon initialization. Defaults to true. */
|
|
15
|
+
immediate?: boolean;
|
|
16
|
+
/** Callback fired when WebSocket connection opens. */
|
|
17
|
+
onConnected?: (ws: WebSocket) => void;
|
|
18
|
+
/** Callback fired when WebSocket connection closes. */
|
|
19
|
+
onDisconnected?: (event: CloseEvent) => void;
|
|
20
|
+
/** Callback fired when WebSocket receives a message. */
|
|
21
|
+
onMessage?: (event: MessageEvent) => void;
|
|
22
|
+
/** Callback fired when WebSocket encounters an error. */
|
|
23
|
+
onError?: (event: Event) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CreateWebSocketReturn<T = unknown> {
|
|
27
|
+
/** Signal accessor containing latest received message data (parsed if JSON). */
|
|
28
|
+
data: Accessor<T | null>;
|
|
29
|
+
/** Signal accessor containing current WebSocket ready state. */
|
|
30
|
+
readyState: Accessor<WebSocketReadyState>;
|
|
31
|
+
/** Signal accessor containing last raw MessageEvent. */
|
|
32
|
+
lastMessage: Accessor<MessageEvent | null>;
|
|
33
|
+
/** Send string data or object payload (auto JSON stringified). */
|
|
34
|
+
send: (data: string | object | ArrayBufferLike | Blob) => boolean;
|
|
35
|
+
/** Open or reconnect WebSocket connection. */
|
|
36
|
+
open: () => void;
|
|
37
|
+
/** Close active WebSocket connection. */
|
|
38
|
+
close: (code?: number, reason?: string) => void;
|
|
39
|
+
/** Signal accessor indicating whether WebSocket is supported in browser environment. */
|
|
40
|
+
isSupported: Accessor<boolean>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* SolidJS reactive primitive for WebSocket client connections, auto-reconnection, and message passing.
|
|
45
|
+
*/
|
|
46
|
+
export function createWebSocket<T = unknown>(
|
|
47
|
+
url: string | Accessor<string>,
|
|
48
|
+
options: CreateWebSocketOptions = {}
|
|
49
|
+
): CreateWebSocketReturn<T> {
|
|
50
|
+
const [data, setData] = createSignal<T | null>(null);
|
|
51
|
+
const [lastMessage, setLastMessage] = createSignal<MessageEvent | null>(null);
|
|
52
|
+
const [readyState, setReadyState] = createSignal<WebSocketReadyState>("CLOSED");
|
|
53
|
+
|
|
54
|
+
const getUrl = (): string => (typeof url === "function" ? url() : url);
|
|
55
|
+
|
|
56
|
+
const isSupported = (): boolean =>
|
|
57
|
+
typeof window !== "undefined" && "WebSocket" in window;
|
|
58
|
+
|
|
59
|
+
let ws: WebSocket | null = null;
|
|
60
|
+
let reconnectCount = 0;
|
|
61
|
+
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
62
|
+
|
|
63
|
+
const mapReadyState = (state: number): WebSocketReadyState => {
|
|
64
|
+
switch (state) {
|
|
65
|
+
case WebSocket.CONNECTING:
|
|
66
|
+
return "CONNECTING";
|
|
67
|
+
case WebSocket.OPEN:
|
|
68
|
+
return "OPEN";
|
|
69
|
+
case WebSocket.CLOSING:
|
|
70
|
+
return "CLOSING";
|
|
71
|
+
case WebSocket.CLOSED:
|
|
72
|
+
default:
|
|
73
|
+
return "CLOSED";
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const close = (code?: number, reason?: string): void => {
|
|
78
|
+
if (reconnectTimer) {
|
|
79
|
+
clearTimeout(reconnectTimer);
|
|
80
|
+
reconnectTimer = null;
|
|
81
|
+
}
|
|
82
|
+
if (ws) {
|
|
83
|
+
const socket = ws;
|
|
84
|
+
ws = null;
|
|
85
|
+
socket.close(code, reason);
|
|
86
|
+
setReadyState("CLOSED");
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const send = (payload: string | object | ArrayBufferLike | Blob): boolean => {
|
|
91
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
if (typeof payload === "object" && !(payload instanceof ArrayBuffer) && !(payload instanceof Blob)) {
|
|
95
|
+
ws.send(JSON.stringify(payload));
|
|
96
|
+
} else {
|
|
97
|
+
ws.send(payload as any);
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const open = (): void => {
|
|
106
|
+
if (!isSupported()) return;
|
|
107
|
+
|
|
108
|
+
if (ws) {
|
|
109
|
+
ws.close();
|
|
110
|
+
ws = null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
setReadyState("CONNECTING");
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const targetUrl = getUrl();
|
|
117
|
+
const socket = new WebSocket(targetUrl, options.protocols);
|
|
118
|
+
ws = socket;
|
|
119
|
+
|
|
120
|
+
socket.onopen = () => {
|
|
121
|
+
reconnectCount = 0;
|
|
122
|
+
setReadyState("OPEN");
|
|
123
|
+
options.onConnected?.(socket);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
socket.onmessage = (event: MessageEvent) => {
|
|
127
|
+
setLastMessage(() => event);
|
|
128
|
+
try {
|
|
129
|
+
const parsed = JSON.parse(event.data);
|
|
130
|
+
setData(() => parsed);
|
|
131
|
+
} catch {
|
|
132
|
+
setData(() => event.data as unknown as T);
|
|
133
|
+
}
|
|
134
|
+
options.onMessage?.(event);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
socket.onerror = (event: Event) => {
|
|
138
|
+
options.onError?.(event);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
socket.onclose = (event: CloseEvent) => {
|
|
142
|
+
setReadyState("CLOSED");
|
|
143
|
+
ws = null;
|
|
144
|
+
options.onDisconnected?.(event);
|
|
145
|
+
|
|
146
|
+
if (
|
|
147
|
+
(options.autoReconnect ?? false) &&
|
|
148
|
+
reconnectCount < (options.maxReconnectAttempts ?? 5)
|
|
149
|
+
) {
|
|
150
|
+
reconnectCount++;
|
|
151
|
+
reconnectTimer = setTimeout(open, options.reconnectInterval ?? 3000);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
} catch {
|
|
155
|
+
setReadyState("CLOSED");
|
|
156
|
+
ws = null;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
createEffect(() => {
|
|
161
|
+
if (options.immediate ?? true) {
|
|
162
|
+
open();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
onCleanup(() => {
|
|
166
|
+
close();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
data,
|
|
172
|
+
readyState,
|
|
173
|
+
lastMessage,
|
|
174
|
+
send,
|
|
175
|
+
open,
|
|
176
|
+
close,
|
|
177
|
+
isSupported,
|
|
178
|
+
};
|
|
179
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -22,4 +22,19 @@ export * from "./create-color-mode";
|
|
|
22
22
|
export * from "./create-form";
|
|
23
23
|
export * from "./create-input-mask";
|
|
24
24
|
export * from "./create-idle";
|
|
25
|
-
export * from "./create-active-element";
|
|
25
|
+
export * from "./create-active-element";
|
|
26
|
+
export * from "./create-infinite-scroll";
|
|
27
|
+
export * from "./create-fullscreen";
|
|
28
|
+
export * from "./create-audio-video";
|
|
29
|
+
export * from "./create-orientation";
|
|
30
|
+
export * from "./create-undo-redo";
|
|
31
|
+
export * from "./create-fetch";
|
|
32
|
+
export * from "./create-geolocation";
|
|
33
|
+
export * from "./create-permission";
|
|
34
|
+
export * from "./create-battery";
|
|
35
|
+
export * from "./create-web-notification";
|
|
36
|
+
export * from "./create-websocket";
|
|
37
|
+
export * from "./create-document-title";
|
|
38
|
+
export * from "./create-favicon";
|
|
39
|
+
export * from "./create-event-source";
|
|
40
|
+
export * from "./create-scroll-into-view";
|