Skip to content
Snippets Groups Projects
CommentState.js 5.41 KiB
Newer Older
chris's avatar
chris committed
/* eslint-disable no-param-reassign */
import { v4 as uuidv4 } from 'uuid';
import { Decoration, DecorationSet } from 'prosemirror-view';
chris's avatar
chris committed
import {
  ySyncPluginKey,
  relativePositionToAbsolutePosition,
  absolutePositionToRelativePosition,
} from 'y-prosemirror';
chris's avatar
chris committed
import CommentDecoration from './CommentDecoration';
chris's avatar
chris committed
import { CommentDecorationPluginKey } from './CommentDecorationPlugin';
chris's avatar
chris committed

const randomId = () => {
  return uuidv4();
};

export default class CommentState {
  constructor(options) {
    this.decorations = DecorationSet.empty;
    this.options = options;
  }

  addComment(action) {
    const { map } = this.options;
    const { from, to, data } = action;
    const id = randomId();
    map.set(id, { id, from, to, data });
  }

  updateComment(action) {
    const { map } = this.options;
    const annotationToUpdate = map.get(action.id);
    if (annotationToUpdate) {
      annotationToUpdate.data = action.data;
    }
  }

  deleteComment(id) {
    const { map } = this.options;
    map.delete(id);
  }

  commentsAt(position, to) {
    return this.decorations.find(position, to || position).map(decoration => {
      return new CommentDecoration(decoration);
    });
  }

  allCommentsList() {
    const { map } = this.options;
    return Array.from(map, ([key, value]) => {
chris's avatar
chris committed
      return { ...value, id: key };
chris's avatar
chris committed
    }).filter(value => {
      return 'from' in value && 'to' in value;
    });
  }

  createDecorations(state) {
chris's avatar
chris committed
    console.log('in create', this.options.map);
chris's avatar
chris committed
    const decorations = [];
chris's avatar
chris committed
    const ystate = ySyncPluginKey.getState(state);
chris's avatar
chris committed

chris's avatar
chris committed
    const { map } = this.options;
chris's avatar
chris committed
    if (ystate?.binding) {
      const { doc, type, binding } = ystate;
chris's avatar
chris committed

chris's avatar
chris committed
      map.forEach((annotation, id) => {
chris's avatar
chris committed
        console.log('in map', annotation);
        if (typeof annotation.data.yjsFrom === 'number') {
          annotation.data.yjsFrom = absolutePositionToRelativePosition(
            annotation.data.yjsFrom,
            type,
            binding.mapping,
chris's avatar
chris committed
          );
        }
chris's avatar
chris committed
        if (typeof annotation.data.yjsTo === 'number') {
          annotation.data.yjsTo = absolutePositionToRelativePosition(
            annotation.data.yjsTo,
chris's avatar
chris committed
            type,
            binding.mapping,
          );
        }
        const from = relativePositionToAbsolutePosition(
          doc,
          type,
chris's avatar
chris committed
          annotation.data.yjsFrom,
chris's avatar
chris committed
          binding.mapping,
        );
        const to = relativePositionToAbsolutePosition(
          doc,
          type,
chris's avatar
chris committed
          annotation.data.yjsTo,
chris's avatar
chris committed
          binding.mapping,
        );

        if (!from || !to) {
          return;
        }

chris's avatar
chris committed
        decorations.push(
          Decoration.inline(
            from,
            to,
            {
              class: 'comment',
              'data-id': annotation.id,
            },
            {
              id: annotation.id,
              data: annotation,
              inclusiveEnd: true,
            },
          ),
        );
      });
    } else {
      this.allCommentsList().forEach(annotation => {
        const { from, to } = annotation;

chris's avatar
chris committed
        decorations.push(
          Decoration.inline(
            from,
            to,
            {
              class: 'comment',
              'data-id': annotation.id,
            },
            {
              id: annotation.id,
              data: annotation,
              inclusiveEnd: true,
            },
          ),
        );
      });
    }

chris's avatar
chris committed
    this.decorations = DecorationSet.create(state.doc, decorations);
  }

chris's avatar
chris committed
  updateCommentPostions(ystate) {
    this.options.map.doc.transact(() => {
      this.decorations.find().forEach(deco => {
        const { id } = deco.spec;
        const newFrom = absolutePositionToRelativePosition(
          deco.from,
          ystate.type,
          ystate.binding.mapping,
        );
        const newTo = absolutePositionToRelativePosition(
          deco.to,
          ystate.type,
          ystate.binding.mapping,
        );

        const annotation = this.options.map.get(id);
chris's avatar
chris committed
        annotation.from = newFrom;
        annotation.to = newTo;

        this.options.map.set(id, annotation);
      });
    }, CommentDecorationPluginKey);
  }

chris's avatar
chris committed
  apply(transaction, state) {
chris's avatar
chris committed
    const action = transaction.getMeta(CommentDecorationPluginKey);
chris's avatar
chris committed
    if (action && action.type) {
      if (action.type === 'addComment') {
        this.addComment(action);
      }
      if (action.type === 'updateComment') {
        this.updateComment(action);
      }
      if (action.type === 'deleteComment') {
        this.deleteComment(action.id);
      }
chris's avatar
chris committed
      if (action.type === 'createDecorations') {
        this.createDecorations(state);
      }
      // this.createDecorations(state);
chris's avatar
chris committed
      return this;
    }

    const ystate = ySyncPluginKey.getState(state);

chris's avatar
chris committed
    if (ystate?.isChangeOrigin) {
chris's avatar
chris committed
      // this.updateCommentPostions(ystate);
      this.createDecorations(state);

      return this;
    }

    this.decorations = this.decorations.map(
      transaction.mapping,
      transaction.doc,
    );

chris's avatar
chris committed
    if (ystate?.binding && ystate?.binding.mapping) {
chris's avatar
chris committed
      this.updateCommentPostions(ystate);
      return this;
chris's avatar
chris committed
      // eslint-disable-next-line no-else-return
    } else {
      this.options.map.forEach((annotation, _) => {
        if ('from' in annotation && 'to' in annotation) {
          annotation.from = transaction.mapping.map(annotation.from);
          annotation.to = transaction.mapping.map(annotation.to);
        }
      });
      this.createDecorations(state);
      return this;
chris's avatar
chris committed
  }
}