From 60e08d8a7667e3e516a00e8e70f0bbdbef23d1fb Mon Sep 17 00:00:00 2001 From: garrettmills Date: Sun, 31 Jan 2021 10:26:07 -0600 Subject: [PATCH] Add mutation handling to norm editor; disable socket connection for now --- .../components/nodes/norm/norm.component.html | 2 + .../components/nodes/norm/norm.component.ts | 19 ++- .../components/wysiwyg/wysiwyg.component.scss | 3 +- .../components/wysiwyg/wysiwyg.component.ts | 112 +++++++++++++++++- 4 files changed, 128 insertions(+), 8 deletions(-) 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();