@effect-tui/react 0.12.3 → 0.13.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 +21 -1
- package/dist/src/dev.d.ts +22 -32
- package/dist/src/dev.d.ts.map +1 -1
- package/dist/src/dev.js +42 -96
- package/dist/src/dev.js.map +1 -1
- package/dist/src/hooks/index.d.ts +3 -0
- package/dist/src/hooks/index.d.ts.map +1 -1
- package/dist/src/hooks/index.js +1 -0
- package/dist/src/hooks/index.js.map +1 -1
- package/dist/src/hooks/use-mouse.d.ts +10 -1
- package/dist/src/hooks/use-mouse.d.ts.map +1 -1
- package/dist/src/hooks/use-mouse.js +10 -2
- package/dist/src/hooks/use-mouse.js.map +1 -1
- package/dist/src/hooks/use-paste.d.ts +13 -3
- package/dist/src/hooks/use-paste.d.ts.map +1 -1
- package/dist/src/hooks/use-paste.js +15 -5
- package/dist/src/hooks/use-paste.js.map +1 -1
- package/dist/src/hooks/use-scroll.d.ts +59 -24
- package/dist/src/hooks/use-scroll.d.ts.map +1 -1
- package/dist/src/hooks/use-scroll.js +238 -79
- package/dist/src/hooks/use-scroll.js.map +1 -1
- package/dist/src/hooks/use-shortcut.d.ts +16 -0
- package/dist/src/hooks/use-shortcut.d.ts.map +1 -0
- package/dist/src/hooks/use-shortcut.js +29 -0
- package/dist/src/hooks/use-shortcut.js.map +1 -0
- package/dist/src/hosts/scroll.d.ts +4 -0
- package/dist/src/hosts/scroll.d.ts.map +1 -1
- package/dist/src/hosts/scroll.js +18 -2
- package/dist/src/hosts/scroll.js.map +1 -1
- package/dist/src/index.d.ts +6 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/renderer/input/InputProcessor.d.ts.map +1 -1
- package/dist/src/renderer/input/InputProcessor.js +8 -1
- package/dist/src/renderer/input/InputProcessor.js.map +1 -1
- package/dist/src/renderer/lifecycle/EventBus.d.ts +2 -2
- package/dist/src/renderer/lifecycle/EventBus.d.ts.map +1 -1
- package/dist/src/renderer/lifecycle/EventBus.js +13 -1
- package/dist/src/renderer/lifecycle/EventBus.js.map +1 -1
- package/dist/src/renderer-types.d.ts +8 -1
- package/dist/src/renderer-types.d.ts.map +1 -1
- package/dist/src/renderer.d.ts +13 -2
- package/dist/src/renderer.d.ts.map +1 -1
- package/dist/src/renderer.js +48 -7
- package/dist/src/renderer.js.map +1 -1
- package/dist/src/shortcuts.d.ts +15 -0
- package/dist/src/shortcuts.d.ts.map +1 -0
- package/dist/src/shortcuts.js +149 -0
- package/dist/src/shortcuts.js.map +1 -0
- package/dist/src/test/mock-streams.d.ts.map +1 -1
- package/dist/src/test/mock-streams.js +0 -3
- package/dist/src/test/mock-streams.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/dev.tsx +59 -107
- package/src/hooks/index.ts +3 -0
- package/src/hooks/use-mouse.ts +19 -3
- package/src/hooks/use-paste.ts +24 -5
- package/src/hooks/use-scroll.ts +345 -105
- package/src/hooks/use-shortcut.ts +54 -0
- package/src/hosts/scroll.ts +21 -2
- package/src/index.ts +19 -6
- package/src/renderer/input/InputProcessor.ts +8 -1
- package/src/renderer/lifecycle/EventBus.ts +14 -4
- package/src/renderer-types.ts +9 -1
- package/src/renderer.ts +96 -9
- package/src/shortcuts.ts +180 -0
- package/src/test/mock-streams.ts +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-tui/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "React bindings for @effect-tui/core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"prepublishOnly": "bun run typecheck && bun run build"
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
|
-
"@effect-tui/core": "^0.
|
|
86
|
+
"@effect-tui/core": "^0.13.0",
|
|
87
87
|
"@effect/platform": "^0.94.0",
|
|
88
88
|
"@effect/platform-bun": "^0.87.0",
|
|
89
89
|
"@effect/rpc": "^0.73.0",
|
package/src/dev.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Development utilities for hot module replacement (HMR).
|
|
3
3
|
*
|
|
4
|
+
* Use render(..., { dev: true, importMeta: import.meta }) to enable HMR.
|
|
5
|
+
*
|
|
4
6
|
* Enables hot reload during development:
|
|
5
7
|
* - File changes trigger re-render without process restart
|
|
6
8
|
* - Terminal stays stable (no screen flash)
|
|
@@ -24,7 +26,6 @@ import { DiagnosticsPanel } from "./debug/DiagnosticsPanel.js"
|
|
|
24
26
|
import { ToastContainer, ToastProvider, useToast } from "./dev/Toast.js"
|
|
25
27
|
import { useKeyboard } from "./hooks/use-keyboard.js"
|
|
26
28
|
import { enableRemote } from "./remote/index.js"
|
|
27
|
-
import { createRenderer, createRoot, type Root } from "./renderer.js"
|
|
28
29
|
import { useRenderer, useTerminalSize } from "./renderer-context.js"
|
|
29
30
|
import type { RendererOptions, TuiRenderer } from "./renderer-types.js"
|
|
30
31
|
|
|
@@ -37,7 +38,7 @@ import type { RendererOptions, TuiRenderer } from "./renderer-types.js"
|
|
|
37
38
|
* @example
|
|
38
39
|
* ```tsx
|
|
39
40
|
* import { Atom } from "@effect-atom/atom-react"
|
|
40
|
-
* import { hmr,
|
|
41
|
+
* import { hmr, render } from "@effect-tui/react"
|
|
41
42
|
*
|
|
42
43
|
* // Just pipe hmr() - that's it!
|
|
43
44
|
* const countAtom = Atom.make(0).pipe(hmr("count"))
|
|
@@ -47,6 +48,8 @@ import type { RendererOptions, TuiRenderer } from "./renderer-types.js"
|
|
|
47
48
|
* const count = useAtomValue(countAtom)
|
|
48
49
|
* // Edit this file - count persists!
|
|
49
50
|
* }
|
|
51
|
+
*
|
|
52
|
+
* render(<App />, { dev: true, importMeta: import.meta })
|
|
50
53
|
* ```
|
|
51
54
|
*/
|
|
52
55
|
export function hmr(key: string): <A,>(self: A) => A {
|
|
@@ -76,38 +79,6 @@ export function hmrState<T>(key: string, create: () => T): T {
|
|
|
76
79
|
return globalValue(Symbol.for(`hmr/${key}`), create)
|
|
77
80
|
}
|
|
78
81
|
|
|
79
|
-
/**
|
|
80
|
-
* Simple self-running dev mode. Just add this at the bottom of your file:
|
|
81
|
-
*
|
|
82
|
-
* @example
|
|
83
|
-
* ```tsx
|
|
84
|
-
* export default function App() { ... }
|
|
85
|
-
*
|
|
86
|
-
* devMain(import.meta)
|
|
87
|
-
* ```
|
|
88
|
-
*
|
|
89
|
-
* That's it! Handles:
|
|
90
|
-
* - Only runs when file is the entry point (import.meta.main)
|
|
91
|
-
* - Only initializes once across HMR reloads
|
|
92
|
-
* - Automatically finds the file path
|
|
93
|
-
*/
|
|
94
|
-
export function devMain(importMeta: { main?: boolean; url: string }, options?: Omit<DevRenderOptions, never>): void {
|
|
95
|
-
// Skip if not main entry point
|
|
96
|
-
if (!importMeta.main) return
|
|
97
|
-
|
|
98
|
-
// Skip if this is a HMR re-import (has cache-busting query string)
|
|
99
|
-
if (importMeta.url.includes("?")) return
|
|
100
|
-
|
|
101
|
-
const key = `devMain/${importMeta.url}`
|
|
102
|
-
const initialized = hmrState(key, () => ({ done: false }))
|
|
103
|
-
|
|
104
|
-
if (!initialized.done) {
|
|
105
|
-
initialized.done = true
|
|
106
|
-
const filePath = new URL(importMeta.url).pathname
|
|
107
|
-
devRender(filePath, options)
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
82
|
// Cache for runtime key extraction
|
|
112
83
|
const keyCache = new Map<string, string>()
|
|
113
84
|
|
|
@@ -151,7 +122,7 @@ function extractVarName(source: string, line: number): string | null {
|
|
|
151
122
|
* @example
|
|
152
123
|
* ```tsx
|
|
153
124
|
* import { Atom } from "@effect-atom/atom-react"
|
|
154
|
-
* import { autoHmr,
|
|
125
|
+
* import { autoHmr, render } from "@effect-tui/react"
|
|
155
126
|
*
|
|
156
127
|
* // No manual key needed - derived from variable name!
|
|
157
128
|
* const countAtom = Atom.make(0).pipe(autoHmr)
|
|
@@ -192,7 +163,7 @@ export function autoHmr<A>(self: A): A {
|
|
|
192
163
|
return hmr(key)(self)
|
|
193
164
|
}
|
|
194
165
|
|
|
195
|
-
export interface
|
|
166
|
+
export interface DevOptions {
|
|
196
167
|
/** Directories to watch (defaults to entry file's directory) */
|
|
197
168
|
watchDirs?: string[]
|
|
198
169
|
/** File extensions to watch (defaults to [".ts", ".tsx"]) */
|
|
@@ -213,12 +184,12 @@ export interface DevRenderOptions extends RendererOptions {
|
|
|
213
184
|
statsTitle?: string
|
|
214
185
|
}
|
|
215
186
|
|
|
216
|
-
export interface
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
/** Manually trigger a reload */
|
|
220
|
-
|
|
221
|
-
/** Stop watching and cleanup */
|
|
187
|
+
export interface DevRuntime {
|
|
188
|
+
/** Promise that resolves after the first render + watcher setup. */
|
|
189
|
+
ready: Promise<void>
|
|
190
|
+
/** Manually trigger a reload. */
|
|
191
|
+
reload: () => Promise<void>
|
|
192
|
+
/** Stop watching and cleanup. */
|
|
222
193
|
stop: () => Promise<void>
|
|
223
194
|
}
|
|
224
195
|
|
|
@@ -262,30 +233,6 @@ async function awaitWriteFinish(path: string, stabilityMs: number): Promise<void
|
|
|
262
233
|
}
|
|
263
234
|
}
|
|
264
235
|
|
|
265
|
-
/**
|
|
266
|
-
* Render a TUI app with hot module replacement support.
|
|
267
|
-
*
|
|
268
|
-
* Uses @parcel/watcher for reliable, cross-platform file watching.
|
|
269
|
-
* When files change, the module is re-imported and the app re-renders
|
|
270
|
-
* without restarting the process or tearing down the terminal.
|
|
271
|
-
*
|
|
272
|
-
* For state preservation, use effect-atom with the hmr() combinator:
|
|
273
|
-
*
|
|
274
|
-
* @example
|
|
275
|
-
* ```tsx
|
|
276
|
-
* import { Atom, useAtomValue } from "@effect-atom/atom-react"
|
|
277
|
-
* import { hmr, devRender } from "@effect-tui/react"
|
|
278
|
-
*
|
|
279
|
-
* const countAtom = Atom.make(0).pipe(hmr("count"))
|
|
280
|
-
*
|
|
281
|
-
* export default function App() {
|
|
282
|
-
* const count = useAtomValue(countAtom)
|
|
283
|
-
* return <text>Count: {count}</text>
|
|
284
|
-
* }
|
|
285
|
-
*
|
|
286
|
-
* devRender(import.meta.path, { mode: "inline" })
|
|
287
|
-
* ```
|
|
288
|
-
*/
|
|
289
236
|
/**
|
|
290
237
|
* Internal component that handles screenshot with toast notification
|
|
291
238
|
*/
|
|
@@ -351,8 +298,8 @@ export interface DevWrapperProps {
|
|
|
351
298
|
* - Auto-show console on errors
|
|
352
299
|
* - Optional renderer stats overlay (showStats)
|
|
353
300
|
*
|
|
354
|
-
* Use this to wrap your app when you need dev features
|
|
355
|
-
* (e.g., when you need to pass props
|
|
301
|
+
* Use this to wrap your app when you need dev UI features without the dev runtime
|
|
302
|
+
* (e.g., when you need to pass props or load the app manually).
|
|
356
303
|
*/
|
|
357
304
|
export function DevWrapper({
|
|
358
305
|
children,
|
|
@@ -405,14 +352,20 @@ export function DevWrapper({
|
|
|
405
352
|
)
|
|
406
353
|
}
|
|
407
354
|
|
|
408
|
-
|
|
355
|
+
type DevRoot = {
|
|
356
|
+
render(element: React.ReactNode, sync?: boolean): void
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function startDevRuntime(
|
|
360
|
+
entryPath: string,
|
|
361
|
+
renderer: TuiRenderer,
|
|
362
|
+
root: DevRoot,
|
|
363
|
+
options?: DevOptions & { mode?: RendererOptions["mode"] },
|
|
364
|
+
): DevRuntime {
|
|
409
365
|
const watchExtensions = options?.watchExtensions ?? [".ts", ".tsx"]
|
|
410
366
|
const debounceMs = options?.debounce ?? 150
|
|
411
367
|
const stabilityMs = options?.awaitWriteFinish ?? 50
|
|
412
368
|
|
|
413
|
-
const renderer = createRenderer(options)
|
|
414
|
-
const root = createRoot(renderer)
|
|
415
|
-
|
|
416
369
|
// Dev mode always enables remote control with entry path for identification
|
|
417
370
|
const stopRemote = enableRemote(renderer, { entryPath })
|
|
418
371
|
|
|
@@ -423,8 +376,7 @@ export async function devRender(entryPath: string, options?: DevRenderOptions):
|
|
|
423
376
|
// Determine project root for cache clearing
|
|
424
377
|
const projectRoot = options?.watchDirs?.[0] ?? dirname(entryPath)
|
|
425
378
|
|
|
426
|
-
|
|
427
|
-
const render = async () => {
|
|
379
|
+
const reload = async () => {
|
|
428
380
|
const thisVersion = ++version
|
|
429
381
|
|
|
430
382
|
try {
|
|
@@ -442,7 +394,7 @@ export async function devRender(entryPath: string, options?: DevRenderOptions):
|
|
|
442
394
|
if (!App) {
|
|
443
395
|
const err = new Error(`No default export found in ${entryPath}`)
|
|
444
396
|
options?.onError?.(err)
|
|
445
|
-
console.error("[
|
|
397
|
+
console.error("[effect-tui] Dev render failed:", err.message)
|
|
446
398
|
return
|
|
447
399
|
}
|
|
448
400
|
|
|
@@ -462,41 +414,41 @@ export async function devRender(entryPath: string, options?: DevRenderOptions):
|
|
|
462
414
|
} catch (err) {
|
|
463
415
|
const error = err instanceof Error ? err : new Error(String(err))
|
|
464
416
|
options?.onError?.(error)
|
|
465
|
-
console.error("[
|
|
417
|
+
console.error("[effect-tui] Dev render error:", error.message)
|
|
466
418
|
}
|
|
467
419
|
}
|
|
468
420
|
|
|
469
|
-
|
|
470
|
-
|
|
421
|
+
const ready = (async () => {
|
|
422
|
+
await reload()
|
|
471
423
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
}
|
|
496
|
-
})
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
424
|
+
try {
|
|
425
|
+
subscription = await watcher.subscribe(projectRoot, async (err, events) => {
|
|
426
|
+
if (err) {
|
|
427
|
+
options?.onError?.(err)
|
|
428
|
+
console.error("[effect-tui] Dev watcher error:", err)
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Filter to relevant file extensions
|
|
433
|
+
const relevantEvents = events.filter((event) => watchExtensions.some((ext) => event.path.endsWith(ext)))
|
|
434
|
+
|
|
435
|
+
if (relevantEvents.length === 0) return
|
|
436
|
+
|
|
437
|
+
// Debounce rapid changes
|
|
438
|
+
if (debounceTimer) clearTimeout(debounceTimer)
|
|
439
|
+
debounceTimer = setTimeout(async () => {
|
|
440
|
+
// Wait for file writes to stabilize
|
|
441
|
+
const changedPath = relevantEvents[0].path
|
|
442
|
+
await awaitWriteFinish(changedPath, stabilityMs)
|
|
443
|
+
|
|
444
|
+
options?.onReload?.()
|
|
445
|
+
await reload()
|
|
446
|
+
}, debounceMs)
|
|
447
|
+
})
|
|
448
|
+
} catch (err) {
|
|
449
|
+
console.error(`[effect-tui] Failed to watch ${projectRoot}:`, err)
|
|
450
|
+
}
|
|
451
|
+
})()
|
|
500
452
|
|
|
501
453
|
const stop = async () => {
|
|
502
454
|
if (debounceTimer) clearTimeout(debounceTimer)
|
|
@@ -507,5 +459,5 @@ export async function devRender(entryPath: string, options?: DevRenderOptions):
|
|
|
507
459
|
renderer.stop()
|
|
508
460
|
}
|
|
509
461
|
|
|
510
|
-
return {
|
|
462
|
+
return { ready, reload, stop }
|
|
511
463
|
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -2,7 +2,10 @@ export type { UseKeyboardOptions } from "./use-keyboard.js"
|
|
|
2
2
|
export { useKeyboard } from "./use-keyboard.js"
|
|
3
3
|
export type { UseMouseOptions } from "./use-mouse.js"
|
|
4
4
|
export { useMouse } from "./use-mouse.js"
|
|
5
|
+
export type { UsePasteOptions } from "./use-paste.js"
|
|
5
6
|
export { usePaste } from "./use-paste.js"
|
|
7
|
+
export type { ShortcutHandler, ShortcutMap, UseShortcutOptions } from "./use-shortcut.js"
|
|
8
|
+
export { useShortcut } from "./use-shortcut.js"
|
|
6
9
|
export { useQuit } from "./use-quit.js"
|
|
7
10
|
export type { ScrollState, UseScrollOptions, UseScrollReturn } from "./use-scroll.js"
|
|
8
11
|
export { useScroll } from "./use-scroll.js"
|
package/src/hooks/use-mouse.ts
CHANGED
|
@@ -3,10 +3,19 @@ import { useEffect, useRef } from "react"
|
|
|
3
3
|
import { useRenderer } from "../renderer.js"
|
|
4
4
|
|
|
5
5
|
export type UseMouseOptions = {
|
|
6
|
-
/** Filter by action type */
|
|
6
|
+
/** Filter by action type (alias: phase) */
|
|
7
7
|
action?: "press" | "release" | "drag" | "move" | "any"
|
|
8
|
+
/** Filter by action type */
|
|
9
|
+
phase?: "press" | "release" | "drag" | "move" | "any"
|
|
8
10
|
/** Filter by button */
|
|
9
11
|
button?: MouseButton | MouseButton[]
|
|
12
|
+
/** Optional predicate to drop events before they reach the handler. */
|
|
13
|
+
filter?: (mouse: MouseMsg) => boolean
|
|
14
|
+
/**
|
|
15
|
+
* If true, call preventDefault when available before invoking handler.
|
|
16
|
+
* This stops further renderer-level handlers.
|
|
17
|
+
*/
|
|
18
|
+
stopPropagation?: boolean
|
|
10
19
|
}
|
|
11
20
|
|
|
12
21
|
/**
|
|
@@ -26,12 +35,16 @@ export type UseMouseOptions = {
|
|
|
26
35
|
*/
|
|
27
36
|
export function useMouse(handler: (mouse: MouseMsg) => void, opts?: UseMouseOptions): void {
|
|
28
37
|
const renderer = useRenderer()
|
|
29
|
-
const action = opts?.action ?? "any"
|
|
38
|
+
const action = opts?.phase ?? opts?.action ?? "any"
|
|
30
39
|
const button = opts?.button
|
|
40
|
+
const filter = opts?.filter
|
|
41
|
+
const stopPropagation = opts?.stopPropagation ?? false
|
|
31
42
|
|
|
32
43
|
// Store handler in ref so we always call the latest version
|
|
33
44
|
const handlerRef = useRef(handler)
|
|
34
45
|
handlerRef.current = handler
|
|
46
|
+
const filterRef = useRef(filter)
|
|
47
|
+
filterRef.current = filter
|
|
35
48
|
|
|
36
49
|
useEffect(() => {
|
|
37
50
|
const wrapped = (mouse: MouseMsg) => {
|
|
@@ -44,8 +57,11 @@ export function useMouse(handler: (mouse: MouseMsg) => void, opts?: UseMouseOpti
|
|
|
44
57
|
if (!buttons.includes(mouse.button)) return
|
|
45
58
|
}
|
|
46
59
|
|
|
60
|
+
if (filterRef.current && !filterRef.current(mouse)) return
|
|
61
|
+
if (stopPropagation && mouse.preventDefault) mouse.preventDefault()
|
|
62
|
+
|
|
47
63
|
handlerRef.current(mouse)
|
|
48
64
|
}
|
|
49
65
|
return renderer.onMouse(wrapped)
|
|
50
|
-
}, [renderer, action, button])
|
|
66
|
+
}, [renderer, action, button, stopPropagation])
|
|
51
67
|
}
|
package/src/hooks/use-paste.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react"
|
|
2
|
+
import type { PasteMsg } from "../renderer-types.js"
|
|
2
3
|
import { useRenderer } from "../renderer.js"
|
|
3
4
|
|
|
5
|
+
export type UsePasteOptions = {
|
|
6
|
+
/** Optional predicate to drop events before they reach the handler. */
|
|
7
|
+
filter?: (paste: PasteMsg) => boolean
|
|
8
|
+
/**
|
|
9
|
+
* If true, call preventDefault when available before invoking handler.
|
|
10
|
+
* This stops further renderer-level handlers.
|
|
11
|
+
*/
|
|
12
|
+
stopPropagation?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
4
15
|
/**
|
|
5
16
|
* Subscribe to bracketed paste events (if supported by renderer/terminal).
|
|
6
17
|
*
|
|
@@ -10,20 +21,28 @@ import { useRenderer } from "../renderer.js"
|
|
|
10
21
|
* @example
|
|
11
22
|
* ```tsx
|
|
12
23
|
* // No useCallback needed - just pass your handler directly
|
|
13
|
-
* usePaste((
|
|
14
|
-
* insertText(text, cursorPosition)
|
|
24
|
+
* usePaste((paste) => {
|
|
25
|
+
* insertText(paste.text, cursorPosition)
|
|
15
26
|
* })
|
|
16
27
|
* ```
|
|
17
28
|
*/
|
|
18
|
-
export function usePaste(handler: (
|
|
29
|
+
export function usePaste(handler: (paste: PasteMsg) => void, opts?: UsePasteOptions): void {
|
|
19
30
|
const renderer = useRenderer()
|
|
31
|
+
const filter = opts?.filter
|
|
32
|
+
const stopPropagation = opts?.stopPropagation ?? false
|
|
20
33
|
|
|
21
34
|
// Store handler in ref so we always call the latest version
|
|
22
35
|
const handlerRef = useRef(handler)
|
|
23
36
|
handlerRef.current = handler
|
|
37
|
+
const filterRef = useRef(filter)
|
|
38
|
+
filterRef.current = filter
|
|
24
39
|
|
|
25
40
|
useEffect(() => {
|
|
26
41
|
if (!renderer.onPaste) return
|
|
27
|
-
return renderer.onPaste((
|
|
28
|
-
|
|
42
|
+
return renderer.onPaste((paste) => {
|
|
43
|
+
if (filterRef.current && !filterRef.current(paste)) return
|
|
44
|
+
if (stopPropagation && paste.preventDefault) paste.preventDefault()
|
|
45
|
+
handlerRef.current(paste)
|
|
46
|
+
})
|
|
47
|
+
}, [renderer, stopPropagation])
|
|
29
48
|
}
|