-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Expand file tree
/
Copy pathTextEditor.tsx
More file actions
125 lines (116 loc) · 3.57 KB
/
Copy pathTextEditor.tsx
File metadata and controls
125 lines (116 loc) · 3.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import type { ViewUpdate } from "@codemirror/view";
import { EditorView, lineNumbers } from "@codemirror/view";
import { CheckIcon, ClipboardIcon } from "@heroicons/react/20/solid";
import type { ReactCodeMirrorProps, UseCodeMirror } from "@uiw/react-codemirror";
import { useCodeMirror } from "@uiw/react-codemirror";
import { useCallback, useEffect, useRef, useState } from "react";
import { cn } from "~/utils/cn";
import { Button } from "../primitives/Buttons";
import { getEditorSetup } from "./codeMirrorSetup";
import { darkTheme } from "./codeMirrorTheme";
export interface TextEditorProps extends Omit<ReactCodeMirrorProps, "onBlur"> {
defaultValue?: string;
readOnly?: boolean;
onChange?: (value: string) => void;
onUpdate?: (update: ViewUpdate) => void;
showCopyButton?: boolean;
additionalActions?: React.ReactNode;
}
export function TextEditor(opts: TextEditorProps) {
const {
defaultValue = "",
readOnly = false,
onChange,
onUpdate,
autoFocus,
showCopyButton = true,
additionalActions,
} = opts;
// Don't use default line numbers from setup — add our own with proper sizing
const extensions = getEditorSetup(false);
extensions.push(EditorView.lineWrapping);
extensions.push(
lineNumbers({
formatNumber: (n) => String(n),
})
);
extensions.push(
EditorView.theme({
".cm-lineNumbers": {
minWidth: "40px",
},
})
);
const editor = useRef<HTMLDivElement>(null);
const settings: Omit<UseCodeMirror, "onBlur"> = {
...opts,
container: editor.current,
extensions,
editable: !readOnly,
contentEditable: !readOnly,
value: defaultValue,
autoFocus,
theme: darkTheme(),
indentWithTab: false,
basicSetup: false,
onChange,
onUpdate,
};
const { setContainer, view } = useCodeMirror(settings);
const [copied, setCopied] = useState(false);
useEffect(() => {
if (editor.current) {
setContainer(editor.current);
}
}, [setContainer]);
useEffect(() => {
if (view !== undefined) {
if (view.state.doc.toString() === defaultValue) return;
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: defaultValue },
});
}
}, [defaultValue, view]);
const copy = useCallback(() => {
if (view === undefined) return;
navigator.clipboard.writeText(view.state.doc.toString());
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}, [view]);
const showToolbar = showCopyButton || additionalActions;
return (
<div
className={cn(
"grid",
showToolbar ? "grid-rows-[2.5rem_1fr]" : "grid-rows-[1fr]",
opts.className
)}
>
{showToolbar && (
<div className="mx-3 flex items-center justify-between gap-2 border-b border-grid-dimmed">
<div className="flex items-center">{additionalActions}</div>
<div className="flex items-center gap-2">
{showCopyButton && (
<Button
type="button"
variant="minimal/small"
TrailingIcon={copied ? CheckIcon : ClipboardIcon}
trailingIconClassName={
copied ? "text-green-500 group-hover:text-green-500" : undefined
}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
copy();
}}
>
Copy
</Button>
)}
</div>
</div>
)}
<div className="min-h-0 min-w-0 overflow-auto" ref={editor} />
</div>
);
}