rojects/source/components/html-editor-panel/html-editor-panel.ts

241 lines
6.8 KiB
TypeScript
Raw Normal View History

import { Editor } from '../../editor/Editor.js';
import { ContextMenuDirectory, ContextMenuReadOnlyEntry } from '../context-menu/context-menu.js';
class HtmlEditorPanel extends HTMLElement
{
currentPath: string | null = null;
_pinned: boolean = false;
_iframe: HTMLIFrameElement | null = null;
_undoStack: string[] = [];
_redoStack: string[] = [];
_mutationObserver: MutationObserver | null = null;
_initialized = false;
_needsRestore = false;
connectedCallback(): void
{
if ( this._initialized ) return;
this._initialized = true;
this.className = 'html-editor-panel';
this.innerHTML = `
<div class="hep-toolbar">
<button class="hep-pin" title="Pin — keep this file when selecting others">Pin</button>
<button class="hep-undo" title="Undo (Ctrl+Z)" disabled></button>
<button class="hep-redo" title="Redo (Ctrl+Y)" disabled></button>
<button class="hep-save" title="Save (Ctrl+S)" disabled>Save</button>
<button class="hep-init" title="Insert Hello World template" disabled>Init</button>
</div>
<div class="hep-empty">Open an HTML file from the file tree</div>
<iframe class="hep-frame" style="display:none"></iframe>
`;
this._iframe = this.querySelector( 'iframe' );
this.querySelector( '.hep-pin' )!.addEventListener( 'click', () => this._togglePin() );
this.querySelector( '.hep-save' )!.addEventListener( 'click', () => this._save() );
this.querySelector( '.hep-undo' )!.addEventListener( 'click', () => this._undo() );
this.querySelector( '.hep-redo' )!.addEventListener( 'click', () => this._redo() );
this.querySelector( '.hep-init' )!.addEventListener( 'click', () => this._initTemplate() );
Editor.get().onDocumentOpened.addListener( ( e ) =>
{
if ( 'html-editor-panel' !== e.editorTag ) return;
if ( ! this._pinned ) this._loadDocument( e.path, e.content );
} );
document.addEventListener( 'keydown', ( e: KeyboardEvent ) =>
{
if ( ! this.currentPath )
{
return;
}
if ( e.ctrlKey && e.key === 's' ) { e.preventDefault(); this._save(); }
if ( e.ctrlKey && ! e.shiftKey && e.key === 'z' ) { e.preventDefault(); this._undo(); }
if ( e.ctrlKey && ( e.key === 'y' || ( e.shiftKey && e.key === 'z' ) ) ) { e.preventDefault(); this._redo(); }
} );
}
disconnectedCallback(): void
{
if ( this._undoStack.length > 0 )
{
this._needsRestore = true;
}
}
_updateTabLabel( path: string ): void
{
const name = path ? path.slice( path.lastIndexOf( '/' ) + 1 ) : '';
this.dispatchEvent( new CustomEvent( 'panel:label-change', { bubbles: true, detail: { label: '📄 ' + name } } ) );
}
_loadDocument( path: string, content: string ): void
{
this.currentPath = path;
this._undoStack = [ content ];
this._redoStack = [];
this._updateTabLabel( path );
this.querySelector( '.hep-empty' )!.setAttribute( 'style', 'display:none' );
this._iframe!.style.display = '';
this._renderContent( content );
this._updateButtons( false );
}
_renderContent( html: string ): void
{
const iframe = this._iframe!;
if ( this._mutationObserver )
{
this._mutationObserver.disconnect();
this._mutationObserver = null;
}
iframe.srcdoc = html;
iframe.onload = () =>
{
if ( this._needsRestore )
{
this._needsRestore = false;
this._renderContent( this._undoStack[ this._undoStack.length - 1 ] );
return;
}
const doc = iframe.contentDocument!;
const pc = doc.querySelector( 'page-content' );
if ( pc )
{
( pc as HTMLElement ).contentEditable = 'true';
( pc as HTMLElement ).style.outline = 'none';
this._mutationObserver = new MutationObserver( () => this._onContentChanged() );
this._mutationObserver.observe( pc, { subtree: true, childList: true, characterData: true, attributes: true } );
}
};
}
_onContentChanged(): void
{
if ( ! this.currentPath || ! this._iframe?.contentDocument )
{
return;
}
const html = '<!DOCTYPE html>\n' + this._iframe.contentDocument.documentElement.outerHTML;
const last = this._undoStack[ this._undoStack.length - 1 ];
if ( html === last )
{
return;
}
this._undoStack.push( html );
if ( this._undoStack.length > 200 )
{
this._undoStack.shift();
}
this._redoStack = [];
Editor.get().markDirty( this.currentPath, html );
this._updateButtons( true );
}
_undo(): void
{
if ( this._undoStack.length < 2 )
{
return;
}
const current = this._undoStack.pop()!;
this._redoStack.push( current );
const prev = this._undoStack[ this._undoStack.length - 1 ];
Editor.get().markDirty( this.currentPath!, prev );
this._renderContent( prev );
this._updateButtons( true );
}
_redo(): void
{
if ( ! this._redoStack.length )
{
return;
}
const next = this._redoStack.pop()!;
this._undoStack.push( next );
Editor.get().markDirty( this.currentPath!, next );
this._renderContent( next );
this._updateButtons( true );
}
_togglePin(): void
{
this._pinned = ! this._pinned;
this.querySelector( '.hep-pin' )!.classList.toggle( 'pinned', this._pinned );
}
_initTemplate(): void
{
if ( ! this.currentPath ) return;
const template = [
'<!DOCTYPE html>',
'<html lang="en">',
'<head>',
' <meta charset="UTF-8">',
' <title>Hello World</title>',
'</head>',
'<body>',
' <page-content>',
' <h1>Hello World</h1>',
' <p>Welcome.</p>',
' </page-content>',
'</body>',
'</html>',
].join( '\n' );
this._undoStack.push( template );
this._redoStack = [];
Editor.get().markDirty( this.currentPath, template );
this._renderContent( template );
this._updateButtons( true );
}
async _save(): Promise<void>
{
if ( ! this.currentPath )
{
return;
}
await Editor.get().save( this.currentPath );
this._updateButtons( false );
}
addContextMenuEntries( dir: ContextMenuDirectory ): void
{
if ( this.currentPath )
{
dir.add( new ContextMenuReadOnlyEntry( dir, `Editing: ${this.currentPath}` ) );
}
else
{
dir.add( new ContextMenuReadOnlyEntry( dir, 'No document open' ) );
}
}
_updateButtons( dirty: boolean ): void
{
( this.querySelector( '.hep-save' ) as HTMLButtonElement ).disabled = ! dirty;
( this.querySelector( '.hep-undo' ) as HTMLButtonElement ).disabled = this._undoStack.length < 2;
( this.querySelector( '.hep-redo' ) as HTMLButtonElement ).disabled = this._redoStack.length === 0;
( this.querySelector( '.hep-init' ) as HTMLButtonElement ).disabled = ! this.currentPath;
}
}
customElements.define( 'html-editor-panel', HtmlEditorPanel );