phx-react
Version:
PHX REACT
304 lines • 12.4 kB
JavaScript
import { TOGGLE_CONNECT_COMMAND } from '@lexical/yjs';
import { COMMAND_PRIORITY_LOW } from 'lexical';
import { useEffect, useState } from 'react';
import { Array as YArray, Map as YMap, YArrayEvent } from 'yjs';
function createUID() {
return Math.random()
.toString(36)
.replace(/[^a-z]+/g, '')
.substr(0, 5);
}
export function createComment(content, author, id, timeStamp, deleted) {
return {
author,
content,
deleted: deleted === undefined ? false : deleted,
id: id === undefined ? createUID() : id,
timeStamp: timeStamp === undefined ? performance.now() : timeStamp,
type: 'comment',
};
}
export function createThread(quote, comments, id) {
return {
comments,
id: id === undefined ? createUID() : id,
quote,
type: 'thread',
};
}
function cloneThread(thread) {
return {
comments: Array.from(thread.comments),
id: thread.id,
quote: thread.quote,
type: 'thread',
};
}
function markDeleted(comment) {
return {
author: comment.author,
content: '[Deleted Comment]',
deleted: true,
id: comment.id,
timeStamp: comment.timeStamp,
type: 'comment',
};
}
function triggerOnChange(commentStore) {
const listeners = commentStore._changeListeners;
listeners.forEach((listener) => listener());
}
export class CommentStore {
constructor(editor) {
this._comments = [];
this._editor = editor;
this._collabProvider = null;
this._changeListeners = new Set();
}
isCollaborative() {
return this._collabProvider !== null;
}
getComments() {
return this._comments;
}
addComment(commentOrThread, thread, offset) {
const nextComments = Array.from(this._comments);
// The YJS types explicitly use `any` as well.
const sharedCommentsArray = this._getCollabComments();
if (thread !== undefined && commentOrThread.type === 'comment') {
for (let i = 0; i < nextComments.length; i++) {
const comment = nextComments[i];
if (comment.type === 'thread' && comment.id === thread.id) {
const newThread = cloneThread(comment);
nextComments.splice(i, 1, newThread);
const insertOffset = offset !== undefined ? offset : newThread.comments.length;
if (this.isCollaborative() && sharedCommentsArray !== null) {
const parentSharedArray = sharedCommentsArray.get(i).get('comments');
this._withRemoteTransaction(() => {
const sharedMap = this._createCollabSharedMap(commentOrThread);
parentSharedArray.insert(insertOffset, [sharedMap]);
});
}
newThread.comments.splice(insertOffset, 0, commentOrThread);
break;
}
}
}
else {
const insertOffset = offset !== undefined ? offset : nextComments.length;
if (this.isCollaborative() && sharedCommentsArray !== null) {
this._withRemoteTransaction(() => {
const sharedMap = this._createCollabSharedMap(commentOrThread);
sharedCommentsArray.insert(insertOffset, [sharedMap]);
});
}
nextComments.splice(insertOffset, 0, commentOrThread);
}
this._comments = nextComments;
triggerOnChange(this);
}
deleteCommentOrThread(commentOrThread, thread) {
const nextComments = Array.from(this._comments);
// The YJS types explicitly use `any` as well.
const sharedCommentsArray = this._getCollabComments();
let commentIndex = null;
if (thread !== undefined) {
for (let i = 0; i < nextComments.length; i++) {
const nextComment = nextComments[i];
if (nextComment.type === 'thread' && nextComment.id === thread.id) {
const newThread = cloneThread(nextComment);
nextComments.splice(i, 1, newThread);
const threadComments = newThread.comments;
commentIndex = threadComments.indexOf(commentOrThread);
if (this.isCollaborative() && sharedCommentsArray !== null) {
const parentSharedArray = sharedCommentsArray.get(i).get('comments');
this._withRemoteTransaction(() => {
parentSharedArray.delete(commentIndex);
});
}
threadComments.splice(commentIndex, 1);
break;
}
}
}
else {
commentIndex = nextComments.indexOf(commentOrThread);
if (this.isCollaborative() && sharedCommentsArray !== null) {
this._withRemoteTransaction(() => {
sharedCommentsArray.delete(commentIndex);
});
}
nextComments.splice(commentIndex, 1);
}
this._comments = nextComments;
triggerOnChange(this);
if (commentOrThread.type === 'comment') {
return {
index: commentIndex,
markedComment: markDeleted(commentOrThread),
};
}
return null;
}
registerOnChange(onChange) {
const changeListeners = this._changeListeners;
changeListeners.add(onChange);
return () => {
changeListeners.delete(onChange);
};
}
_withRemoteTransaction(fn) {
const provider = this._collabProvider;
if (provider !== null) {
// @ts-ignore doc does exist
const doc = provider.doc;
doc.transact(fn, this);
}
}
_withLocalTransaction(fn) {
const collabProvider = this._collabProvider;
try {
this._collabProvider = null;
fn();
}
finally {
this._collabProvider = collabProvider;
}
}
_getCollabComments() {
const provider = this._collabProvider;
if (provider !== null) {
// @ts-ignore doc does exist
const doc = provider.doc;
return doc.get('comments', YArray);
}
return null;
}
_createCollabSharedMap(commentOrThread) {
const sharedMap = new YMap();
const type = commentOrThread.type;
const id = commentOrThread.id;
sharedMap.set('type', type);
sharedMap.set('id', id);
if (type === 'comment') {
sharedMap.set('author', commentOrThread.author);
sharedMap.set('content', commentOrThread.content);
sharedMap.set('deleted', commentOrThread.deleted);
sharedMap.set('timeStamp', commentOrThread.timeStamp);
}
else {
sharedMap.set('quote', commentOrThread.quote);
const commentsArray = new YArray();
commentOrThread.comments.forEach((comment, i) => {
const sharedChildComment = this._createCollabSharedMap(comment);
commentsArray.insert(i, [sharedChildComment]);
});
sharedMap.set('comments', commentsArray);
}
return sharedMap;
}
registerCollaboration(provider) {
this._collabProvider = provider;
const sharedCommentsArray = this._getCollabComments();
const connect = () => {
provider.connect();
};
const disconnect = () => {
try {
provider.disconnect();
}
catch (e) {
// Do nothing
}
};
const unsubscribe = this._editor.registerCommand(TOGGLE_CONNECT_COMMAND, (payload) => {
if (connect !== undefined && disconnect !== undefined) {
const shouldConnect = payload;
if (shouldConnect) {
// eslint-disable-next-line no-console
connect();
}
else {
// eslint-disable-next-line no-console
disconnect();
}
}
return false;
}, COMMAND_PRIORITY_LOW);
const onSharedCommentChanges = (
// The YJS types explicitly use `any` as well.
events, transaction) => {
if (transaction.origin !== this) {
for (let i = 0; i < events.length; i++) {
const event = events[i];
if (event instanceof YArrayEvent) {
const target = event.target;
const deltas = event.delta;
let offset = 0;
for (let s = 0; s < deltas.length; s++) {
const delta = deltas[s];
const insert = delta.insert;
const retain = delta.retain;
const del = delta.delete;
const parent = target.parent;
const parentThread = target === sharedCommentsArray
? undefined
: parent instanceof YMap &&
this._comments.find((t) => t.id === parent.get('id'));
if (Array.isArray(insert)) {
insert
.slice()
.reverse()
.forEach((map) => {
const id = map.get('id');
const type = map.get('type');
const commentOrThread = type === 'thread'
? createThread(map.get('quote'), map
.get('comments')
.toArray()
.map((innerComment) => createComment(innerComment.get('content'), innerComment.get('author'), innerComment.get('id'), innerComment.get('timeStamp'), innerComment.get('deleted'))), id)
: createComment(map.get('content'), map.get('author'), id, map.get('timeStamp'), map.get('deleted'));
this._withLocalTransaction(() => {
this.addComment(commentOrThread, parentThread, offset);
});
});
}
else if (typeof retain === 'number') {
offset += retain;
}
else if (typeof del === 'number') {
for (let d = 0; d < del; d++) {
const commentOrThread = parentThread === undefined || parentThread === false
? this._comments[offset]
: parentThread.comments[offset];
this._withLocalTransaction(() => {
this.deleteCommentOrThread(commentOrThread, parentThread);
});
offset++;
}
}
}
}
}
}
};
if (sharedCommentsArray === null) {
return () => null;
}
sharedCommentsArray.observeDeep(onSharedCommentChanges);
connect();
return () => {
sharedCommentsArray.unobserveDeep(onSharedCommentChanges);
unsubscribe();
this._collabProvider = null;
};
}
}
export function useCommentStore(commentStore) {
const [comments, setComments] = useState(commentStore.getComments());
useEffect(() => commentStore.registerOnChange(() => {
setComments(commentStore.getComments());
}), [commentStore]);
return comments;
}
//# sourceMappingURL=index.js.map