Move WYSIWYG editor to separate component
This commit is contained in:
parent
c4e797b6a8
commit
c76fc2e82a
@ -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 {}
|
||||||
|
@ -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>
|
@ -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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
83
src/app/components/wysiwyg/wysiwyg.component.html
Normal file
83
src/app/components/wysiwyg/wysiwyg.component.html
Normal 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>
|
46
src/app/components/wysiwyg/wysiwyg.component.scss
Normal file
46
src/app/components/wysiwyg/wysiwyg.component.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
102
src/app/components/wysiwyg/wysiwyg.component.ts
Normal file
102
src/app/components/wysiwyg/wysiwyg.component.ts
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user