Add mutation handling to norm editor; disable socket connection for now
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing

This commit is contained in:
Garrett Mills 2021-01-31 10:26:07 -06:00
parent c4e641545c
commit 60e08d8a76
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
4 changed files with 129 additions and 9 deletions

View File

@ -1,7 +1,9 @@
<div [ngClass]="isDark() ? 'container dark' : 'container'">
<wysiwyg-editor
#wysiwygComponent
[contents]="contents"
(contentsChanged)="onContentsChanged($event)"
(contentsMutated)="onContentsMutated($event)"
(selectionChanged)="onSelectionChanged($event)"
[readonly]="isReadonly"
[editingUsers]="editorGroupUsers"

View File

@ -4,7 +4,7 @@ import {EditorService} from '../../../service/editor.service';
import {FlitterSocketConnection, FlitterSocketServerClientTransaction} from '../../../flitter-socket';
import {ApiService} from '../../../service/api.service';
import {debug} from '../../../utility';
import { EditingUserSelect } from '../../wysiwyg/wysiwyg.component';
import {EditingUserSelect, MutationBroadcast, WysiwygComponent} from '../../wysiwyg/wysiwyg.component';
@Component({
selector: 'editor-norm',
@ -13,6 +13,7 @@ import { EditingUserSelect } from '../../wysiwyg/wysiwyg.component';
})
export class NormComponent extends EditorNodeContract implements OnInit, OnDestroy {
@ViewChild('editable') editable;
@ViewChild('wysiwygComponent') wysiwygComponent: WysiwygComponent;
@Input() nodeId: string;
@Input() editorUUID?: string;
@ -93,6 +94,7 @@ export class NormComponent extends EditorNodeContract implements OnInit, OnDestr
public async performLoad(): Promise<void> {
// 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 [

View File

@ -61,10 +61,11 @@
}
.remote-cursors-container {
padding-left: 20px;
.remote-cursor {
min-height: 20px;
width: 3px;
//position: absolute;
position: fixed;
}
}

View File

@ -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];
sel.top = rect.top;
sel.left = rect.left;
}
const rect = range.getBoundingClientRect();
console.log({sel, editable: this.editable});
sel.top = rect.top;
// 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();