rojects/workspace/reference/editor-singleton/index.html

286 lines
13 KiB
HTML
Raw Permalink Normal View History

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Editor Singleton &amp; 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 &amp; 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&lt;string, &#123; content, dirty &#125;&gt;</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&lt;void&gt;</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&lt;void&gt;</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 =&gt; ... )</code>.
Listeners are called synchronously when the event is dispatched.
</p>
</div>
<div class="card">
<h3><code>onDocumentOpened</code><code>EventSlot&lt;DocumentOpenedEvent&gt;</code></h3>
<pre><code>interface DocumentOpenedEvent &#123;
path: string; // file path relative to project root
content: string; // raw file content
editorTag: string; // custom element tag resolved by FileEditorRegistry
&#125;</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 ) =&gt;
&#123;
if ( 'my-panel' !== e.editorTag ) return;
this._loadDocument( e.path, e.content );
&#125; );</code></pre>
</div>
<div class="card">
<h3><code>onDocumentDirty</code><code>EventSlot&lt;DocumentPathEvent&gt;</code></h3>
<pre><code>interface DocumentPathEvent &#123; path: string; &#125;</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&lt;DocumentPathEvent&gt;</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&lt;void&gt;</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&lt;DocumentPathEvent&gt;</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/&lt;name&gt;/&lt;name&gt;.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 &mdash; editor singleton &amp; 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>