Editor: auth redirect, targeted file open, Godot file types

- editor-shell: redirect to / if not authenticated on load
- file-tree: open file in first clean matching panel, create new panel if all dirty
- Editor: add openDocumentIn() with targetElement for precise panel targeting
- panels: skip onDocumentOpened if targeted at a different panel
- FileEditorRegistry: add Godot extensions (gd, gdshader, gdshaderinc, gdinclude, glsl, tscn, tres, cs, gdextension)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Rokojori 2026-07-17 22:22:42 +02:00
parent bbf208b3be
commit 98729fc333
7 changed files with 97 additions and 3 deletions

View File

@ -52,6 +52,7 @@ class CodePanel extends HTMLElement
Editor.get().onDocumentOpened.addListener( ( e ) => Editor.get().onDocumentOpened.addListener( ( e ) =>
{ {
if ( 'code-panel' !== e.editorTag ) return; if ( 'code-panel' !== e.editorTag ) return;
if ( e.targetElement && e.targetElement !== this ) return;
if ( ! this._pinned ) this._loadDocument( e.path, e.content ); if ( ! this._pinned ) this._loadDocument( e.path, e.content );
} ); } );

View File

@ -58,6 +58,9 @@ class EditorShell extends HTMLElement {
private _saveTimer: ReturnType<typeof setTimeout> | null = null; private _saveTimer: ReturnType<typeof setTimeout> | null = null;
async connectedCallback(): Promise<void> { async connectedCallback(): Promise<void> {
const authRes = await fetch( '/api/auth/me' );
if ( !authRes.ok ) { location.href = '/'; return; }
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const projectId = params.get('project') ?? ''; const projectId = params.get('project') ?? '';
const projectName = params.get('name') ?? 'Project'; const projectName = params.get('name') ?? 'Project';

View File

@ -106,11 +106,66 @@ class FileTreePanel extends HTMLElement {
bindTree( tree: Element ): void { bindTree( tree: Element ): void {
const state = Editor.get(); const state = Editor.get();
tree.querySelectorAll( '.ftp-file' ).forEach( el => { tree.querySelectorAll( '.ftp-file' ).forEach( el => {
el.addEventListener( 'click', () => { el.addEventListener( 'click', async () => {
const path = ( el as HTMLElement ).dataset.path!; const path = ( el as HTMLElement ).dataset.path!;
this.selectedPath = path; this.selectedPath = path;
tree.querySelectorAll( '.ftp-file, .ftp-dir-label' ).forEach( f => f.classList.remove( 'active' ) ); tree.querySelectorAll( '.ftp-file, .ftp-dir-label' ).forEach( f => f.classList.remove( 'active' ) );
el.classList.add( 'active' ); el.classList.add( 'active' );
await state.fileEditorRegistry.load( state.projectId );
const editorTag = state.fileEditorRegistry.resolve( path );
if ( editorTag )
{
const containers = Array.from( document.querySelectorAll( 'tab-container' ) );
let found: { panel: HTMLElement; tc: any; tabId: string } | null = null;
for ( const tc of containers )
{
const tabs = ( tc as any ).tabs as Array<{ id: string; element: HTMLElement; dirty: boolean }>;
for ( const tab of tabs )
{
if ( tab.element.tagName.toLowerCase() === editorTag && !tab.dirty )
{
found = { panel: tab.element, tc, tabId: tab.id };
break;
}
}
if ( found ) break;
}
if ( found )
{
( found.tc as any ).activateTab( found.tabId );
state.openDocumentIn( path, found.panel );
return;
}
const panelTypeMap: Record<string, { panelType: string; label: string }> =
{
'html-editor-panel': { panelType: 'html-editor', label: 'HTML Editor' },
'code-panel': { panelType: 'code-panel', label: 'Code' },
'rojo-settings-panel': { panelType: 'rojo-settings', label: 'Rojo' },
};
const info = panelTypeMap[ editorTag ];
if ( info )
{
const targetTc = ( containers.find( tc =>
( tc as any ).tabs.some( ( t: any ) => t.element.tagName.toLowerCase() === editorTag )
) ?? document.querySelector( '[data-panel="center"] tab-container' ) ?? containers[ 0 ] ) as any;
if ( targetTc )
{
const newId = info.panelType + '-' + Math.random().toString( 36 ).slice( 2 );
targetTc.addTab( { id: newId, label: info.label, panelType: info.panelType }, () => document.createElement( editorTag ) );
const newPanel = targetTc.tabs[ targetTc.tabs.length - 1 ].element as HTMLElement;
state.openDocumentIn( path, newPanel );
return;
}
}
}
state.openDocument( path ); state.openDocument( path );
} ); } );
el.addEventListener( 'contextmenu', ( e: Event ) => { el.addEventListener( 'contextmenu', ( e: Event ) => {

View File

@ -41,6 +41,7 @@ class HtmlEditorPanel extends HTMLElement
Editor.get().onDocumentOpened.addListener( ( e ) => Editor.get().onDocumentOpened.addListener( ( e ) =>
{ {
if ( 'html-editor-panel' !== e.editorTag ) return; if ( 'html-editor-panel' !== e.editorTag ) return;
if ( e.targetElement && e.targetElement !== this ) return;
if ( ! this._pinned ) this._loadDocument( e.path, e.content ); if ( ! this._pinned ) this._loadDocument( e.path, e.content );
} ); } );

View File

@ -200,6 +200,7 @@ class RojoSettingsPanel extends HTMLElement
Editor.get().onDocumentOpened.addListener( e => Editor.get().onDocumentOpened.addListener( e =>
{ {
if ( 'rojo-settings-panel' !== e.editorTag ) return; if ( 'rojo-settings-panel' !== e.editorTag ) return;
if ( e.targetElement && e.targetElement !== this ) return;
this._loadDocument( e.path, e.content ); this._loadDocument( e.path, e.content );
} ); } );

View File

@ -7,6 +7,7 @@ export interface DocumentOpenedEvent
path: string; path: string;
content: string; content: string;
editorTag: string; editorTag: string;
targetElement?: HTMLElement;
} }
export interface DocumentPathEvent export interface DocumentPathEvent
@ -78,6 +79,29 @@ export class Editor
this.onDocumentDirty.dispatch( { path: filePath } ); this.onDocumentDirty.dispatch( { path: filePath } );
} }
async openDocumentIn( filePath: string, panelElement: HTMLElement ): Promise<void>
{
await this.fileEditorRegistry.load( this.projectId );
const editorTag = this.fileEditorRegistry.resolve( filePath );
if ( editorTag === null )
{
this.onFileTypeUnknown.dispatch( { path: filePath } );
return;
}
if ( !this.openDocs.has( filePath ) )
{
const res = await fetch( `/api/files/${this.projectId}/${filePath}` );
const content = await res.text();
this.openDocs.set( filePath, { content, dirty: false } );
}
this.activeDoc = filePath;
const entry = this.openDocs.get( filePath )!;
this.onDocumentOpened.dispatch( { path: filePath, content: entry.content, editorTag, targetElement: panelElement } );
}
async save( filePath: string ): Promise<void> async save( filePath: string ): Promise<void>
{ {
const doc = this.openDocs.get( filePath ); const doc = this.openDocs.get( filePath );

View File

@ -21,8 +21,17 @@ export class FileEditorRegistry
{ suffix: 'xml', editor: 'CodePanel' }, { suffix: 'xml', editor: 'CodePanel' },
{ suffix: 'yaml', editor: 'CodePanel' }, { suffix: 'yaml', editor: 'CodePanel' },
{ suffix: 'yml', editor: 'CodePanel' }, { suffix: 'yml', editor: 'CodePanel' },
{ suffix: 'sh', editor: 'CodePanel' }, { suffix: 'sh', editor: 'CodePanel' },
{ suffix: 'py', editor: 'CodePanel' }, { suffix: 'py', editor: 'CodePanel' },
{ suffix: 'cs', editor: 'CodePanel' },
{ suffix: 'gd', editor: 'CodePanel' },
{ suffix: 'gdshader', editor: 'CodePanel' },
{ suffix: 'gdshaderinc', editor: 'CodePanel' },
{ suffix: 'gdinclude', editor: 'CodePanel' },
{ suffix: 'glsl', editor: 'CodePanel' },
{ suffix: 'tscn', editor: 'CodePanel' },
{ suffix: 'tres', editor: 'CodePanel' },
{ suffix: 'gdextension', editor: 'CodePanel' },
]; ];
static readonly EditorTagNames: Record<string, string> = static readonly EditorTagNames: Record<string, string> =