Files
lucko-paste/src/components/EditorTextArea.js

306 lines
7.7 KiB
JavaScript
Raw Normal View History

2021-05-21 11:56:05 +01:00
import { useState, useEffect, useRef } from 'react';
2021-03-27 11:49:46 +00:00
import styled from 'styled-components';
2021-03-26 22:00:12 +00:00
import ReactEditor from 'react-simple-code-editor';
2021-03-27 11:49:46 +00:00
import EditorPrismStyle from './EditorPrismStyle';
2021-04-02 18:54:37 +01:00
import { getHighlighter } from '../util/highlighting';
2021-03-26 22:00:12 +00:00
2021-03-27 14:17:28 +00:00
export default function EditorTextArea({ code, setCode, language, fontSize }) {
const [isSelected, isSelectionMiddle, toggleSelected] = useSelectedLine();
2021-03-26 22:00:12 +00:00
const highlight = getHighlighter(language);
function highlightWithLineNumbers(input, grammar) {
return highlight(input, grammar)
.split(/\r?\n/)
2021-05-21 11:56:05 +01:00
.map((line, i) => (
<span key={i}>
<LineNumber
lineNo={i + 1}
selected={isSelected(i + 1)}
shouldScroll={isSelectionMiddle(i + 1)}
2021-05-21 11:56:05 +01:00
toggleSelected={toggleSelected}
/>
<span dangerouslySetInnerHTML={{ __html: line }} />
</span>
))
.reduce((acc, curr, idx) => {
if (idx !== 0) {
acc.push('\n');
}
acc.push(curr);
return acc;
}, []);
2021-03-26 22:00:12 +00:00
}
2021-05-20 16:19:23 +01:00
const autoBracketState = useState(null);
2021-03-28 17:39:16 +01:00
const editorRef = useRef();
function keydown(e) {
2021-05-20 16:19:23 +01:00
handleKeydown(e, editorRef.current, autoBracketState);
2021-03-28 17:39:16 +01:00
}
2021-03-26 22:00:12 +00:00
return (
2021-03-27 11:49:46 +00:00
<EditorPrismStyle>
<StyledReactEditor
2021-03-28 17:39:16 +01:00
ref={editorRef}
2021-03-26 22:00:12 +00:00
value={code}
onValueChange={setCode}
highlight={highlightWithLineNumbers}
2021-03-27 20:05:15 +00:00
placeholder={'Paste (or type) some code...'}
2021-03-26 22:00:12 +00:00
padding={10}
2021-03-27 14:17:28 +00:00
size={fontSize}
2021-04-02 13:05:15 +01:00
textareaId="code-area"
autoFocus={true}
2021-03-28 17:39:16 +01:00
onKeyDown={keydown}
2021-03-26 22:00:12 +00:00
/>
2021-03-27 11:49:46 +00:00
</EditorPrismStyle>
2021-04-02 13:05:15 +01:00
);
2021-03-26 22:00:12 +00:00
}
2021-03-27 11:49:46 +00:00
const LineNumber = ({ lineNo, selected, shouldScroll, toggleSelected }) => {
const autoScroll = useAutoScroll(shouldScroll);
2021-05-21 11:56:05 +01:00
function click(e) {
toggleSelected(lineNo, e.shiftKey);
}
return selected ? (
<HighlightedLineNumber ref={autoScroll} onClick={click}>
{lineNo}
</HighlightedLineNumber>
2021-05-21 11:56:05 +01:00
) : (
<PlainLineNumber ref={autoScroll} onClick={click}>
{lineNo}
</PlainLineNumber>
2021-05-21 11:56:05 +01:00
);
};
2021-03-27 11:49:46 +00:00
const StyledReactEditor = styled(ReactEditor)`
counter-reset: line;
2021-03-27 14:17:28 +00:00
font-size: ${props => props.size}px;
2021-03-27 11:49:46 +00:00
outline: 0;
2021-03-27 14:17:28 +00:00
min-height: calc(100vh - 2rem);
2021-03-27 11:49:46 +00:00
#code-area {
outline: none;
padding-left: 60px !important;
}
pre {
padding-left: 60px !important;
}
2021-05-21 11:56:05 +01:00
`;
2021-03-27 11:49:46 +00:00
2021-05-21 11:56:05 +01:00
const PlainLineNumber = styled.span`
position: absolute;
left: 0px;
color: ${props => props.theme.editor.lineNumber};
text-align: right;
width: 40px;
font-weight: 100;
user-select: none;
// override parent <pre>
pointer-events: auto;
cursor: pointer;
2021-03-27 11:49:46 +00:00
`;
2021-03-28 17:39:16 +01:00
2021-05-21 11:56:05 +01:00
const HighlightedLineNumber = styled(PlainLineNumber)`
color: ${props => props.theme.editor.lineNumberHl};
background-color: ${props => props.theme.editor.lineNumberHlBackground};
font-weight: bold;
`;
function useSelectedLine() {
// extract highlighted lines from window hash
const [selected, setSelected] = useState(() => {
const hash = window.location.hash;
if (/^#L\d+(-\d+)?$/.test(hash)) {
const [start, end] = hash.substring(2).split('-').map(Number);
return [start, isNaN(end) ? -1 : end];
} else {
return [-1, -1];
}
});
// update window hash when a new line is highlighted
useEffect(() => {
if (selected[0] !== -1) {
if (selected[1] !== -1) {
2021-05-21 13:57:36 +01:00
const start = Math.min(...selected);
const end = Math.max(...selected);
window.location.hash = `#L${start}-${end}`;
2021-05-21 11:56:05 +01:00
} else {
window.location.hash = `#L${selected[0]}`;
}
}
}, [selected]);
// toggle the highlighting for a given line
function toggleSelected(lineNo, shift) {
if (selected[0] === lineNo && selected[1] === -1) {
setSelected([-1, -1]);
} else if (selected[0] === -1 || !shift) {
setSelected([lineNo, -1]);
} else {
setSelected([selected[0], lineNo]);
}
}
// should a line be highlighted in the viewer?
function isSelected(lineNo) {
if (selected[0] === -1) {
return false;
}
if (selected[1] === -1) {
return selected[0] === lineNo;
}
return lineNo >= Math.min(...selected) && lineNo <= Math.max(...selected);
}
// is a line in the middle of the selection
function isSelectionMiddle(lineNo) {
if (selected[0] === -1) {
return false;
}
if (selected[1] === -1) {
return selected[0] === lineNo;
}
return (
lineNo === Math.floor((Math.min(...selected) + Math.max(...selected)) / 2)
);
}
return [isSelected, isSelectionMiddle, toggleSelected];
}
function useAutoScroll(shouldScroll) {
const [firstRender, setFirstRender] = useState(true);
const ref = useRef(null);
useEffect(() => {
// only attempt to autoscroll if this is the first render.
if (!firstRender) {
return;
}
setFirstRender(false);
if (shouldScroll) {
ref.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, [shouldScroll, firstRender]);
return ref;
2021-05-21 11:56:05 +01:00
}
2021-03-28 17:39:16 +01:00
const KEYCODE_ENTER = 13;
const KEYCODE_PARENS = 57;
2021-05-20 16:17:54 +01:00
const KEYCODE_PARENS_CLOSE = 48;
2021-03-28 17:39:16 +01:00
const KEYCODE_BRACKETS = 219;
2021-05-20 16:17:54 +01:00
const KEYCODE_BRACKETS_CLOSE = 221;
2021-03-28 17:39:16 +01:00
const KEYCODE_QUOTE = 222;
const KEYCODE_BACK_QUOTE = 192;
2021-05-20 16:17:54 +01:00
function getPair({ keyCode, shiftKey }) {
if (keyCode === KEYCODE_PARENS && shiftKey) {
2021-03-28 17:39:16 +01:00
return ['(', ')'];
2021-05-20 16:17:54 +01:00
} else if (keyCode === KEYCODE_BRACKETS) {
if (shiftKey) {
2021-03-28 17:39:16 +01:00
return ['{', '}'];
} else {
return ['[', ']'];
}
2021-05-20 16:17:54 +01:00
} else if (keyCode === KEYCODE_QUOTE) {
if (shiftKey) {
2021-03-28 17:39:16 +01:00
return ['"', '"'];
} else {
return ["'", "'"];
}
2021-05-20 16:17:54 +01:00
} else if (keyCode === KEYCODE_BACK_QUOTE && !shiftKey) {
2021-03-28 17:39:16 +01:00
return ['`', '`'];
}
return null;
}
2021-05-20 16:17:54 +01:00
function handleKeydown(e, editor, [autoBracket, setAutoBracket]) {
2021-03-28 17:39:16 +01:00
const { value, selectionStart, selectionEnd } = e.target;
if (selectionStart !== selectionEnd) {
return;
}
2021-05-20 16:17:54 +01:00
// If the user types a closing bracket explictly, just jump to after the automatically added one
if (
selectionStart !== 0 &&
autoBracket === e.key &&
(e.keyCode === KEYCODE_BRACKETS_CLOSE || e.keyCode === KEYCODE_PARENS_CLOSE)
) {
e.preventDefault();
editor._applyEdits({
value: value,
selectionStart: selectionStart + 1,
selectionEnd: selectionStart + 1,
});
setAutoBracket(null);
return;
}
// reset auto brackets
setAutoBracket(null);
2021-03-28 17:39:16 +01:00
// When entering an open bracket/quote, add the closing one
const pair = getPair(e);
if (pair) {
2021-05-20 16:17:54 +01:00
// don't add double apostrophes if it looks like a sentence
if (
e.keyCode === KEYCODE_QUOTE &&
!e.shiftKey &&
selectionStart !== 0 &&
/[a-zA-Z]/.test(value.charAt(selectionStart - 1))
) {
return;
}
2021-03-28 17:39:16 +01:00
e.preventDefault();
editor._applyEdits({
2021-04-02 13:05:15 +01:00
value:
value.substring(0, selectionStart) +
pair[0] +
pair[1] +
value.substring(selectionEnd),
2021-03-28 17:39:16 +01:00
selectionStart: selectionStart + 1,
2021-04-02 13:05:15 +01:00
selectionEnd: selectionStart + 1,
2021-03-28 17:39:16 +01:00
});
2021-05-20 16:17:54 +01:00
setAutoBracket(pair[1]);
2021-03-28 17:39:16 +01:00
}
// When pressing enter immediately after an open bracket, automatically add a newline plus extra indent
2021-04-02 13:05:15 +01:00
if (
e.keyCode === KEYCODE_ENTER &&
selectionEnd !== 0 &&
value[selectionEnd - 1] === '{'
) {
2021-03-28 17:39:16 +01:00
const line = editor._getLines(value, selectionStart).pop();
const matches = line.match(/^\s+/);
2021-04-02 13:05:15 +01:00
const existingIndent = matches ? matches[0] : '';
2021-03-28 17:39:16 +01:00
const indent = ' ';
2021-04-02 13:05:15 +01:00
const updatedValue =
value.substring(0, selectionStart) +
'\n' +
existingIndent +
indent +
'\n' +
existingIndent +
value.substring(selectionEnd);
const updatedSelection =
selectionStart + 1 /* newline */ + existingIndent.length + indent.length;
2021-03-28 17:39:16 +01:00
e.preventDefault();
editor._applyEdits({
value: updatedValue,
selectionStart: updatedSelection,
2021-04-02 13:05:15 +01:00
selectionEnd: updatedSelection,
});
2021-03-28 17:39:16 +01:00
}
}