|
|
|
@ -1,4 +1,4 @@
|
|
|
|
|
import {Component, EventEmitter, HostListener, Input, OnInit, Output, ViewChild} from '@angular/core';
|
|
|
|
|
import {Component, ElementRef, EventEmitter, HostListener, Input, OnInit, Output, ViewChild} from '@angular/core';
|
|
|
|
|
import {debug} from '../../utility';
|
|
|
|
|
import {DomSanitizer} from '@angular/platform-browser';
|
|
|
|
|
|
|
|
|
@ -13,6 +13,14 @@ export interface EditingUserSelect {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface MutationBroadcast {
|
|
|
|
|
path: string;
|
|
|
|
|
type: 'characterData' | 'attributes' | 'childList';
|
|
|
|
|
data: string;
|
|
|
|
|
fullContents: string;
|
|
|
|
|
addedNodes?: Array<{previousSiblingPath: string, type: string, data: string}>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
|
selector: 'wysiwyg-editor',
|
|
|
|
|
templateUrl: './wysiwyg.component.html',
|
|
|
|
@ -52,6 +60,7 @@ export class WysiwygComponent implements OnInit {
|
|
|
|
|
|
|
|
|
|
@Output() contentsChanged: EventEmitter<string> = new EventEmitter<string>();
|
|
|
|
|
@Output() selectionChanged: EventEmitter<{path: string, offset: number}> = new EventEmitter<{path: string; offset: number}>();
|
|
|
|
|
@Output() contentsMutated: EventEmitter<MutationBroadcast> = new EventEmitter<MutationBroadcast>();
|
|
|
|
|
|
|
|
|
|
public currentContents = '';
|
|
|
|
|
protected editingContents = '';
|
|
|
|
@ -59,6 +68,8 @@ export class WysiwygComponent implements OnInit {
|
|
|
|
|
public isFocused = false;
|
|
|
|
|
protected hadOneFocusOut = false;
|
|
|
|
|
protected isEditOnly = false;
|
|
|
|
|
protected applyingRemoteMutation = false;
|
|
|
|
|
protected ignoreNextMutation = false;
|
|
|
|
|
|
|
|
|
|
public get editonly() {
|
|
|
|
|
return this.isEditOnly;
|
|
|
|
@ -178,22 +189,34 @@ export class WysiwygComponent implements OnInit {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
processSelections() {
|
|
|
|
|
if ( !this.editable ) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const editableBase = this.editable.nativeElement;
|
|
|
|
|
|
|
|
|
|
for ( const sel of this.privEditingUserSelections ) {
|
|
|
|
|
if ( !sel.path ) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sel.element = this.getElementFromPath(sel.path);
|
|
|
|
|
if ( sel.element ) {
|
|
|
|
|
sel.top = sel.element.offsetTop;
|
|
|
|
|
sel.left = sel.element.offsetLeft;
|
|
|
|
|
|
|
|
|
|
const range = document.createRange();
|
|
|
|
|
range.collapse(true);
|
|
|
|
|
range.setStart(sel.element, sel.offset);
|
|
|
|
|
range.setEnd(sel.element, sel.offset);
|
|
|
|
|
|
|
|
|
|
const rect = range.getClientRects()[0];
|
|
|
|
|
const rect = range.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
sel.top = rect.top;
|
|
|
|
|
sel.left = rect.left;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log({sel, editable: this.editable});
|
|
|
|
|
// FIXME I'm not sure why the 45px offset is needed, but it is...
|
|
|
|
|
sel.left = (rect.left - editableBase.getBoundingClientRect().left) + 45;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -220,14 +243,91 @@ export class WysiwygComponent implements OnInit {
|
|
|
|
|
document.execCommand(cmd, false, '');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onContentsChanged(contents: string) {
|
|
|
|
|
onContentsChanged(mutation: MutationRecord) {
|
|
|
|
|
if ( this.applyingRemoteMutation ) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( this.ignoreNextMutation ) {
|
|
|
|
|
this.ignoreNextMutation = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const target = this.getPathToElement(mutation.target);
|
|
|
|
|
console.log({mutation, target, type: mutation.type, data: mutation.target.textContent});
|
|
|
|
|
|
|
|
|
|
const innerHTML = this.editable.nativeElement.innerHTML;
|
|
|
|
|
|
|
|
|
|
if ( mutation.type === 'characterData' ) {
|
|
|
|
|
this.contentsMutated.emit({
|
|
|
|
|
path: target,
|
|
|
|
|
type: mutation.type,
|
|
|
|
|
data: mutation.target.textContent,
|
|
|
|
|
fullContents: innerHTML,
|
|
|
|
|
});
|
|
|
|
|
} else if ( mutation.type === 'childList' ) {
|
|
|
|
|
const addedNodes: Array<{previousSiblingPath: string, type: string, data: string}> = [];
|
|
|
|
|
|
|
|
|
|
mutation.addedNodes.forEach(value => {
|
|
|
|
|
const previousSiblingPath = this.getPathToElement(value.previousSibling);
|
|
|
|
|
if ( previousSiblingPath ) {
|
|
|
|
|
addedNodes.push({
|
|
|
|
|
previousSiblingPath,
|
|
|
|
|
type: value.nodeName,
|
|
|
|
|
data: value.textContent,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.contentsMutated.emit({
|
|
|
|
|
path: target,
|
|
|
|
|
type: mutation.type,
|
|
|
|
|
data: mutation.target.textContent,
|
|
|
|
|
fullContents: innerHTML,
|
|
|
|
|
addedNodes,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( this.contents !== innerHTML ) {
|
|
|
|
|
this.contents = innerHTML;
|
|
|
|
|
this.contentsChanged.emit(innerHTML);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
applyRemoteContentMutation(mutation: MutationBroadcast) {
|
|
|
|
|
this.applyingRemoteMutation = true;
|
|
|
|
|
console.log('got remote content mutation', mutation);
|
|
|
|
|
|
|
|
|
|
if ( this.editable ) {
|
|
|
|
|
const target = this.getElementFromPath(mutation.path);
|
|
|
|
|
console.log(target);
|
|
|
|
|
if ( target ) {
|
|
|
|
|
if ( mutation.type === 'characterData' ) {
|
|
|
|
|
this.ignoreNextMutation = true;
|
|
|
|
|
target.nodeValue = mutation.data;
|
|
|
|
|
} else if ( mutation.type === 'childList' ) {
|
|
|
|
|
if ( Array.isArray(mutation.addedNodes) ) {
|
|
|
|
|
for ( const addedNode of mutation.addedNodes ) {
|
|
|
|
|
const previousSibling = this.getElementFromPath(addedNode.previousSiblingPath);
|
|
|
|
|
const elem = document.createElement(addedNode.type);
|
|
|
|
|
elem.nodeValue = addedNode.data;
|
|
|
|
|
|
|
|
|
|
this.ignoreNextMutation = true;
|
|
|
|
|
previousSibling.parentElement.insertBefore(elem, previousSibling.nextSibling);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if ( this.contents !== mutation.fullContents ) {
|
|
|
|
|
this.ignoreNextMutation = true;
|
|
|
|
|
this.contents = mutation.fullContents;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.applyingRemoteMutation = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@HostListener('document:keydown.tab', ['$event'])
|
|
|
|
|
onIndent(event) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|