diff --git a/src/app/components/nodes/norm/norm.component.html b/src/app/components/nodes/norm/norm.component.html
index eee556d..352a9f5 100644
--- a/src/app/components/nodes/norm/norm.component.html
+++ b/src/app/components/nodes/norm/norm.component.html
@@ -1,7 +1,9 @@
{
// This is called after the Node record has been loaded.
+ return;
// FIXME need to find a consistent way of doing this on prod/development
// FIXME Probably make use of the systemBase, but allow overriding it in the environment
@@ -138,6 +140,12 @@ export class NormComponent extends EditorNodeContract implements OnInit, OnDestr
}
}
+ applyRemoteContentMutation(transaction: FlitterSocketServerClientTransaction, socket: any) {
+ if ( this.wysiwygComponent && transaction?.incoming?.mutation ) {
+ this.wysiwygComponent.applyRemoteContentMutation(transaction.incoming.mutation);
+ }
+ }
+
async onSelectionChanged(selection: { path: string, offset: number }) {
if ( this.editorGroupSocket && this.editorGroupId ) {
await this.editorGroupSocket.asyncRequest('set_member_selection', {
@@ -147,6 +155,15 @@ export class NormComponent extends EditorNodeContract implements OnInit, OnDestr
}
}
+ async onContentsMutated(data: MutationBroadcast) {
+ if ( this.editorGroupSocket && this.editorGroupId ) {
+ await this.editorGroupSocket.asyncRequest('broadcast_content_mutation', {
+ editor_group_id: this.editorGroupId,
+ data,
+ });
+ }
+ }
+
async refreshRemoteSelections() {
if ( this.editorGroupSocket && this.editorGroupId ) {
const [
diff --git a/src/app/components/wysiwyg/wysiwyg.component.scss b/src/app/components/wysiwyg/wysiwyg.component.scss
index 0ba9242..0bad0e1 100644
--- a/src/app/components/wysiwyg/wysiwyg.component.scss
+++ b/src/app/components/wysiwyg/wysiwyg.component.scss
@@ -61,10 +61,11 @@
}
.remote-cursors-container {
+ padding-left: 20px;
+
.remote-cursor {
min-height: 20px;
width: 3px;
- //position: absolute;
position: fixed;
}
}
diff --git a/src/app/components/wysiwyg/wysiwyg.component.ts b/src/app/components/wysiwyg/wysiwyg.component.ts
index 4118577..e9a6d0a 100644
--- a/src/app/components/wysiwyg/wysiwyg.component.ts
+++ b/src/app/components/wysiwyg/wysiwyg.component.ts
@@ -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 = new EventEmitter();
@Output() selectionChanged: EventEmitter<{path: string, offset: number}> = new EventEmitter<{path: string; offset: number}>();
+ @Output() contentsMutated: EventEmitter = new EventEmitter();
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();