feat(project-editor): add reusable contentEditable HTML editor component

This commit is contained in:
sdarbinyan
2026-07-13 09:12:59 +04:00
parent 16a134ca42
commit fdf11a7766
3 changed files with 139 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
<div class="html-editor">
<div class="html-editor-toolbar">
@for (item of toolbar; track item.id) {
<button type="button" (click)="runCommand(item)">{{ item.label }}</button>
}
<button type="button" (click)="toggleCode()">{{ showCode() ? 'Preview' : 'Code' }}</button>
</div>
@if (showCode()) {
<textarea
class="html-editor-code"
rows="10"
[value]="codeValue()"
(input)="updateCode($any($event.target).value)"
></textarea>
} @else {
<div
#surface
class="html-editor-surface"
contenteditable="true"
(input)="onInput()"
></div>
}
</div>

View File

@@ -0,0 +1,27 @@
.html-editor {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.html-editor-toolbar {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.html-editor-surface {
min-height: 160px;
border: 1px solid var(--border, #ccc);
border-radius: 4px;
padding: 0.5rem;
overflow-y: auto;
}
.html-editor-code {
min-height: 160px;
font-family: monospace;
border: 1px solid var(--border, #ccc);
border-radius: 4px;
padding: 0.5rem;
}

View File

@@ -0,0 +1,88 @@
import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewChild, signal } from '@angular/core';
export interface HtmlEditorToolbarCommand {
id: string;
label: string;
command: string;
value?: string;
}
export const HTML_EDITOR_TOOLBAR: HtmlEditorToolbarCommand[] = [
{ id: 'bold', label: 'B', command: 'bold' },
{ id: 'italic', label: 'I', command: 'italic' },
{ id: 'underline', label: 'U', command: 'underline' },
{ id: 'h2', label: 'H2', command: 'formatBlock', value: 'H2' },
{ id: 'h3', label: 'H3', command: 'formatBlock', value: 'H3' },
{ id: 'ul', label: 'List', command: 'insertUnorderedList' },
{ id: 'ol', label: '1,2,3', command: 'insertOrderedList' },
{ id: 'link', label: 'Link', command: 'createLink' },
{ id: 'image', label: 'Image', command: 'insertImage' },
{ id: 'table', label: 'Table', command: 'insertHTML', value: '<table><tr><td>&nbsp;</td><td>&nbsp;</td></tr></table>' },
];
@Component({
selector: 'app-marketplace-html-editor',
standalone: true,
imports: [],
templateUrl: './marketplace-html-editor.component.html',
styleUrls: ['./marketplace-html-editor.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MarketplaceHtmlEditorComponent implements OnChanges {
@Input() html = '';
@Output() htmlChange = new EventEmitter<string>();
@ViewChild('surface', { static: true }) surface!: ElementRef<HTMLDivElement>;
readonly toolbar = HTML_EDITOR_TOOLBAR;
readonly showCode = signal(false);
readonly codeValue = signal('');
ngOnChanges(changes: SimpleChanges): void {
if (changes['html'] && this.surface && this.surface.nativeElement.innerHTML !== (this.html || '')) {
this.surface.nativeElement.innerHTML = this.html || '';
}
}
runCommand(item: HtmlEditorToolbarCommand): void {
this.surface.nativeElement.focus();
if (item.command === 'createLink') {
const url = window.prompt('URL');
if (!url) {
return;
}
document.execCommand('createLink', false, url);
} else if (item.command === 'insertImage') {
const url = window.prompt('Image URL');
if (!url) {
return;
}
document.execCommand('insertImage', false, url);
} else {
document.execCommand(item.command, false, item.value);
}
this.emitChange();
}
onInput(): void {
this.emitChange();
}
toggleCode(): void {
if (!this.showCode()) {
this.codeValue.set(this.surface.nativeElement.innerHTML);
this.showCode.set(true);
return;
}
this.surface.nativeElement.innerHTML = this.codeValue();
this.showCode.set(false);
this.emitChange();
}
updateCode(value: string): void {
this.codeValue.set(value);
}
private emitChange(): void {
this.htmlChange.emit(this.surface.nativeElement.innerHTML);
}
}