Add mutation handling to norm editor; disable socket connection for now
This commit is contained in:
parent
c4e641545c
commit
60e08d8a76
@ -1,7 +1,9 @@
|
|||||||
<div [ngClass]="isDark() ? 'container dark' : 'container'">
|
<div [ngClass]="isDark() ? 'container dark' : 'container'">
|
||||||
<wysiwyg-editor
|
<wysiwyg-editor
|
||||||
|
#wysiwygComponent
|
||||||
[contents]="contents"
|
[contents]="contents"
|
||||||
(contentsChanged)="onContentsChanged($event)"
|
(contentsChanged)="onContentsChanged($event)"
|
||||||
|
(contentsMutated)="onContentsMutated($event)"
|
||||||
(selectionChanged)="onSelectionChanged($event)"
|
(selectionChanged)="onSelectionChanged($event)"
|
||||||
[readonly]="isReadonly"
|
[readonly]="isReadonly"
|
||||||
[editingUsers]="editorGroupUsers"
|
[editingUsers]="editorGroupUsers"
|
||||||
|
@ -4,7 +4,7 @@ import {EditorService} from '../../../service/editor.service';
|
|||||||
import {FlitterSocketConnection, FlitterSocketServerClientTransaction} from '../../../flitter-socket';
|
import {FlitterSocketConnection, FlitterSocketServerClientTransaction} from '../../../flitter-socket';
|
||||||
import {ApiService} from '../../../service/api.service';
|
import {ApiService} from '../../../service/api.service';
|
||||||
import {debug} from '../../../utility';
|
import {debug} from '../../../utility';
|
||||||
import { EditingUserSelect } from '../../wysiwyg/wysiwyg.component';
|
import {EditingUserSelect, MutationBroadcast, WysiwygComponent} from '../../wysiwyg/wysiwyg.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'editor-norm',
|
selector: 'editor-norm',
|
||||||
@ -13,6 +13,7 @@ import { EditingUserSelect } from '../../wysiwyg/wysiwyg.component';
|
|||||||
})
|
})
|
||||||
export class NormComponent extends EditorNodeContract implements OnInit, OnDestroy {
|
export class NormComponent extends EditorNodeContract implements OnInit, OnDestroy {
|
||||||
@ViewChild('editable') editable;
|
@ViewChild('editable') editable;
|
||||||
|
@ViewChild('wysiwygComponent') wysiwygComponent: WysiwygComponent;
|
||||||
@Input() nodeId: string;
|
@Input() nodeId: string;
|
||||||
@Input() editorUUID?: string;
|
@Input() editorUUID?: string;
|
||||||
|
|
||||||
@ -93,6 +94,7 @@ export class NormComponent extends EditorNodeContract implements OnInit, OnDestr
|
|||||||
|
|
||||||
public async performLoad(): Promise<void> {
|
public async performLoad(): Promise<void> {
|
||||||
// This is called after the Node record has been loaded.
|
// 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 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
|
// 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 }) {
|
async onSelectionChanged(selection: { path: string, offset: number }) {
|
||||||
if ( this.editorGroupSocket && this.editorGroupId ) {
|
if ( this.editorGroupSocket && this.editorGroupId ) {
|
||||||
await this.editorGroupSocket.asyncRequest('set_member_selection', {
|
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() {
|
async refreshRemoteSelections() {
|
||||||
if ( this.editorGroupSocket && this.editorGroupId ) {
|
if ( this.editorGroupSocket && this.editorGroupId ) {
|
||||||
const [
|
const [
|
||||||
|
@ -61,10 +61,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.remote-cursors-container {
|
.remote-cursors-container {
|
||||||
|
padding-left: 20px;
|
||||||
|
|
||||||
.remote-cursor {
|
.remote-cursor {
|
||||||
min-height: 20px;
|
min-height: 20px;
|
||||||
width: 3px;
|
width: 3px;
|
||||||
//position: absolute;
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {debug} from '../../utility';
|
||||||
import {DomSanitizer} from '@angular/platform-browser';
|
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({
|
@Component({
|
||||||
selector: 'wysiwyg-editor',
|
selector: 'wysiwyg-editor',
|
||||||
templateUrl: './wysiwyg.component.html',
|
templateUrl: './wysiwyg.component.html',
|
||||||
@ -52,6 +60,7 @@ export class WysiwygComponent implements OnInit {
|
|||||||
|
|
||||||
@Output() contentsChanged: EventEmitter<string> = new EventEmitter<string>();
|
@Output() contentsChanged: EventEmitter<string> = new EventEmitter<string>();
|
||||||
@Output() selectionChanged: EventEmitter<{path: string, offset: number}> = new EventEmitter<{path: string; offset: number}>();
|
@Output() selectionChanged: EventEmitter<{path: string, offset: number}> = new EventEmitter<{path: string; offset: number}>();
|
||||||
|
@Output() contentsMutated: EventEmitter<MutationBroadcast> = new EventEmitter<MutationBroadcast>();
|
||||||
|
|
||||||
public currentContents = '';
|
public currentContents = '';
|
||||||
protected editingContents = '';
|
protected editingContents = '';
|
||||||
@ -59,6 +68,8 @@ export class WysiwygComponent implements OnInit {
|
|||||||
public isFocused = false;
|
public isFocused = false;
|
||||||
protected hadOneFocusOut = false;
|
protected hadOneFocusOut = false;
|
||||||
protected isEditOnly = false;
|
protected isEditOnly = false;
|
||||||
|
protected applyingRemoteMutation = false;
|
||||||
|
protected ignoreNextMutation = false;
|
||||||
|
|
||||||
public get editonly() {
|
public get editonly() {
|
||||||
return this.isEditOnly;
|
return this.isEditOnly;
|
||||||
@ -178,22 +189,34 @@ export class WysiwygComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
processSelections() {
|
processSelections() {
|
||||||
|
if ( !this.editable ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const editableBase = this.editable.nativeElement;
|
||||||
|
|
||||||
for ( const sel of this.privEditingUserSelections ) {
|
for ( const sel of this.privEditingUserSelections ) {
|
||||||
|
if ( !sel.path ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
sel.element = this.getElementFromPath(sel.path);
|
sel.element = this.getElementFromPath(sel.path);
|
||||||
if ( sel.element ) {
|
if ( sel.element ) {
|
||||||
sel.top = sel.element.offsetTop;
|
sel.top = sel.element.offsetTop;
|
||||||
sel.left = sel.element.offsetLeft;
|
sel.left = sel.element.offsetLeft;
|
||||||
|
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
|
range.collapse(true);
|
||||||
range.setStart(sel.element, sel.offset);
|
range.setStart(sel.element, sel.offset);
|
||||||
range.setEnd(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});
|
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, '');
|
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;
|
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 ) {
|
if ( this.contents !== innerHTML ) {
|
||||||
this.contents = innerHTML;
|
this.contents = innerHTML;
|
||||||
this.contentsChanged.emit(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'])
|
@HostListener('document:keydown.tab', ['$event'])
|
||||||
onIndent(event) {
|
onIndent(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
Loading…
Reference in New Issue
Block a user