feat(project-editor): add reusable contentEditable HTML editor component
This commit is contained in:
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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> </td><td> </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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user