@fivem-nui/svelte-components 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 +101 -2
- package/dist/components/actions/NuiButton.svelte +292 -0
- package/dist/components/actions/NuiButton.svelte.d.ts +60 -0
- package/dist/components/data/NuiBadge.svelte +277 -0
- package/dist/components/data/NuiBadge.svelte.d.ts +74 -0
- package/dist/components/data/NuiCodeBlock.svelte +256 -0
- package/dist/components/data/NuiCodeBlock.svelte.d.ts +49 -0
- package/dist/components/data/NuiList.svelte +341 -0
- package/dist/components/data/NuiList.svelte.d.ts +52 -0
- package/dist/components/data/NuiProgressBar.svelte +264 -0
- package/dist/components/data/NuiProgressBar.svelte.d.ts +51 -0
- package/dist/components/data/NuiRichText.svelte +260 -0
- package/dist/components/data/NuiRichText.svelte.d.ts +33 -0
- package/dist/components/data/NuiTable.svelte +470 -0
- package/dist/components/data/NuiTable.svelte.d.ts +53 -0
- package/dist/components/data/chart/NuiChart.svelte +384 -0
- package/dist/components/data/chart/NuiChart.svelte.d.ts +49 -0
- package/dist/components/data/chart/color.d.ts +9 -0
- package/dist/components/data/chart/color.js +78 -0
- package/dist/components/data/chart/index.d.ts +10 -0
- package/dist/components/data/chart/index.js +10 -0
- package/dist/components/data/chart/registry.d.ts +14 -0
- package/dist/components/data/chart/registry.js +53 -0
- package/dist/components/data/chart/renderers/bar.d.ts +8 -0
- package/dist/components/data/chart/renderers/bar.js +79 -0
- package/dist/components/data/chart/renderers/base.d.ts +18 -0
- package/dist/components/data/chart/renderers/base.js +10 -0
- package/dist/components/data/chart/renderers/line.d.ts +8 -0
- package/dist/components/data/chart/renderers/line.js +89 -0
- package/dist/components/data/chart/renderers/pie.d.ts +10 -0
- package/dist/components/data/chart/renderers/pie.js +55 -0
- package/dist/components/data/chart/scale.d.ts +22 -0
- package/dist/components/data/chart/scale.js +101 -0
- package/dist/components/data/chart/types.d.ts +84 -0
- package/dist/components/data/chart/types.js +1 -0
- package/dist/components/data/chart/validator.d.ts +10 -0
- package/dist/components/data/chart/validator.js +82 -0
- package/dist/components/forms/NuiCheckbox.svelte +218 -0
- package/dist/components/forms/NuiCheckbox.svelte.d.ts +37 -0
- package/dist/components/forms/NuiDatepicker.svelte +755 -0
- package/dist/components/forms/NuiDatepicker.svelte.d.ts +34 -0
- package/dist/components/forms/NuiFileInput.svelte +1047 -0
- package/dist/components/forms/NuiFileInput.svelte.d.ts +93 -0
- package/dist/components/forms/NuiFileInput.types.d.ts +28 -0
- package/dist/components/forms/NuiFileInput.types.js +1 -0
- package/dist/components/forms/NuiForm.svelte +391 -0
- package/dist/components/forms/NuiForm.svelte.d.ts +69 -0
- package/dist/components/forms/NuiInput.svelte +296 -0
- package/dist/components/forms/NuiInput.svelte.d.ts +62 -0
- package/dist/components/forms/NuiRadioGroup.svelte +238 -0
- package/dist/components/forms/NuiRadioGroup.svelte.d.ts +47 -0
- package/dist/components/forms/NuiSelect.svelte +819 -0
- package/dist/components/forms/NuiSelect.svelte.d.ts +49 -0
- package/dist/components/layout/NuiCard.svelte +314 -0
- package/dist/components/layout/NuiCard.svelte.d.ts +48 -0
- package/dist/components/layout/NuiCardGrid.svelte +98 -0
- package/dist/components/layout/NuiCardGrid.svelte.d.ts +22 -0
- package/dist/components/layout/NuiFlex.svelte +121 -0
- package/dist/components/layout/NuiFlex.svelte.d.ts +23 -0
- package/dist/components/layout/NuiGrid.svelte +66 -0
- package/dist/components/layout/NuiGrid.svelte.d.ts +14 -0
- package/dist/components/layout/NuiStack.svelte +138 -0
- package/dist/components/layout/NuiStack.svelte.d.ts +26 -0
- package/dist/components/navigation/NuiAccordion.svelte +323 -0
- package/dist/components/navigation/NuiAccordion.svelte.d.ts +38 -0
- package/dist/components/navigation/NuiBreadcrumbs.svelte +133 -0
- package/dist/components/navigation/NuiBreadcrumbs.svelte.d.ts +19 -0
- package/dist/components/navigation/NuiMenu.svelte +596 -0
- package/dist/components/navigation/NuiMenu.svelte.d.ts +28 -0
- package/dist/components/navigation/NuiPagination.svelte +306 -0
- package/dist/components/navigation/NuiPagination.svelte.d.ts +40 -0
- package/dist/components/navigation/NuiSidebar.svelte +186 -0
- package/dist/components/navigation/NuiSidebar.svelte.d.ts +26 -0
- package/dist/components/navigation/NuiTabs.svelte +249 -0
- package/dist/components/navigation/NuiTabs.svelte.d.ts +31 -0
- package/dist/components/overlay/NuiDialog.svelte +118 -0
- package/dist/components/overlay/NuiDialog.svelte.d.ts +34 -0
- package/dist/components/overlay/NuiModal.svelte +992 -0
- package/dist/components/overlay/NuiModal.svelte.d.ts +72 -0
- package/dist/components/overlay/NuiTooltip.svelte +159 -0
- package/dist/components/overlay/NuiTooltip.svelte.d.ts +13 -0
- package/dist/composables/useClickOutside.d.ts +43 -0
- package/dist/composables/useClickOutside.js +73 -0
- package/dist/composables/useFormValidation.d.ts +23 -0
- package/dist/composables/useFormValidation.js +72 -0
- package/dist/composables/useIdle.svelte.d.ts +18 -0
- package/dist/composables/useIdle.svelte.js +144 -0
- package/dist/composables/useKeyboardShortcut.svelte.d.ts +114 -0
- package/dist/composables/useKeyboardShortcut.svelte.js +153 -0
- package/dist/composables/useLocalStorage.svelte.d.ts +22 -0
- package/dist/composables/useLocalStorage.svelte.js +105 -0
- package/dist/composables/useMediaQuery.svelte.d.ts +51 -0
- package/dist/composables/useMediaQuery.svelte.js +80 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +42 -0
- package/package.json +58 -3
- package/src/components/actions/NuiButton.svelte +292 -0
- package/src/components/data/NuiBadge.svelte +277 -0
- package/src/components/data/NuiCodeBlock.svelte +256 -0
- package/src/components/data/NuiList.svelte +341 -0
- package/src/components/data/NuiProgressBar.svelte +264 -0
- package/src/components/data/NuiRichText.svelte +260 -0
- package/src/components/data/NuiTable.svelte +470 -0
- package/src/components/data/chart/NuiChart.svelte +384 -0
- package/src/components/data/chart/color.ts +83 -0
- package/src/components/data/chart/index.ts +10 -0
- package/src/components/data/chart/registry.ts +71 -0
- package/src/components/data/chart/renderers/bar.ts +90 -0
- package/src/components/data/chart/renderers/base.ts +32 -0
- package/src/components/data/chart/renderers/line.ts +99 -0
- package/src/components/data/chart/renderers/pie.ts +66 -0
- package/src/components/data/chart/scale.ts +139 -0
- package/src/components/data/chart/types.ts +98 -0
- package/src/components/data/chart/validator.ts +98 -0
- package/src/components/forms/NuiCheckbox.svelte +218 -0
- package/src/components/forms/NuiDatepicker.svelte +755 -0
- package/src/components/forms/NuiFileInput.svelte +1047 -0
- package/src/components/forms/NuiFileInput.types.ts +29 -0
- package/src/components/forms/NuiForm.svelte +391 -0
- package/src/components/forms/NuiInput.svelte +296 -0
- package/src/components/forms/NuiRadioGroup.svelte +238 -0
- package/src/components/forms/NuiSelect.svelte +819 -0
- package/src/components/layout/NuiCard.svelte +314 -0
- package/src/components/layout/NuiCardGrid.svelte +98 -0
- package/src/components/layout/NuiFlex.svelte +121 -0
- package/src/components/layout/NuiGrid.svelte +66 -0
- package/src/components/layout/NuiStack.svelte +138 -0
- package/src/components/navigation/NuiAccordion.svelte +323 -0
- package/src/components/navigation/NuiBreadcrumbs.svelte +133 -0
- package/src/components/navigation/NuiMenu.svelte +596 -0
- package/src/components/navigation/NuiPagination.svelte +306 -0
- package/src/components/navigation/NuiSidebar.svelte +186 -0
- package/src/components/navigation/NuiTabs.svelte +249 -0
- package/src/components/overlay/NuiDialog.svelte +118 -0
- package/src/components/overlay/NuiModal.svelte +992 -0
- package/src/components/overlay/NuiTooltip.svelte +159 -0
- package/src/composables/useClickOutside.ts +132 -0
- package/src/composables/useFormValidation.ts +105 -0
- package/src/composables/useIdle.svelte.ts +191 -0
- package/src/composables/useKeyboardShortcut.svelte.ts +278 -0
- package/src/composables/useLocalStorage.svelte.ts +137 -0
- package/src/composables/useMediaQuery.svelte.ts +102 -0
- package/src/index.ts +66 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export type { FileInfo, FileMetadata } from "./NuiFileInput.types";
|
|
2
|
+
import type { SvelteNuiClient } from "@fivem-nui/svelte";
|
|
3
|
+
import type { FileInfo } from "./NuiFileInput.types";
|
|
4
|
+
interface Props {
|
|
5
|
+
/** Optional HTML id attribute. Auto-generated via $props.id() if omitted. */
|
|
6
|
+
id?: string;
|
|
7
|
+
/** Form input name attribute. */
|
|
8
|
+
name?: string;
|
|
9
|
+
/** Visual label text displayed above the control. */
|
|
10
|
+
label?: string;
|
|
11
|
+
/** Secondary description text displayed below label. */
|
|
12
|
+
description?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Accepted file types, e.g. "image/*", ".pdf", "image/png,image/jpeg".
|
|
15
|
+
* Validates both MIME types and file extensions with fallback for empty MIME types.
|
|
16
|
+
* Evaluated during user interactions (file picker, drag & drop).
|
|
17
|
+
* Defaults to empty string (all file types accepted).
|
|
18
|
+
*/
|
|
19
|
+
accept?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Whether multiple file selection is enabled. Defaults to false.
|
|
22
|
+
* If dynamically switched at runtime, values are automatically normalized.
|
|
23
|
+
*/
|
|
24
|
+
multiple?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Maximum allowed file size in bytes per single file. Must be > 0.
|
|
27
|
+
* Evaluated during user interactions (file picker, drag & drop).
|
|
28
|
+
*/
|
|
29
|
+
maxSize?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Maximum allowed total number of files when multiple=true. Must be > 0.
|
|
32
|
+
* Evaluated during user interactions (file picker, drag & drop) and reactively derived
|
|
33
|
+
* as an external limit error if the parent supplies or configures a limit lower than
|
|
34
|
+
* the current file count (without destructively truncating the parent's external data).
|
|
35
|
+
*/
|
|
36
|
+
maxFiles?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Selected raw DOM File object or array of File objects.
|
|
39
|
+
* Serves as the single canonical source of truth for selected files.
|
|
40
|
+
* When multiple=false: File | null.
|
|
41
|
+
* When multiple=true: File[].
|
|
42
|
+
* Supports two-way binding.
|
|
43
|
+
*/
|
|
44
|
+
value?: File | File[] | null;
|
|
45
|
+
/**
|
|
46
|
+
* Read-only metadata array of all currently selected files.
|
|
47
|
+
* Represents the active UI metadata, including temporary ObjectURLs when showPreview=true.
|
|
48
|
+
* Supports two-way binding.
|
|
49
|
+
*/
|
|
50
|
+
files?: FileInfo[];
|
|
51
|
+
/** Whether the file input is disabled. */
|
|
52
|
+
disabled?: boolean;
|
|
53
|
+
/** Placeholder prompt text displayed in the dropzone when no files are chosen. */
|
|
54
|
+
placeholder?: string;
|
|
55
|
+
/** Whether to render preview list and generate image ObjectURLs. Defaults to true. */
|
|
56
|
+
showPreview?: boolean;
|
|
57
|
+
/** Accessible label for screen readers when visual label is omitted. */
|
|
58
|
+
ariaLabel?: string;
|
|
59
|
+
/** External validation error message displayed below the input. */
|
|
60
|
+
error?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Optional NUI event name sent on file addition/removal.
|
|
63
|
+
* Transmits clean FileMetadata ({ fileInfos: FileMetadata[], count }).
|
|
64
|
+
*/
|
|
65
|
+
eventName?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Callback fired when the user adds or removes files through UI interactions
|
|
68
|
+
* (native file picker dialog, drag-and-drop, or remove button).
|
|
69
|
+
* Does not fire on external programmatic changes to the `value` prop to prevent circular update loops.
|
|
70
|
+
*/
|
|
71
|
+
onChange?: (files: FileInfo[], rawFiles: File | File[] | null) => void;
|
|
72
|
+
/** Callback fired when validation fails (e.g. exceeds maxSize or maxFiles). */
|
|
73
|
+
onError?: (errorMessage: string) => void;
|
|
74
|
+
/** SvelteNuiClient instance for event transport. */
|
|
75
|
+
client?: SvelteNuiClient;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* NuiFileInput
|
|
79
|
+
* Accessible file input component conforming to W3C standards with drag-and-drop,
|
|
80
|
+
* per-file size constraints, total file count limits, reactive preview generation,
|
|
81
|
+
* memory leak-free ObjectURL management, and FiveM NUI event synchronization.
|
|
82
|
+
*
|
|
83
|
+
* Important note for FiveM NUI:
|
|
84
|
+
* Due to CEF browser sandbox and JSON serialization boundaries, the NUI event transport
|
|
85
|
+
* sends clean file metadata (FileMetadata: id, name, size, type), NOT temporary browser
|
|
86
|
+
* ObjectURLs ('blob:...') nor binary file contents. If the C# FiveM resource requires raw
|
|
87
|
+
* file content, serialize it via FileReader (ArrayBuffer/base64) before transmission.
|
|
88
|
+
* Client-side 'accept' and size validation serve as immediate UX guards; server/resource
|
|
89
|
+
* handlers should always independently re-validate transmitted file data.
|
|
90
|
+
*/
|
|
91
|
+
declare const NuiFileInput: import("svelte").Component<Props, {}, "value" | "files">;
|
|
92
|
+
type NuiFileInput = ReturnType<typeof NuiFileInput>;
|
|
93
|
+
export default NuiFileInput;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI representation of a selected file, including optional browser preview URL.
|
|
3
|
+
*/
|
|
4
|
+
export interface FileInfo {
|
|
5
|
+
/** Unique stable identifier for the file entry. */
|
|
6
|
+
id: string;
|
|
7
|
+
/** File name including extension. */
|
|
8
|
+
name: string;
|
|
9
|
+
/** File size in bytes. */
|
|
10
|
+
size: number;
|
|
11
|
+
/** MIME type of the file. */
|
|
12
|
+
type: string;
|
|
13
|
+
/** Browser-only temporary ObjectURL for image preview (if preview enabled). */
|
|
14
|
+
url?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Serialized metadata payload transmitted over FiveM NUI (omits ephemeral browser URLs).
|
|
18
|
+
*/
|
|
19
|
+
export interface FileMetadata {
|
|
20
|
+
/** Unique stable identifier for the file entry. */
|
|
21
|
+
id: string;
|
|
22
|
+
/** File name including extension. */
|
|
23
|
+
name: string;
|
|
24
|
+
/** File size in bytes. */
|
|
25
|
+
size: number;
|
|
26
|
+
/** MIME type of the file. */
|
|
27
|
+
type: string;
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component NuiForm
|
|
3
|
+
Accessible form container for FiveM NUI applications with client-side validation rules,
|
|
4
|
+
asynchronous submission locking, CEF-safe confirmation dialog support, resilient error
|
|
5
|
+
handling, and dual event synchronization.
|
|
6
|
+
-->
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import { onDestroy } from "svelte";
|
|
9
|
+
import { nui, isBrowser } from "@fivem-nui/svelte";
|
|
10
|
+
import type { SvelteNuiClient } from "@fivem-nui/svelte";
|
|
11
|
+
// Universal form validation engine
|
|
12
|
+
import { validateForm } from "../../composables/useFormValidation";
|
|
13
|
+
import type { ValidationRules } from "../../composables/useFormValidation";
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
/** Main event sent on submit with {@link payload} (or {@link values}). */
|
|
17
|
+
eventName?: string;
|
|
18
|
+
/** Secondary or pre-submit event sent just before {@link eventName} with {@link onSubmitPayload} (or {@link values}). */
|
|
19
|
+
onSubmit?: string;
|
|
20
|
+
/** Event sent when form submission succeeds. */
|
|
21
|
+
onSubmitSuccess?: string;
|
|
22
|
+
/** Event sent when form submission fails. */
|
|
23
|
+
onSubmitError?: string;
|
|
24
|
+
/** Specific payload sent with {@link onSubmit} instead of values. Defaults to {@link values}. */
|
|
25
|
+
onSubmitPayload?: unknown;
|
|
26
|
+
/** Specific payload sent with {@link eventName} instead of values. Defaults to {@link values}. */
|
|
27
|
+
payload?: unknown;
|
|
28
|
+
/** SvelteNuiClient instance for event transport. */
|
|
29
|
+
client?: SvelteNuiClient;
|
|
30
|
+
/**
|
|
31
|
+
* Whether network transport is currently in progress. Supports two-way binding.
|
|
32
|
+
*
|
|
33
|
+
* The component manages this flag only during its own transport phase.
|
|
34
|
+
* It will not reset `loading` to `false` unless it was the one that set it to `true`.
|
|
35
|
+
* If a parent binds `loading` for independent operations, those will not be clobbered.
|
|
36
|
+
*/
|
|
37
|
+
loading?: boolean;
|
|
38
|
+
/** Whether the form is disabled. Supports two-way binding. */
|
|
39
|
+
disabled?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Whether to automatically reset validation error display status on success.
|
|
42
|
+
* Note: This only resets the submission visibility flag (`submitted`),
|
|
43
|
+
* it does not reset field `values` or native input DOM values.
|
|
44
|
+
* Defaults to false.
|
|
45
|
+
*/
|
|
46
|
+
autoReset?: boolean;
|
|
47
|
+
/** Whether confirmation is required before submitting. Defaults to false. */
|
|
48
|
+
confirm?: boolean;
|
|
49
|
+
/** Confirmation message prompt text. Defaults to 'Are you sure you want to submit?'. */
|
|
50
|
+
confirmMessage?: string;
|
|
51
|
+
/** Optional async confirmation handler (e.g. opens custom NuiDialog) safe for FiveM CEF. */
|
|
52
|
+
confirmCallback?: (message: string) => boolean | Promise<boolean>;
|
|
53
|
+
/** Field values validated against rules before the submission event is sent. */
|
|
54
|
+
values?: Record<string, unknown>;
|
|
55
|
+
/** Validation rules mapped by field name. */
|
|
56
|
+
rules?: Record<string, ValidationRules>;
|
|
57
|
+
/**
|
|
58
|
+
* Called locally after a successful submit.
|
|
59
|
+
* Receives the primary event payload (`payload ?? values ?? {}`).
|
|
60
|
+
*/
|
|
61
|
+
onSubmitted?: (payload: unknown) => void | Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Called locally when submission is rejected due to validation failure,
|
|
64
|
+
* validation rule exception, CEF confirmation unavailability,
|
|
65
|
+
* confirmation callback exception, or backend transport error.
|
|
66
|
+
* Supports synchronous or asynchronous handlers.
|
|
67
|
+
*/
|
|
68
|
+
onRejected?: (error: unknown) => void | Promise<void>;
|
|
69
|
+
children?: import("svelte").Snippet;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let {
|
|
73
|
+
eventName = "",
|
|
74
|
+
onSubmit = "",
|
|
75
|
+
onSubmitSuccess = "",
|
|
76
|
+
onSubmitError = "",
|
|
77
|
+
onSubmitPayload = undefined,
|
|
78
|
+
payload = undefined,
|
|
79
|
+
client = nui,
|
|
80
|
+
loading = $bindable(false),
|
|
81
|
+
disabled = $bindable(false),
|
|
82
|
+
autoReset = false,
|
|
83
|
+
confirm = false,
|
|
84
|
+
confirmMessage = "Are you sure you want to submit?",
|
|
85
|
+
confirmCallback,
|
|
86
|
+
values = {},
|
|
87
|
+
rules = {},
|
|
88
|
+
onSubmitted,
|
|
89
|
+
onRejected,
|
|
90
|
+
children,
|
|
91
|
+
}: Props = $props();
|
|
92
|
+
|
|
93
|
+
let submitted = $state(false);
|
|
94
|
+
let isSubmitting = false;
|
|
95
|
+
let isDestroyed = false;
|
|
96
|
+
|
|
97
|
+
onDestroy(() => {
|
|
98
|
+
isDestroyed = true;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Tracks whether the component itself activated `loading`.
|
|
103
|
+
* Prevents `finally` from clobbering a parent-controlled `loading` binding.
|
|
104
|
+
*/
|
|
105
|
+
let ownLoading = false;
|
|
106
|
+
|
|
107
|
+
// Real-time validation errors derived from current values and rules
|
|
108
|
+
const errors = $derived(
|
|
109
|
+
Object.keys(rules).length
|
|
110
|
+
? validateForm(values, rules)
|
|
111
|
+
: ({} as Record<string, string>),
|
|
112
|
+
);
|
|
113
|
+
const isValid = $derived(Object.keys(errors).length === 0);
|
|
114
|
+
|
|
115
|
+
// Live active errors displayed to the user once submission has been attempted
|
|
116
|
+
const activeErrors = $derived(
|
|
117
|
+
submitted ? errors : ({} as Record<string, string>),
|
|
118
|
+
);
|
|
119
|
+
const hasErrors = $derived(submitted && Object.keys(errors).length > 0);
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Resilient event dispatcher that safely captures synchronous exceptions
|
|
123
|
+
* and unhandled Promise rejections from custom or asynchronous NUI clients.
|
|
124
|
+
*/
|
|
125
|
+
function safeSend(event: string, data: unknown) {
|
|
126
|
+
if (!event) return;
|
|
127
|
+
try {
|
|
128
|
+
const res = client.send(event, data) as unknown;
|
|
129
|
+
if (res && typeof (res as Promise<unknown>).catch === "function") {
|
|
130
|
+
(res as Promise<unknown>).catch((err: unknown) => {
|
|
131
|
+
console.error(
|
|
132
|
+
`[fivem-nui-components] Async error sending form event '${event}':`,
|
|
133
|
+
err,
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.error(
|
|
139
|
+
`[fivem-nui-components] Failed to send form event '${event}':`,
|
|
140
|
+
err,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Safely invokes the onRejected callback, protecting against unhandled rejections
|
|
147
|
+
* or exceptions thrown by consumer application code.
|
|
148
|
+
*/
|
|
149
|
+
function safeReject(reason: unknown) {
|
|
150
|
+
try {
|
|
151
|
+
const res = onRejected?.(reason);
|
|
152
|
+
if (res && typeof (res as Promise<void>).catch === "function") {
|
|
153
|
+
(res as Promise<void>).catch((err: unknown) => {
|
|
154
|
+
console.error(
|
|
155
|
+
"[fivem-nui-components] NuiForm: Unhandled rejection in onRejected callback:",
|
|
156
|
+
err,
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.error(
|
|
162
|
+
"[fivem-nui-components] NuiForm: Exception in onRejected callback:",
|
|
163
|
+
err,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Safely invokes the onSubmitted callback, ensuring consumer application errors
|
|
170
|
+
* are isolated and never misclassified as transport failures.
|
|
171
|
+
*/
|
|
172
|
+
function safeSubmitted(body: unknown) {
|
|
173
|
+
try {
|
|
174
|
+
const res = onSubmitted?.(body);
|
|
175
|
+
if (res && typeof (res as Promise<void>).catch === "function") {
|
|
176
|
+
(res as Promise<void>).catch((err: unknown) => {
|
|
177
|
+
console.error(
|
|
178
|
+
"[fivem-nui-components] NuiForm: Unhandled rejection in onSubmitted callback:",
|
|
179
|
+
err,
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
} catch (callbackError) {
|
|
184
|
+
console.error(
|
|
185
|
+
"[fivem-nui-components] NuiForm: Exception in onSubmitted callback:",
|
|
186
|
+
callbackError,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Resets the submission and error display state of the form (`submitted = false`).
|
|
193
|
+
* Does not mutate or clear field `values`.
|
|
194
|
+
*/
|
|
195
|
+
export function reset() {
|
|
196
|
+
submitted = false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function handleSubmit(event: SubmitEvent) {
|
|
200
|
+
event.preventDefault();
|
|
201
|
+
// 1. Immediate lock against concurrent / double submissions
|
|
202
|
+
if (disabled || loading || isSubmitting) return;
|
|
203
|
+
|
|
204
|
+
submitted = true;
|
|
205
|
+
|
|
206
|
+
// 2. Validate fields — wrapped as an error boundary so a broken custom
|
|
207
|
+
// rule cannot crash handleSubmit with an unhandled exception.
|
|
208
|
+
let validationFailed = false;
|
|
209
|
+
try {
|
|
210
|
+
validationFailed = !isValid;
|
|
211
|
+
} catch (validationError) {
|
|
212
|
+
console.error(
|
|
213
|
+
"[fivem-nui-components] NuiForm: Exception during validation:",
|
|
214
|
+
validationError,
|
|
215
|
+
);
|
|
216
|
+
safeReject(validationError);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (validationFailed) {
|
|
221
|
+
safeReject(errors);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Lock submission lifecycle immediately
|
|
226
|
+
isSubmitting = true;
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
// 3. CEF-safe confirmation handling
|
|
230
|
+
if (confirm) {
|
|
231
|
+
let confirmed = false;
|
|
232
|
+
try {
|
|
233
|
+
if (typeof confirmCallback === "function") {
|
|
234
|
+
confirmed = await confirmCallback(confirmMessage);
|
|
235
|
+
} else if (
|
|
236
|
+
isBrowser() &&
|
|
237
|
+
typeof window !== "undefined" &&
|
|
238
|
+
typeof window.confirm === "function"
|
|
239
|
+
) {
|
|
240
|
+
confirmed = window.confirm(confirmMessage);
|
|
241
|
+
} else {
|
|
242
|
+
const cefError = new Error(
|
|
243
|
+
"window.confirm() is unavailable in FiveM CEF and confirmCallback was not provided.",
|
|
244
|
+
);
|
|
245
|
+
console.warn(
|
|
246
|
+
"[fivem-nui-components] NuiForm:",
|
|
247
|
+
cefError.message,
|
|
248
|
+
"Aborting submission.",
|
|
249
|
+
);
|
|
250
|
+
confirmed = false;
|
|
251
|
+
safeReject(cefError);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
} catch (confirmError) {
|
|
255
|
+
console.error(
|
|
256
|
+
"[fivem-nui-components] NuiForm: Exception in confirmCallback:",
|
|
257
|
+
confirmError,
|
|
258
|
+
);
|
|
259
|
+
confirmed = false;
|
|
260
|
+
safeReject(confirmError);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Re-check state after async confirmation:
|
|
265
|
+
// - component unmounted while dialog was open
|
|
266
|
+
// - user cancelled
|
|
267
|
+
// - parent set disabled = true while dialog was open
|
|
268
|
+
// - parent set loading = true for an independent operation
|
|
269
|
+
if (isDestroyed || !confirmed || disabled || loading) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 4. Mark loading as true only during actual backend transport.
|
|
275
|
+
// Track ownership so finally does not clobber a parent-controlled binding.
|
|
276
|
+
loading = true;
|
|
277
|
+
ownLoading = true;
|
|
278
|
+
|
|
279
|
+
// Decouple onSubmitPayload vs payload, falling back to values.
|
|
280
|
+
const submitBody = onSubmitPayload ?? values ?? {};
|
|
281
|
+
const eventBody = payload ?? values ?? {};
|
|
282
|
+
|
|
283
|
+
// 5. Isolated backend transport execution.
|
|
284
|
+
// Events are sent sequentially, NOT atomically — if onSubmit succeeds
|
|
285
|
+
// but eventName fails, the first operation's side effects persist.
|
|
286
|
+
// For atomic semantics, handle both steps in a single C# callback.
|
|
287
|
+
try {
|
|
288
|
+
if (onSubmit) await client.sendAndWait(onSubmit, submitBody);
|
|
289
|
+
if (isDestroyed) return;
|
|
290
|
+
if (eventName) await client.sendAndWait(eventName, eventBody);
|
|
291
|
+
if (isDestroyed) return;
|
|
292
|
+
} catch (transportError) {
|
|
293
|
+
if (isDestroyed) return;
|
|
294
|
+
const message =
|
|
295
|
+
transportError instanceof Error
|
|
296
|
+
? transportError.message
|
|
297
|
+
: String(transportError);
|
|
298
|
+
if (onSubmitError) safeSend(onSubmitError, { error: message });
|
|
299
|
+
safeReject(transportError);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 6. Transport succeeded: emit success event safely if still mounted
|
|
304
|
+
if (!isDestroyed && onSubmitSuccess) safeSend(onSubmitSuccess, { success: true });
|
|
305
|
+
|
|
306
|
+
if (!isDestroyed && autoReset) {
|
|
307
|
+
submitted = false;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 7. Invoke local onSubmitted callback with eventBody (main event payload)
|
|
311
|
+
if (!isDestroyed) safeSubmitted(eventBody);
|
|
312
|
+
} finally {
|
|
313
|
+
if (!isDestroyed) {
|
|
314
|
+
if (ownLoading) {
|
|
315
|
+
loading = false;
|
|
316
|
+
ownLoading = false;
|
|
317
|
+
}
|
|
318
|
+
isSubmitting = false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
</script>
|
|
323
|
+
|
|
324
|
+
<form
|
|
325
|
+
class="nui-form"
|
|
326
|
+
class:is-invalid={hasErrors}
|
|
327
|
+
class:is-disabled={disabled}
|
|
328
|
+
class:is-loading={loading}
|
|
329
|
+
onsubmit={handleSubmit}
|
|
330
|
+
>
|
|
331
|
+
<div class="nui-form-content">
|
|
332
|
+
{@render children?.()}
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
{#if hasErrors}
|
|
336
|
+
<ul class="nui-form-errors" role="alert" aria-live="polite">
|
|
337
|
+
{#each Object.entries(activeErrors) as [field, message] (field)}
|
|
338
|
+
<li class="nui-form-error-item" data-field={field}>
|
|
339
|
+
<span class="nui-form-error-field">{field}:</span>
|
|
340
|
+
{message}
|
|
341
|
+
</li>
|
|
342
|
+
{/each}
|
|
343
|
+
</ul>
|
|
344
|
+
{/if}
|
|
345
|
+
</form>
|
|
346
|
+
|
|
347
|
+
<style>
|
|
348
|
+
.nui-form {
|
|
349
|
+
display: flex;
|
|
350
|
+
flex-direction: column;
|
|
351
|
+
gap: 0.75rem;
|
|
352
|
+
width: 100%;
|
|
353
|
+
font-family: inherit;
|
|
354
|
+
box-sizing: border-box;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.nui-form-content {
|
|
358
|
+
display: flex;
|
|
359
|
+
flex-direction: column;
|
|
360
|
+
gap: 0.75rem;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.nui-form.is-disabled {
|
|
364
|
+
opacity: 0.5;
|
|
365
|
+
pointer-events: none;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.nui-form.is-loading {
|
|
369
|
+
cursor: wait;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.nui-form-errors {
|
|
373
|
+
margin: 0;
|
|
374
|
+
padding-left: 1.25rem;
|
|
375
|
+
color: var(--nui-danger, #ef4444);
|
|
376
|
+
font-size: 0.8125rem;
|
|
377
|
+
line-height: 1.4;
|
|
378
|
+
display: flex;
|
|
379
|
+
flex-direction: column;
|
|
380
|
+
gap: 0.25rem;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.nui-form-error-item {
|
|
384
|
+
word-break: break-word;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.nui-form-error-field {
|
|
388
|
+
font-weight: 600;
|
|
389
|
+
text-transform: capitalize;
|
|
390
|
+
}
|
|
391
|
+
</style>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { SvelteNuiClient } from "@fivem-nui/svelte";
|
|
2
|
+
import type { ValidationRules } from "../../composables/useFormValidation";
|
|
3
|
+
interface Props {
|
|
4
|
+
/** Main event sent on submit with {@link payload} (or {@link values}). */
|
|
5
|
+
eventName?: string;
|
|
6
|
+
/** Secondary or pre-submit event sent just before {@link eventName} with {@link onSubmitPayload} (or {@link values}). */
|
|
7
|
+
onSubmit?: string;
|
|
8
|
+
/** Event sent when form submission succeeds. */
|
|
9
|
+
onSubmitSuccess?: string;
|
|
10
|
+
/** Event sent when form submission fails. */
|
|
11
|
+
onSubmitError?: string;
|
|
12
|
+
/** Specific payload sent with {@link onSubmit} instead of values. Defaults to {@link values}. */
|
|
13
|
+
onSubmitPayload?: unknown;
|
|
14
|
+
/** Specific payload sent with {@link eventName} instead of values. Defaults to {@link values}. */
|
|
15
|
+
payload?: unknown;
|
|
16
|
+
/** SvelteNuiClient instance for event transport. */
|
|
17
|
+
client?: SvelteNuiClient;
|
|
18
|
+
/**
|
|
19
|
+
* Whether network transport is currently in progress. Supports two-way binding.
|
|
20
|
+
*
|
|
21
|
+
* The component manages this flag only during its own transport phase.
|
|
22
|
+
* It will not reset `loading` to `false` unless it was the one that set it to `true`.
|
|
23
|
+
* If a parent binds `loading` for independent operations, those will not be clobbered.
|
|
24
|
+
*/
|
|
25
|
+
loading?: boolean;
|
|
26
|
+
/** Whether the form is disabled. Supports two-way binding. */
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Whether to automatically reset validation error display status on success.
|
|
30
|
+
* Note: This only resets the submission visibility flag (`submitted`),
|
|
31
|
+
* it does not reset field `values` or native input DOM values.
|
|
32
|
+
* Defaults to false.
|
|
33
|
+
*/
|
|
34
|
+
autoReset?: boolean;
|
|
35
|
+
/** Whether confirmation is required before submitting. Defaults to false. */
|
|
36
|
+
confirm?: boolean;
|
|
37
|
+
/** Confirmation message prompt text. Defaults to 'Are you sure you want to submit?'. */
|
|
38
|
+
confirmMessage?: string;
|
|
39
|
+
/** Optional async confirmation handler (e.g. opens custom NuiDialog) safe for FiveM CEF. */
|
|
40
|
+
confirmCallback?: (message: string) => boolean | Promise<boolean>;
|
|
41
|
+
/** Field values validated against rules before the submission event is sent. */
|
|
42
|
+
values?: Record<string, unknown>;
|
|
43
|
+
/** Validation rules mapped by field name. */
|
|
44
|
+
rules?: Record<string, ValidationRules>;
|
|
45
|
+
/**
|
|
46
|
+
* Called locally after a successful submit.
|
|
47
|
+
* Receives the primary event payload (`payload ?? values ?? {}`).
|
|
48
|
+
*/
|
|
49
|
+
onSubmitted?: (payload: unknown) => void | Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Called locally when submission is rejected due to validation failure,
|
|
52
|
+
* validation rule exception, CEF confirmation unavailability,
|
|
53
|
+
* confirmation callback exception, or backend transport error.
|
|
54
|
+
* Supports synchronous or asynchronous handlers.
|
|
55
|
+
*/
|
|
56
|
+
onRejected?: (error: unknown) => void | Promise<void>;
|
|
57
|
+
children?: import("svelte").Snippet;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* NuiForm
|
|
61
|
+
* Accessible form container for FiveM NUI applications with client-side validation rules,
|
|
62
|
+
* asynchronous submission locking, CEF-safe confirmation dialog support, resilient error
|
|
63
|
+
* handling, and dual event synchronization.
|
|
64
|
+
*/
|
|
65
|
+
declare const NuiForm: import("svelte").Component<Props, {
|
|
66
|
+
reset: () => void;
|
|
67
|
+
}, "loading" | "disabled">;
|
|
68
|
+
type NuiForm = ReturnType<typeof NuiForm>;
|
|
69
|
+
export default NuiForm;
|