Move the highlightable textarea into its own component.

This commit is contained in:
Tom Alexander 2024-01-28 17:34:38 -05:00
parent 33ed9c4f56
commit d158fafd77
Signed by: talexander
GPG Key ID: D3A179C9A53C0EDE
6 changed files with 122 additions and 88 deletions

52
src/Editor.module.css Normal file
View File

@ -0,0 +1,52 @@
.EditorTextWrapper {
/* TODO: This flex should be in the parent */
flex: 1;
flex-basis: 0;
position: relative;
background: white;
font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas,
"DejaVu Sans Mono", monospace;
font-weight: normal;
font-size: 1.3rem;
text-align: initial;
}
.EditorTextArea,
.EditorUnderlay {
font-family: inherit;
font-weight: inherit;
font-size: inherit;
padding: 5px;
box-sizing: border-box;
margin: 0;
line-height: normal;
}
.EditorTextArea {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
border: none;
background-color: transparent;
resize: none;
outline: none;
}
.EditorUnderlay {
width: 100%;
height: 100%;
pointer-events: none;
color: transparent;
white-space: pre-wrap;
word-wrap: break-word;
overflow-y: scroll;
.EditorHighlighted {
background: #ffff00;
}
}

4
src/Editor.module.css.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
export const EditorTextWrapper: string;
export const EditorTextArea: string;
export const EditorUnderlay: string;
export const EditorHighlighted: string;

View File

@ -0,0 +1,54 @@
import React, { useEffect, useRef } from "react";
import { Highlight } from "./highlight";
import { buildShadow } from "./shadow";
import styles from "./Editor.module.css";
interface EditorProps {
value: string;
setValue: Function;
highlights: Highlight[];
clearHighlights: Function;
}
function Editor({
value,
setValue,
highlights,
clearHighlights,
}: EditorProps): React.ReactNode {
function handleChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
setValue(event.target.value);
clearHighlights();
}
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const shadowRef = useRef<HTMLDivElement>(null);
function onTextAreaScroll() {
if (shadowRef.current !== null && textAreaRef.current !== null) {
const textAreaScrollTop = textAreaRef.current.scrollTop;
shadowRef.current.scrollTop = textAreaScrollTop;
}
}
useEffect(() => {
// Make sure the text area and shadow div start out in sync.
onTextAreaScroll();
}, []);
return (
<div className={styles.EditorTextWrapper}>
<textarea
ref={textAreaRef}
onChange={handleChange}
className={styles.EditorTextArea}
value={value}
onScroll={onTextAreaScroll}
/>
<div ref={shadowRef} className={styles.EditorUnderlay}>
{buildShadow(highlights, value)}
<br />
</div>
</div>
);
}
export default Editor;

View File

@ -4,55 +4,3 @@
height: 100%;
width: 100%;
}
.Explorer-textwrapper {
flex: 1;
flex-basis: 0;
position: relative;
background: white;
font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas,
"DejaVu Sans Mono", monospace;
font-weight: normal;
font-size: 1.3rem;
text-align: initial;
}
.Explorer-textarea,
.Explorer-underlay {
font-family: inherit;
font-weight: inherit;
font-size: inherit;
padding: 5px;
box-sizing: border-box;
margin: 0;
line-height: normal;
}
.Explorer-textarea {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
border: none;
background-color: transparent;
resize: none;
outline: none;
}
.Explorer-underlay {
width: 100%;
height: 100%;
pointer-events: none;
color: transparent;
white-space: pre-wrap;
word-wrap: break-word;
overflow-y: scroll;
.highlighted {
background: #ffff00;
}
}

View File

@ -1,9 +1,9 @@
import React, { ReactNode, useEffect, useMemo, useRef, useState } from "react";
import React, { useMemo, useState } from "react";
import "./Explorer.css";
import { Highlight } from "./highlight";
import { buildShadow } from "./shadow";
import OrgAst, { OrgNodeReference } from "./OrgAst";
import { parse_org } from "../../organic/target/wasm32-unknown-unknown/js/wasm";
import Editor from "./Editor";
const default_org_source: string = `* Welcome to the Organic Ast Explorer!
@ -25,11 +25,6 @@ interface ExplorerProps {
defaultValue?: string;
}
function Explorer({ defaultValue = default_org_source }: ExplorerProps) {
function handleChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
setValue(event.target.value);
clearHighlights();
}
const [value, setValue] = useState(defaultValue);
const [highlights, setHighlights] = useState<Array<Highlight>>([]);
@ -66,35 +61,15 @@ function Explorer({ defaultValue = default_org_source }: ExplorerProps) {
setHighlights([]);
}
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const shadowRef = useRef<HTMLDivElement>(null);
function onTextAreaScroll() {
if (shadowRef.current !== null && textAreaRef.current !== null) {
const textAreaScrollTop = textAreaRef.current.scrollTop;
shadowRef.current.scrollTop = textAreaScrollTop;
}
}
useEffect(() => {
// Make sure the text area and shadow div start out in sync.
onTextAreaScroll();
}, []);
return (
<div className="Explorer">
<div className="Explorer-textwrapper">
<textarea
ref={textAreaRef}
onChange={handleChange}
className="Explorer-textarea"
value={value}
onScroll={onTextAreaScroll}
/>
<div ref={shadowRef} className="Explorer-underlay">
{buildShadow(highlights, value)}
<br />
</div>
</div>
<Editor
value={value}
setValue={setValue}
highlights={highlights}
clearHighlights={clearHighlights}
/>
<OrgAst
setHighlight={setHighlight}
clearHighlights={clearHighlights}

View File

@ -1,5 +1,6 @@
import React, { ReactNode, useState } from "react";
import React, { ReactNode } from "react";
import { Highlight } from "./highlight";
import styles from "./Editor.module.css";
function buildShadow(highlights: Highlight[], text: string): ReactNode[] {
let output: ReactNode[] = [];
@ -22,7 +23,7 @@ function buildShadow(highlights: Highlight[], text: string): ReactNode[] {
} else if (state == ShadowState.Highlight && !thisCharHighlighted) {
// End the span
output.push(
<span key={i} className="highlighted">
<span key={i} className={styles.EditorHighlighted}>
{buffer}
</span>,
);
@ -37,7 +38,7 @@ function buildShadow(highlights: Highlight[], text: string): ReactNode[] {
output.push(buffer);
} else if (state == ShadowState.Highlight) {
output.push(
<span key={i} className="highlighted">
<span key={i} className={styles.EditorHighlighted}>
{buffer}
</span>,
);