240 lines
6.8 KiB
TypeScript
240 lines
6.8 KiB
TypeScript
|
|
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 ( ! 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 );
|