diff --git a/package.json b/package.json index 300e3a7d..f62aaef3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dodona/papyros", - "version": "0.4.1", + "version": "0.4.3", "private": false, "homepage": ".", "devDependencies": { diff --git a/src/App.ts b/src/App.ts index 5536abb7..d7042020 100644 --- a/src/App.ts +++ b/src/App.ts @@ -5,8 +5,28 @@ import { } from "./Constants"; import { Papyros, PapyrosConfig } from "./Papyros"; import { InputMode } from "./InputManager"; +import { BatchInputHandler } from "./input/BatchInputHandler"; +import { CodeMirrorEditor } from "./editor/CodeMirrorEditor"; -const LOCAL_STORAGE_CODE_KEY = addPapyrosPrefix("previous-code"); +const LOCAL_STORAGE_KEYS = { + code: addPapyrosPrefix("previous-code"), + input: addPapyrosPrefix("previous-batch-input") +}; + +function setUpEditor(editor: CodeMirrorEditor, storageKey: string): void { + const previousValue = window.localStorage.getItem(storageKey); + if (previousValue) { + editor.setText(previousValue); + } + editor.onChange( + { + onChange: (text: string) => { + window.localStorage.setItem(storageKey, text); + }, + delay: 0 + } + ); +} async function startPapyros(): Promise { // Retrieve initial locale and programming language from URL @@ -40,19 +60,11 @@ async function startPapyros(): Promise { }, darkMode: darkMode }); - // Restore previous code if it existed - const previousCode = window.localStorage.getItem(LOCAL_STORAGE_CODE_KEY); - if (previousCode) { - papyros.setCode(previousCode); + setUpEditor(papyros.codeRunner.editor, LOCAL_STORAGE_KEYS.code); + const handler = papyros.codeRunner.inputManager.getInputHandler(InputMode.Batch); + if (handler instanceof BatchInputHandler) { + setUpEditor((handler as BatchInputHandler).batchEditor, LOCAL_STORAGE_KEYS.input); } - papyros.codeRunner.editor.onChange( - { - onChange: (code: string) => { - window.localStorage.setItem(LOCAL_STORAGE_CODE_KEY, code); - }, - delay: 0 - } - ); await papyros.launch(); } diff --git a/src/InputManager.ts b/src/InputManager.ts index 7668bb25..e15733b1 100644 --- a/src/InputManager.ts +++ b/src/InputManager.ts @@ -5,7 +5,7 @@ import { } from "./Constants"; import { BackendEvent, BackendEventType } from "./BackendEvent"; import { - addListener, + addListener, getElement, } from "./util/Util"; import { InteractiveInputHandler } from "./input/InteractiveInputHandler"; import { UserInputHandler } from "./input/UserInputHandler"; @@ -38,6 +38,7 @@ export class InputManager extends Renderable { constructor(sendInput: (input: string) => void, inputMode: InputMode) { super(); + this.onUserInput = this.onUserInput.bind(this); this.inputHandlers = this.buildInputHandlerMap(); this.inputMode = inputMode; this.sendInput = sendInput; @@ -50,9 +51,9 @@ export class InputManager extends Renderable { private buildInputHandlerMap(): Map { const interactiveInputHandler: UserInputHandler = - new InteractiveInputHandler(() => this.onUserInput()); + new InteractiveInputHandler(this.onUserInput); const batchInputHandler: UserInputHandler = - new BatchInputHandler(() => this.onUserInput()); + new BatchInputHandler(this.onUserInput); return new Map([ [InputMode.Interactive, interactiveInputHandler], [InputMode.Batch, batchInputHandler] @@ -70,8 +71,12 @@ export class InputManager extends Renderable { this.inputHandler.toggle(true); } + public getInputHandler(inputMode: InputMode): UserInputHandler { + return this.inputHandlers.get(inputMode)!; + } + public get inputHandler(): UserInputHandler { - return this.inputHandlers.get(this.inputMode)!; + return this.getInputHandler(this.inputMode)!; } public isWaiting(): boolean { @@ -108,14 +113,9 @@ ${switchMode}`); this.inputHandler.waitWithPrompt(this.waiting, this.prompt); } - private onUserInput(): void { - if (this.inputHandler.hasNext()) { - const line = this.inputHandler.next(); - this.sendInput(line); - this.waitWithPrompt(false); - } else { - this.waitWithPrompt(true, this.prompt); - } + private onUserInput(line: string): void { + this.sendInput(line); + this.waitWithPrompt(false); } /** @@ -123,16 +123,18 @@ ${switchMode}`); * @param {BackendEvent} e Event containing the input data */ private onInputRequest(e: BackendEvent): void { - this.prompt = e.data; - this.onUserInput(); + this.waitWithPrompt(true, e.data); } private onRunStart(): void { + // Prevent switching input mode during runs + getElement(SWITCH_INPUT_MODE_A_ID).hidden = true; this.waitWithPrompt(false); this.inputHandler.onRunStart(); } private onRunEnd(): void { + getElement(SWITCH_INPUT_MODE_A_ID).hidden = false; this.inputHandler.onRunEnd(); this.waitWithPrompt(false); } diff --git a/src/Papyros.css b/src/Papyros.css index 9285acf2..a89d84ba 100644 --- a/src/Papyros.css +++ b/src/Papyros.css @@ -116,7 +116,7 @@ Ensure the default browser behavior of the `hidden` attribute. */ [hidden] { - display: none; + display: none !important; } .cm-content, diff --git a/src/Translations.js b/src/Translations.js index 99090663..60cbd157 100644 --- a/src/Translations.js +++ b/src/Translations.js @@ -51,7 +51,7 @@ const ENGLISH_TRANSLATION = { "run_modes": { "doctest": "Run doctests" }, - "used_input": "This line was used as input by your code.", + "used_input": "This line has already been used as input.", "used_input_with_prompt": "This line was used as input for the following prompt: %{prompt}" }; @@ -104,7 +104,7 @@ const DUTCH_TRANSLATION = { "run_modes": { "doctest": "Run doctests" }, - "used_input": "Deze regel werd gebruikt als invoer.", + "used_input": "Deze regel werd al gebruikt als invoer.", "used_input_with_prompt": "Deze regel werd gebruikt als invoer voor de volgende vraag: %{prompt}" }; diff --git a/src/editor/BatchInputEditor.ts b/src/editor/BatchInputEditor.ts index c8914268..d1970f49 100644 --- a/src/editor/BatchInputEditor.ts +++ b/src/editor/BatchInputEditor.ts @@ -1,18 +1,29 @@ import { CodeMirrorEditor } from "./CodeMirrorEditor"; import { UsedInputGutters, UsedInputGutterInfo } from "./Gutters"; +import { ViewUpdate } from "@codemirror/view"; /** - * Editor to handle and highlight user input + * Arguments used to higlight lines in the Editor */ -export class BatchInputEditor extends CodeMirrorEditor { +interface HighlightArgs { /** - * Style classes used to highlight lines + * Whether the user's code is currently running and using input */ - private static HIGHLIGHT_CLASSES = ["cm-activeLine"]; + running: boolean; + /** + * Function to obtain gutter info per line (1-based indexing) + */ + getInfo: (lineInfo: number) => UsedInputGutterInfo; +} +/** + * Editor to handle and highlight user input + */ +export class BatchInputEditor extends CodeMirrorEditor { /** * Gutters to show which lines were used */ private usedInputGutters: UsedInputGutters; + private lastHighlightArgs?: HighlightArgs; constructor() { super(new Set([CodeMirrorEditor.PLACEHOLDER, CodeMirrorEditor.STYLE]), { @@ -21,24 +32,57 @@ export class BatchInputEditor extends CodeMirrorEditor { "dark:_tw-bg-dark-mode-bg", "dark:_tw-border-dark-mode-content", "focus:_tw-outline-none", "focus:_tw-ring-1", "focus:_tw-ring-blue-500"], minHeight: "10vh", - maxHeight: "20vh" - } - ); + maxHeight: "20vh", + theme: {} + }); this.usedInputGutters = new UsedInputGutters(); this.addExtension(this.usedInputGutters.toExtension()); } + private getLastHighlightArgs(): HighlightArgs { + return this.lastHighlightArgs || { + running: false, + getInfo: lineNr => { + return { + lineNr, + on: false, + title: "" + }; + } + }; + } + + protected override onViewUpdate(v: ViewUpdate): void { + super.onViewUpdate(v); + // Ensure that highlighting occurs after CodeMirrors internal update + // so that the style classes are not overwritten + setTimeout(() => { + this.highlight(this.getLastHighlightArgs()); + }, 10); + } + /** * Apply highlighting to the lines in the Editor - * @param {function(number): UsedInputGutterInfo} getInfo Function to obtain gutter - * info per line (1-based indexing) + * @param {HightlightArgs} args Arguments for highlighting + * @param {Array} highlightClasses HTML classes to use for consumed lines */ - public highlight(getInfo: (lineNr: number) => UsedInputGutterInfo): void { + public highlight(args: HighlightArgs, + highlightClasses = ["_tw-bg-slate-200", "dark:_tw-bg-slate-500"]): void { + this.lastHighlightArgs = args; + const { + running, getInfo + } = args; + let nextLineToUse = 0; this.editorView.dom.querySelectorAll(".cm-line").forEach((line, i) => { const info = getInfo(i + 1); - BatchInputEditor.HIGHLIGHT_CLASSES.forEach(c => { - line.classList.toggle(c, info.on); + if (info.on) { + nextLineToUse += 1; + } + line.classList.toggle("cm-activeLine", running && i === nextLineToUse); + highlightClasses.forEach(className => { + line.classList.toggle(className, i !== nextLineToUse && info.on); }); + line.setAttribute("contenteditable", "" + (!running || !info.on)); this.usedInputGutters.setMarker(this.editorView, info); }); } diff --git a/src/editor/CodeEditor.ts b/src/editor/CodeEditor.ts index 8fe18971..1f26cabe 100644 --- a/src/editor/CodeEditor.ts +++ b/src/editor/CodeEditor.ts @@ -17,7 +17,7 @@ import { } from "@codemirror/language"; import { highlightSelectionMatches, searchKeymap } from "@codemirror/search"; import { EditorState, Extension } from "@codemirror/state"; -import { oneDark } from "@codemirror/theme-one-dark"; +import { oneDarkHighlightStyle } from "@codemirror/theme-one-dark"; import { EditorView, showPanel, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, @@ -25,6 +25,7 @@ import { } from "@codemirror/view"; import { Diagnostic, linter, lintGutter, lintKeymap } from "@codemirror/lint"; import { CodeMirrorEditor } from "./CodeMirrorEditor"; +import { darkTheme } from "./DarkTheme"; /** * Component that provides useful features to users writing code @@ -76,9 +77,9 @@ export class CodeEditor extends CodeMirrorEditor { public override setDarkMode(darkMode: boolean): void { let styleExtensions: Extension = []; if (darkMode) { - styleExtensions = oneDark; + styleExtensions = [darkTheme, syntaxHighlighting(oneDarkHighlightStyle)]; } else { - styleExtensions = syntaxHighlighting(defaultHighlightStyle, { fallback: true }); + styleExtensions = syntaxHighlighting(defaultHighlightStyle); } this.reconfigure([CodeMirrorEditor.STYLE, styleExtensions]); } diff --git a/src/editor/CodeMirrorEditor.ts b/src/editor/CodeMirrorEditor.ts index 076c491b..16713966 100644 --- a/src/editor/CodeMirrorEditor.ts +++ b/src/editor/CodeMirrorEditor.ts @@ -2,7 +2,7 @@ import { Compartment, EditorState, Extension, StateEffect } from "@codemirror/st import { EditorView, placeholder, ViewUpdate } from "@codemirror/view"; import { Renderable, RenderOptions, renderWithOptions } from "../util/Rendering"; import { StyleSpec } from "style-mod"; -import { oneDark } from "@codemirror/theme-one-dark"; +import { darkTheme } from "./DarkTheme"; /** * Data structure containing common elements for styling @@ -107,15 +107,16 @@ export abstract class CodeMirrorEditor extends Renderable { state: EditorState.create({ extensions: [ configurableExtensions, - EditorView.updateListener.of((v: ViewUpdate) => { - if (v.docChanged) { - this.handleChange(); - } - }) + EditorView.updateListener.of(this.onViewUpdate.bind(this)) ] }) }); - this.setStyling(styling); + } + + protected onViewUpdate(v: ViewUpdate): void { + if (v.docChanged) { + this.handleChange(); + } } /** @@ -178,9 +179,7 @@ export abstract class CodeMirrorEditor extends Renderable { public setDarkMode(darkMode: boolean): void { let styleExtensions: Extension = []; if (darkMode) { - styleExtensions = oneDark; - } else { - styleExtensions = []; + styleExtensions = [darkTheme]; } this.reconfigure([CodeMirrorEditor.STYLE, styleExtensions]); } @@ -195,7 +194,10 @@ export abstract class CodeMirrorEditor extends Renderable { CodeMirrorEditor.THEME, EditorView.theme({ ".cm-scroller": { overflow: "auto" }, - "&": { maxHeight: this.styling.maxHeight, height: "100%" }, + "&": { + "maxHeight": this.styling.maxHeight, "height": "100%", + "font-size": "14px" // use proper size to align gutters with editor + }, ".cm-gutter,.cm-content": { minHeight: this.styling.minHeight }, ...(this.styling.theme || {}) }) @@ -203,6 +205,7 @@ export abstract class CodeMirrorEditor extends Renderable { } protected override _render(options: RenderOptions): void { + this.setStyling(this.styling); this.setDarkMode(options.darkMode || false); const wrappingDiv = document.createElement("div"); wrappingDiv.classList.add(...this.styling.classes); diff --git a/src/editor/DarkTheme.ts b/src/editor/DarkTheme.ts new file mode 100644 index 00000000..fa3a2440 --- /dev/null +++ b/src/editor/DarkTheme.ts @@ -0,0 +1,85 @@ +// Based on https://github.com/codemirror/theme-one-dark/blob/main/src/one-dark.ts with slight edits +import { EditorView } from "@codemirror/view"; + +// Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors + +const ivory = "#abb2bf"; +const stone = "#7d8799"; // Brightened compared to original to increase contrast +const darkBackground = "#21252b"; +const highlightBackground = "#2c313a"; +const background = "#282c34"; +const tooltipBackground = "#353a42"; +const selection = "#3E4451"; +const cursor = "#528bff"; + +// / The editor theme styles +export const darkTheme = EditorView.theme({ + "&": { + color: ivory, + backgroundColor: background + }, + + ".cm-content": { + caretColor: cursor + }, + + ".cm-cursor, .cm-dropCursor": { borderLeftColor: cursor }, + // eslint-disable-next-line max-len + "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { backgroundColor: selection }, + + ".cm-panels": { backgroundColor: darkBackground, color: ivory }, + ".cm-panels.cm-panels-top": { borderBottom: "2px solid black" }, + ".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" }, + + ".cm-searchMatch": { + backgroundColor: "#72a1ff59", + outline: "1px solid #457dff" + }, + ".cm-searchMatch.cm-searchMatch-selected": { + backgroundColor: "#6199ff2f" + }, + + ".cm-activeLine": { backgroundColor: highlightBackground }, + ".cm-selectionMatch": { backgroundColor: "#aafe661a" }, + + "&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": { + backgroundColor: "#bad0f847", + outline: "1px solid #515a6b" + }, + + ".cm-gutters": { + // make gutters darker + backgroundColor: darkBackground, + color: stone, + border: "none" + }, + + ".cm-activeLineGutter": { + backgroundColor: highlightBackground + }, + + ".cm-foldPlaceholder": { + backgroundColor: "transparent", + border: "none", + color: "#ddd" + }, + + ".cm-tooltip": { + border: "none", + backgroundColor: tooltipBackground + }, + ".cm-tooltip .cm-tooltip-arrow:before": { + borderTopColor: "transparent", + borderBottomColor: "transparent" + }, + ".cm-tooltip .cm-tooltip-arrow:after": { + borderTopColor: tooltipBackground, + borderBottomColor: tooltipBackground + }, + ".cm-tooltip-autocomplete": { + "& > ul > li[aria-selected]": { + backgroundColor: highlightBackground, + color: ivory + } + } +}, { dark: true }); diff --git a/src/editor/Gutters.ts b/src/editor/Gutters.ts index cfb59c44..dff78b2b 100644 --- a/src/editor/Gutters.ts +++ b/src/editor/Gutters.ts @@ -2,6 +2,7 @@ import { StateEffectType, StateField } from "@codemirror/state"; import { Extension, StateEffect } from "@codemirror/state"; import { BlockInfo, gutter, GutterMarker } from "@codemirror/view"; import { EditorView } from "@codemirror/view"; +import { appendClasses } from "../util/Rendering"; /** * Helper class to create markers in the gutter @@ -38,6 +39,10 @@ export interface IGutterConfig { * Name of this Gutter */ name: string; + /** + * HTML class names for the marker icons + */ + markerClasses?: string; /** * Handler for when a Gutter element is clicked */ @@ -91,15 +96,44 @@ export abstract class Gutters< */ protected abstract marker(info: Info): GutterMarker; + private applyClasses(marker: GutterMarker): GutterMarker { + const classes = { classNames: this.config.markerClasses }; + appendClasses(classes, "_tw-px-1 papyros-gutter-marker"); + marker.elementClass += classes.classNames; + return marker; + } + + public hasMarker(view: EditorView, lineNr: number): boolean { + const guttersInfo: Map = view.state.field(this.state); + return guttersInfo.has(lineNr) && guttersInfo.get(lineNr)!.on; + } + /** * Set a marker with the given info * @param {EditorView} view View in which the Gutters live * @param {Info} info Info used to render the marker */ public setMarker(view: EditorView, info: Info): void { - view.dispatch({ - effects: this.effect.of(info) + if (this.hasMarker(view, info.lineNr) !== info.on) { + view.dispatch({ + effects: this.effect.of(info) + }); + } + } + + /** + * @param {EditorView} view The view in which the Gutters live + * @return {Set} The 1-based line numbers with a breakpoint + */ + public getMarkedLines(view: EditorView): Set { + const markedLines: Set = new Set(); + const guttersInfo: Map = view.state.field(this.state); + guttersInfo.forEach((info: GutterInfo, lineNr: number) => { + if (info.on) { + markedLines.add(lineNr); + } }); + return markedLines; } /** @@ -126,7 +160,7 @@ export abstract class Gutters< const guttersInfo: Map = view.state.field(this.state); const lineNr = view.state.doc.lineAt(line.from).number; if (guttersInfo.has(lineNr) && guttersInfo.get(lineNr)!.on) { - return this.marker(guttersInfo.get(lineNr)!); + return this.applyClasses(this.marker(guttersInfo.get(lineNr)!)); } else { return null; } @@ -135,7 +169,7 @@ export abstract class Gutters< return update.startState.field(this.state) !== update.state.field(this.state); }, initialSpacer: () => { - return this.marker({ lineNr: -1, on: true } as Info)!; + return this.applyClasses(this.marker({ lineNr: -1, on: true } as Info)!); }, domEventHandlers: handlers }), @@ -170,21 +204,6 @@ export class BreakpointsGutter extends Gutters { protected override marker(): GutterMarker { return new SimpleMarker(() => document.createTextNode("🔴")); } - - /** - * @param {EditorView} view The view in which the Gutters live - * @return {Set} The 1-based line numbers with a breakpoint - */ - public getBreakpoints(view: EditorView): Set { - const breakpoints: Set = new Set(); - const guttersInfo: Map = view.state.field(this.state); - guttersInfo.forEach((info: GutterInfo, lineNr: number) => { - if (info.on) { - breakpoints.add(lineNr); - } - }); - return breakpoints; - } } /** @@ -209,7 +228,6 @@ export class UsedInputGutters extends Gutters { protected override marker(info: UsedInputGutterInfo): GutterMarker { return new SimpleMarker(() => { const node = document.createElement("div"); - node.classList.add("_tw-text-lime-400"); node.replaceChildren(document.createTextNode("✔")); node.setAttribute("title", info.title); // Text interface tells us that more complex node will be processed into Text nodes diff --git a/src/input/BatchInputHandler.ts b/src/input/BatchInputHandler.ts index 997e60e4..bfcbae65 100644 --- a/src/input/BatchInputHandler.ts +++ b/src/input/BatchInputHandler.ts @@ -12,6 +12,10 @@ export class BatchInputHandler extends UserInputHandler { * Messages used when asking for user input */ private prompts: Array; + /** + * Whether a run is occurring + */ + private running: boolean; /** * Editor containing the input of the user */ @@ -26,10 +30,11 @@ export class BatchInputHandler extends UserInputHandler { * Construct a new BatchInputHandler * @param {function()} inputCallback Callback for when the user has entered a value */ - constructor(inputCallback: () => void) { + constructor(inputCallback: (line: string) => void) { super(inputCallback); this.lineNr = 0; this.previousInput = ""; + this.running = false; this.prompts = []; this.batchEditor = new BatchInputEditor(); this.batchEditor.onChange({ @@ -43,17 +48,15 @@ export class BatchInputHandler extends UserInputHandler { * @param {string} newInput The new user input */ private handleInputChanged(newInput: string): void { - if (!newInput) { - this.highlight(() => false); - } else { - const newLines = newInput.split("\n"); - if (this.waiting && newLines.length > this.lineNr + 1) { - // Require explicitly pressing enter - this.inputCallback(); - } - this.highlight(); + const newLines = newInput ? newInput.split("\n") : []; + if (newLines.length < this.lineNr) { + this.lineNr = newLines.length; } - + if (this.waiting && newLines.length > this.lineNr + 1) { + // Require explicitly pressing enter + this.inputCallback(this.next()); + } + this.highlight(this.running); this.previousInput = newInput; } @@ -81,40 +84,49 @@ export class BatchInputHandler extends UserInputHandler { return this.lineNr < this.lines.length; } - private highlight(whichLines = (i: number) => i < this.lineNr): void { - this.batchEditor.highlight((lineNr: number) => { - let message = t("Papyros.used_input"); - const index = lineNr - 1; - const shouldShow = whichLines(index); - if (index < this.prompts.length && this.prompts[index]) { - message = t("Papyros.used_input_with_prompt", - { prompt: this.prompts[index] }); + private highlight(running: boolean, whichLines = (i: number) => i < this.lineNr): void { + this.batchEditor.highlight({ + running, + getInfo: (lineNr: number) => { + let message = t("Papyros.used_input"); + const index = lineNr - 1; + const shouldShow = whichLines(index); + if (index < this.prompts.length && this.prompts[index]) { + message = t("Papyros.used_input_with_prompt", + { prompt: this.prompts[index] }); + } + return { lineNr, on: shouldShow, title: message }; } - return { lineNr, on: shouldShow, title: message }; }); } public override next(): string { const nextLine = this.lines[this.lineNr]; this.lineNr += 1; - this.highlight(); + this.highlight(true); return nextLine; } public override onRunStart(): void { + this.running = true; this.lineNr = 0; - this.highlight(() => false); + this.prompts = []; + this.highlight(true, () => false); } public override onRunEnd(): void { - // Intentionally empty + this.running = false; + this.highlight(false); } public override waitWithPrompt(waiting: boolean, prompt?: string): void { - if (waiting) { + super.waitWithPrompt(waiting, prompt); + if (this.waiting) { this.prompts.push(prompt || ""); + if (this.hasNext()) { + this.inputCallback(this.next()); + } } - super.waitWithPrompt(waiting, prompt); } protected setPlaceholder(placeholderValue: string): void { @@ -130,6 +142,6 @@ export class BatchInputHandler extends UserInputHandler { if (options.inputStyling) { this.batchEditor.setStyling(options.inputStyling); } - this.highlight(); + this.highlight(this.running); } } diff --git a/src/input/InteractiveInputHandler.ts b/src/input/InteractiveInputHandler.ts index 1d87ba58..29817491 100644 --- a/src/input/InteractiveInputHandler.ts +++ b/src/input/InteractiveInputHandler.ts @@ -40,18 +40,20 @@ export class InteractiveInputHandler extends UserInputHandler { } public override waitWithPrompt(waiting: boolean, prompt?: string): void { - super.waitWithPrompt(waiting, prompt); + this.waiting = waiting; this.sendButton.disabled = !waiting; this.inputArea.disabled = !waiting; - if (this.inputArea.disabled) { - // Remove placeholder as it is disabled - this.inputArea.setAttribute("placeholder", ""); - this.inputArea.setAttribute("title", t("Papyros.input_disabled")); - } + super.waitWithPrompt(waiting, prompt); } protected override setPlaceholder(placeholder: string): void { - this.inputArea.setAttribute("placeholder", placeholder); + if (this.waiting) { + this.inputArea.setAttribute("placeholder", placeholder); + this.inputArea.setAttribute("title", ""); + } else { + this.inputArea.setAttribute("placeholder", ""); + this.inputArea.setAttribute("title", t("Papyros.input_disabled")); + } } public focus(): void { @@ -87,10 +89,10 @@ export class InteractiveInputHandler extends UserInputHandler { ${buttonHTML} `); - addListener(SEND_INPUT_BTN_ID, () => this.inputCallback(), "click"); + addListener(SEND_INPUT_BTN_ID, () => this.inputCallback(this.next()), "click"); this.inputArea.addEventListener("keydown", (ev: KeyboardEvent) => { if (this.waiting && ev.key && ev.key.toLowerCase() === "enter") { - this.inputCallback(); + this.inputCallback(this.next()); } }); } diff --git a/src/input/UserInputHandler.ts b/src/input/UserInputHandler.ts index 64052291..1fbf1d0a 100644 --- a/src/input/UserInputHandler.ts +++ b/src/input/UserInputHandler.ts @@ -13,13 +13,13 @@ export abstract class UserInputHandler extends Renderable void; + protected inputCallback: (line: string) => void; /** * Construct a new UserInputHandler * @param {function()} inputCallback Callback for when the user has entered a value */ - constructor(inputCallback: () => void) { + constructor(inputCallback: (line: string) => void) { super(); this.waiting = false; this.inputCallback = inputCallback; @@ -76,7 +76,7 @@ export abstract class UserInputHandler extends Renderable