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