Move WYSIWYG editor to separate component

This commit is contained in:
Garrett Mills 2020-11-10 08:20:22 -06:00
parent c4e797b6a8
commit c76fc2e82a
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
6 changed files with 247 additions and 155 deletions

View File

@ -33,6 +33,7 @@ import {MarkdownModule} from 'ngx-markdown';
import {VersionModalComponent} from './version-modal/version-modal.component'; import {VersionModalComponent} from './version-modal/version-modal.component';
import {EditorPageRoutingModule} from '../pages/editor/editor-routing.module'; import {EditorPageRoutingModule} from '../pages/editor/editor-routing.module';
import {EditorPage} from '../pages/editor/editor.page'; import {EditorPage} from '../pages/editor/editor.page';
import {WysiwygComponent} from './wysiwyg/wysiwyg.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -61,6 +62,7 @@ import {EditorPage} from '../pages/editor/editor.page';
MarkdownEditorComponent, MarkdownEditorComponent,
VersionModalComponent, VersionModalComponent,
EditorPage, EditorPage,
WysiwygComponent,
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -100,6 +102,7 @@ import {EditorPage} from '../pages/editor/editor.page';
MarkdownEditorComponent, MarkdownEditorComponent,
VersionModalComponent, VersionModalComponent,
EditorPage, EditorPage,
WysiwygComponent,
], ],
exports: [ exports: [
NodePickerComponent, NodePickerComponent,
@ -127,6 +130,7 @@ import {EditorPage} from '../pages/editor/editor.page';
MarkdownEditorComponent, MarkdownEditorComponent,
VersionModalComponent, VersionModalComponent,
EditorPage, EditorPage,
WysiwygComponent,
] ]
}) })
export class ComponentsModule {} export class ComponentsModule {}

View File

@ -1,83 +1,7 @@
<div [ngClass]="isDark() ? 'container dark' : 'container'" <div [ngClass]="isDark() ? 'container dark' : 'container'">
(dblclick)="onFocusIn($event)"> <wysiwyg-editor
<div class="toolbar-base" *ngIf="isFocused"> [contents]="contents"
<button class="toolbar-button" title="Bold" (click)="documentCommand('bold')"> (contentsChanged)="onContentsChanged($event)"
<i class="icon fa fa-bold"></i> [readonly]="isReadonly"
</button> ></wysiwyg-editor>
<button class="toolbar-button" title="Italic" (click)="documentCommand('italic')">
<i class="icon fa fa-italic"></i>
</button>
<button class="toolbar-button" title="Underline" (click)="documentCommand('underline')">
<i class="icon fa fa-underline"></i>
</button>
<button class="toolbar-button" title="Strikethrough" (click)="documentCommand('strikeThrough')">
<i class="icon fa fa-strikethrough"></i>
</button>
<div class="toolbar-sep"></div>
<button class="toolbar-button" title="Align Left" (click)="documentCommand('justifyLeft')">
<i class="icon fa fa-align-left"></i>
</button>
<button class="toolbar-button" title="Align Center" (click)="documentCommand('justifyCenter')">
<i class="icon fa fa-align-center"></i>
</button>
<button class="toolbar-button" title="Align Right" (click)="documentCommand('justifyRight')">
<i class="icon fa fa-align-right"></i>
</button>
<div class="toolbar-sep"></div>
<button class="toolbar-button" title="Undo" (click)="documentCommand('undo')">
<i class="icon fa fa-undo"></i>
</button>
<button class="toolbar-button" title="Redo" (click)="documentCommand('redo')">
<i class="icon fa fa-redo"></i>
</button>
<div class="toolbar-sep"></div>
<button class="toolbar-button" title="Make text bigger" (click)="documentCommand('increaseFontSize')">
<i class="icon fa fa-font"></i>
<i class="icon fa fa-long-arrow-alt-up"></i>
</button>
<button class="toolbar-button" title="Make text smaller" (click)="documentCommand('decreaseFontSize')">
<i class="icon fa fa-font"></i>
<i class="icon fa fa-long-arrow-alt-down"></i>
</button>
<button class="toolbar-button" title="Make text superscript" (click)="documentCommand('superscript')">
<i class="icon fa fa-superscript"></i>
</button>
<button class="toolbar-button" title="Make text subscript" (click)="documentCommand('subscript')">
<i class="icon fa fa-subscript"></i>
</button>
<div class="toolbar-sep"></div>
<button class="toolbar-button" title="Insert unordered list" (click)="documentCommand('insertUnorderedList')">
<i class="icon fa fa-list-ul"></i>
</button>
<button class="toolbar-button" title="Insert ordered list" (click)="documentCommand('insertOrderedList')">
<i class="icon fa fa-list-ol"></i>
</button>
<button class="toolbar-button" title="Insert horizontal rule" (click)="documentCommand('insertHorizontalRule')">
</button>
</div>
<div
class="editable-base"
[ngClass]="isFocused ? 'focused' : ''"
contenteditable
appDomChange
*ngIf="isFocused"
[innerHTML]="initialValue"
#editable
(domChange)="onContentsChanged($event)"
></div>
<div
class="editable-base"
*ngIf="!isFocused"
[innerHTML]="displayContents"
></div>
</div> </div>

View File

@ -12,16 +12,10 @@ export class NormComponent extends EditorNodeContract implements OnInit {
@Input() nodeId: string; @Input() nodeId: string;
@Input() editorUUID?: string; @Input() editorUUID?: string;
public isFocused = false;
public initialValue = 'Click to edit...'; public initialValue = 'Click to edit...';
protected savedValue = 'Click to edit...'; protected savedValue = 'Click to edit...';
public contents = ''; public contents = '';
private dirtyOverride = false; private dirtyOverride = false;
protected hadOneFocusOut = false;
public get displayContents() {
return this.contents.replace(/</g, '\n<').replace(/(https?:\/\/[^\s]+)/g, (val) => `<a href="${val}" target="_blank">${val}</a>`);
}
constructor( constructor(
public editorService: EditorService, public editorService: EditorService,
@ -39,6 +33,10 @@ export class NormComponent extends EditorNodeContract implements OnInit {
return this.dirtyOverride || this.contents !== this.savedValue; return this.dirtyOverride || this.contents !== this.savedValue;
} }
public get isReadonly(): boolean {
return !this.editorService.canEdit();
}
public writeChangesToNode(): void | Promise<void> { public writeChangesToNode(): void | Promise<void> {
this.nodeRec.value = this.contents; this.nodeRec.value = this.contents;
this.savedValue = this.contents; this.savedValue = this.contents;
@ -60,75 +58,10 @@ export class NormComponent extends EditorNodeContract implements OnInit {
}); });
} }
onFocusIn(event: MouseEvent) {
this.isFocused = this.editorService.canEdit();
}
@HostListener('document:keyup.escape', ['$event'])
onFocusOut(event) {
if ( !this.hadOneFocusOut ) {
this.hadOneFocusOut = true;
setTimeout(() => {
this.hadOneFocusOut = false;
}, 500);
} else {
this.isFocused = false;
this.hadOneFocusOut = false;
}
}
documentCommand(cmd: string) {
// Yes, technically this is deprecated, but it'll be poly-filled if necessary. Bite me.
document.execCommand(cmd, false, '');
}
onContentsChanged(contents: string) { onContentsChanged(contents: string) {
const innerHTML = this.editable.nativeElement.innerHTML; if ( this.contents !== contents ) {
if ( this.contents !== innerHTML ) { this.contents = contents;
this.contents = innerHTML;
this.editorService.triggerSave(); this.editorService.triggerSave();
} }
} }
@HostListener('document:keydown.tab', ['$event'])
onIndent(event) {
event.preventDefault();
this.documentCommand('indent');
}
@HostListener('document:keydown.shift.tab', ['$event'])
onOutdent(event) {
event.preventDefault();
this.documentCommand('outdent');
}
@HostListener('document:keydown.control.b', ['$event'])
onBold(event) {
event.preventDefault();
this.documentCommand('bold');
}
@HostListener('document:keydown.control.i', ['$event'])
onItalic(event) {
event.preventDefault();
this.documentCommand('italic');
}
@HostListener('document:keydown.control.u', ['$event'])
onUnderline(event) {
event.preventDefault();
this.documentCommand('underline');
}
@HostListener('document:keydown.control.z', ['$event'])
onUndo(event) {
event.preventDefault();
this.documentCommand('undo');
}
@HostListener('document:keydown.control.shift.z', ['$event'])
onRedo(event) {
event.preventDefault();
this.documentCommand('redo');
}
} }

View File

@ -0,0 +1,83 @@
<div [ngClass]="isDark() ? 'container dark' : 'container'"
(dblclick)="onFocusIn($event)">
<div class="toolbar-base" *ngIf="isFocused">
<button class="toolbar-button" title="Bold" (click)="documentCommand('bold')">
<i class="icon fa fa-bold"></i>
</button>
<button class="toolbar-button" title="Italic" (click)="documentCommand('italic')">
<i class="icon fa fa-italic"></i>
</button>
<button class="toolbar-button" title="Underline" (click)="documentCommand('underline')">
<i class="icon fa fa-underline"></i>
</button>
<button class="toolbar-button" title="Strikethrough" (click)="documentCommand('strikeThrough')">
<i class="icon fa fa-strikethrough"></i>
</button>
<div class="toolbar-sep"></div>
<button class="toolbar-button" title="Align Left" (click)="documentCommand('justifyLeft')">
<i class="icon fa fa-align-left"></i>
</button>
<button class="toolbar-button" title="Align Center" (click)="documentCommand('justifyCenter')">
<i class="icon fa fa-align-center"></i>
</button>
<button class="toolbar-button" title="Align Right" (click)="documentCommand('justifyRight')">
<i class="icon fa fa-align-right"></i>
</button>
<div class="toolbar-sep"></div>
<button class="toolbar-button" title="Undo" (click)="documentCommand('undo')">
<i class="icon fa fa-undo"></i>
</button>
<button class="toolbar-button" title="Redo" (click)="documentCommand('redo')">
<i class="icon fa fa-redo"></i>
</button>
<div class="toolbar-sep"></div>
<button class="toolbar-button" title="Make text bigger" (click)="documentCommand('increaseFontSize')">
<i class="icon fa fa-font"></i>
<i class="icon fa fa-long-arrow-alt-up"></i>
</button>
<button class="toolbar-button" title="Make text smaller" (click)="documentCommand('decreaseFontSize')">
<i class="icon fa fa-font"></i>
<i class="icon fa fa-long-arrow-alt-down"></i>
</button>
<button class="toolbar-button" title="Make text superscript" (click)="documentCommand('superscript')">
<i class="icon fa fa-superscript"></i>
</button>
<button class="toolbar-button" title="Make text subscript" (click)="documentCommand('subscript')">
<i class="icon fa fa-subscript"></i>
</button>
<div class="toolbar-sep"></div>
<button class="toolbar-button" title="Insert unordered list" (click)="documentCommand('insertUnorderedList')">
<i class="icon fa fa-list-ul"></i>
</button>
<button class="toolbar-button" title="Insert ordered list" (click)="documentCommand('insertOrderedList')">
<i class="icon fa fa-list-ol"></i>
</button>
<button class="toolbar-button" title="Insert horizontal rule" (click)="documentCommand('insertHorizontalRule')">
</button>
</div>
<div
class="editable-base"
[ngClass]="isFocused ? 'focused' : ''"
contenteditable
appDomChange
*ngIf="isFocused"
[innerHTML]="initialValue"
#editable
(domChange)="onContentsChanged($event)"
></div>
<div
class="editable-base"
*ngIf="!isFocused"
[innerHTML]="displayContents"
></div>
</div>

View File

@ -0,0 +1,46 @@
.editable-base {
padding: 20px;
&.focused {
background: aliceblue;
}
}
.toolbar-base {
height: 40px;
border: 1px solid lightgray;
border-radius: 5px;
display: flex;
flex-direction: row;
.toolbar-button {
height: calc(100% - 6px);
min-width: 30px;
display: flex;
justify-content: center;
align-items: center;
margin: 3px;
&:hover {
background: lightgrey;
cursor: pointer;
}
}
.toolbar-sep {
height: 100%;
width: 1px;
border: 1px solid lightgrey;
margin: 0 5px;
}
}
.container.dark {
.editable-base.focused {
background: #404040 !important;
}
.toolbar-base .toolbar-button:hover {
background: #404040;
}
}

View File

@ -0,0 +1,102 @@
import {Component, EventEmitter, HostListener, Input, OnInit, Output, ViewChild} from '@angular/core';
@Component({
selector: 'wysiwyg-editor',
templateUrl: './wysiwyg.component.html',
styleUrls: ['./wysiwyg.component.scss'],
})
export class WysiwygComponent implements OnInit {
@ViewChild('editable') editable;
@Input() readonly = false;
@Input() contents = '';
@Output() contentsChanged: EventEmitter<string> = new EventEmitter<string>();
public isFocused = false;
protected hadOneFocusOut = false;
public initialValue = '';
public get displayContents() {
return this.contents.replace(/</g, '\n<').replace(/(https?:\/\/[^\s]+)/g, (val) => `<a href="${val}" target="_blank">${val}</a>`);
}
public isDark() {
return document.body.classList.contains('dark');
}
ngOnInit() {
this.initialValue = this.contents;
}
onFocusIn(event: MouseEvent) {
console.log('on focus in', event);
this.isFocused = !this.readonly;
}
@HostListener('document:keyup.escape', ['$event'])
onFocusOut(event) {
if ( !this.hadOneFocusOut ) {
this.hadOneFocusOut = true;
setTimeout(() => {
this.hadOneFocusOut = false;
}, 500);
} else {
this.isFocused = false;
this.hadOneFocusOut = false;
}
}
documentCommand(cmd: string) {
// Yes, technically this is deprecated, but it'll be poly-filled if necessary. Bite me.
document.execCommand(cmd, false, '');
}
onContentsChanged(contents: string) {
const innerHTML = this.editable.nativeElement.innerHTML;
if ( this.contents !== innerHTML ) {
this.contents = innerHTML;
this.contentsChanged.emit(innerHTML);
}
}
@HostListener('document:keydown.tab', ['$event'])
onIndent(event) {
event.preventDefault();
this.documentCommand('indent');
}
@HostListener('document:keydown.shift.tab', ['$event'])
onOutdent(event) {
event.preventDefault();
this.documentCommand('outdent');
}
@HostListener('document:keydown.control.b', ['$event'])
onBold(event) {
event.preventDefault();
this.documentCommand('bold');
}
@HostListener('document:keydown.control.i', ['$event'])
onItalic(event) {
event.preventDefault();
this.documentCommand('italic');
}
@HostListener('document:keydown.control.u', ['$event'])
onUnderline(event) {
event.preventDefault();
this.documentCommand('underline');
}
@HostListener('document:keydown.control.z', ['$event'])
onUndo(event) {
event.preventDefault();
this.documentCommand('undo');
}
@HostListener('document:keydown.control.shift.z', ['$event'])
onRedo(event) {
event.preventDefault();
this.documentCommand('redo');
}
}