custom dialog for editing text labels (no more window.prompt)

This commit is contained in:
Joeri Exelmans 2025-10-19 20:30:56 +02:00
parent 20f28d8382
commit 88dee7e3b9
6 changed files with 98 additions and 62 deletions

View file

@ -184,7 +184,7 @@ export function App() {
flexGrow:1, flexGrow:1,
overflow:'auto', overflow:'auto',
}}> }}>
<VisualEditor {...{ast, setAST, rt: rt.at(rtIdx!), setRT, errors, setErrors, mode, highlightActive, highlightTransitions}}/> <VisualEditor {...{ast, setAST, rt: rt.at(rtIdx!), setRT, errors, setErrors, mode, highlightActive, highlightTransitions, setModal}}/>
</Box> </Box>
{/* right sidebar */} {/* right sidebar */}

26
src/App/TextDialog.tsx Normal file
View file

@ -0,0 +1,26 @@
import { Dispatch, ReactElement, SetStateAction, useState } from "react";
export function TextDialog(props: {setModal: Dispatch<SetStateAction<ReactElement|null>>, text: string, done: (newText: string|undefined) => void}) {
const [text, setText] = useState(props.text);
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Enter") {
if (!e.shiftKey) {
e.preventDefault();
props.done(text);
props.setModal(null);
}
}
if (e.key === "Escape") {
props.setModal(null);
e.stopPropagation();
}
e.stopPropagation();
}
return <span onKeyDown={onKeyDown} style={{backgroundColor:'white'}}>
<textarea style={{fontFamily: 'Roboto', width:500, height: 100}} onChange={e=>setText(e.target.value)}>{text}</textarea>
<br/>
<span style={{backgroundColor:'lightyellow'}}>
Tip: <kbd>Shift</kbd>+<kbd>Enter</kbd> to insert newline.
</span>
</span>;
}

View file

@ -75,38 +75,40 @@ export function TopPanel({rt, rtIdx, time, setTime, onInit, onClear, onRaise, on
useEffect(() => { useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => { const onKeyDown = (e: KeyboardEvent) => {
if (e.key === " ") { if (!e.ctrlKey) {
e.preventDefault(); if (e.key === " ") {
if (rt) e.preventDefault();
onChangePaused(time.kind !== "paused", Math.round(performance.now())); if (rt)
}; onChangePaused(time.kind !== "paused", Math.round(performance.now()));
if (e.key === "i") { };
e.preventDefault(); if (e.key === "i") {
onInit(); e.preventDefault();
} onInit();
if (e.key === "c") { }
e.preventDefault(); if (e.key === "c") {
onClear(); e.preventDefault();
} onClear();
if (e.key === "Tab") { }
e.preventDefault(); if (e.key === "Tab") {
onSkip(); e.preventDefault();
} onSkip();
if (e.key === "s") { }
e.preventDefault(); if (e.key === "s") {
onSlower(); e.preventDefault();
} onSlower();
if (e.key === "f") { }
e.preventDefault(); if (e.key === "f") {
onFaster(); e.preventDefault();
} onFaster();
if (e.key === "`") { }
e.preventDefault(); if (e.key === "`") {
setShowKeys(show => !show); e.preventDefault();
} setShowKeys(show => !show);
if (e.key === "Backspace") { }
e.preventDefault(); if (e.key === "Backspace") {
onBack(); e.preventDefault();
onBack();
}
} }
}; };
window.addEventListener("keydown", onKeyDown); window.addEventListener("keydown", onKeyDown);

View file

@ -3,26 +3,28 @@ import { InsertMode } from "../VisualEditor/VisualEditor";
export function getKeyHandler(setMode: Dispatch<SetStateAction<InsertMode>>) { export function getKeyHandler(setMode: Dispatch<SetStateAction<InsertMode>>) {
return function onKeyDown(e: KeyboardEvent) { return function onKeyDown(e: KeyboardEvent) {
if (e.key === "a") { if (!e.ctrlKey) {
setMode("and"); if (e.key === "a") {
} setMode("and");
if (e.key === "o") { }
setMode("or"); if (e.key === "o") {
} setMode("or");
if (e.key === "p") { }
setMode("pseudo"); if (e.key === "p") {
} setMode("pseudo");
if (e.key === "t") { }
setMode("transition"); if (e.key === "t") {
} setMode("transition");
if (e.key === "x") { }
setMode("text"); if (e.key === "x") {
} setMode("text");
if (e.key === "h") { }
setMode(oldMode => { if (e.key === "h") {
if (oldMode === "shallow") return "deep"; setMode(oldMode => {
return "shallow"; if (oldMode === "shallow") return "deep";
}) return "shallow";
})
}
} }
} }
} }

View file

@ -1,7 +1,9 @@
import { TextDialog } from "@/App/TextDialog";
import { TraceableError } from "..//statecharts/parser"; import { TraceableError } from "..//statecharts/parser";
import {Text} from "../statecharts/concrete_syntax"; import {Text} from "../statecharts/concrete_syntax";
import { Dispatch, ReactElement, SetStateAction } from "react";
export function TextSVG(props: {text: Text, error: TraceableError|undefined, selected: boolean, highlight: boolean, onEdit: (newText: string) => void}) { export function TextSVG(props: {text: Text, error: TraceableError|undefined, selected: boolean, highlight: boolean, onEdit: (newText: string) => void, setModal: Dispatch<SetStateAction<ReactElement|null>>}) {
const commonProps = { const commonProps = {
"data-uid": props.text.uid, "data-uid": props.text.uid,
"data-parts": "text", "data-parts": "text",
@ -9,6 +11,7 @@ export function TextSVG(props: {text: Text, error: TraceableError|undefined, sel
className: "draggableText" className: "draggableText"
+ (props.selected ? " selected":"") + (props.selected ? " selected":"")
+ (props.highlight ? " highlight":""), + (props.highlight ? " highlight":""),
style: {whiteSpace: "preserve"},
} }
let textNode; let textNode;
@ -32,12 +35,13 @@ export function TextSVG(props: {text: Text, error: TraceableError|undefined, sel
key={props.text.uid} key={props.text.uid}
transform={`translate(${props.text.topLeft.x} ${props.text.topLeft.y})`} transform={`translate(${props.text.topLeft.x} ${props.text.topLeft.y})`}
onDoubleClick={() => { onDoubleClick={() => {
const newText = prompt("", props.text.text); props.setModal(<TextDialog setModal={props.setModal} text={props.text.text} done={newText => {
if (newText) { if (newText) {
props.onEdit(newText); props.onEdit(newText);
} }
}} />)
}}> }}>
{textNode} {textNode}
<text className="draggableText helper" textAnchor="middle" data-uid={props.text.uid} data-parts="text">{props.text.text}</text> <text className="draggableText helper" textAnchor="middle" data-uid={props.text.uid} data-parts="text" style={{whiteSpace: "preserve"}}>{props.text.text}</text>
</g>; </g>;
} }

View file

@ -1,4 +1,4 @@
import { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from "react"; import { Dispatch, ReactElement, SetStateAction, useEffect, useMemo, useRef, useState } from "react";
import { Statechart } from "../statecharts/abstract_syntax"; import { Statechart } from "../statecharts/abstract_syntax";
import { Arrow, ArrowPart, Diamond, History, Rountangle, RountanglePart, Text, VisualEditorState, emptyState } from "../statecharts/concrete_syntax"; import { Arrow, ArrowPart, Diamond, History, Rountangle, RountanglePart, Text, VisualEditorState, emptyState } from "../statecharts/concrete_syntax";
@ -68,9 +68,10 @@ type VisualEditorProps = {
mode: InsertMode, mode: InsertMode,
highlightActive: Set<string>, highlightActive: Set<string>,
highlightTransitions: string[], highlightTransitions: string[],
setModal: Dispatch<SetStateAction<ReactElement|null>>,
}; };
export function VisualEditor({ast, setAST, rt, errors, setErrors, mode, highlightActive, highlightTransitions}: VisualEditorProps) { export function VisualEditor({ast, setAST, rt, errors, setErrors, mode, highlightActive, highlightTransitions, setModal}: VisualEditorProps) {
const [historyState, setHistoryState] = useState<HistoryState>({current: emptyState, history: [], future: []}); const [historyState, setHistoryState] = useState<HistoryState>({current: emptyState, history: [], future: []});
const state = historyState.current; const state = historyState.current;
@ -324,7 +325,7 @@ export function VisualEditor({ast, setAST, rt, errors, setErrors, mode, highligh
setSelection([]); setSelection([]);
}; };
const onMouseMove = (e: {pageX: number, pageY: number}) => { const onMouseMove = (e: {pageX: number, pageY: number, movementX: number, movementY: number}) => {
const currentPointer = getCurrentPointer(e); const currentPointer = getCurrentPointer(e);
if (dragging) { if (dragging) {
// const pointerDelta = subtractV2D(currentPointer, dragging.lastMousePos); // const pointerDelta = subtractV2D(currentPointer, dragging.lastMousePos);
@ -812,6 +813,7 @@ export function VisualEditor({ast, setAST, rt, errors, setErrors, mode, highligh
selected={Boolean(selection.find(s => s.uid === txt.uid)?.parts?.length)} selected={Boolean(selection.find(s => s.uid === txt.uid)?.parts?.length)}
highlight={textsToHighlight.hasOwnProperty(txt.uid)} highlight={textsToHighlight.hasOwnProperty(txt.uid)}
onEdit={newText => onEditText(txt, newText)} onEdit={newText => onEditText(txt, newText)}
setModal={setModal}
/> />
})} })}