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 {EditorPageRoutingModule} from '../pages/editor/editor-routing.module';
|
||||
import {EditorPage} from '../pages/editor/editor.page';
|
||||
import {WysiwygComponent} from './wysiwyg/wysiwyg.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -61,6 +62,7 @@ import {EditorPage} from '../pages/editor/editor.page';
|
||||
MarkdownEditorComponent,
|
||||
VersionModalComponent,
|
||||
EditorPage,
|
||||
WysiwygComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@ -100,6 +102,7 @@ import {EditorPage} from '../pages/editor/editor.page';
|
||||
MarkdownEditorComponent,
|
||||
VersionModalComponent,
|
||||
EditorPage,
|
||||
WysiwygComponent,
|
||||
],
|
||||
exports: [
|
||||
NodePickerComponent,
|
||||
@ -127,6 +130,7 @@ import {EditorPage} from '../pages/editor/editor.page';
|
||||
MarkdownEditorComponent,
|
||||
VersionModalComponent,
|
||||
EditorPage,
|
||||
WysiwygComponent,
|
||||
]
|
||||
})
|
||||
export class ComponentsModule {}
|
||||
|
@ -1,83 +1,7 @@
|
||||
<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 [ngClass]="isDark() ? 'container dark' : 'container'">
|
||||
<wysiwyg-editor
|
||||
[contents]="contents"
|
||||
(contentsChanged)="onContentsChanged($event)"
|
||||
[readonly]="isReadonly"
|
||||
></wysiwyg-editor>
|
||||
</div>
|
@ -12,16 +12,10 @@ export class NormComponent extends EditorNodeContract implements OnInit {
|
||||
@Input() nodeId: string;
|
||||
@Input() editorUUID?: string;
|
||||
|
||||
public isFocused = false;
|
||||
public initialValue = 'Click to edit...';
|
||||
protected savedValue = 'Click to edit...';
|
||||
public contents = '';
|
||||
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(
|
||||
public editorService: EditorService,
|
||||
@ -39,6 +33,10 @@ export class NormComponent extends EditorNodeContract implements OnInit {
|
||||
return this.dirtyOverride || this.contents !== this.savedValue;
|
||||
}
|
||||
|
||||
public get isReadonly(): boolean {
|
||||
return !this.editorService.canEdit();
|
||||
}
|
||||
|
||||
public writeChangesToNode(): void | Promise<void> {
|
||||
this.nodeRec.value = 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) {
|
||||
const innerHTML = this.editable.nativeElement.innerHTML;
|
||||
if ( this.contents !== innerHTML ) {
|
||||
this.contents = innerHTML;
|
||||
if ( this.contents !== contents ) {
|
||||
this.contents = contents;
|
||||
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