feat: F4.6f2 the text editor on the document's Y.Text, files both ways while the simulation runs

Oracle TextEditorWindowRun uses Java's text editor window and its file
dialog as a person does. The open file's text is its Y.Text in the
document (plan 7.3): typed is in the file at once, for everyone and for
the running engine. Core openText/saveTextAs without Java's mistakes
(lost saves after save-as onto a file or a removed file, files named
'ordner/d.txt' or ''); doc files.ts (changeFiles, fileText, nodeFiles,
readFilesInto); the room takes a person's files into the engine at once
and writes the engine's files back at the tick they changed; the editor's
window, Java's dialog, the text area following others' changes with the
caret in place, its own undo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 17:50:27 +00:00
co-authored by Claude Opus 5.5
parent 3d6655e736
commit 2c7733b575
29 changed files with 3268 additions and 76 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 369 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 B

+14 -5
View File
@@ -12,9 +12,9 @@
// bar, as Java's windows do; on a phone it fills the surface. A program's
// icon opens its window over the desktop (ProgramFrame), the home button
// leaves it; so far the command line (F4.6b), the web browser and the web
// server (F4.6d), the e-mail program and server (F4.6e) have one, the others
// follow. The browser's window takes its
// title from its page, as Java's does.
// server (F4.6d), the e-mail program and server (F4.6e), the text editor
// (F4.6f2) have one, the others follow. The browser's window takes its title
// from its page, the text editor's from its open file, as Java's do.
import { useCallback, useEffect, useState, type PointerEvent as ReactPointerEvent } from 'react';
import type * as Y from 'yjs';
@@ -34,6 +34,7 @@ import { EmailServerPanel } from './EmailServerPanel.js';
import { pageTitle } from './browserPage.js';
import { InstallationPanel } from './InstallationPanel.js';
import { TerminalPanel } from './TerminalPanel.js';
import { TextEditorPanel, useTextEditorWindow } from './TextEditorPanel.js';
import { WebServerPanel } from './WebServerPanel.js';
import './desktop.css';
@@ -63,7 +64,7 @@ export interface DesktopWindowProps {
export type DesktopWindowName = DesktopProgram | 'installation';
/** The programs whose windows are built so far. */
const WITH_WINDOW: ReadonlySet<DesktopProgram> = new Set(['commandPrompt', 'webBrowser', 'webServer', 'emailClient', 'emailServer']);
const WITH_WINDOW: ReadonlySet<DesktopProgram> = new Set(['commandPrompt', 'webBrowser', 'webServer', 'emailClient', 'emailServer', 'textEditor']);
/** The document's desktop of a computer, read anew after every change (a program installed, the label changed). */
function useDesktopView(doc: Y.Doc, id: string) {
@@ -81,6 +82,7 @@ export function DesktopWindow({ doc, id, simulation, active, x, y, z, program: o
const t = useTranslate();
const language = useLanguage();
const view = useDesktopView(doc, id);
const textEditor = useTextEditorWindow(doc, id);
const { watch } = simulation;
useEffect(() => watch([`node:${id}`]), [watch, id]);
const settings = simulation.nodes.get(id);
@@ -157,7 +159,13 @@ export function DesktopWindow({ doc, id, simulation, active, x, y, z, program: o
) : shown === null ? null : (
<ProgramFrame
icon={JAVA_PROGRAMS[shown].icon}
title={shown === 'webBrowser' ? browserTitle(browserView(simulation, id), t('program.webBrowser'), (page) => pageTitle(page, t)) : t(`program.${shown}`)}
title={
shown === 'webBrowser'
? browserTitle(browserView(simulation, id), t('program.webBrowser'), (page) => pageTitle(page, t))
: shown === 'textEditor'
? textEditor.title(t('program.textEditor'))
: t(`program.${shown}`)
}
program={shown}
>
{shown === 'commandPrompt' ? <TerminalPanel node={id} simulation={simulation} onExit={home} /> : null}
@@ -165,6 +173,7 @@ export function DesktopWindow({ doc, id, simulation, active, x, y, z, program: o
{shown === 'webServer' ? <WebServerPanel node={id} simulation={simulation} /> : null}
{shown === 'emailClient' ? <EmailClientPanel node={id} simulation={simulation} /> : null}
{shown === 'emailServer' ? <EmailServerPanel node={id} simulation={simulation} /> : null}
{shown === 'textEditor' ? <TextEditorPanel doc={doc} node={id} /> : null}
</ProgramFrame>
)}
{info ? (
@@ -0,0 +1,94 @@
// Java's file dialog for a program's "open" and "save" (DMTNFileChooser),
// over the program's window: its title, the folder and „Ordner aufwärts“,
// the folder's entries in the order they were made with Java's icons, the
// name field, the dialog's own button and „Abbrechen“. It starts in the
// root, as Java's does.
//
// Better than Java: the folder is shown by its path (Java's label shows its
// name only, the root as "root"); a single tap opens a folder or takes a
// file's name (Java wants a double click, which a finger rarely makes); what
// the program refuses is said in the dialog, which stays open.
import { useState, type FormEvent } from 'react';
import { folderEntries, parentPath, type VirtualFileSystem } from '@mdg-filius/core';
import fileIcon from '../assets/desktop/datei.png';
import folderIcon from '../assets/desktop/ordner.png';
import { useTranslate } from '../i18n/i18n.js';
export interface FileDialogProps {
readonly kind: 'open' | 'save';
/** The computer's files, as they are now. */
readonly files: VirtualFileSystem;
/** The dialog's folder and name chosen: what the program says against it, or null when it is done. */
readonly onChoose: (folder: string, name: string) => string | null;
readonly onCancel: () => void;
}
export function FileDialog({ kind, files, onChoose, onCancel }: FileDialogProps) {
const t = useTranslate();
const [folder, setFolder] = useState('/');
const [name, setName] = useState('');
const [problem, setProblem] = useState<string | null>(null);
// A folder removed meanwhile: the root.
const shown = files.contains(folder) && files.entry(folder).content.kind === 'directory' ? files.entry(folder).path : '/';
const entries = folderEntries(files, shown);
const choose = (event: FormEvent) => {
event.preventDefault();
setProblem(onChoose(shown, name));
};
const title = t(kind === 'open' ? 'fileDialog.openTitle' : 'fileDialog.saveTitle');
return (
<form className="mail-dialog file-dialog" role="dialog" aria-label={title} onSubmit={choose}>
<h4>{title}</h4>
<div className="file-dialog-folder">
<span>{`${t('fileDialog.folder')} ${shown}`}</span>
<button type="button" disabled={shown === '/'} onClick={() => setFolder(parentPath(shown))}>
{t('fileDialog.up')}
</button>
</div>
<ul className="file-dialog-entries" aria-label={t('fileDialog.entries')}>
{entries.map((entry) => (
<li key={entry.path}>
<button
type="button"
data-kind={entry.kind}
aria-pressed={entry.kind === 'file' ? entry.name === name : undefined}
onClick={() => {
setProblem(null);
if (entry.kind === 'folder') setFolder(entry.path);
else setName(entry.name);
}}
>
<img src={entry.kind === 'folder' ? folderIcon : fileIcon} alt="" />
<span>{entry.name}</span>
</button>
</li>
))}
</ul>
<label>
<span>{t('fileDialog.name')}</span>
<input
value={name}
autoCapitalize="off"
autoCorrect="off"
spellCheck={false}
onChange={(event) => {
setName(event.target.value);
setProblem(null);
}}
/>
</label>
{problem === null ? null : (
<p className="mail-notice" role="alert">
{problem}
</p>
)}
<div className="mail-dialog-buttons">
<button type="submit">{t(kind === 'open' ? 'fileDialog.open' : 'fileDialog.save')}</button>
<button type="button" onClick={onCancel}>
{t('fileDialog.cancel')}
</button>
</div>
</form>
);
}
@@ -0,0 +1,126 @@
// A computer's text editor, after Java's GUIApplicationTextEditorWindow: the
// buttons „Neu“, „Öffnen“, „Speichern“, „Speichern unter“ above, the text
// below in Java's Courier, Java's file dialog over it (FileDialog). What it
// does is the window's (TextEditorWindow, textEditor.ts, with what it does
// better than Java); the text area follows the open file's Y.Text, so that
// two who have it open type into the same text. Ctrl+Z and Ctrl+Y (or
// Ctrl+Shift+Z) take back and redo this window's own typing in the file.
//
// The file is the document's, so it opens without the simulation's room;
// while the simulation runs, the room takes what is typed into the engine's
// files at once.
import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore, type KeyboardEvent } from 'react';
import type * as Y from 'yjs';
import { entryName } from '@mdg-filius/core';
import { nodeFiles } from '@mdg-filius/doc';
import { useTranslate } from '../i18n/i18n.js';
import { FileDialog } from './FileDialog.js';
import { followText } from './textBinding.js';
import { textEditorWindow, type TextEditorWindow } from './textEditor.js';
/** A computer's text editor window, drawn anew with each change of it. */
export function useTextEditorWindow(doc: Y.Doc, node: string): TextEditorWindow {
const window = textEditorWindow(doc, node);
useSyncExternalStore(
(listener) => window.subscribe(listener),
() => window.version,
);
return window;
}
/** The document's files of the computer, read anew after every change while the dialog is open. */
function useFiles(doc: Y.Doc, node: string, reading: boolean) {
const [, setVersion] = useState(0);
useEffect(() => {
if (!reading) return;
const update = () => setVersion((version) => version + 1);
doc.on('update', update);
return () => doc.off('update', update);
}, [doc, reading]);
return reading ? nodeFiles(doc, node) : undefined;
}
export function TextEditorPanel({ doc, node }: { readonly doc: Y.Doc; readonly node: string }) {
const t = useTranslate();
const window = useTextEditorWindow(doc, node);
const [dialog, setDialog] = useState<'open' | 'save' | null>(null);
const files = useFiles(doc, node, dialog !== null);
const area = useRef<HTMLTextAreaElement>(null);
const file = window.file;
const path = window.path;
// The open file followed (bound once per file: what one types is in the area already).
useLayoutEffect(() => {
const element = area.current;
return element === null || file === undefined ? undefined : followText(element, file, window.origin);
}, [file, window]);
// A new text set when it changes (Neu, a file gone), not while it is typed.
useLayoutEffect(() => {
const element = area.current;
if (element !== null && file === undefined && element.value !== window.text) element.value = window.text;
}, [file, window, window.version]);
const onKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
if (!(event.ctrlKey || event.metaKey) || window.file === undefined) return;
const key = event.key.toLowerCase();
if (key === 'z' && !event.shiftKey) window.undo();
else if (key === 'y' || (key === 'z' && event.shiftKey)) window.redo();
else return;
event.preventDefault();
};
const open = (folder: string, name: string): string | null => {
const result = window.open(folder, name);
if (result === 'opened') {
setDialog(null);
return null;
}
return t(`textEditor.${result}`, { name });
};
const saveAs = (folder: string, name: string): string | null => {
if (name === '') return t('textEditor.noName');
const result = window.saveAs(folder, name);
if (result === 'saved') {
setDialog(null);
return null;
}
return t(`textEditor.${result}`, { name });
};
return (
<div className="text-editor">
<div className="text-editor-bar">
<button type="button" onClick={() => window.newText()}>
{t('textEditor.new')}
</button>
<button type="button" onClick={() => setDialog('open')}>
{t('textEditor.open')}
</button>
<button type="button" disabled={path !== null} title={path === null ? undefined : t('textEditor.savedLive')} onClick={() => window.save() === 'askName' && setDialog('save')}>
{t('textEditor.save')}
</button>
<button type="button" onClick={() => setDialog('save')}>
{t('textEditor.saveAs')}
</button>
</div>
{window.gone === null ? null : (
<p className="mail-notice text-editor-notice" role="status">
{t('textEditor.gone', { name: entryName(window.gone) })}
</p>
)}
<textarea
ref={area}
className="text-editor-text"
aria-label={t('textEditor.text')}
spellCheck={false}
autoCapitalize="off"
autoCorrect="off"
defaultValue={window.text}
onInput={(event) => window.input(event.currentTarget.value)}
onKeyDown={onKeyDown}
/>
{dialog === null || files === undefined ? null : <FileDialog kind={dialog} files={files} onChoose={dialog === 'open' ? open : saveAs} onCancel={() => setDialog(null)} />}
</div>
);
}
+96
View File
@@ -809,3 +809,99 @@
.mail-question button + button {
margin-left: 8px;
}
/* The text editor (GUIApplicationTextEditorWindow): Java's four buttons
* above, the text below in Courier; 16 pixels, as Safari on the iPad zooms
* into smaller fields. */
.text-editor {
position: relative;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
gap: 5px;
padding: 5px;
background: #eeeeee;
}
.text-editor-bar {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 5px;
flex: none;
}
.text-editor-bar button,
.file-dialog-folder button {
min-width: 44px;
min-height: 40px;
padding: 0 12px;
border: 1px solid #a0a0a0;
border-radius: 6px;
background: #f8f8f8;
color: #000000;
font: 13px Dialog, sans-serif;
cursor: pointer;
}
.text-editor-bar button:disabled,
.file-dialog-folder button:disabled {
color: #a0a0a0;
cursor: default;
}
.text-editor-text {
flex: 1;
min-height: 0;
resize: none;
padding: 4px;
border: 1px solid #a0a0a0;
background: #ffffff;
color: #000000;
font: 16px 'Courier New', Courier, monospace;
}
/* Java's file dialog (DMTNFileChooser) over the program. */
.file-dialog-folder {
display: flex;
align-items: center;
gap: 10px;
flex: none;
}
.file-dialog-folder span {
flex: 1;
min-width: 0;
overflow-wrap: anywhere;
}
.file-dialog-entries {
flex: 1;
min-height: 96px;
margin: 0;
padding: 0;
overflow: auto;
border: 1px solid #a0a0a0;
background: #ffffff;
list-style: none;
}
.file-dialog-entries button {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
min-height: 32px;
padding: 0 6px;
border: 0;
background: transparent;
color: #000000;
font: 13px Dialog, sans-serif;
text-align: left;
cursor: pointer;
}
.file-dialog-entries button[aria-pressed='true'] {
background: #b8cfe5;
}
@@ -0,0 +1,50 @@
// A text area that follows a document's Y.Text (the text editor's open
// file, plan §7.3): what another wrote — another person, the simulation,
// an undo — comes into the area, the caret and the selection kept where they
// were in the text around them. What one types goes the other way through
// the window (TextEditorWindow.input), with the origin this leaves out.
import type * as Y from 'yjs';
/** A change of a Y.Text as its event gives it (retain, insert, delete). */
export type TextDelta = readonly {
readonly retain?: number;
readonly insert?: unknown;
readonly delete?: number;
}[];
/**
* Where a place in the text is after another's change: moved by what was
* inserted before it and by what was deleted before it; an insertion just
* at it leaves it before the new text (the other's typing goes on after the
* caret, not under it).
*/
export function mapIndex(index: number, delta: TextDelta): number {
let old = 0;
let shift = 0;
for (const op of delta) {
if (old >= index) break;
if (op.retain !== undefined) old += op.retain;
else if (op.insert !== undefined) shift += typeof op.insert === 'string' ? op.insert.length : 1;
else if (op.delete !== undefined) {
shift -= Math.min(op.delete, index - old);
old += op.delete;
}
}
return index + shift;
}
/** Lets the area follow the text, but for the changes of the origin (its own typing); returns the end of it. */
export function followText(area: HTMLTextAreaElement, text: Y.Text, origin: object): () => void {
area.value = text.toString();
const onChange = (event: Y.YTextEvent, transaction: Y.Transaction) => {
if (transaction.origin === origin) return;
const delta = event.delta as TextDelta;
const { selectionStart, selectionEnd, selectionDirection } = area;
const focused = area.ownerDocument.activeElement === area;
area.value = text.toString();
if (focused) area.setSelectionRange(mapIndex(selectionStart, delta), mapIndex(selectionEnd, delta), selectionDirection);
};
text.observe(onChange);
return () => text.unobserve(onChange);
}
+191
View File
@@ -0,0 +1,191 @@
// A computer's text editor window, after Java's GUIApplicationTextEditorWindow
// (measured with tools/java-oracle's TextEditorWindowRun): „Neu“, „Öffnen“,
// „Speichern“, „Speichern unter“ over Java's file dialog, the title with the
// open file's name. Without React, so that the window's steps can be played
// against Java's (test/textEditor.test.ts); the panel draws it.
//
// The open file's text is its Y.Text in the document (plan §7.3): what one
// types is in the file at once, for everyone who has it open and for the
// running simulation — Java's editor keeps a copy until „Speichern“ and takes
// what another program wrote only while nothing is typed. A new text not yet
// saved is the window's own.
//
// Better than Java (each a mistake of Java's, measured):
// - „Speichern unter“ onto a file that is there writes that file and goes on
// in it (Java goes on in a copy no folder holds: every later „Speichern“
// is lost), the title shows the file's own name (Java's the name typed);
// - a file removed while it is open leaves its text as a new text, and
// „Speichern“ asks for a name (Java's „Speichern“ is lost);
// - „Öffnen“ of a name that is not there, or no name, says so and keeps the
// open file (Java forgets it silently), and opens no image (Java shows its
// Base64 text);
// - „Speichern unter“ refuses an empty name and one with blanks at its ends,
// and takes "ordner/d.txt" as the file d.txt in ordner (Java makes files
// of such names).
import * as Y from 'yjs';
import { entryName, openText, saveTextAs, type Id } from '@mdg-filius/core';
import { changeFiles, fileText, nodeFiles, replaceText } from '@mdg-filius/doc';
export type OpenResult = 'opened' | 'notFound' | 'notText';
export type SaveResult = 'saved' | 'askName';
export type SaveAsResult = 'saved' | 'folderMissing' | 'isFolder' | 'invalidName';
export class TextEditorWindow {
private pathValue: string | null = null;
private draft = '';
private goneValue: string | null = null;
private bound: Y.Text | undefined;
private lastText = '';
private undoManager: Y.UndoManager | undefined;
private readonly listeners = new Set<() => void>();
private versionValue = 0;
/** The window's origin: what it writes into the document, its own undo takes back. */
readonly origin: object = {};
constructor(
private readonly doc: Y.Doc,
private readonly node: Id,
) {
doc.on('update', () => this.refresh());
}
/** The open file's path; null for a new text. */
get path(): string | null {
return this.pathValue;
}
/** The open file's text in the document; none for a new text. */
get file(): Y.Text | undefined {
return this.bound;
}
/** The path of the open file that was removed while it was open, until the window opens or saves another or starts anew. */
get gone(): string | null {
return this.goneValue;
}
get text(): string {
return this.bound === undefined ? this.draft : this.bound.toString();
}
/** Changes with each change of what the window shows (for React). */
get version(): number {
return this.versionValue;
}
subscribe(listener: () => void): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
private changed(): void {
this.versionValue += 1;
for (const listener of this.listeners) listener();
}
/** GUIApplicationWindow.setTitle: the program's name, " - " and the open file's name. */
title(programName: string): string {
return this.pathValue === null ? programName : `${programName} - ${entryName(this.pathValue)}`;
}
private readonly remember = () => {
this.lastText = this.bound?.toString() ?? '';
};
private bind(text: Y.Text | undefined, path: string | null, gone: string | null = null): void {
this.goneValue = gone;
this.bound?.unobserve(this.remember);
this.undoManager?.destroy();
this.bound = text;
this.pathValue = path;
this.undoManager = text === undefined ? undefined : new Y.UndoManager(text, { trackedOrigins: new Set([this.origin]), captureTimeout: 500 });
text?.observe(this.remember);
this.remember();
}
/** After a change of the document: the open file found again (another may have replaced its entry), or gone. */
private refresh(): void {
if (this.pathValue === null) return;
const now = fileText(this.doc, this.node, this.pathValue);
if (now === this.bound) return;
if (now === undefined) {
this.draft = this.lastText;
this.bind(undefined, null, this.pathValue);
} else {
this.bind(now, this.pathValue);
}
this.changed();
}
/** What the person typed: into the open file (where it differs), or the new text. */
input(next: string): void {
if (this.bound === undefined) {
if (this.draft === next) return;
this.draft = next;
} else {
const text = this.bound;
this.doc.transact(() => replaceText(text, next), this.origin);
}
this.changed();
}
/** „Neu“: an empty new text. */
newText(): void {
this.draft = '';
this.bind(undefined, null);
this.changed();
}
/** „Öffnen“ with the dialog's folder and name. */
open(folder: string, name: string): OpenResult {
const files = nodeFiles(this.doc, this.node);
const found = files === undefined ? 'notFound' : openText(files, folder, name);
if (typeof found === 'string') return found;
this.bind(fileText(this.doc, this.node, found.path), found.path);
this.changed();
return 'opened';
}
/** „Speichern“: the open file has the text already; a new text asks for a name („Speichern unter“). */
save(): SaveResult {
return this.pathValue === null ? 'askName' : 'saved';
}
/** „Speichern unter“ with the dialog's folder and name: the text as that file, which is then the open one. */
saveAs(folder: string, name: string): SaveAsResult {
const text = this.text;
let path: string | undefined;
const result =
changeFiles(this.doc, this.node, this.origin, (files) => {
const saved = saveTextAs(files, folder, name, text);
if (saved === 'saved') path = files.entry(`${folder === '/' ? '' : folder}/${name}`).path;
return saved;
}) ?? 'folderMissing';
if (path !== undefined) {
this.bind(fileText(this.doc, this.node, path), path);
this.changed();
}
return result;
}
/** The window's own undo in the open file (what it typed). */
undo(): void {
this.undoManager?.undo();
}
redo(): void {
this.undoManager?.redo();
}
}
const windows = new WeakMap<Y.Doc, Map<Id, TextEditorWindow>>();
/** A computer's text editor window, the same while the editor runs (Java's desktop keeps its windows). */
export function textEditorWindow(doc: Y.Doc, node: Id): TextEditorWindow {
let ofDoc = windows.get(doc);
if (ofDoc === undefined) windows.set(doc, (ofDoc = new Map()));
let window = ofDoc.get(node);
if (window === undefined) ofDoc.set(node, (window = new TextEditorWindow(doc, node)));
return window;
}
+22
View File
@@ -122,6 +122,15 @@
"emailServer.username": "Benutzername:",
"emailServer.whiteSpace": "Der Benutzername darf keine Leerzeichen enthalten!",
"emailServer.yes": "Ja",
"fileDialog.cancel": "Abbrechen",
"fileDialog.entries": "Inhalt des Ordners",
"fileDialog.folder": "Ordner:",
"fileDialog.name": "Dateiname:",
"fileDialog.open": "Öffnen",
"fileDialog.openTitle": "Datei Öffnen",
"fileDialog.save": "Speichern",
"fileDialog.saveTitle": "Datei Speichern",
"fileDialog.up": "Ordner aufwärts",
"firewall.accept": "akzeptieren",
"firewall.action": "Aktion",
"firewall.active": "Firewall aktivieren",
@@ -257,6 +266,19 @@
"terminal.interrupt": "Befehl abbrechen (Strg+C)",
"terminal.next": "Nächster Befehl",
"terminal.previous": "Vorheriger Befehl",
"textEditor.folderMissing": "Den Ordner für „{name}“ gibt es nicht.",
"textEditor.gone": "Die Datei „{name}“ wurde gelöscht. Der Text ist noch da; „Speichern“ legt ihn als neue Datei ab.",
"textEditor.invalidName": "„{name}“ ist kein zulässiger Dateiname.",
"textEditor.isFolder": "„{name}“ ist ein Ordner.",
"textEditor.new": "Neu",
"textEditor.noName": "Geben Sie einen Dateinamen ein.",
"textEditor.notFound": "Die Datei „{name}“ gibt es nicht.",
"textEditor.notText": "„{name}“ ist keine Textdatei.",
"textEditor.open": "Öffnen",
"textEditor.save": "Speichern",
"textEditor.saveAs": "Speichern unter",
"textEditor.savedLive": "Was Sie tippen, steht sofort in der Datei.",
"textEditor.text": "Text",
"webServer.folder": "Unterverzeichnis",
"webServer.hostName": "Hostname",
"webServer.useVirtualHosts": "Verwende virtuelle Hosts",
+22
View File
@@ -122,6 +122,15 @@
"emailServer.username": "User:",
"emailServer.whiteSpace": "User names may not contain whitespace!",
"emailServer.yes": "Yes",
"fileDialog.cancel": "Cancel",
"fileDialog.entries": "Contents of the folder",
"fileDialog.folder": "Folder:",
"fileDialog.name": "File:",
"fileDialog.open": "Open",
"fileDialog.openTitle": "Open file",
"fileDialog.save": "Save",
"fileDialog.saveTitle": "Save file",
"fileDialog.up": "Folder up",
"firewall.accept": "accept",
"firewall.action": "Action",
"firewall.active": "Activate firewall",
@@ -257,6 +266,19 @@
"terminal.interrupt": "Stop the command (Ctrl+C)",
"terminal.next": "Next command",
"terminal.previous": "Previous command",
"textEditor.folderMissing": "There is no folder for “{name}”.",
"textEditor.gone": "The file “{name}” was deleted. The text is still here; “Save” stores it as a new file.",
"textEditor.invalidName": "“{name}” is not a valid file name.",
"textEditor.isFolder": "“{name}” is a folder.",
"textEditor.new": "New",
"textEditor.noName": "Enter a file name.",
"textEditor.notFound": "There is no file “{name}”.",
"textEditor.notText": "“{name}” is not a text file.",
"textEditor.open": "Open",
"textEditor.save": "Save",
"textEditor.saveAs": "Save as",
"textEditor.savedLive": "What you type is in the file at once.",
"textEditor.text": "Text",
"webServer.folder": "Subdirectory",
"webServer.hostName": "Hostname",
"webServer.useVirtualHosts": "Enable virtual hosts",
+11 -3
View File
@@ -7,7 +7,7 @@
import { StrictMode, useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import * as Y from 'yjs';
import { desktopView, deviceSheet, emptyProject, topologyView, writeProject } from '@mdg-filius/doc';
import { desktopView, deviceSheet, emptyProject, fileText, topologyView, writeProject } from '@mdg-filius/doc';
import { openProjectArchive } from '@mdg-filius/fls';
import { startBridge } from './bridge/dashboardBridge.js';
import { Editor } from './Editor.js';
@@ -60,9 +60,17 @@ function App() {
.then((buffer) => setDoc(docOf(new Uint8Array(buffer))));
}, [example]);
// The development page lets its browser tests read the document's drawing (never the editor embedded in the dashboard).
// The development page lets its browser tests read the document's drawing and write into a file as another would (never the editor embedded in the dashboard).
useEffect(() => {
(window as unknown as { filiusDev?: unknown }).filiusDev = doc === null ? undefined : { view: () => topologyView(doc), sheet: (id: string) => deviceSheet(doc, id), desktop: (id: string) => desktopView(doc, id) };
(window as unknown as { filiusDev?: unknown }).filiusDev =
doc === null
? undefined
: {
view: () => topologyView(doc),
sheet: (id: string) => deviceSheet(doc, id),
desktop: (id: string) => desktopView(doc, id),
insertText: (id: string, path: string, index: number, text: string) => doc.transact(() => fileText(doc, id, path)?.insert(index, text), 'other'),
};
}, [doc]);
const t = (key: string) => translate(language, key);