@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.
Files changed (222) hide show
  1. package/README.md +284 -2
  2. package/dist/actions/clickOutside/clickOutside.d.ts +24 -0
  3. package/dist/actions/clickOutside/clickOutside.d.ts.map +1 -0
  4. package/dist/actions/clickOutside/clickOutside.js +62 -0
  5. package/dist/actions/clickOutside/clickOutside.js.map +1 -0
  6. package/dist/actions/clickOutside/handler.d.ts +11 -0
  7. package/dist/actions/clickOutside/handler.d.ts.map +1 -0
  8. package/dist/actions/clickOutside/handler.js +45 -0
  9. package/dist/actions/clickOutside/handler.js.map +1 -0
  10. package/dist/actions/clickOutside/index.d.ts +4 -0
  11. package/dist/actions/clickOutside/index.d.ts.map +1 -0
  12. package/dist/actions/clickOutside/index.js +4 -0
  13. package/dist/actions/clickOutside/index.js.map +1 -0
  14. package/dist/actions/clickOutside/types.d.ts +36 -0
  15. package/dist/actions/clickOutside/types.d.ts.map +1 -0
  16. package/dist/actions/clickOutside/types.js +6 -0
  17. package/dist/actions/clickOutside/types.js.map +1 -0
  18. package/dist/actions/clickOutside.d.ts +63 -0
  19. package/dist/actions/clickOutside.d.ts.map +1 -0
  20. package/dist/actions/clickOutside.js +106 -0
  21. package/dist/actions/clickOutside.js.map +1 -0
  22. package/dist/actions/escape/escape.d.ts +26 -0
  23. package/dist/actions/escape/escape.d.ts.map +1 -0
  24. package/dist/actions/escape/escape.js +74 -0
  25. package/dist/actions/escape/escape.js.map +1 -0
  26. package/dist/actions/escape/handler.d.ts +11 -0
  27. package/dist/actions/escape/handler.d.ts.map +1 -0
  28. package/dist/actions/escape/handler.js +50 -0
  29. package/dist/actions/escape/handler.js.map +1 -0
  30. package/dist/actions/escape/index.d.ts +4 -0
  31. package/dist/actions/escape/index.d.ts.map +1 -0
  32. package/dist/actions/escape/index.js +4 -0
  33. package/dist/actions/escape/index.js.map +1 -0
  34. package/dist/actions/escape/types.d.ts +70 -0
  35. package/dist/actions/escape/types.d.ts.map +1 -0
  36. package/dist/actions/escape/types.js +12 -0
  37. package/dist/actions/escape/types.js.map +1 -0
  38. package/dist/actions/escape.d.ts +99 -0
  39. package/dist/actions/escape.d.ts.map +1 -0
  40. package/dist/actions/escape.js +128 -0
  41. package/dist/actions/escape.js.map +1 -0
  42. package/dist/actions/focusTrap/focusTrap.d.ts +19 -0
  43. package/dist/actions/focusTrap/focusTrap.d.ts.map +1 -0
  44. package/dist/actions/focusTrap/focusTrap.js +198 -0
  45. package/dist/actions/focusTrap/focusTrap.js.map +1 -0
  46. package/dist/actions/focusTrap/focusables.d.ts +39 -0
  47. package/dist/actions/focusTrap/focusables.d.ts.map +1 -0
  48. package/dist/actions/focusTrap/focusables.js +142 -0
  49. package/dist/actions/focusTrap/focusables.js.map +1 -0
  50. package/dist/actions/focusTrap/index.d.ts +5 -0
  51. package/dist/actions/focusTrap/index.d.ts.map +1 -0
  52. package/dist/actions/focusTrap/index.js +5 -0
  53. package/dist/actions/focusTrap/index.js.map +1 -0
  54. package/dist/actions/focusTrap/trapStack.d.ts +33 -0
  55. package/dist/actions/focusTrap/trapStack.d.ts.map +1 -0
  56. package/dist/actions/focusTrap/trapStack.js +84 -0
  57. package/dist/actions/focusTrap/trapStack.js.map +1 -0
  58. package/dist/actions/focusTrap/types.d.ts +50 -0
  59. package/dist/actions/focusTrap/types.d.ts.map +1 -0
  60. package/dist/actions/focusTrap/types.js +10 -0
  61. package/dist/actions/focusTrap/types.js.map +1 -0
  62. package/dist/actions/focusTrap.d.ts +24 -0
  63. package/dist/actions/focusTrap.d.ts.map +1 -0
  64. package/dist/actions/focusTrap.js +201 -0
  65. package/dist/actions/focusTrap.js.map +1 -0
  66. package/dist/actions/focusTrap.types.d.ts +50 -0
  67. package/dist/actions/focusTrap.types.d.ts.map +1 -0
  68. package/dist/actions/focusTrap.types.js +10 -0
  69. package/dist/actions/focusTrap.types.js.map +1 -0
  70. package/dist/actions/focusables.d.ts +39 -0
  71. package/dist/actions/focusables.d.ts.map +1 -0
  72. package/dist/actions/focusables.js +142 -0
  73. package/dist/actions/focusables.js.map +1 -0
  74. package/dist/actions/index.d.ts +5 -0
  75. package/dist/actions/index.d.ts.map +1 -0
  76. package/dist/actions/index.js +5 -0
  77. package/dist/actions/index.js.map +1 -0
  78. package/dist/actions/layer/index.d.ts +4 -0
  79. package/dist/actions/layer/index.d.ts.map +1 -0
  80. package/dist/actions/layer/index.js +4 -0
  81. package/dist/actions/layer/index.js.map +1 -0
  82. package/dist/actions/layer/layer.d.ts +28 -0
  83. package/dist/actions/layer/layer.d.ts.map +1 -0
  84. package/dist/actions/layer/layer.js +118 -0
  85. package/dist/actions/layer/layer.js.map +1 -0
  86. package/dist/actions/layer/layerStack.d.ts +10 -0
  87. package/dist/actions/layer/layerStack.d.ts.map +1 -0
  88. package/dist/actions/layer/layerStack.js +153 -0
  89. package/dist/actions/layer/layerStack.js.map +1 -0
  90. package/dist/actions/layer/types.d.ts +105 -0
  91. package/dist/actions/layer/types.d.ts.map +1 -0
  92. package/dist/actions/layer/types.js +14 -0
  93. package/dist/actions/layer/types.js.map +1 -0
  94. package/dist/actions/layer.d.ts +133 -0
  95. package/dist/actions/layer.d.ts.map +1 -0
  96. package/dist/actions/layer.js +269 -0
  97. package/dist/actions/layer.js.map +1 -0
  98. package/dist/actions/trapStack.d.ts +33 -0
  99. package/dist/actions/trapStack.d.ts.map +1 -0
  100. package/dist/actions/trapStack.js +84 -0
  101. package/dist/actions/trapStack.js.map +1 -0
  102. package/dist/client.d.ts +179 -0
  103. package/dist/client.d.ts.map +1 -0
  104. package/dist/client.js +176 -0
  105. package/dist/client.js.map +1 -0
  106. package/dist/context/index.d.ts +68 -0
  107. package/dist/context/index.d.ts.map +1 -0
  108. package/dist/context/index.js +112 -0
  109. package/dist/context/index.js.map +1 -0
  110. package/dist/index.d.ts +22 -0
  111. package/dist/index.d.ts.map +1 -0
  112. package/dist/index.js +30 -0
  113. package/dist/index.js.map +1 -0
  114. package/dist/mock/index.d.ts +27 -0
  115. package/dist/mock/index.d.ts.map +1 -0
  116. package/dist/mock/index.js +137 -0
  117. package/dist/mock/index.js.map +1 -0
  118. package/dist/mock/interceptor.d.ts +19 -0
  119. package/dist/mock/interceptor.d.ts.map +1 -0
  120. package/dist/mock/interceptor.js +128 -0
  121. package/dist/mock/interceptor.js.map +1 -0
  122. package/dist/mock/types.d.ts +100 -0
  123. package/dist/mock/types.d.ts.map +1 -0
  124. package/dist/mock/types.js +27 -0
  125. package/dist/mock/types.js.map +1 -0
  126. package/dist/mock/url.d.ts +14 -0
  127. package/dist/mock/url.d.ts.map +1 -0
  128. package/dist/mock/url.js +38 -0
  129. package/dist/mock/url.js.map +1 -0
  130. package/dist/runes/clickOutside.svelte.d.ts +29 -0
  131. package/dist/runes/clickOutside.svelte.d.ts.map +1 -0
  132. package/dist/runes/clickOutside.svelte.js +42 -0
  133. package/dist/runes/clickOutside.svelte.js.map +1 -0
  134. package/dist/runes/escape.svelte.d.ts +34 -0
  135. package/dist/runes/escape.svelte.d.ts.map +1 -0
  136. package/dist/runes/escape.svelte.js +44 -0
  137. package/dist/runes/escape.svelte.js.map +1 -0
  138. package/dist/runes/focusTrap.svelte.d.ts +21 -0
  139. package/dist/runes/focusTrap.svelte.d.ts.map +1 -0
  140. package/dist/runes/focusTrap.svelte.js +37 -0
  141. package/dist/runes/focusTrap.svelte.js.map +1 -0
  142. package/dist/runes/index.svelte.d.ts +8 -0
  143. package/dist/runes/index.svelte.d.ts.map +1 -0
  144. package/dist/runes/index.svelte.js +8 -0
  145. package/dist/runes/index.svelte.js.map +1 -0
  146. package/dist/runes/layer.svelte.d.ts +36 -0
  147. package/dist/runes/layer.svelte.d.ts.map +1 -0
  148. package/dist/runes/layer.svelte.js +48 -0
  149. package/dist/runes/layer.svelte.js.map +1 -0
  150. package/dist/runes/query.svelte.d.ts +104 -0
  151. package/dist/runes/query.svelte.d.ts.map +1 -0
  152. package/dist/runes/query.svelte.js +179 -0
  153. package/dist/runes/query.svelte.js.map +1 -0
  154. package/dist/runes/state.svelte.d.ts +72 -0
  155. package/dist/runes/state.svelte.d.ts.map +1 -0
  156. package/dist/runes/state.svelte.js +98 -0
  157. package/dist/runes/state.svelte.js.map +1 -0
  158. package/dist/runes/window.svelte.d.ts +120 -0
  159. package/dist/runes/window.svelte.d.ts.map +1 -0
  160. package/dist/runes/window.svelte.js +196 -0
  161. package/dist/runes/window.svelte.js.map +1 -0
  162. package/dist/types/index.d.ts +2 -0
  163. package/dist/types/index.d.ts.map +1 -0
  164. package/dist/types/index.js +3 -0
  165. package/dist/types/index.js.map +1 -0
  166. package/dist/types/nui.d.ts +34 -0
  167. package/dist/types/nui.d.ts.map +1 -0
  168. package/dist/types/nui.js +17 -0
  169. package/dist/types/nui.js.map +1 -0
  170. package/dist/utils/env.d.ts +21 -0
  171. package/dist/utils/env.d.ts.map +1 -0
  172. package/dist/utils/env.js +36 -0
  173. package/dist/utils/env.js.map +1 -0
  174. package/dist/utils/error.d.ts +11 -0
  175. package/dist/utils/error.d.ts.map +1 -0
  176. package/dist/utils/error.js +30 -0
  177. package/dist/utils/error.js.map +1 -0
  178. package/dist/utils/index.d.ts +3 -0
  179. package/dist/utils/index.d.ts.map +1 -0
  180. package/dist/utils/index.js +3 -0
  181. package/dist/utils/index.js.map +1 -0
  182. package/dist/utils/keyboard.d.ts +127 -0
  183. package/dist/utils/keyboard.d.ts.map +1 -0
  184. package/dist/utils/keyboard.js +152 -0
  185. package/dist/utils/keyboard.js.map +1 -0
  186. package/package.json +76 -4
  187. package/src/actions/clickOutside/clickOutside.ts +78 -0
  188. package/src/actions/clickOutside/handler.ts +49 -0
  189. package/src/actions/clickOutside/index.ts +11 -0
  190. package/src/actions/clickOutside/types.ts +54 -0
  191. package/src/actions/escape/escape.ts +90 -0
  192. package/src/actions/escape/handler.ts +52 -0
  193. package/src/actions/escape/index.ts +11 -0
  194. package/src/actions/escape/types.ts +88 -0
  195. package/src/actions/focusTrap/focusTrap.ts +228 -0
  196. package/src/actions/focusTrap/focusables.ts +155 -0
  197. package/src/actions/focusTrap/index.ts +22 -0
  198. package/src/actions/focusTrap/trapStack.ts +109 -0
  199. package/src/actions/focusTrap/types.ts +64 -0
  200. package/src/actions/index.ts +35 -0
  201. package/src/actions/layer/index.ts +13 -0
  202. package/src/actions/layer/layer.ts +140 -0
  203. package/src/actions/layer/layerStack.ts +169 -0
  204. package/src/actions/layer/types.ts +132 -0
  205. package/src/client.ts +323 -0
  206. package/src/context/index.ts +127 -0
  207. package/src/index.ts +53 -0
  208. package/src/mock/index.ts +176 -0
  209. package/src/mock/interceptor.ts +177 -0
  210. package/src/mock/types.ts +125 -0
  211. package/src/mock/url.ts +37 -0
  212. package/src/runes/clickOutside.svelte.ts +53 -0
  213. package/src/runes/escape.svelte.ts +71 -0
  214. package/src/runes/focusTrap.svelte.ts +56 -0
  215. package/src/runes/index.svelte.ts +38 -0
  216. package/src/runes/layer.svelte.ts +74 -0
  217. package/src/runes/query.svelte.ts +313 -0
  218. package/src/runes/state.svelte.ts +175 -0
  219. package/src/runes/window.svelte.ts +308 -0
  220. package/src/utils/env.ts +36 -0
  221. package/src/utils/index.ts +2 -0
  222. package/src/utils/keyboard.ts +244 -0
@@ -0,0 +1,152 @@
1
+ import { onDestroy } from 'svelte';
2
+ /**
3
+ * Keyboard modifier keys.
4
+ */
5
+ export var NuiKeyboardModifier;
6
+ (function (NuiKeyboardModifier) {
7
+ NuiKeyboardModifier["Ctrl"] = "ctrl";
8
+ NuiKeyboardModifier["Alt"] = "alt";
9
+ NuiKeyboardModifier["Shift"] = "shift";
10
+ NuiKeyboardModifier["Meta"] = "meta";
11
+ })(NuiKeyboardModifier || (NuiKeyboardModifier = {}));
12
+ /**
13
+ * Standard special key names commonly used in FiveM NUI shortcuts.
14
+ */
15
+ export var NuiSpecialKey;
16
+ (function (NuiSpecialKey) {
17
+ NuiSpecialKey["Escape"] = "Escape";
18
+ NuiSpecialKey["Enter"] = "Enter";
19
+ NuiSpecialKey["Tab"] = "Tab";
20
+ NuiSpecialKey["Space"] = " ";
21
+ NuiSpecialKey["Backspace"] = "Backspace";
22
+ NuiSpecialKey["ArrowUp"] = "ArrowUp";
23
+ NuiSpecialKey["ArrowDown"] = "ArrowDown";
24
+ NuiSpecialKey["ArrowLeft"] = "ArrowLeft";
25
+ NuiSpecialKey["ArrowRight"] = "ArrowRight";
26
+ })(NuiSpecialKey || (NuiSpecialKey = {}));
27
+ function isTextInputActive() {
28
+ if (typeof document === 'undefined')
29
+ return false;
30
+ const el = document.activeElement;
31
+ if (!el)
32
+ return false;
33
+ const tag = el.tagName.toLowerCase();
34
+ if (tag === 'input' || tag === 'textarea' || tag === 'select') {
35
+ return true;
36
+ }
37
+ return el.hasAttribute('contenteditable') && el.getAttribute('contenteditable') !== 'false';
38
+ }
39
+ /**
40
+ * Registers a keyboard shortcut with automatic lifecycle management.
41
+ *
42
+ * Designed for FiveM NUI with support for:
43
+ * - `ignoreInputs`: prevents hotkeys from firing while typing in input fields.
44
+ * - Modifier keys (Ctrl, Alt, Shift, Meta).
45
+ * - Automatic cleanup via `onDestroy` and `AbortController`.
46
+ *
47
+ * @param keys - Key or array of keys (case-insensitive) to listen for.
48
+ * @param handler - Callback function invoked when matching keydown occurs.
49
+ * @param options - Optional shortcut configuration including modifiers and lifecycle strategy.
50
+ * @returns Cleanup function to remove event listeners.
51
+ *
52
+ * @example
53
+ * ```svelte
54
+ * <script>
55
+ * import { useNuiKey } from '@fivem-nui/svelte';
56
+ * useNuiKey('e', () => interact(), { ignoreInputs: true });
57
+ * </script>
58
+ * ```
59
+ */
60
+ export function useNuiKey(keys, handler, options = {}) {
61
+ const targetKeys = (Array.isArray(keys) ? keys : [keys]).map((k) => k.toLowerCase());
62
+ const { capture = false, preventDefault = false, stopPropagation = false, enabled, ignoreInputs = false, ignoreRepeat = false, ctrl, alt, shift, meta } = options;
63
+ let controller = null;
64
+ function isAllowed() {
65
+ if (enabled === undefined)
66
+ return true;
67
+ return typeof enabled === 'function' ? enabled() : enabled;
68
+ }
69
+ function onKeyDown(event) {
70
+ if (!isAllowed())
71
+ return;
72
+ if (ignoreRepeat && event.repeat)
73
+ return;
74
+ if (ignoreInputs && isTextInputActive())
75
+ return;
76
+ if (ctrl !== undefined && event.ctrlKey !== ctrl)
77
+ return;
78
+ if (alt !== undefined && event.altKey !== alt)
79
+ return;
80
+ if (shift !== undefined && event.shiftKey !== shift)
81
+ return;
82
+ if (meta !== undefined && event.metaKey !== meta)
83
+ return;
84
+ if (targetKeys.includes(event.key.toLowerCase())) {
85
+ if (preventDefault) {
86
+ event.preventDefault();
87
+ }
88
+ if (stopPropagation) {
89
+ event.stopPropagation();
90
+ }
91
+ try {
92
+ handler(event);
93
+ }
94
+ catch (err) {
95
+ console.error('[NuiKey] Error in key handler callback:', err);
96
+ }
97
+ }
98
+ }
99
+ if (typeof window !== 'undefined') {
100
+ controller = new AbortController();
101
+ window.addEventListener('keydown', onKeyDown, {
102
+ capture,
103
+ signal: controller.signal
104
+ });
105
+ }
106
+ let isCleanedUp = false;
107
+ function cleanup() {
108
+ if (isCleanedUp)
109
+ return;
110
+ isCleanedUp = true;
111
+ if (controller) {
112
+ controller.abort();
113
+ controller = null;
114
+ }
115
+ }
116
+ if (options.autoDestroy !== false) {
117
+ try {
118
+ onDestroy(cleanup);
119
+ }
120
+ catch (lifecycleError) {
121
+ if (options.autoDestroy === true) {
122
+ cleanup();
123
+ throw lifecycleError;
124
+ }
125
+ // Best-effort fallback when autoDestroy is undefined and called outside component
126
+ }
127
+ }
128
+ return cleanup;
129
+ }
130
+ /**
131
+ * Listens for the `Escape` key to close or dismiss NUI panels.
132
+ * In FiveM, ESC is the universal standard for closing NUI windows.
133
+ *
134
+ * @param onEscape - Callback function invoked when the Escape key is pressed.
135
+ * @param options - Optional configuration overriding default key options (`preventDefault` defaults to `true`).
136
+ * @returns Cleanup function to remove the Escape listener.
137
+ *
138
+ * @example
139
+ * ```svelte
140
+ * <script>
141
+ * import { useNuiEscape, nui } from '@fivem-nui/svelte';
142
+ * useNuiEscape(() => nui.notify('close'));
143
+ * </script>
144
+ * ```
145
+ */
146
+ export function useNuiEscape(onEscape, options = {}) {
147
+ return useNuiKey(NuiSpecialKey.Escape, onEscape, {
148
+ preventDefault: true,
149
+ ...options
150
+ });
151
+ }
152
+ //# sourceMappingURL=keyboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyboard.js","sourceRoot":"","sources":["../../src/utils/keyboard.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,78 @@
1
1
  {
2
2
  "name": "@fivem-nui/svelte",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Svelte adapter and toolkit for FiveM NUI communication",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "svelte": "src/index.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "svelte": "./src/index.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./mock": {
16
+ "types": "./dist/mock/index.d.ts",
17
+ "default": "./dist/mock/index.js"
18
+ },
19
+ "./actions": {
20
+ "types": "./dist/actions/index.d.ts",
21
+ "default": "./dist/actions/index.js"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "src"
27
+ ],
28
+ "scripts": {
29
+ "build": "tsc",
30
+ "watch": "tsc --watch",
31
+ "check": "tsc --noEmit",
32
+ "test": "vitest run",
33
+ "test:watch": "vitest",
34
+ "coverage": "vitest run --coverage",
35
+ "prepublishOnly": "npm run build"
36
+ },
37
+ "keywords": [
38
+ "fivem",
39
+ "nui",
40
+ "svelte",
41
+ "runes",
42
+ "typescript",
43
+ "bridge"
44
+ ],
45
+ "author": "Rainbe",
46
+ "license": "MIT",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/rian-be/FiveM.Nui.git",
50
+ "directory": "packages/svelte/bridge"
51
+ },
52
+ "bugs": {
53
+ "url": "https://github.com/rian-be/FiveM.Nui/issues"
54
+ },
55
+ "homepage": "https://github.com/rian-be/FiveM.Nui/tree/main/packages/svelte/bridge#readme",
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "engines": {
60
+ "node": ">=22.12.0"
61
+ },
62
+ "peerDependencies": {
63
+ "svelte": ">=5.0.0"
64
+ },
65
+ "dependencies": {
66
+ "@fivem-nui/core": "^1.0.0",
67
+ "@fivem-nui/error": "^1.0.0"
68
+ },
69
+ "devDependencies": {
70
+ "@sveltejs/vite-plugin-svelte": "^7.3.1",
71
+ "@vitest/coverage-v8": "^5.0.4",
72
+ "happy-dom": "^21.0.3",
73
+ "svelte": "^5.0.0",
74
+ "typescript": "^7.0.2",
75
+ "vite": "^6.0.0",
76
+ "vitest": "^5.0.4"
77
+ }
78
+ }
@@ -0,0 +1,78 @@
1
+ import type { Action } from 'svelte/action';
2
+ import {
3
+ type NuiClickOutsideInput,
4
+ toClickOutsideOptions
5
+ } from './types';
6
+ import { createClickOutsideHandler } from './handler';
7
+
8
+ /**
9
+ * Svelte Action for reliable outside click detection in FiveM CEF.
10
+ *
11
+ * Uses `event.composedPath()` to eliminate the detached DOM node bug when
12
+ * inner elements mutate during click events.
13
+ *
14
+ * @remarks
15
+ * **Multiple Overlays in CEF**:
16
+ * Because `pointerdown` is used with `capture: true`, clicking into another open NUI panel
17
+ * will trigger an outside click unless that other panel is explicitly included in `ignore`.
18
+ *
19
+ * @param node - The DOM element to monitor for outside clicks.
20
+ * @param parameter - Callback function or configuration options object.
21
+ *
22
+ * @example
23
+ * ```svelte
24
+ * <div use:nuiClickOutside={() => (isOpen = false)}>...</div>
25
+ * <div use:nuiClickOutside={{ onClickOutside: close, ignore: [triggerEl, '.header-btn'] }}>...</div>
26
+ * ```
27
+ */
28
+ export const nuiClickOutside: Action<HTMLElement, NuiClickOutsideInput> = (
29
+ node,
30
+ parameter
31
+ ) => {
32
+ let currentParam = parameter;
33
+ let controller: AbortController | null = null;
34
+
35
+ const handler = createClickOutsideHandler(
36
+ () => node,
37
+ () => toClickOutsideOptions(currentParam)
38
+ );
39
+
40
+ function getCapture(): boolean {
41
+ const opts = toClickOutsideOptions(currentParam);
42
+ return opts?.capture ?? true;
43
+ }
44
+
45
+ function setup() {
46
+ teardown();
47
+ controller = new AbortController();
48
+ document.addEventListener('pointerdown', handler, {
49
+ capture: getCapture(),
50
+ signal: controller.signal
51
+ });
52
+ }
53
+
54
+ function teardown() {
55
+ if (controller) {
56
+ controller.abort();
57
+ controller = null;
58
+ }
59
+ }
60
+
61
+ setup();
62
+
63
+ return {
64
+ update(newParam) {
65
+ const prevCapture = getCapture();
66
+ currentParam = newParam;
67
+ // Only changes to `capture` require re-binding the document listener.
68
+ // All other options (`enabled`, `onClickOutside`, `ignore`)
69
+ // are read lazily on each pointerdown event via getOptions().
70
+ if (getCapture() !== prevCapture) {
71
+ setup();
72
+ }
73
+ },
74
+ destroy() {
75
+ teardown();
76
+ }
77
+ };
78
+ };
@@ -0,0 +1,49 @@
1
+ import type { ClickOutsideTarget, NuiClickOutsideParameter } from './types';
2
+
3
+ /**
4
+ * Resolves a DOM Element from an Element reference, CSS selector string, or getter function.
5
+ */
6
+ export function resolveElement(target: ClickOutsideTarget): Element | null {
7
+ if (typeof target === 'string') {
8
+ return document.querySelector(target);
9
+ }
10
+ if (typeof target === 'function') {
11
+ return target() ?? null;
12
+ }
13
+ return target ?? null;
14
+ }
15
+
16
+ /**
17
+ * Creates a reusable pointerdown event handler for outside click detection.
18
+ * Shared between Svelte action and composable to eliminate duplication.
19
+ */
20
+ export function createClickOutsideHandler(
21
+ getNode: () => Element | null | undefined,
22
+ getOptions: () => NuiClickOutsideParameter | undefined
23
+ ) {
24
+ return (event: PointerEvent) => {
25
+ const node = getNode();
26
+ const opts = getOptions();
27
+ if (!node?.isConnected || !opts) return;
28
+
29
+ const enabled = typeof opts.enabled === 'function' ? opts.enabled() : opts.enabled ?? true;
30
+ if (!enabled || event.button !== 0) return;
31
+
32
+ const path = event.composedPath();
33
+ if (path.includes(node)) return;
34
+
35
+ for (const item of opts.ignore ?? []) {
36
+ const el = resolveElement(item);
37
+ if (el && path.includes(el)) return;
38
+ }
39
+
40
+ const callback = opts.onClickOutside ?? opts.callback;
41
+ if (callback) {
42
+ try {
43
+ callback(event);
44
+ } catch (err) {
45
+ console.error('[NuiClickOutside] Error in onClickOutside callback:', err);
46
+ }
47
+ }
48
+ };
49
+ }
@@ -0,0 +1,11 @@
1
+ export { nuiClickOutside } from './clickOutside';
2
+ export {
3
+ type ClickOutsideTarget,
4
+ type NuiClickOutsideParameter,
5
+ type NuiClickOutsideInput,
6
+ toClickOutsideOptions
7
+ } from './types';
8
+ export {
9
+ resolveElement,
10
+ createClickOutsideHandler
11
+ } from './handler';
@@ -0,0 +1,54 @@
1
+ export type ClickOutsideTarget =
2
+ | Element
3
+ | string
4
+ | null
5
+ | undefined
6
+ | (() => Element | null | undefined);
7
+
8
+ export interface NuiClickOutsideParameter {
9
+ /**
10
+ * Callback invoked when a click or touch occurs outside the target node.
11
+ */
12
+ onClickOutside?: (event: PointerEvent) => void;
13
+
14
+ /**
15
+ * @deprecated Use `onClickOutside` instead.
16
+ */
17
+ callback?: (event: PointerEvent) => void;
18
+
19
+ /**
20
+ * Whether the outside click detector is active.
21
+ * Can be a static boolean or a reactive getter `() => boolean`.
22
+ * @defaultValue true
23
+ */
24
+ enabled?: boolean | (() => boolean);
25
+
26
+ /**
27
+ * Elements, selectors, or getters to exclude from triggering outside click.
28
+ *
29
+ * @remarks
30
+ * **Trigger / Toggle Buttons**:
31
+ * Because `capture: true` listens on the capturing phase (`pointerdown` fires before `click`),
32
+ * clicking a toggle/trigger button without adding it to `ignore` will cause an immediate
33
+ * flicker (the outside click closes the popover during `pointerdown`, and the button's
34
+ * subsequent `click` immediately re-opens it). Always pass your toggle button in `ignore`.
35
+ */
36
+ ignore?: ClickOutsideTarget[];
37
+
38
+ /**
39
+ * Listen during capture phase to intercept before bubbling mutations.
40
+ * @defaultValue true
41
+ */
42
+ capture?: boolean;
43
+ }
44
+
45
+ export type NuiClickOutsideInput =
46
+ | ((event: PointerEvent) => void)
47
+ | NuiClickOutsideParameter;
48
+
49
+ export function toClickOutsideOptions(
50
+ param: NuiClickOutsideInput | undefined
51
+ ): NuiClickOutsideParameter | undefined {
52
+ if (!param) return undefined;
53
+ return typeof param === 'function' ? { onClickOutside: param } : param;
54
+ }
@@ -0,0 +1,90 @@
1
+ import type { Action } from 'svelte/action';
2
+ import {
3
+ type NuiEscapeInput,
4
+ toEscapeOptions
5
+ } from './types';
6
+ import { resolveTarget, createEscapeHandler } from './handler';
7
+
8
+ /**
9
+ * Svelte Action for handling Escape key presses in FiveM NUI.
10
+ *
11
+ * Designed specifically for FiveM NUI where Escape is the universal close/dismiss action.
12
+ * Defaults to listening on `document` so closing works without requiring focus on a specific `div`.
13
+ *
14
+ * @remarks
15
+ * **Nested Modals & Target Scoping**:
16
+ * When using `target: NuiEscapeTarget.Document` (default), all registered listeners on `document` will receive
17
+ * keydown in registration order. If you have nested modals where only the topmost modal should close,
18
+ * consider using `nuiLayer` (which manages a centralized layer stack so only the topmost active layer dismisses),
19
+ * or configure `ignoreHandled: true`, or use `target: NuiEscapeTarget.Element` with a focus trap (`nuiFocusTrap`) and `stopPropagation: true`.
20
+ *
21
+ * @param node - The DOM element attaching the action.
22
+ * @param parameter - Callback function or configuration options object.
23
+ *
24
+ * @example
25
+ * ```svelte
26
+ * <div use:nuiEscapeAction={() => win.close()}>...</div>
27
+ * <div use:nuiEscapeAction={{ onEscape: close, stopPropagation: true }}>...</div>
28
+ * ```
29
+ */
30
+ export const nuiEscapeAction: Action<HTMLElement, NuiEscapeInput> = (
31
+ node,
32
+ parameter
33
+ ) => {
34
+ let currentParam = parameter;
35
+ let controller: AbortController | null = null;
36
+
37
+ const handler = createEscapeHandler(() => toEscapeOptions(currentParam));
38
+
39
+ function getTarget(): EventTarget {
40
+ const opts = toEscapeOptions(currentParam);
41
+ return resolveTarget(node, opts?.target);
42
+ }
43
+
44
+ function getCapture(): boolean {
45
+ const opts = toEscapeOptions(currentParam);
46
+ return opts?.capture ?? false;
47
+ }
48
+
49
+ function setup() {
50
+ teardown();
51
+ const eventTarget = getTarget();
52
+ controller = new AbortController();
53
+ eventTarget.addEventListener('keydown', handler as EventListener, {
54
+ capture: getCapture(),
55
+ signal: controller.signal
56
+ });
57
+ }
58
+
59
+ function teardown() {
60
+ if (controller) {
61
+ controller.abort();
62
+ controller = null;
63
+ }
64
+ }
65
+
66
+ setup();
67
+
68
+ return {
69
+ update(newParam) {
70
+ const prevOpts = toEscapeOptions(currentParam);
71
+ currentParam = newParam;
72
+ const nextOpts = toEscapeOptions(currentParam);
73
+
74
+ const prevTarget = resolveTarget(node, prevOpts?.target);
75
+ const nextTarget = resolveTarget(node, nextOpts?.target);
76
+ const prevCapture = prevOpts?.capture ?? false;
77
+ const nextCapture = nextOpts?.capture ?? false;
78
+
79
+ // Only target or capture changes require re-binding DOM listeners.
80
+ // Other options (enabled, onEscape, preventDefault, stopPropagation, ignoreRepeat)
81
+ // are read dynamically inside createEscapeHandler on each keydown event.
82
+ if (prevTarget !== nextTarget || prevCapture !== nextCapture) {
83
+ setup();
84
+ }
85
+ },
86
+ destroy() {
87
+ teardown();
88
+ }
89
+ };
90
+ };
@@ -0,0 +1,52 @@
1
+ import { NuiEscapeTarget, type NuiEscapeParameter } from './types';
2
+
3
+ /**
4
+ * Resolves the appropriate EventTarget to attach the Escape key listener to.
5
+ */
6
+ export function resolveTarget(node: HTMLElement, targetType?: NuiEscapeTarget): EventTarget {
7
+ if (targetType === NuiEscapeTarget.Window) return window;
8
+ if (targetType === NuiEscapeTarget.Element) return node;
9
+ return document;
10
+ }
11
+
12
+ /**
13
+ * Creates a reusable keydown event handler for Escape key detection.
14
+ * Shared between Svelte action and composable to eliminate duplication.
15
+ */
16
+ export function createEscapeHandler(
17
+ getOptions: () => NuiEscapeParameter | undefined
18
+ ) {
19
+ return (event: KeyboardEvent) => {
20
+ const opts = getOptions();
21
+ if (!opts || event.key !== 'Escape' || event.isComposing) return;
22
+
23
+ if (opts.ignoreHandled && event.defaultPrevented) return;
24
+
25
+ const enabled = typeof opts.enabled === 'function' ? opts.enabled() : opts.enabled ?? true;
26
+ const handler = opts.onEscape ?? opts.callback;
27
+ if (!enabled || !handler) return;
28
+
29
+ if ((opts.ignoreRepeat ?? true) && event.repeat) {
30
+ if (opts.preventDefault ?? true) {
31
+ event.preventDefault();
32
+ }
33
+ return;
34
+ }
35
+
36
+ if (opts.preventDefault ?? true) {
37
+ event.preventDefault();
38
+ }
39
+ if (opts.stopPropagation) {
40
+ event.stopPropagation();
41
+ }
42
+ if (opts.stopImmediatePropagation) {
43
+ event.stopImmediatePropagation();
44
+ }
45
+
46
+ try {
47
+ handler(event);
48
+ } catch (err) {
49
+ console.error('[NuiEscape] Error in onEscape callback:', err);
50
+ }
51
+ };
52
+ }
@@ -0,0 +1,11 @@
1
+ export { nuiEscapeAction } from './escape';
2
+ export {
3
+ NuiEscapeTarget,
4
+ type NuiEscapeParameter,
5
+ type NuiEscapeInput,
6
+ toEscapeOptions
7
+ } from './types';
8
+ export {
9
+ resolveTarget,
10
+ createEscapeHandler
11
+ } from './handler';
@@ -0,0 +1,88 @@
1
+ export enum NuiEscapeTarget {
2
+ Element = 'element',
3
+ Window = 'window',
4
+ Document = 'document'
5
+ }
6
+
7
+ export interface NuiEscapeParameter {
8
+ /**
9
+ * Callback invoked when the Escape key is pressed.
10
+ */
11
+ onEscape?: (event: KeyboardEvent) => void;
12
+
13
+ /**
14
+ * @deprecated Use `onEscape` instead.
15
+ */
16
+ callback?: (event: KeyboardEvent) => void;
17
+
18
+ /**
19
+ * Whether the Escape listener is active.
20
+ * Accepts a boolean or a reactive getter `() => boolean`.
21
+ * @defaultValue true
22
+ */
23
+ enabled?: boolean | (() => boolean);
24
+
25
+ /**
26
+ * Where to attach the listener:
27
+ * - `NuiEscapeTarget.Document`: listens globally on the document (default; does not require element focus)
28
+ * - `NuiEscapeTarget.Window`: listens globally on the window
29
+ * - `NuiEscapeTarget.Element`: listens only for keydown events within this element (requires the element to have focus, e.g. tabindex="-1")
30
+ * @defaultValue NuiEscapeTarget.Document
31
+ */
32
+ target?: NuiEscapeTarget;
33
+
34
+ /**
35
+ * Whether to listen during the capturing phase.
36
+ * Useful when inner elements might stop propagation before bubbling reaches window/document.
37
+ * @defaultValue false
38
+ */
39
+ capture?: boolean;
40
+
41
+ /**
42
+ * Whether to call `event.preventDefault()`.
43
+ * @defaultValue true
44
+ */
45
+ preventDefault?: boolean;
46
+
47
+ /**
48
+ * Whether to stop event propagation to parent elements.
49
+ * @defaultValue false
50
+ */
51
+ stopPropagation?: boolean;
52
+
53
+ /**
54
+ * Whether to stop immediate propagation to other listeners on the same target.
55
+ * @defaultValue false
56
+ */
57
+ stopImmediatePropagation?: boolean;
58
+
59
+ /**
60
+ * Whether to ignore repeated keydown events fired while holding the Escape key down.
61
+ *
62
+ * @remarks
63
+ * If `true` and `preventDefault: true`, `event.preventDefault()` will still be invoked
64
+ * on repeated keydown events to suppress browser/CEF default actions, but the callback itself
65
+ * will only be executed once on the initial keydown.
66
+ * @defaultValue true
67
+ */
68
+ ignoreRepeat?: boolean;
69
+
70
+ /**
71
+ * Whether to ignore the Escape keydown event if an earlier handler has already called `event.preventDefault()`.
72
+ * Useful when multiple independent Escape listeners are registered on `document` to avoid firing handlers
73
+ * after an inner/prior component has already handled the event.
74
+ * @defaultValue false
75
+ */
76
+ ignoreHandled?: boolean;
77
+ }
78
+
79
+ export type NuiEscapeInput =
80
+ | ((event: KeyboardEvent) => void)
81
+ | NuiEscapeParameter;
82
+
83
+ export function toEscapeOptions<T extends NuiEscapeParameter = NuiEscapeParameter>(
84
+ param?: ((event: KeyboardEvent) => void) | T
85
+ ): T | undefined {
86
+ if (!param) return undefined;
87
+ return typeof param === 'function' ? ({ onEscape: param } as T) : param;
88
+ }