Skip to content
Snippets Groups Projects
RightArea.js 7.52 KiB
Newer Older
chris's avatar
chris committed
/* eslint-disable no-param-reassign */
chris's avatar
chris committed
/* eslint react/prop-types: 0 */
import React, { useContext, useState, useMemo, useCallback } from 'react';
chris's avatar
chris committed
import useDeepCompareEffect from 'use-deep-compare-effect';
import { each, uniqBy, sortBy, groupBy } from 'lodash';
chris's avatar
chris committed
import { WaxContext, DocumentHelpers } from 'wax-prosemirror-core';
chris's avatar
chris committed
import BoxList from './BoxList';
export default ({ area, users }) => {
chris's avatar
chris committed
  const {
chris's avatar
chris committed
    pmViews,
    pmViews: { main },
chris's avatar
chris committed
    app,
    activeView,
    options: { comments },
chris's avatar
chris committed
  } = useContext(WaxContext);
chris's avatar
chris committed
  const commentPlugin = app.PmPlugins.get('commentPlugin');
chris's avatar
chris committed
  const trakChangePlugin = app.PmPlugins.get('trackChangePlugin');
chris's avatar
chris committed
  const [marksNodes, setMarksNodes] = useState([]);
chris's avatar
chris committed

chris's avatar
chris committed
  const [position, setPosition] = useState();
  const [isFirstRun, setFirstRun] = useState(true);
chris's avatar
chris committed

  const setTops = useCallback(() => {
    const result = [];
chris's avatar
chris committed
    let markNodeEl = null;
chris's avatar
chris committed
    let annotationTop = 0;
    let boxHeight = 0;
    let top = 0;
chris's avatar
chris committed
    let WaxSurface = {};
    let WaxSurfaceMarginTop = '';
    const allCommentsTop = [];
chris's avatar
chris committed
    let panelWrapper = {};
    let panelWrapperHeight = {};
    if (main) {
      WaxSurface = main.dom.getBoundingClientRect();
      WaxSurfaceMarginTop = window.getComputedStyle(main.dom).marginTop;
chris's avatar
chris committed
    }
chris's avatar
chris committed

chris's avatar
chris committed
    each(marksNodes[area], (markNode, pos) => {
chris's avatar
chris committed
      let id = '';

      if (markNode?.node?.attrs.id) {
        id = markNode.node.attrs.id;
      } else if (markNode?.attrs?.id) {
        id = markNode.attrs.id;
      } else {
        id = markNode.id;
      }

chris's avatar
chris committed
      let activeTrackChange = null;
chris's avatar
chris committed
      const activeComment = commentPlugin.getState(activeView.state).comment;
chris's avatar
chris committed
      if (trakChangePlugin)
        activeTrackChange = trakChangePlugin.getState(activeView.state)
          .trackChange;
chris's avatar
chris committed

chris's avatar
chris committed
      let isActive = false;
chris's avatar
chris committed
      if (
chris's avatar
chris committed
        (activeComment && id === activeComment.id) ||
chris's avatar
chris committed
        (activeTrackChange && id === activeTrackChange.attrs.id)
      )
        isActive = true;
chris's avatar
chris committed

chris's avatar
chris committed
      // annotation top
      if (area === 'main') {
chris's avatar
chris committed
        markNodeEl = document.querySelector(`[data-id="${id}"]`);
chris's avatar
chris committed
        if (markNodeEl)
          annotationTop =
            markNodeEl.getBoundingClientRect().top -
            WaxSurface.top +
            parseInt(WaxSurfaceMarginTop.slice(0, -2), 10);
chris's avatar
chris committed
      } else {
chris's avatar
chris committed
        // Notes
chris's avatar
chris committed
        panelWrapper = document.getElementsByClassName('panelWrapper');
        panelWrapperHeight = panelWrapper[0].getBoundingClientRect().height;

chris's avatar
chris committed
        markNodeEl = document
          .querySelector('#notes-container')
          .querySelector(`[data-id="${id}"]`);
chris's avatar
chris committed
        if (markNodeEl) {
          const WaxContainerTop = document
            .querySelector('#wax-container')
            .getBoundingClientRect().top;

chris's avatar
chris committed
          annotationTop =
chris's avatar
chris committed
            markNodeEl.getBoundingClientRect().top -
            panelWrapperHeight -
            WaxContainerTop -
            50;
        }
chris's avatar
chris committed
      }

chris's avatar
chris committed
      let boxEl = null;
chris's avatar
chris committed
      // get height of this markNode box
chris's avatar
chris committed
      if (markNodeEl) {
        boxEl = document.querySelector(`div[data-box="${id}"]`);
      }
chris's avatar
chris committed
      if (boxEl) {
        boxHeight = parseInt(boxEl.offsetHeight, 10);
        // where the box should move to
        top = annotationTop;
      }
chris's avatar
chris committed
      // if the above comment box has already taken up the height, move down
      if (pos > 0) {
chris's avatar
chris committed
        const previousBox = marksNodes[area][pos - 1];
chris's avatar
chris committed
        const previousEndHeight = previousBox.endHeight;
        if (annotationTop < previousEndHeight) {
          top = previousEndHeight + 2;
        }
      }
      // store where the box ends to be aware of overlaps in the next box
chris's avatar
chris committed
      markNode.endHeight = top + boxHeight + 4;
chris's avatar
chris committed
      result[pos] = top;
      allCommentsTop.push({ [id]: result[pos] });
chris's avatar
chris committed

      // if active, move as many boxes above as needed to bring it to the annotation's height
      if (isActive) {
chris's avatar
chris committed
        markNode.endHeight = annotationTop + boxHeight + 3;
chris's avatar
chris committed
        result[pos] = annotationTop;
        allCommentsTop[pos][id] = result[pos];
chris's avatar
chris committed
        let b = true;
        let i = pos;

        // first one active, none above
chris's avatar
chris committed
        if (i === 0) b = false;
chris's avatar
chris committed

        while (b) {
chris's avatar
chris committed
          const boxAbove = marksNodes[area][i - 1];
chris's avatar
chris committed
          const boxAboveEnds = boxAbove.endHeight;
          const currentTop = result[i];

          const doesOverlap = boxAboveEnds > currentTop;
chris's avatar
chris committed
          if (doesOverlap) {
            const overlap = boxAboveEnds - currentTop;
            result[i - 1] -= overlap;
chris's avatar
chris committed
            let previousMarkNode = '';

            if (marksNodes[area][i - 1]?.node?.attrs.id) {
              previousMarkNode = marksNodes[area][i - 1].node.attrs.id;
            } else if (marksNodes[area][i - 1]?.attrs?.id) {
              previousMarkNode = marksNodes[area][i - 1].attrs.id;
            } else {
              previousMarkNode = marksNodes[area][i - 1].id;
            }
chris's avatar
chris committed
            allCommentsTop[i - 1][previousMarkNode] = result[i - 1];
chris's avatar
chris committed
          }

          if (!doesOverlap) b = false;
          if (i <= 1) b = false;
          i -= 1;
        }
      }
    });
    return allCommentsTop;
  });

chris's avatar
chris committed
  const recalculateTops = () => {
    setTimeout(() => {
      setPosition(setTops());
    });
  };

chris's avatar
chris committed
  useDeepCompareEffect(() => {
    setMarksNodes(updateMarks(pmViews, comments));
    if (isFirstRun) {
      setTimeout(() => {
        setPosition(setTops());
        setFirstRun(false);
      }, 400);
    } else {
chris's avatar
chris committed
      setPosition(setTops());
  }, [updateMarks(pmViews, comments), setTops()]);
  const CommentTrackComponent = useMemo(
chris's avatar
chris committed
    () => (
      <BoxList
        area={area}
chris's avatar
chris committed
        commentsTracks={marksNodes[area] || []}
chris's avatar
chris committed
        position={position}
chris's avatar
chris committed
        recalculateTops={recalculateTops}
chris's avatar
chris committed
        view={main}
chris's avatar
chris committed
      />
    ),
    [marksNodes[area] || [], position, users],
chris's avatar
chris committed
  );
chris's avatar
chris committed
  return <>{CommentTrackComponent}</>;
const updateMarks = (views, comments) => {
  const newComments = groupBy(comments, comm => comm.data.group) || [];

chris's avatar
chris committed
  if (views.main) {
    const allInlineNodes = [];

chris's avatar
chris committed
    Object.keys(views).forEach(eachView => {
chris's avatar
chris committed
      allInlineNodes.push(
chris's avatar
chris committed
        ...DocumentHelpers.findInlineNodes(views[eachView].state.doc),
chris's avatar
chris committed
      );
chris's avatar
chris committed
    const allBlockNodes = DocumentHelpers.findBlockNodes(views.main.state.doc);
    const finalMarks = [];
chris's avatar
chris committed
    const finalNodes = [];

    allInlineNodes.map(node => {
      if (node.node.marks.length > 0) {
        node.node.marks.filter(mark => {
          if (
chris's avatar
chris committed
            mark.type.name === 'insertion' ||
            mark.type.name === 'deletion' ||
            mark.type.name === 'format_change'
            mark.from = node.pos;
            finalMarks.push(mark);
          }
        });
      }
chris's avatar
chris committed

chris's avatar
chris committed
    allBlockNodes.map(node => {
      if (node.node.attrs.track && node.node.attrs.track.length > 0) {
        finalNodes.push(node);
      }
    });
chris's avatar
chris committed
    const nodesAndMarks = [...uniqBy(finalMarks, 'attrs.id'), ...finalNodes];
    const groupedMarkNodes = { main: [], notes: [] };
    nodesAndMarks.forEach(markNode => {
chris's avatar
chris committed
      const markNodeAttrs = markNode.attrs
        ? markNode.attrs
        : markNode.node.attrs;

      if (!groupedMarkNodes[markNodeAttrs.group]) {
        groupedMarkNodes[markNodeAttrs.group] = [markNode];
      } else {
        groupedMarkNodes[markNodeAttrs.group].push(markNode);
    if (newComments?.main?.length > 0)
      groupedMarkNodes.main = groupedMarkNodes.main.concat(newComments.main);
    if (newComments?.notes?.length > 0)
      groupedMarkNodes.notes = groupedMarkNodes.notes.concat(newComments.notes);
chris's avatar
chris committed
    return {
      main: sortBy(groupedMarkNodes.main, ['from']),
chris's avatar
chris committed
      notes: groupedMarkNodes.notes,
    };
chris's avatar
chris committed
  }
  return [];
};