diff --git a/wax-prosemirror-components/package.json b/wax-prosemirror-components/package.json index 62e8b017c43b4ca8d748379d4f24a5268ca0076a..d26175b1a0202ce69d469ddd7ca16b11d42bee81 100644 --- a/wax-prosemirror-components/package.json +++ b/wax-prosemirror-components/package.json @@ -17,6 +17,7 @@ "react-dom": "^16.8.6", "react-dropdown": "^1.6.2", "styled-components": "^4.2.0", - "uuid": "^3.3.2" + "uuid": "^3.3.2", + "react-transition-group": "^4.3.0" } } diff --git a/wax-prosemirror-components/src/components/comments/CommentBox.js b/wax-prosemirror-components/src/components/comments/CommentBox.js index 32e0042cb9420d80be1fd128f5adb4b3db1dab72..f5f0119617d7bc21dd772834b74cf90c4eaba937 100644 --- a/wax-prosemirror-components/src/components/comments/CommentBox.js +++ b/wax-prosemirror-components/src/components/comments/CommentBox.js @@ -1,10 +1,7 @@ -import React, { useEffect, useRef, useContext } from "react"; +import React, { Fragment, useState, useCallback, useEffect } from "react"; +import { Transition } from "react-transition-group"; import styled from "styled-components"; -// export default ({ node, view }) => { -// useEffect(() => {}, []); -// }; - const CommentBoxStyled = styled.div` height: 50px; width: 50px; @@ -12,7 +9,42 @@ const CommentBoxStyled = styled.div` flex-direction: column; margin-top: 10px; background: black; + position: absolute; + transition: ${({ state }) => "top 1s, opacity 1.5s"}; + top: ${props => (props.top ? `${props.top}px` : 0)}; + + opacity: ${({ state }) => { + switch (state) { + case "exited": + return 0.2; + case "exiting": + return 0.4; + case "entering": + return 0.6; + case "entered": + return 1; + } + }}; `; -const CommentBox = () => <CommentBoxStyled />; -export default CommentBox; +export default ({ mark, view, top, dataComment }) => { + const [animate, setAnimate] = useState(false); + + useEffect(() => { + setAnimate(true); + }, []); + + return ( + <Fragment> + <Transition in={animate} timeout={1000}> + {state => ( + <CommentBoxStyled + top={top} + state={state} + data-comment={dataComment} + /> + )} + </Transition> + </Fragment> + ); +}; diff --git a/wax-prosemirror-components/src/components/comments/CommentBubbleComponent.js b/wax-prosemirror-components/src/components/comments/CommentBubbleComponent.js index 4e925fdd855ee3c370f7d5c02e4392122e117ec3..763ca2a20e81a6563a6cc60723b751426f799761 100644 --- a/wax-prosemirror-components/src/components/comments/CommentBubbleComponent.js +++ b/wax-prosemirror-components/src/components/comments/CommentBubbleComponent.js @@ -2,9 +2,10 @@ import React, { useLayoutEffect, useState, useContext } from "react"; import styled from "styled-components"; import { Commands } from "wax-prosemirror-utilities"; import { WaxContext } from "wax-prosemirror-core/src/ioc-react"; +import { DocumentHelpers } from "wax-prosemirror-utilities"; const CommentBubbleComponent = ({ setPosition, position }) => { - const { view: { main }, activeView } = useContext(WaxContext); + const { activeView, activeViewId } = useContext(WaxContext); const { state, dispatch } = activeView; useLayoutEffect( () => { @@ -23,17 +24,29 @@ const CommentBubbleComponent = ({ setPosition, position }) => { const createComment = event => { event.preventDefault(); - Commands.createComment(state, dispatch); + Commands.createComment(state, dispatch, activeViewId); + }; + + const isSelectionComment = () => { + const commentMark = activeView.state.schema.marks["comment"]; + const mark = DocumentHelpers.findMark(state, commentMark, true); + const { selection: { $from, $to }, doc } = state; + + //TODO Overlapping comments . for now don't allow + if (mark.length >= 1) return true; + return false; }; return ( - <button - onClick={event => { - createComment(event); - }} - > - create - </button> + !isSelectionComment() && ( + <button + onClick={event => { + createComment(event); + }} + > + create + </button> + ) ); }; diff --git a/wax-prosemirror-components/src/components/comments/CommentsBoxList.js b/wax-prosemirror-components/src/components/comments/CommentsBoxList.js index 01299fe178e4b00f71775474764ea1f0fe5c1caf..8b63ba02501d9bd4c1bf544d664003011d5829b9 100644 --- a/wax-prosemirror-components/src/components/comments/CommentsBoxList.js +++ b/wax-prosemirror-components/src/components/comments/CommentsBoxList.js @@ -1,12 +1,105 @@ -import React, { Fragment } from "react"; +import React, { Fragment, useState, useEffect, useCallback } from "react"; +import { each } from "lodash"; import CommentBox from "./CommentBox"; +//TODO find from marks actual comment mark export default ({ comments, view }) => { + const [position, setPosition] = useState(); + + const setTops = useCallback(() => { + const result = []; + const boxes = []; + let commentEl = null; + let annotationTop = 0; + let boxHeight = 0; + let top = 0; + const allCommentsTop = {}; + + each(comments, (entry, pos) => { + const WaxSurface = view.dom.getBoundingClientRect(); + const id = entry.node.marks[0].attrs.id; + let isActive = false; + // if (entry.id === active) isActive = true + commentEl = document.getElementById(id); + //annotation top + annotationTop = commentEl.getBoundingClientRect().top - WaxSurface.top; + // get height of this comment box + const boxEl = document.querySelector(`div[data-comment="comment-${id}"]`); + if (boxEl) boxHeight = parseInt(boxEl.offsetHeight); + + // keep the elements to add the tops to at the end + boxes.push(boxEl); + + // where the box should move to + top = annotationTop; + + // if the above comment box has already taken up the height, move down + if (pos > 0) { + const previousBox = comments[pos - 1]; + const previousEndHeight = previousBox.endHeight; + if (annotationTop < previousEndHeight) { + top = previousEndHeight + 2; + } + } + // store where the box ends to be aware of overlaps in the next box + entry.endHeight = top + boxHeight + 2; + result[pos] = top; + + // if active, move as many boxes above as needed to bring it to the annotation's height + if (isActive) { + entry.endHeight = annotationTop + boxHeight + 2; + result[pos] = annotationTop; + + let b = true; + let i = pos; + + // first one active, none above + if (i === 0) b = false; + + while (b) { + const boxAbove = comments[i - 1]; + const boxAboveEnds = boxAbove.endHeight; + const currentTop = result[i]; + + const doesOverlap = boxAboveEnds > currentTop; + if (doesOverlap) { + const overlap = boxAboveEnds - currentTop; + result[i - 1] -= overlap; + } + + if (!doesOverlap) b = false; + if (i <= 1) b = false; + i -= 1; + } + } + + allCommentsTop[id] = top; + }); + + return allCommentsTop; + }); + + useEffect( + () => { + setPosition(setTops()); + }, + [JSON.stringify(setTops())] + ); + return ( <Fragment> - {comments.map(comment => ( - <CommentBox key="" node={comment.node} view={view} /> - ))} + {comments.map(comment => { + const id = comment.node.marks[0].attrs.id; + return ( + <CommentBox + key={comment.node.marks[0].attrs.id} + mark={comment.node.marks[0]} + view={view} + top={position[id]} + dataComment={`comment-${id}`} + /> + ); + })} </Fragment> ); }; diff --git a/wax-prosemirror-schema/src/marks/commentMark.js b/wax-prosemirror-schema/src/marks/commentMark.js index 5c47ceae3702804977441ead1606ce2523bc1aaf..a8a5d918600690f9e6def88399784c194c19012d 100644 --- a/wax-prosemirror-schema/src/marks/commentMark.js +++ b/wax-prosemirror-schema/src/marks/commentMark.js @@ -1,14 +1,18 @@ const comment = { attrs: { + id: { default: "" }, + viewId: { default: "" }, conversation: [] }, inclusive: false, - excludes: "", + // excludes: "", parseDOM: [ { tag: "span.comment[data-conversation]", getAttrs(dom) { return { + id: dom.id, + viewId: dom.dataset.viewid, conversation: JSON.parse(dom.dataset.conversation) }; } @@ -19,6 +23,8 @@ const comment = { "span", { class: "comment", + id: node.attrs.id, + "data-viewId": node.attrs.viewId, "data-conversation": JSON.stringify(node.attrs.conversation) } ]; diff --git a/wax-prosemirror-services/src/NoteService/NoteEditor.js b/wax-prosemirror-services/src/NoteService/NoteEditor.js index 177582a2ab0b0d910e2fed5d39d9cc90c4265bdc..7cf4f57522743738fe5b978de33cfbcc0dced83c 100644 --- a/wax-prosemirror-services/src/NoteService/NoteEditor.js +++ b/wax-prosemirror-services/src/NoteService/NoteEditor.js @@ -1,12 +1,12 @@ -import React from "react"; +import React, { Fragment } from "react"; import Editor from "./Editor"; export default ({ notes, view }) => { return ( - <div> + <Fragment> {notes.map(note => ( <Editor key={note.node.attrs.id} node={note.node} view={view} /> ))} - </div> + </Fragment> ); }; diff --git a/wax-prosemirror-utilities/src/commands/Commands.js b/wax-prosemirror-utilities/src/commands/Commands.js index 2f119ea080aa341340f10c72f8181330558c2780..27b25c3b5a95fc4b0123036da47c2ffc9ba9acb1 100644 --- a/wax-prosemirror-utilities/src/commands/Commands.js +++ b/wax-prosemirror-utilities/src/commands/Commands.js @@ -1,3 +1,5 @@ +import { v4 as uuid } from "uuid"; + const markActive = type => state => { const { from, $from, to, empty } = state.selection; @@ -57,13 +59,15 @@ const createLink = (state, dispatch) => { ); }; -const createComment = (state, dispatch) => { +const createComment = (state, dispatch, activeViewId) => { const { selection: { $from, $to } } = state; dispatch( state.tr.addMark( $from.pos, $to.pos, state.schema.marks.comment.create({ + id: uuid(), + viewId: activeViewId, conversation: [] }) ) diff --git a/wax-prosemirror-utilities/src/document/DocumentHelpers.js b/wax-prosemirror-utilities/src/document/DocumentHelpers.js index 62af5f2b1efae3206ec8fc0734c019c519c1fa3b..716b5c35a2b624c645ed78264f18776e2890949e 100644 --- a/wax-prosemirror-utilities/src/document/DocumentHelpers.js +++ b/wax-prosemirror-utilities/src/document/DocumentHelpers.js @@ -1,9 +1,10 @@ -const findMark = (state, PMmark) => { +const findMark = (state, PMmark, toArr = false) => { const { selection: { $from, $to }, doc } = state; const fromMark = $from.marks().find(mark => mark.type === PMmark); const toMark = $to.marks().find(mark => mark.type === PMmark); let markFound; + const marksFound = []; doc.nodesBetween($from.pos, $to.pos, (node, from) => { if (node.marks) { const actualMark = node.marks.find(mark => mark.type === PMmark); @@ -14,9 +15,11 @@ const findMark = (state, PMmark) => { attrs: actualMark.attrs, contained: !fromMark || !toMark || fromMark === toMark }; + marksFound.push(markFound); } } }); + if (toArr) return marksFound; return markFound; };