Skip to content

Commit 44e0253

Browse files
syntax highlighting in demo app code editor
1 parent 3ec973d commit 44e0253

File tree

3 files changed

+219
-4
lines changed

3 files changed

+219
-4
lines changed

demo/src/App.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import Form from 'react-bootstrap/Form';
77
import Button from 'react-bootstrap/Button';
88
import ToastContainer from 'react-bootstrap/ToastContainer';
99
import Toast from 'react-bootstrap/Toast';
10-
import SyntaxHighlighter from 'react-syntax-highlighter';
10+
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
11+
import Editor from './Editor';
1112
import { atomOneLight } from 'react-syntax-highlighter/dist/esm/styles/hljs';
1213
import { compareVersions } from 'compare-versions';
1314
import { convertRequests, loadSchema, listFormats } from "@elastic/request-converter";
@@ -77,7 +78,6 @@ function App() {
7778

7879
const onRequestChanged = (ev: React.ChangeEvent<HTMLSelectElement>): any => {
7980
setSource(ev.target.value);
80-
ev.target.style.height = ev.target.scrollHeight + "px";
8181
};
8282

8383
const copyToClipboard = async () => {
@@ -136,7 +136,7 @@ function App() {
136136
<Form id="main-form">
137137
<Row id="main-row">
138138
<Col className="col-6">
139-
<Form.Control className={error ? "is-invalid" : ""} as="textarea" id="source" value={source} onChange={(ev: any) => onRequestChanged(ev)} />
139+
<Editor className={error ? "is-invalid" : ""} id="source" value={source} onChange={(ev: any) => onRequestChanged(ev)} />
140140
{error && <Form.Control.Feedback type="invalid">{error}</Form.Control.Feedback>}
141141
</Col>
142142
<Col className="col-6">

demo/src/Editor.tsx

+211
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import React, { useRef, useState, useEffect } from 'react';
2+
import Form from 'react-bootstrap/Form';
3+
import Dropdown from 'react-bootstrap/Dropdown';
4+
import {Light as SyntaxHighlighter} from 'react-syntax-highlighter';
5+
import { atomOneLight } from 'react-syntax-highlighter/dist/esm/styles/hljs';
6+
7+
const TAB = " ";
8+
9+
SyntaxHighlighter.registerLanguage('foo', (hljs) => ({
10+
case_insensitive: true, // language is case-insensitive
11+
keywords: {
12+
keyword: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
13+
literal: ['true', 'false', 'null'],
14+
},
15+
contains: [
16+
{
17+
className: 'attr',
18+
begin: /"(\\.|[^\\"\r\n])*"(?=\s*:)/,
19+
relevance: 1.01
20+
},
21+
{
22+
match: /[{}[\],:]/,
23+
className: "punctuation",
24+
relevance: 0
25+
},
26+
hljs.QUOTE_STRING_MODE,
27+
hljs.C_NUMBER_MODE,
28+
hljs.C_LINE_COMMENT_MODE,
29+
hljs.HASH_COMMENT_MODE,
30+
{
31+
className: 'meta',
32+
begin: '\/[^\\s]*',
33+
},
34+
],
35+
}));
36+
37+
type EditorProps = {
38+
className: string,
39+
id: string,
40+
value: string,
41+
onChange: (ev: any) => any,
42+
};
43+
44+
export default function Editor({ className, id, value, onChange }: EditorProps) {
45+
const editorRef = useRef(null);
46+
const highlightRef = useRef(null);
47+
const [width, setWidth] = useState(0);
48+
49+
const updateWidth = () => {
50+
if (width != editorRef.current.parentElement.clientWidth - 1) {
51+
setWidth(editorRef.current.parentElement.clientWidth - 1);
52+
}
53+
};
54+
55+
useEffect(() => {
56+
updateWidth();
57+
}, [editorRef, width]);
58+
59+
useEffect(() => {
60+
window.addEventListener('resize', updateWidth);
61+
return () => window.removeEventListener('resize', updateWidth);
62+
}, []);
63+
64+
let height = Math.min(15, Math.max(6, value.split('\n').length)) + 1;
65+
66+
const syncScroll = () => {
67+
highlightRef.current.style.height = editorRef.current.clientHeight + "px";
68+
highlightRef.current.scrollTop = editorRef.current.scrollTop;
69+
highlightRef.current.scrollLeft = editorRef.current.scrollLeft;
70+
};
71+
72+
const handleKeyDown = ev => {
73+
const before = editorRef.current.value.slice(0, editorRef.current.selectionStart);
74+
const after = editorRef.current.value.slice(editorRef.current.selectionEnd, editorRef.current.value.length);
75+
const atEol = (after === "" || after.startsWith("\n"));
76+
const cursor_pos = editorRef.current.selectionEnd;
77+
if (ev.key == "{" && atEol) {
78+
ev.preventDefault();
79+
editorRef.current.value = before + "{}" + after;
80+
if (onChange) {
81+
onChange({target: editorRef.current});
82+
}
83+
editorRef.current.selectionStart = editorRef.current.selectionEnd = cursor_pos + 1;
84+
}
85+
else if (ev.key == "[" && atEol) {
86+
ev.preventDefault();
87+
editorRef.current.value = before + "[]" + after;
88+
if (onChange) {
89+
onChange({target: editorRef.current});
90+
}
91+
editorRef.current.selectionStart = editorRef.current.selectionEnd = cursor_pos + 1;
92+
}
93+
else if (ev.key == "\"" && atEol) {
94+
ev.preventDefault();
95+
editorRef.current.value = before + "\"\"" + after;
96+
if (onChange) {
97+
onChange({target: editorRef.current});
98+
}
99+
editorRef.current.selectionStart = editorRef.current.selectionEnd = cursor_pos + 1;
100+
}
101+
else if (ev.key == "\"" && after.startsWith("\"")) {
102+
ev.preventDefault();
103+
editorRef.current.selectionStart = editorRef.current.selectionEnd = cursor_pos + 1;
104+
}
105+
else if (ev.key == "Backspace") {
106+
const keyPairs = ["{}", "[]", "\"\""];
107+
let matches = false;
108+
console.log(after.startsWith);
109+
for (const keyPair of keyPairs) {
110+
if (before.endsWith(keyPair[0]) && ((after === keyPair[1]) || (after.startsWith(`${keyPair[1]}\n`)))) {
111+
matches = true;
112+
break;
113+
}
114+
}
115+
if (matches) {
116+
ev.preventDefault();
117+
editorRef.current.value = before.slice(0, before.length - 1) + after.slice(1);
118+
if (onChange) {
119+
onChange({target: editorRef.current});
120+
}
121+
editorRef.current.selectionStart = editorRef.current.selectionEnd = cursor_pos - 1;
122+
}
123+
}
124+
else if (ev.key == "Enter" && (before.endsWith("{") || before.endsWith("["))) {
125+
ev.preventDefault();
126+
const lines = before.split("\n");
127+
const currentLine = lines[lines.length - 1];
128+
const indent = currentLine.split(/[^\s]/, 1)[0];
129+
const extra = (after.startsWith("}") || after.startsWith("]")) ? `\n${indent}` : "";
130+
editorRef.current.value = `${before}\n${indent}${TAB}${extra}${after}`;
131+
if (onChange) {
132+
onChange({target: editorRef.current});
133+
}
134+
editorRef.current.selectionStart = editorRef.current.selectionEnd = cursor_pos + indent.length + TAB.length + 1;
135+
height += 2;
136+
setTimeout(syncScroll, 50); // in case height changes due to this update
137+
}
138+
else if (ev.key == "Enter") {
139+
ev.preventDefault();
140+
const lines = before.split("\n");
141+
const currentLine = lines[lines.length - 1];
142+
const indent = currentLine.split(/[^\s]/, 1)[0];
143+
editorRef.current.value = `${before}\n${indent}${after}`;
144+
if (onChange) {
145+
onChange({target: editorRef.current});
146+
}
147+
editorRef.current.selectionStart = cursor_pos + indent.length + 1;
148+
editorRef.current.selectionEnd = cursor_pos + indent.length + 1;
149+
height += 1;
150+
setTimeout(syncScroll, 50); // in case height changes due to this update
151+
}
152+
else if (ev.key == "Tab") {
153+
ev.preventDefault();
154+
editorRef.current.value = `${before}${TAB}${after}`;
155+
if (onChange) {
156+
onChange({target: editorRef.current});
157+
}
158+
editorRef.current.selectionStart = editorRef.current.selectionEnd = cursor_pos + TAB.length;
159+
}
160+
};
161+
162+
const changedEvent = ev => {
163+
if (onChange) {
164+
onChange(ev);
165+
}
166+
syncScroll();
167+
};
168+
169+
return (
170+
<>
171+
<Form.Control
172+
className={className}
173+
as="textarea"
174+
rows={height}
175+
spellCheck="false"
176+
id={id}
177+
value={value}
178+
style={{
179+
position: 'absolute',
180+
color: 'transparent',
181+
background: 'transparent',
182+
caretColor: 'black',
183+
width: (width - 24) + "px",
184+
tabSize: 2,
185+
}}
186+
onChange={changedEvent}
187+
onScroll={syncScroll}
188+
onKeyDown={handleKeyDown}
189+
ref={editorRef}
190+
/>
191+
<div className={(className ? className + " " : "") + "form-control"} style={{
192+
borderColor: 'transparent',
193+
overflow: 'auto',
194+
whiteSpace: 'pre',
195+
width: (width - 24) + "px",
196+
}} ref={highlightRef}>
197+
<SyntaxHighlighter
198+
wrapLongLines={true}
199+
language="foo"
200+
style={atomOneLight}
201+
customStyle={{
202+
margin: 0,
203+
padding: 0,
204+
tabSize: 2,
205+
}}
206+
codeTagProps={{"aria-hidden": false}}
207+
>{value + " "}</SyntaxHighlighter>
208+
</div>
209+
</>
210+
);
211+
}

demo/src/index.css

+5-1
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@ h1 {
1010

1111
textarea.form-control {
1212
resize: none;
13-
min-height: 200px;
13+
min-height: 6em;
1414
font-family: monospace;
1515
font-size: 14px;
1616
}
1717

18+
.is-invalid.form-control {
19+
background-position: top calc(.375em + .1875rem) right calc(.375em + .1875rem);
20+
}
21+
1822
#main-container {
1923
margin-top: 10px;
2024
}

0 commit comments

Comments
 (0)