@fivem-nui/svelte 0.0.0-stage → 1.0.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 +284 -2
- package/dist/actions/clickOutside/clickOutside.d.ts +24 -0
- package/dist/actions/clickOutside/clickOutside.d.ts.map +1 -0
- package/dist/actions/clickOutside/clickOutside.js +62 -0
- package/dist/actions/clickOutside/clickOutside.js.map +1 -0
- package/dist/actions/clickOutside/handler.d.ts +11 -0
- package/dist/actions/clickOutside/handler.d.ts.map +1 -0
- package/dist/actions/clickOutside/handler.js +45 -0
- package/dist/actions/clickOutside/handler.js.map +1 -0
- package/dist/actions/clickOutside/index.d.ts +4 -0
- package/dist/actions/clickOutside/index.d.ts.map +1 -0
- package/dist/actions/clickOutside/index.js +4 -0
- package/dist/actions/clickOutside/index.js.map +1 -0
- package/dist/actions/clickOutside/types.d.ts +36 -0
- package/dist/actions/clickOutside/types.d.ts.map +1 -0
- package/dist/actions/clickOutside/types.js +6 -0
- package/dist/actions/clickOutside/types.js.map +1 -0
- package/dist/actions/clickOutside.d.ts +63 -0
- package/dist/actions/clickOutside.d.ts.map +1 -0
- package/dist/actions/clickOutside.js +106 -0
- package/dist/actions/clickOutside.js.map +1 -0
- package/dist/actions/escape/escape.d.ts +26 -0
- package/dist/actions/escape/escape.d.ts.map +1 -0
- package/dist/actions/escape/escape.js +74 -0
- package/dist/actions/escape/escape.js.map +1 -0
- package/dist/actions/escape/handler.d.ts +11 -0
- package/dist/actions/escape/handler.d.ts.map +1 -0
- package/dist/actions/escape/handler.js +50 -0
- package/dist/actions/escape/handler.js.map +1 -0
- package/dist/actions/escape/index.d.ts +4 -0
- package/dist/actions/escape/index.d.ts.map +1 -0
- package/dist/actions/escape/index.js +4 -0
- package/dist/actions/escape/index.js.map +1 -0
- package/dist/actions/escape/types.d.ts +70 -0
- package/dist/actions/escape/types.d.ts.map +1 -0
- package/dist/actions/escape/types.js +12 -0
- package/dist/actions/escape/types.js.map +1 -0
- package/dist/actions/escape.d.ts +99 -0
- package/dist/actions/escape.d.ts.map +1 -0
- package/dist/actions/escape.js +128 -0
- package/dist/actions/escape.js.map +1 -0
- package/dist/actions/focusTrap/focusTrap.d.ts +19 -0
- package/dist/actions/focusTrap/focusTrap.d.ts.map +1 -0
- package/dist/actions/focusTrap/focusTrap.js +198 -0
- package/dist/actions/focusTrap/focusTrap.js.map +1 -0
- package/dist/actions/focusTrap/focusables.d.ts +39 -0
- package/dist/actions/focusTrap/focusables.d.ts.map +1 -0
- package/dist/actions/focusTrap/focusables.js +142 -0
- package/dist/actions/focusTrap/focusables.js.map +1 -0
- package/dist/actions/focusTrap/index.d.ts +5 -0
- package/dist/actions/focusTrap/index.d.ts.map +1 -0
- package/dist/actions/focusTrap/index.js +5 -0
- package/dist/actions/focusTrap/index.js.map +1 -0
- package/dist/actions/focusTrap/trapStack.d.ts +33 -0
- package/dist/actions/focusTrap/trapStack.d.ts.map +1 -0
- package/dist/actions/focusTrap/trapStack.js +84 -0
- package/dist/actions/focusTrap/trapStack.js.map +1 -0
- package/dist/actions/focusTrap/types.d.ts +50 -0
- package/dist/actions/focusTrap/types.d.ts.map +1 -0
- package/dist/actions/focusTrap/types.js +10 -0
- package/dist/actions/focusTrap/types.js.map +1 -0
- package/dist/actions/focusTrap.d.ts +24 -0
- package/dist/actions/focusTrap.d.ts.map +1 -0
- package/dist/actions/focusTrap.js +201 -0
- package/dist/actions/focusTrap.js.map +1 -0
- package/dist/actions/focusTrap.types.d.ts +50 -0
- package/dist/actions/focusTrap.types.d.ts.map +1 -0
- package/dist/actions/focusTrap.types.js +10 -0
- package/dist/actions/focusTrap.types.js.map +1 -0
- package/dist/actions/focusables.d.ts +39 -0
- package/dist/actions/focusables.d.ts.map +1 -0
- package/dist/actions/focusables.js +142 -0
- package/dist/actions/focusables.js.map +1 -0
- package/dist/actions/index.d.ts +5 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +5 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/actions/layer/index.d.ts +4 -0
- package/dist/actions/layer/index.d.ts.map +1 -0
- package/dist/actions/layer/index.js +4 -0
- package/dist/actions/layer/index.js.map +1 -0
- package/dist/actions/layer/layer.d.ts +28 -0
- package/dist/actions/layer/layer.d.ts.map +1 -0
- package/dist/actions/layer/layer.js +118 -0
- package/dist/actions/layer/layer.js.map +1 -0
- package/dist/actions/layer/layerStack.d.ts +10 -0
- package/dist/actions/layer/layerStack.d.ts.map +1 -0
- package/dist/actions/layer/layerStack.js +153 -0
- package/dist/actions/layer/layerStack.js.map +1 -0
- package/dist/actions/layer/types.d.ts +105 -0
- package/dist/actions/layer/types.d.ts.map +1 -0
- package/dist/actions/layer/types.js +14 -0
- package/dist/actions/layer/types.js.map +1 -0
- package/dist/actions/layer.d.ts +133 -0
- package/dist/actions/layer.d.ts.map +1 -0
- package/dist/actions/layer.js +269 -0
- package/dist/actions/layer.js.map +1 -0
- package/dist/actions/trapStack.d.ts +33 -0
- package/dist/actions/trapStack.d.ts.map +1 -0
- package/dist/actions/trapStack.js +84 -0
- package/dist/actions/trapStack.js.map +1 -0
- package/dist/client.d.ts +179 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +176 -0
- package/dist/client.js.map +1 -0
- package/dist/context/index.d.ts +68 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +112 -0
- package/dist/context/index.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/mock/index.d.ts +27 -0
- package/dist/mock/index.d.ts.map +1 -0
- package/dist/mock/index.js +137 -0
- package/dist/mock/index.js.map +1 -0
- package/dist/mock/interceptor.d.ts +19 -0
- package/dist/mock/interceptor.d.ts.map +1 -0
- package/dist/mock/interceptor.js +128 -0
- package/dist/mock/interceptor.js.map +1 -0
- package/dist/mock/types.d.ts +100 -0
- package/dist/mock/types.d.ts.map +1 -0
- package/dist/mock/types.js +27 -0
- package/dist/mock/types.js.map +1 -0
- package/dist/mock/url.d.ts +14 -0
- package/dist/mock/url.d.ts.map +1 -0
- package/dist/mock/url.js +38 -0
- package/dist/mock/url.js.map +1 -0
- package/dist/runes/clickOutside.svelte.d.ts +29 -0
- package/dist/runes/clickOutside.svelte.d.ts.map +1 -0
- package/dist/runes/clickOutside.svelte.js +42 -0
- package/dist/runes/clickOutside.svelte.js.map +1 -0
- package/dist/runes/escape.svelte.d.ts +34 -0
- package/dist/runes/escape.svelte.d.ts.map +1 -0
- package/dist/runes/escape.svelte.js +44 -0
- package/dist/runes/escape.svelte.js.map +1 -0
- package/dist/runes/focusTrap.svelte.d.ts +21 -0
- package/dist/runes/focusTrap.svelte.d.ts.map +1 -0
- package/dist/runes/focusTrap.svelte.js +37 -0
- package/dist/runes/focusTrap.svelte.js.map +1 -0
- package/dist/runes/index.svelte.d.ts +8 -0
- package/dist/runes/index.svelte.d.ts.map +1 -0
- package/dist/runes/index.svelte.js +8 -0
- package/dist/runes/index.svelte.js.map +1 -0
- package/dist/runes/layer.svelte.d.ts +36 -0
- package/dist/runes/layer.svelte.d.ts.map +1 -0
- package/dist/runes/layer.svelte.js +48 -0
- package/dist/runes/layer.svelte.js.map +1 -0
- package/dist/runes/query.svelte.d.ts +104 -0
- package/dist/runes/query.svelte.d.ts.map +1 -0
- package/dist/runes/query.svelte.js +179 -0
- package/dist/runes/query.svelte.js.map +1 -0
- package/dist/runes/state.svelte.d.ts +72 -0
- package/dist/runes/state.svelte.d.ts.map +1 -0
- package/dist/runes/state.svelte.js +98 -0
- package/dist/runes/state.svelte.js.map +1 -0
- package/dist/runes/window.svelte.d.ts +120 -0
- package/dist/runes/window.svelte.d.ts.map +1 -0
- package/dist/runes/window.svelte.js +196 -0
- package/dist/runes/window.svelte.js.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/nui.d.ts +34 -0
- package/dist/types/nui.d.ts.map +1 -0
- package/dist/types/nui.js +17 -0
- package/dist/types/nui.js.map +1 -0
- package/dist/utils/env.d.ts +21 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +36 -0
- package/dist/utils/env.js.map +1 -0
- package/dist/utils/error.d.ts +11 -0
- package/dist/utils/error.d.ts.map +1 -0
- package/dist/utils/error.js +30 -0
- package/dist/utils/error.js.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/keyboard.d.ts +127 -0
- package/dist/utils/keyboard.d.ts.map +1 -0
- package/dist/utils/keyboard.js +152 -0
- package/dist/utils/keyboard.js.map +1 -0
- package/package.json +76 -4
- package/src/actions/clickOutside/clickOutside.ts +78 -0
- package/src/actions/clickOutside/handler.ts +49 -0
- package/src/actions/clickOutside/index.ts +11 -0
- package/src/actions/clickOutside/types.ts +54 -0
- package/src/actions/escape/escape.ts +90 -0
- package/src/actions/escape/handler.ts +52 -0
- package/src/actions/escape/index.ts +11 -0
- package/src/actions/escape/types.ts +88 -0
- package/src/actions/focusTrap/focusTrap.ts +228 -0
- package/src/actions/focusTrap/focusables.ts +155 -0
- package/src/actions/focusTrap/index.ts +22 -0
- package/src/actions/focusTrap/trapStack.ts +109 -0
- package/src/actions/focusTrap/types.ts +64 -0
- package/src/actions/index.ts +35 -0
- package/src/actions/layer/index.ts +13 -0
- package/src/actions/layer/layer.ts +140 -0
- package/src/actions/layer/layerStack.ts +169 -0
- package/src/actions/layer/types.ts +132 -0
- package/src/client.ts +323 -0
- package/src/context/index.ts +127 -0
- package/src/index.ts +53 -0
- package/src/mock/index.ts +176 -0
- package/src/mock/interceptor.ts +177 -0
- package/src/mock/types.ts +125 -0
- package/src/mock/url.ts +37 -0
- package/src/runes/clickOutside.svelte.ts +53 -0
- package/src/runes/escape.svelte.ts +71 -0
- package/src/runes/focusTrap.svelte.ts +56 -0
- package/src/runes/index.svelte.ts +38 -0
- package/src/runes/layer.svelte.ts +74 -0
- package/src/runes/query.svelte.ts +313 -0
- package/src/runes/state.svelte.ts +175 -0
- package/src/runes/window.svelte.ts +308 -0
- package/src/utils/env.ts +36 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/keyboard.ts +244 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { onDestroy } from 'svelte';
|
|
2
|
+
import type { NuiWindowAction } from '@fivem-nui/core';
|
|
3
|
+
import { globalErrorHandler } from '@fivem-nui/error';
|
|
4
|
+
import type { SvelteNuiClient } from '../client';
|
|
5
|
+
import { useNuiClient } from '../context';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Window lifecycle actions dispatched from C# backend to NUI UI.
|
|
9
|
+
*/
|
|
10
|
+
export enum NuiWindowActionType {
|
|
11
|
+
Open = 'open',
|
|
12
|
+
Close = 'close',
|
|
13
|
+
Focus = 'focus',
|
|
14
|
+
Unfocus = 'unfocus'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Window lifecycle notifications dispatched from UI to C# backend.
|
|
19
|
+
*/
|
|
20
|
+
export enum NuiWindowNotification {
|
|
21
|
+
Ready = 'ready',
|
|
22
|
+
Close = 'close'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface NuiWindowOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Explicit client instance override.
|
|
28
|
+
* Defaults to the client provided via context (`useNuiClient()`) or global client.
|
|
29
|
+
*/
|
|
30
|
+
client?: SvelteNuiClient;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Initial visibility without focus or backend notification.
|
|
34
|
+
* @defaultValue false
|
|
35
|
+
*/
|
|
36
|
+
initialVisible?: boolean;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Automatically closes the window when Escape is pressed.
|
|
40
|
+
* @defaultValue true
|
|
41
|
+
*/
|
|
42
|
+
closeOnEscape?: boolean;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Callback invoked when window visibility changes from hidden to visible.
|
|
46
|
+
*/
|
|
47
|
+
onOpen?: () => void;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Callback invoked when window visibility changes from visible to hidden.
|
|
51
|
+
*/
|
|
52
|
+
onClose?: () => void;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Callback invoked when window focus state changes.
|
|
56
|
+
*/
|
|
57
|
+
onFocusChange?: (focused: boolean) => void;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Controls automatic cleanup through Svelte's component lifecycle.
|
|
61
|
+
*
|
|
62
|
+
* - `true`: Strictly requires component initialization lifecycle; throws if called outside.
|
|
63
|
+
* - `false`: Disables automatic lifecycle cleanup; call `destroy()` manually.
|
|
64
|
+
* - `undefined`: Attempts automatic cleanup, falling back to manual cleanup outside components.
|
|
65
|
+
*
|
|
66
|
+
* @defaultValue undefined
|
|
67
|
+
*/
|
|
68
|
+
autoDestroy?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Controller interface representing a reactive NUI window.
|
|
73
|
+
*/
|
|
74
|
+
export interface NuiWindow {
|
|
75
|
+
/** Whether the window is currently visible. */
|
|
76
|
+
readonly visible: boolean;
|
|
77
|
+
|
|
78
|
+
/** Whether the window currently has input focus. */
|
|
79
|
+
readonly focused: boolean;
|
|
80
|
+
|
|
81
|
+
/** Opens the window, sets focused to true, and sends a 'ready' notification to C#. */
|
|
82
|
+
open(): void;
|
|
83
|
+
|
|
84
|
+
/** Closes the window, clears focus, and sends a 'close' notification to C#. */
|
|
85
|
+
close(): void;
|
|
86
|
+
|
|
87
|
+
/** Toggles window visibility between open and closed. */
|
|
88
|
+
toggle(): void;
|
|
89
|
+
|
|
90
|
+
/** Destroys the window controller, unsubscribing from C# window events and key listeners. */
|
|
91
|
+
destroy(): void;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function safeCallback<TArgs extends unknown[]>(
|
|
95
|
+
operation: string,
|
|
96
|
+
callback: ((...args: TArgs) => void) | undefined,
|
|
97
|
+
...args: TArgs
|
|
98
|
+
): void {
|
|
99
|
+
if (!callback) return;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
callback(...args);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
void globalErrorHandler
|
|
105
|
+
.handle(
|
|
106
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
107
|
+
{ args },
|
|
108
|
+
operation
|
|
109
|
+
)
|
|
110
|
+
.catch(() => {});
|
|
111
|
+
console.error(error);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Controller managing reactive visibility, focus, and backend events for a named NUI window.
|
|
117
|
+
*/
|
|
118
|
+
export class NuiWindowController implements NuiWindow {
|
|
119
|
+
visible = $state(false);
|
|
120
|
+
focused = $state(false);
|
|
121
|
+
|
|
122
|
+
private destroyed = false;
|
|
123
|
+
private listeningForEscape = false;
|
|
124
|
+
private readonly unsubscribe: () => void;
|
|
125
|
+
|
|
126
|
+
constructor(
|
|
127
|
+
private readonly windowName: string,
|
|
128
|
+
private readonly options: NuiWindowOptions = {},
|
|
129
|
+
private readonly client: SvelteNuiClient
|
|
130
|
+
) {
|
|
131
|
+
this.visible = options.initialVisible ?? false;
|
|
132
|
+
this.unsubscribe = client.onWindow(this.handleWindowAction);
|
|
133
|
+
|
|
134
|
+
if (this.visible) {
|
|
135
|
+
this.syncEscapeListener(true);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private syncEscapeListener(shouldListen: boolean): void {
|
|
140
|
+
if (typeof window === 'undefined' || this.options.closeOnEscape === false) return;
|
|
141
|
+
if (this.listeningForEscape === shouldListen) return;
|
|
142
|
+
|
|
143
|
+
if (shouldListen) {
|
|
144
|
+
window.addEventListener('keydown', this.handleEscape);
|
|
145
|
+
} else {
|
|
146
|
+
window.removeEventListener('keydown', this.handleEscape);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this.listeningForEscape = shouldListen;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private handleEscape = (event: KeyboardEvent): void => {
|
|
153
|
+
if (event.key !== 'Escape' || !this.visible || this.destroyed) return;
|
|
154
|
+
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
this.close();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
private transition(
|
|
160
|
+
nextVisible?: boolean,
|
|
161
|
+
nextFocused?: boolean
|
|
162
|
+
): { visibility: boolean; focus: boolean } {
|
|
163
|
+
const changes = {
|
|
164
|
+
visibility: nextVisible !== undefined && this.visible !== nextVisible,
|
|
165
|
+
focus: nextFocused !== undefined && this.focused !== nextFocused
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
if (nextVisible !== undefined && changes.visibility) {
|
|
169
|
+
this.visible = nextVisible;
|
|
170
|
+
this.syncEscapeListener(nextVisible);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (nextFocused !== undefined && changes.focus) {
|
|
174
|
+
this.focused = nextFocused;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return changes;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private notifyAndDispatch(
|
|
181
|
+
changes: { visibility: boolean; focus: boolean },
|
|
182
|
+
notification?: NuiWindowNotification
|
|
183
|
+
): void {
|
|
184
|
+
if (notification && (changes.visibility || changes.focus)) {
|
|
185
|
+
try {
|
|
186
|
+
this.client.notify(notification, { window: this.windowName });
|
|
187
|
+
} catch (error) {
|
|
188
|
+
void globalErrorHandler
|
|
189
|
+
.handle(
|
|
190
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
191
|
+
{ window: this.windowName, notification },
|
|
192
|
+
'useNuiWindow:notify'
|
|
193
|
+
)
|
|
194
|
+
.catch(() => {});
|
|
195
|
+
console.error('[NuiWindow] Failed to dispatch notify to backend:', error);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (changes.visibility) {
|
|
200
|
+
safeCallback('useNuiWindow:visibility', this.visible ? this.options.onOpen : this.options.onClose);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (changes.focus) {
|
|
204
|
+
safeCallback('useNuiWindow:focus', this.options.onFocusChange, this.focused);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
open = (): void => {
|
|
209
|
+
if (this.destroyed) return;
|
|
210
|
+
|
|
211
|
+
const changes = this.transition(true, true);
|
|
212
|
+
this.notifyAndDispatch(changes, NuiWindowNotification.Ready);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
close = (): void => {
|
|
216
|
+
if (this.destroyed) return;
|
|
217
|
+
|
|
218
|
+
const changes = this.transition(false, false);
|
|
219
|
+
this.notifyAndDispatch(changes, NuiWindowNotification.Close);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
toggle = (): void => {
|
|
223
|
+
if (this.destroyed) return;
|
|
224
|
+
|
|
225
|
+
if (this.visible) this.close();
|
|
226
|
+
else this.open();
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
private handleWindowAction = (
|
|
230
|
+
action: NuiWindowAction,
|
|
231
|
+
targetWindow: string | undefined
|
|
232
|
+
): void => {
|
|
233
|
+
if (this.destroyed || (targetWindow && targetWindow !== this.windowName)) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
switch (action) {
|
|
238
|
+
case NuiWindowActionType.Open:
|
|
239
|
+
this.notifyAndDispatch(this.transition(true, true));
|
|
240
|
+
break;
|
|
241
|
+
|
|
242
|
+
case NuiWindowActionType.Close:
|
|
243
|
+
this.notifyAndDispatch(this.transition(false, false));
|
|
244
|
+
break;
|
|
245
|
+
|
|
246
|
+
case NuiWindowActionType.Focus:
|
|
247
|
+
if (this.visible) {
|
|
248
|
+
this.notifyAndDispatch(this.transition(undefined, true));
|
|
249
|
+
}
|
|
250
|
+
break;
|
|
251
|
+
|
|
252
|
+
case NuiWindowActionType.Unfocus:
|
|
253
|
+
this.notifyAndDispatch(this.transition(undefined, false));
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
destroy = (): void => {
|
|
259
|
+
if (this.destroyed) return;
|
|
260
|
+
|
|
261
|
+
this.destroyed = true;
|
|
262
|
+
this.syncEscapeListener(false);
|
|
263
|
+
this.unsubscribe();
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Reactive controller for a named NUI window.
|
|
269
|
+
* Synchronizes visibility and focus states with the FiveM C# backend.
|
|
270
|
+
*
|
|
271
|
+
* @param windowName - Window identifier matching backend events.
|
|
272
|
+
* @param options - Optional configuration (callbacks, Escape handling).
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* ```svelte
|
|
276
|
+
* <script>
|
|
277
|
+
* import { useNuiWindow } from '@fivem-nui/svelte';
|
|
278
|
+
*
|
|
279
|
+
* const win = useNuiWindow('inventory', { closeOnEscape: true });
|
|
280
|
+
* </script>
|
|
281
|
+
*
|
|
282
|
+
* {#if win.visible}
|
|
283
|
+
* <div class="panel">
|
|
284
|
+
* <button onclick={() => win.close()}>Close</button>
|
|
285
|
+
* </div>
|
|
286
|
+
* {/if}
|
|
287
|
+
* ```
|
|
288
|
+
*/
|
|
289
|
+
export function useNuiWindow(
|
|
290
|
+
windowName: string,
|
|
291
|
+
options: NuiWindowOptions = {}
|
|
292
|
+
): NuiWindow {
|
|
293
|
+
const client = options.client ?? useNuiClient();
|
|
294
|
+
const controller = new NuiWindowController(windowName, options, client);
|
|
295
|
+
|
|
296
|
+
if (options.autoDestroy !== false) {
|
|
297
|
+
try {
|
|
298
|
+
onDestroy(controller.destroy);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
if (options.autoDestroy === true) {
|
|
301
|
+
controller.destroy();
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return controller;
|
|
308
|
+
}
|
package/src/utils/env.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects whether the current page is running inside FiveM's Chromium Embedded Framework (CEF).
|
|
3
|
+
* In FiveM CEF, `window.GetParentResourceName` is injected as a native global function.
|
|
4
|
+
*
|
|
5
|
+
* @returns `true` if executing within FiveM CEF, `false` otherwise.
|
|
6
|
+
*/
|
|
7
|
+
export function isNui(): boolean {
|
|
8
|
+
return typeof (globalThis as { GetParentResourceName?: unknown }).GetParentResourceName === 'function';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Returns true when running in a standard browser (Vite dev server, standalone preview), outside FiveM.
|
|
13
|
+
*
|
|
14
|
+
* @returns `true` if running in a standard browser outside FiveM, `false` if inside CEF.
|
|
15
|
+
*/
|
|
16
|
+
export function isBrowser(): boolean {
|
|
17
|
+
return !isNui();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Safely resolves the parent resource name if running in FiveM CEF, or returns fallback in browser.
|
|
22
|
+
*
|
|
23
|
+
* @param fallback - Resource name to return if not running in FiveM CEF or if resolution fails.
|
|
24
|
+
* @returns The FiveM resource name or the provided fallback.
|
|
25
|
+
*/
|
|
26
|
+
export function getResourceName(fallback = 'unknown_resource'): string {
|
|
27
|
+
const g = globalThis as { GetParentResourceName?: () => string };
|
|
28
|
+
if (typeof g.GetParentResourceName === 'function') {
|
|
29
|
+
try {
|
|
30
|
+
return g.GetParentResourceName();
|
|
31
|
+
} catch {
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return fallback;
|
|
36
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { onDestroy } from 'svelte';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Keyboard modifier keys.
|
|
5
|
+
*/
|
|
6
|
+
export enum NuiKeyboardModifier {
|
|
7
|
+
Ctrl = 'ctrl',
|
|
8
|
+
Alt = 'alt',
|
|
9
|
+
Shift = 'shift',
|
|
10
|
+
Meta = 'meta'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Standard special key names commonly used in FiveM NUI shortcuts.
|
|
15
|
+
*/
|
|
16
|
+
export enum NuiSpecialKey {
|
|
17
|
+
Escape = 'Escape',
|
|
18
|
+
Enter = 'Enter',
|
|
19
|
+
Tab = 'Tab',
|
|
20
|
+
Space = ' ',
|
|
21
|
+
Backspace = 'Backspace',
|
|
22
|
+
ArrowUp = 'ArrowUp',
|
|
23
|
+
ArrowDown = 'ArrowDown',
|
|
24
|
+
ArrowLeft = 'ArrowLeft',
|
|
25
|
+
ArrowRight = 'ArrowRight'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Configuration options for keyboard event handlers and shortcuts.
|
|
30
|
+
*/
|
|
31
|
+
export interface KeyOptions {
|
|
32
|
+
/**
|
|
33
|
+
* Listen during capture phase.
|
|
34
|
+
* @defaultValue false
|
|
35
|
+
*/
|
|
36
|
+
capture?: boolean;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Automatically call `event.preventDefault()`.
|
|
40
|
+
* @defaultValue false
|
|
41
|
+
*/
|
|
42
|
+
preventDefault?: boolean;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Automatically call `event.stopPropagation()`.
|
|
46
|
+
* @defaultValue false
|
|
47
|
+
*/
|
|
48
|
+
stopPropagation?: boolean;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whether the key shortcut is enabled.
|
|
52
|
+
* Accepts a static boolean or a reactive getter `() => boolean`.
|
|
53
|
+
* @defaultValue true
|
|
54
|
+
*/
|
|
55
|
+
enabled?: boolean | (() => boolean);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* If true, suppresses shortcut execution when the active element is an input,
|
|
59
|
+
* textarea, select, or contenteditable.
|
|
60
|
+
* Crucial in FiveM NUI panels to prevent game hotkeys firing while typing messages/searches.
|
|
61
|
+
* @defaultValue false
|
|
62
|
+
*/
|
|
63
|
+
ignoreInputs?: boolean;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Ignore repeating keydown events triggered while holding a key down.
|
|
67
|
+
* @defaultValue false
|
|
68
|
+
*/
|
|
69
|
+
ignoreRepeat?: boolean;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Require Ctrl key to be pressed (or Cmd on Mac).
|
|
73
|
+
*/
|
|
74
|
+
ctrl?: boolean;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Require Alt key to be pressed.
|
|
78
|
+
*/
|
|
79
|
+
alt?: boolean;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Require Shift key to be pressed.
|
|
83
|
+
*/
|
|
84
|
+
shift?: boolean;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Require Meta (Windows / Cmd) key to be pressed.
|
|
88
|
+
*/
|
|
89
|
+
meta?: boolean;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Component lifecycle registration strategy:
|
|
93
|
+
* - `true`: Strictly requires component initialization lifecycle; registers `onDestroy`.
|
|
94
|
+
* If called outside a component, unsubscribes immediately to prevent memory leaks and throws.
|
|
95
|
+
* - `false`: Disables automatic lifecycle cleanup. The caller must invoke the returned cleanup function manually.
|
|
96
|
+
* - `undefined` (default): Attempts to register `onDestroy` within a component lifecycle;
|
|
97
|
+
* falls back gracefully to manual cleanup if called outside a component.
|
|
98
|
+
* @defaultValue undefined
|
|
99
|
+
*/
|
|
100
|
+
autoDestroy?: boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isTextInputActive(): boolean {
|
|
104
|
+
if (typeof document === 'undefined') return false;
|
|
105
|
+
const el = document.activeElement;
|
|
106
|
+
if (!el) return false;
|
|
107
|
+
|
|
108
|
+
const tag = el.tagName.toLowerCase();
|
|
109
|
+
if (tag === 'input' || tag === 'textarea' || tag === 'select') {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
return el.hasAttribute('contenteditable') && el.getAttribute('contenteditable') !== 'false';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Registers a keyboard shortcut with automatic lifecycle management.
|
|
117
|
+
*
|
|
118
|
+
* Designed for FiveM NUI with support for:
|
|
119
|
+
* - `ignoreInputs`: prevents hotkeys from firing while typing in input fields.
|
|
120
|
+
* - Modifier keys (Ctrl, Alt, Shift, Meta).
|
|
121
|
+
* - Automatic cleanup via `onDestroy` and `AbortController`.
|
|
122
|
+
*
|
|
123
|
+
* @param keys - Key or array of keys (case-insensitive) to listen for.
|
|
124
|
+
* @param handler - Callback function invoked when matching keydown occurs.
|
|
125
|
+
* @param options - Optional shortcut configuration including modifiers and lifecycle strategy.
|
|
126
|
+
* @returns Cleanup function to remove event listeners.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```svelte
|
|
130
|
+
* <script>
|
|
131
|
+
* import { useNuiKey } from '@fivem-nui/svelte';
|
|
132
|
+
* useNuiKey('e', () => interact(), { ignoreInputs: true });
|
|
133
|
+
* </script>
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
export function useNuiKey(
|
|
137
|
+
keys: string | string[],
|
|
138
|
+
handler: (event: KeyboardEvent) => void,
|
|
139
|
+
options: KeyOptions = {}
|
|
140
|
+
): () => void {
|
|
141
|
+
const targetKeys = (Array.isArray(keys) ? keys : [keys]).map((k) => k.toLowerCase());
|
|
142
|
+
const {
|
|
143
|
+
capture = false,
|
|
144
|
+
preventDefault = false,
|
|
145
|
+
stopPropagation = false,
|
|
146
|
+
enabled,
|
|
147
|
+
ignoreInputs = false,
|
|
148
|
+
ignoreRepeat = false,
|
|
149
|
+
ctrl,
|
|
150
|
+
alt,
|
|
151
|
+
shift,
|
|
152
|
+
meta
|
|
153
|
+
} = options;
|
|
154
|
+
|
|
155
|
+
let controller: AbortController | null = null;
|
|
156
|
+
|
|
157
|
+
function isAllowed(): boolean {
|
|
158
|
+
if (enabled === undefined) return true;
|
|
159
|
+
return typeof enabled === 'function' ? enabled() : enabled;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function onKeyDown(event: KeyboardEvent) {
|
|
163
|
+
if (!isAllowed()) return;
|
|
164
|
+
if (ignoreRepeat && event.repeat) return;
|
|
165
|
+
if (ignoreInputs && isTextInputActive()) return;
|
|
166
|
+
|
|
167
|
+
if (ctrl !== undefined && event.ctrlKey !== ctrl) return;
|
|
168
|
+
if (alt !== undefined && event.altKey !== alt) return;
|
|
169
|
+
if (shift !== undefined && event.shiftKey !== shift) return;
|
|
170
|
+
if (meta !== undefined && event.metaKey !== meta) return;
|
|
171
|
+
|
|
172
|
+
if (targetKeys.includes(event.key.toLowerCase())) {
|
|
173
|
+
if (preventDefault) {
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
}
|
|
176
|
+
if (stopPropagation) {
|
|
177
|
+
event.stopPropagation();
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
handler(event);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.error('[NuiKey] Error in key handler callback:', err);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (typeof window !== 'undefined') {
|
|
188
|
+
controller = new AbortController();
|
|
189
|
+
window.addEventListener('keydown', onKeyDown, {
|
|
190
|
+
capture,
|
|
191
|
+
signal: controller.signal
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let isCleanedUp = false;
|
|
196
|
+
function cleanup() {
|
|
197
|
+
if (isCleanedUp) return;
|
|
198
|
+
isCleanedUp = true;
|
|
199
|
+
if (controller) {
|
|
200
|
+
controller.abort();
|
|
201
|
+
controller = null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (options.autoDestroy !== false) {
|
|
206
|
+
try {
|
|
207
|
+
onDestroy(cleanup);
|
|
208
|
+
} catch (lifecycleError) {
|
|
209
|
+
if (options.autoDestroy === true) {
|
|
210
|
+
cleanup();
|
|
211
|
+
throw lifecycleError;
|
|
212
|
+
}
|
|
213
|
+
// Best-effort fallback when autoDestroy is undefined and called outside component
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return cleanup;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Listens for the `Escape` key to close or dismiss NUI panels.
|
|
222
|
+
* In FiveM, ESC is the universal standard for closing NUI windows.
|
|
223
|
+
*
|
|
224
|
+
* @param onEscape - Callback function invoked when the Escape key is pressed.
|
|
225
|
+
* @param options - Optional configuration overriding default key options (`preventDefault` defaults to `true`).
|
|
226
|
+
* @returns Cleanup function to remove the Escape listener.
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```svelte
|
|
230
|
+
* <script>
|
|
231
|
+
* import { useNuiEscape, nui } from '@fivem-nui/svelte';
|
|
232
|
+
* useNuiEscape(() => nui.notify('close'));
|
|
233
|
+
* </script>
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
export function useNuiEscape(
|
|
237
|
+
onEscape: (event: KeyboardEvent) => void,
|
|
238
|
+
options: KeyOptions = {}
|
|
239
|
+
): () => void {
|
|
240
|
+
return useNuiKey(NuiSpecialKey.Escape, onEscape, {
|
|
241
|
+
preventDefault: true,
|
|
242
|
+
...options
|
|
243
|
+
});
|
|
244
|
+
}
|