Add Displayable abstract class

This commit is contained in:
Quinten Kock 2025-11-30 03:35:25 +01:00
parent d9584e7543
commit 2f3d640ffb
4 changed files with 90 additions and 38 deletions

78
src/app/displayable.ts Normal file
View File

@ -0,0 +1,78 @@
export type KeyHandler = (e: KeyboardEvent) => void;
function canonicalizeEventKey(e: KeyboardEvent) {
const mods = [] as string[];
if (e.ctrlKey) mods.push("Ctrl");
if (e.altKey) mods.push("Alt");
if (e.shiftKey) mods.push("Shift");
if (e.metaKey) mods.push("Meta");
let k = e.key;
if (k.length === 1) k = k.toLowerCase();
mods.push(k);
return mods.join("-");
}
export abstract class Displayable {
protected deleteFn?: () => void;
private shortcuts = new Map<string, KeyHandler>();
constructor() {
// Attempt to install handlers shortly after construction. If `dom` is not
// available yet, retry a few times.
setTimeout(() => this.installHandlers(0), 0);
// Add general shortcuts
this.addShortcut("Ctrl-w", () => this.close());
this.addShortcut("Alt--", () => this.changeWidth(-100));
this.addShortcut("Alt-=", () => this.changeWidth(100));
}
setDeleteFunction(fn: () => void) {
this.deleteFn = fn;
}
addShortcut(k: string, handler: KeyHandler) {
this.shortcuts.set(k, handler);
}
private handleKeyEvent(e: KeyboardEvent) {
const k = canonicalizeEventKey(e);
const h = this.shortcuts.get(k);
if (h) {
if (e.type == "keydown") h(e);
e.preventDefault();
}
}
changeWidth(increment: number) {
const w = parseInt(window.getComputedStyle(this.dom).width, 10);
this.dom.style.width = w + increment + "px";
return true;
}
private installHandlers(attempt: number) {
try {
const root = this.dom;
if (!root) throw new Error("no dom");
const keyHandler = (e: KeyboardEvent) => this.handleKeyEvent(e);
root.addEventListener("keydown", keyHandler, { capture: true });
root.addEventListener("keyup", keyHandler, { capture: true });
root.addEventListener("focusin", () => {
this.dom.scrollIntoView({ behavior: "smooth" });
});
} catch (err) {
if (attempt < 5) {
setTimeout(() => this.installHandlers(attempt + 1), 50);
} else {
console.error("Failed to install key handlers:", err);
}
}
}
abstract focus(): void;
abstract title(): string;
abstract close(): boolean;
abstract get dom(): HTMLElement;
}

View File

@ -33,9 +33,9 @@ import { languages } from "@codemirror/language-data";
import { autocompletion, closeBrackets } from "@codemirror/autocomplete";
import { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
import van from "vanjs-core";
import { Displayable } from "./displayable";
import { OpenFile } from "./filestate";
import { Displayable } from "./editorgrid";
const fixedHeightEditor = EditorView.theme({
"&": {
@ -74,11 +74,9 @@ function fileStatusPanel(view: EditorView) {
"padding: 2px 10px; font-size: 12px; color: white; background: #900;";
return { top: true, dom };
}
export class Editor implements Displayable {
export class Editor extends Displayable {
view: EditorView;
file: OpenFile;
deleteFn?: () => void;
private wordWrapCompartment = new Compartment();
private languageCompartment = new Compartment();
@ -91,6 +89,7 @@ export class Editor implements Displayable {
}
constructor(file: OpenFile) {
super();
this.file = file;
const kmap = keymap.of([
...defaultKeymap,
@ -105,7 +104,6 @@ export class Editor implements Displayable {
return true;
},
},
{ key: "Mod-w", run: () => this.close() },
{
key: "Alt-z",
run: () => {
@ -116,8 +114,6 @@ export class Editor implements Displayable {
return true;
},
},
{ key: "Alt--", run: () => this.changeWidth(-100) },
{ key: "Alt-=", run: () => this.changeWidth(100) },
]);
this.view = new EditorView({
doc: file.rootState.val.doc,
@ -149,9 +145,6 @@ export class Editor implements Displayable {
// lintKeymap,
],
});
this.view.dom.addEventListener("focusin", () =>
this.view.dom.scrollIntoView({ behavior: "smooth" }),
);
van.derive(() => {
LanguageDescription.matchFilename(languages, file.filePath.val)
@ -185,12 +178,6 @@ export class Editor implements Displayable {
return this.file.filePath.val + (this.file.isDirty() ? "*" : "");
}
changeWidth(increment: number) {
const w = parseInt(window.getComputedStyle(this.view.dom).width, 10);
this.view.dom.style.width = w + increment + "px";
return true;
}
close() {
if (this.deleteFn) {
this.file.removeEditor(this, this.deleteFn);
@ -205,8 +192,4 @@ export class Editor implements Displayable {
effects: compartment.reconfigure(on ? [] : extension),
});
}
setDeleteFunction(fn: () => void) {
this.deleteFn = fn;
}
}

View File

@ -6,14 +6,7 @@ import { OpenFile } from "./filestate";
import * as u from "./utils";
import { Editor } from "./editor";
import { Terminal } from "./terminal";
export interface Displayable {
setDeleteFunction(del: () => void): void;
title(): string;
close(): void;
focus(): void;
dom: HTMLElement;
}
import { Displayable } from "./displayable";
const EditorWrapper = (
editor: State<Displayable>,

View File

@ -1,13 +1,12 @@
import { Displayable } from "./editorgrid";
import { Displayable } from "./displayable";
import * as xterm from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import van, { State } from "vanjs-core";
const v = van.tags;
export class Terminal implements Displayable {
export class Terminal extends Displayable {
term: xterm.Terminal;
currentTitle: State<string> = van.state("Terminal");
del: () => void;
dom: HTMLElement;
private terminalId: string | null = null;
private fitAddon: FitAddon;
@ -15,15 +14,12 @@ export class Terminal implements Displayable {
private unsubTerminalData?: () => void;
private unsubTerminalExit?: () => void;
setDeleteFunction(del: () => void): void {
this.del = del;
}
title(): string {
return this.currentTitle.val;
}
constructor() {
super();
this.term = new xterm.Terminal({
// cursorBlink: true,
// fontSize: 14,
@ -33,8 +29,9 @@ export class Terminal implements Displayable {
this.fitAddon = new FitAddon();
this.term.loadAddon(this.fitAddon);
this.dom = v.div({ class: "h-full w-2xl resize-x overflow-x-hidden scroll-m-[100px]" });
this.dom.addEventListener("focusin", () => this.focus());
this.dom = v.div({
class: "h-full w-2xl resize-x overflow-x-hidden scroll-m-[100px]",
});
const loaded = van.state(false);
@ -124,6 +121,7 @@ export class Terminal implements Displayable {
if (this.unsubTerminalData) this.unsubTerminalData();
if (this.unsubTerminalExit) this.unsubTerminalExit();
this.term.dispose();
this.del();
if (this.deleteFn) this.deleteFn();
return true;
}
}