286 lines
13 KiB
HTML
286 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Editor Singleton & Client/Server Split — Roject</title>
|
|
<link rel="stylesheet" href="../../_assets_/styles.css">
|
|
<link rel="stylesheet" href="../../_assets_/nav.css">
|
|
</head>
|
|
<body>
|
|
<div class="page">
|
|
|
|
<header>
|
|
<h1>Editor Singleton & Client/Server Split</h1>
|
|
<p class="subtitle">
|
|
The <code>Editor</code> class is the central hub of the frontend editor.
|
|
This page documents its events, methods, and properties, and explains the
|
|
TypeScript compilation split between client and server code.
|
|
</p>
|
|
</header>
|
|
|
|
<section>
|
|
<h2>The Editor Singleton</h2>
|
|
|
|
<div class="card">
|
|
<p>
|
|
<code>Editor</code> lives at <code>src/editor/Editor.ts</code> and compiles
|
|
to <code>public/editor/Editor.js</code>. It is a singleton accessed everywhere
|
|
via <code>Editor.get()</code>. It owns all open document state, the
|
|
<code>FileEditorRegistry</code>, and the events that panels and the tab
|
|
container subscribe to. It is the only place where files are fetched from
|
|
the server and where saves are sent back.
|
|
</p>
|
|
<p style="margin-top:0.75rem">
|
|
<strong>Never import <code>Editor</code> in server-side code.</strong> It is a
|
|
browser-only module. The split between client and server code is described in
|
|
the section below.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Accessing the singleton</h3>
|
|
<pre><code>import { Editor } from '../../editor/Editor.js';
|
|
|
|
const editor = Editor.get();</code></pre>
|
|
<p style="margin-top:0.75rem">
|
|
The <code>.js</code> extension is required in all client-side imports because
|
|
the TypeScript output is consumed directly by the browser as ES modules
|
|
(no bundler).
|
|
</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Properties</h3>
|
|
<table style="width:100%;border-collapse:collapse;font-size:0.85rem">
|
|
<thead>
|
|
<tr style="border-bottom:1px solid #2a2d3a;text-align:left">
|
|
<th style="padding:6px 8px">Property</th>
|
|
<th style="padding:6px 8px">Type</th>
|
|
<th style="padding:6px 8px">Description</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr style="border-bottom:1px solid #1e2030">
|
|
<td style="padding:6px 8px"><code>projectId</code></td>
|
|
<td style="padding:6px 8px"><code>string</code></td>
|
|
<td style="padding:6px 8px">UUID of the open project. Set by <code>EditorShell</code> on init from the URL query string.</td>
|
|
</tr>
|
|
<tr style="border-bottom:1px solid #1e2030">
|
|
<td style="padding:6px 8px"><code>projectName</code></td>
|
|
<td style="padding:6px 8px"><code>string</code></td>
|
|
<td style="padding:6px 8px">Display name of the open project. Set by <code>EditorShell</code> on init.</td>
|
|
</tr>
|
|
<tr style="border-bottom:1px solid #1e2030">
|
|
<td style="padding:6px 8px"><code>openDocs</code></td>
|
|
<td style="padding:6px 8px"><code>Map<string, { content, dirty }></code></td>
|
|
<td style="padding:6px 8px">In-memory cache of all documents fetched this session. Keyed by file path.</td>
|
|
</tr>
|
|
<tr style="border-bottom:1px solid #1e2030">
|
|
<td style="padding:6px 8px"><code>activeDoc</code></td>
|
|
<td style="padding:6px 8px"><code>string | null</code></td>
|
|
<td style="padding:6px 8px">File path of the most recently opened document.</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding:6px 8px"><code>fileEditorRegistry</code></td>
|
|
<td style="padding:6px 8px"><code>FileEditorRegistry</code></td>
|
|
<td style="padding:6px 8px">Suffix-to-editor-tag resolver. Loaded lazily on first <code>openDocument</code> call.</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Methods</h3>
|
|
|
|
<p><strong><code>Editor.get(): Editor</code></strong></p>
|
|
<p>Returns the singleton instance, creating it on first call.</p>
|
|
|
|
<p style="margin-top:1rem"><strong><code>openDocument( filePath: string ): Promise<void></code></strong></p>
|
|
<p>
|
|
The main entry point for opening a file. Loads the registry (once), resolves
|
|
the <code>editorTag</code> for the file's suffix. If no tag is found, dispatches
|
|
<code>onFileTypeUnknown</code> and returns early. Otherwise fetches the file
|
|
content (cached after first fetch), then dispatches <code>onDocumentOpened</code>.
|
|
</p>
|
|
|
|
<p style="margin-top:1rem"><strong><code>markDirty( filePath: string, content: string ): void</code></strong></p>
|
|
<p>
|
|
Called by a panel whenever the user edits content. Updates the in-memory cache
|
|
and dispatches <code>onDocumentDirty</code>. The tab container uses this to
|
|
show the dirty dot.
|
|
</p>
|
|
|
|
<p style="margin-top:1rem"><strong><code>save( filePath: string ): Promise<void></code></strong></p>
|
|
<p>
|
|
PUTs the cached content to <code>/api/files/${projectId}/${filePath}</code>
|
|
with <code>Content-Type: text/plain</code>. Clears the dirty flag and
|
|
dispatches <code>onDocumentSaved</code>.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>Events</h2>
|
|
|
|
<div class="card">
|
|
<p>
|
|
All events use <code>EventSlot</code> from the shared library —
|
|
not DOM events and not a pub/sub bus. Add a listener with
|
|
<code>Editor.get().onSomeEvent.addListener( e => ... )</code>.
|
|
Listeners are called synchronously when the event is dispatched.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3><code>onDocumentOpened</code> — <code>EventSlot<DocumentOpenedEvent></code></h3>
|
|
<pre><code>interface DocumentOpenedEvent {
|
|
path: string; // file path relative to project root
|
|
content: string; // raw file content
|
|
editorTag: string; // custom element tag resolved by FileEditorRegistry
|
|
}</code></pre>
|
|
<p style="margin-top:0.75rem">
|
|
Fired after the file is fetched and the editor tag is resolved. Every editor
|
|
panel listens to this. <strong>Panels must check <code>editorTag</code> and
|
|
return early if it does not match their own tag</strong> — all panels receive
|
|
every event.
|
|
</p>
|
|
<pre><code>Editor.get().onDocumentOpened.addListener( ( e ) =>
|
|
{
|
|
if ( 'my-panel' !== e.editorTag ) return;
|
|
this._loadDocument( e.path, e.content );
|
|
} );</code></pre>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3><code>onDocumentDirty</code> — <code>EventSlot<DocumentPathEvent></code></h3>
|
|
<pre><code>interface DocumentPathEvent { path: string; }</code></pre>
|
|
<p style="margin-top:0.75rem">
|
|
Fired by <code>markDirty</code>. The <code>TabContainer</code> listens and
|
|
sets <code>tab.dirty = true</code> for the tab whose panel's
|
|
<code>currentPath</code> matches. Panels do not need to listen to this
|
|
themselves — they call <code>markDirty</code> and update their own Save button.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3><code>onDocumentSaved</code> — <code>EventSlot<DocumentPathEvent></code></h3>
|
|
<p style="margin-top:0.25rem">
|
|
Fired by <code>save</code> after a successful PUT. The <code>TabContainer</code>
|
|
listens and clears the dirty dot for the matching tab.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3><code>onFilesChanged</code> — <code>EventSlot<void></code></h3>
|
|
<p>
|
|
Fired with no payload when the file tree changes (file or folder created,
|
|
renamed, or deleted). <code>FileTreePanel</code> listens and re-fetches the
|
|
tree. Dispatch it after any operation that modifies the filesystem:
|
|
</p>
|
|
<pre><code>Editor.get().onFilesChanged.dispatch();</code></pre>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3><code>onFileTypeUnknown</code> — <code>EventSlot<DocumentPathEvent></code></h3>
|
|
<p>
|
|
Fired when <code>openDocument</code> is called for a file whose suffix has no
|
|
entry in <code>FileEditorRegistry</code>. <code>FileTreePanel</code> listens
|
|
and shows a 3-second error banner. No file content is fetched when this fires.
|
|
</p>
|
|
</div>
|
|
|
|
</section>
|
|
|
|
<section>
|
|
<h2>Client / Server Split</h2>
|
|
|
|
<div class="card">
|
|
<h3>Two separate TypeScript pipelines</h3>
|
|
<p>
|
|
Client and server TypeScript are compiled independently and must never import
|
|
from each other's side.
|
|
</p>
|
|
<table style="width:100%;border-collapse:collapse;font-size:0.85rem;margin-top:0.75rem">
|
|
<thead>
|
|
<tr style="border-bottom:1px solid #2a2d3a;text-align:left">
|
|
<th style="padding:6px 8px">Side</th>
|
|
<th style="padding:6px 8px">Source</th>
|
|
<th style="padding:6px 8px">Output</th>
|
|
<th style="padding:6px 8px">How it runs</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr style="border-bottom:1px solid #1e2030">
|
|
<td style="padding:6px 8px">Client</td>
|
|
<td style="padding:6px 8px"><code>src/</code></td>
|
|
<td style="padding:6px 8px"><code>public/</code></td>
|
|
<td style="padding:6px 8px"><code>npm run build</code> → <code>tsc --build tsconfig.client.json</code></td>
|
|
</tr>
|
|
<tr style="border-bottom:1px solid #1e2030">
|
|
<td style="padding:6px 8px">Server</td>
|
|
<td style="padding:6px 8px"><code>server/</code></td>
|
|
<td style="padding:6px 8px">none (in-process)</td>
|
|
<td style="padding:6px 8px"><code>npm start</code> → <code>ts-node</code> with <code>tsconfig.ts-node.json</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding:6px 8px">Library (browser)</td>
|
|
<td style="padding:6px 8px"><code>src/library-ts/browser/</code></td>
|
|
<td style="padding:6px 8px"><code>public/library-ts/browser/</code></td>
|
|
<td style="padding:6px 8px">Compiled via TypeScript project references as part of <code>npm run build</code></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>CSS is not compiled</h3>
|
|
<p>
|
|
Component CSS is written directly in
|
|
<code>public/components/<name>/<name>.css</code> and is never
|
|
processed by TypeScript. Do not put CSS files in <code>src/</code>.
|
|
Edit the file in <code>public/</code> directly; the browser picks it up
|
|
on next reload with no build step.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>After changing code</h3>
|
|
<ul style="line-height:1.9">
|
|
<li><strong>Client TypeScript changed</strong> — run <code>npm run build</code>, then reload the browser.</li>
|
|
<li><strong>Server TypeScript changed</strong> — restart the server (<code>npm start</code>). No build step.</li>
|
|
<li><strong>CSS changed</strong> — reload the browser. No build step.</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Module imports on the client</h3>
|
|
<p>
|
|
Client TypeScript uses <code>module: ESNext</code> and
|
|
<code>moduleResolution: bundler</code>. There is no bundler — the browser
|
|
receives individual <code>.js</code> files as ES modules.
|
|
<strong>All imports in client code must include the <code>.js</code>
|
|
extension</strong>, even though the source files end in <code>.ts</code>:
|
|
</p>
|
|
<pre><code>import { Editor } from '../../editor/Editor.js';
|
|
import { EventSlot } from '../library-ts/browser/events/EventSlot.js';</code></pre>
|
|
<p style="margin-top:0.75rem">
|
|
TypeScript resolves these correctly during compilation because
|
|
<code>moduleResolution: bundler</code> allows importing <code>.js</code>
|
|
paths that correspond to <code>.ts</code> source files.
|
|
</p>
|
|
</div>
|
|
|
|
</section>
|
|
|
|
<footer>
|
|
Roject — editor singleton & client/server split
|
|
</footer>
|
|
|
|
</div>
|
|
<script>var NAV_ROOT = '../../';</script>
|
|
<script src="../../_assets_/nav-data.js"></script>
|
|
<script src="../../_assets_/nav.js"></script>
|
|
</body>
|
|
</html>
|