diff --git a/editors/demo/src/Editoria/config/config.js b/editors/demo/src/Editoria/config/config.js index 5d2fffb61040fa6f621e7bd31bdf4eff256d25c2..d3928aee7130b30ddcb0a4142d91185818c7db75 100644 --- a/editors/demo/src/Editoria/config/config.js +++ b/editors/demo/src/Editoria/config/config.js @@ -178,6 +178,9 @@ export default { ), ], ImageService: { showAlt: true }, + CommentsService: { + showTitle: true + }, CustomTagService: { tags: [ { label: 'custom-tag-label-1', tagType: 'inline' }, diff --git a/editors/demo/src/locale/en.js b/editors/demo/src/locale/en.js index 44286df53fb7d0929cfccb8cea6df5b611396a13..c76abe7066034f466f043b936453aeaaa56d751d 100644 --- a/editors/demo/src/locale/en.js +++ b/editors/demo/src/locale/en.js @@ -147,6 +147,7 @@ const en = { Cancel: 'Cancel', Reply: 'Reply', 'Write comment': 'Write comment', + 'Write title': 'Write title', }, Various: { Add: 'Add', diff --git a/editors/demo/src/locale/es.js b/editors/demo/src/locale/es.js index ce58ba8d8449681110e22b93b1affb3aed3ea49c..556f873a5df00e2895ed963f1e4c9d5b583ca3ae 100644 --- a/editors/demo/src/locale/es.js +++ b/editors/demo/src/locale/es.js @@ -148,6 +148,7 @@ const es = { Cancel: 'Cancelar', Reply: 'Responder', 'Write comment': 'Escribir comentario', + 'Write title': 'Escribir titulo', }, Various: { Add: 'Agregar', diff --git a/wax-prosemirror-services/src/CommentsService/CommentsService.js b/wax-prosemirror-services/src/CommentsService/CommentsService.js index 18a1ab640adadf51e74679efcd71288160baa3eb..fc32e5b5cfb5aa2c662071a32bd24f2ecdfcb9c1 100644 --- a/wax-prosemirror-services/src/CommentsService/CommentsService.js +++ b/wax-prosemirror-services/src/CommentsService/CommentsService.js @@ -31,10 +31,12 @@ export default class CommentsService extends Service { } register() { + const commentConfig = this.config.get('config.CommentsService'); const createMark = this.container.get('CreateMark'); + createMark( { - comment: commentMark, + comment: commentMark(commentConfig?.showTitle || false), }, { toWaxSchema: true }, ); diff --git a/wax-prosemirror-services/src/CommentsService/components/ConnectedComment.js b/wax-prosemirror-services/src/CommentsService/components/ConnectedComment.js index 785e423a25777059172f30a11a8a048acb44f71f..bc7501c2a230415530a9d6476c654d7f293f1f11 100644 --- a/wax-prosemirror-services/src/CommentsService/components/ConnectedComment.js +++ b/wax-prosemirror-services/src/CommentsService/components/ConnectedComment.js @@ -1,3 +1,4 @@ +/* eslint-disable no-param-reassign */ /* eslint react/prop-types: 0 */ import React, { useContext, useMemo, useState, useEffect } from 'react'; import { TextSelection } from 'prosemirror-state'; @@ -55,7 +56,10 @@ export default ({ comment, top, commentId, recalculateTops }) => { }; const commentConfig = app.config.get('config.CommentsService'); - const isReadOnly = commentConfig ? commentConfig.readOnly : false; + const isReadOnly = + commentConfig && commentConfig.readOnly ? commentConfig.readOnly : false; + const showTitle = + commentConfig && commentConfig.showTitle ? commentConfig.showTitle : false; const commentPlugin = app.PmPlugins.get('commentPlugin'); const activeComment = commentPlugin.getState(activeView.state).comment; @@ -68,16 +72,17 @@ export default ({ comment, top, commentId, recalculateTops }) => { } }, [activeComment]); - const onClickPost = content => { - const { tr } = state; + const onClickPost = ({ commentValue, title }) => { setClickPost(true); const obj = { - content, + content: commentValue, displayName: user.username, timestamp: Math.floor(Date.now()), }; + comment.attrs.title = title || comment.attrs.title; comment.attrs.conversation.push(obj); + const id = uuidv4(); allCommentsWithSameId.forEach(singleComment => { activeView.dispatch( @@ -98,6 +103,7 @@ export default ({ comment, top, commentId, recalculateTops }) => { group: comment.attrs.group, viewid: comment.attrs.viewid, conversation: comment.attrs.conversation, + title: comment.attrs.title, }), ) .setMeta('forceUpdate', true), @@ -148,17 +154,13 @@ export default ({ comment, top, commentId, recalculateTops }) => { activeView.focus(); }; - const onTextAreaBlur = (content, isNewComment) => { + const onTextAreaBlur = () => { // TODO Save into local storage // if (content !== '') { // onClickPost(content); // } setTimeout(() => { - if ( - comment.attrs.conversation.length === 0 && - isNewComment && - !clickPost - ) { + if (comment.attrs.conversation.length === 0 && !clickPost) { onClickResolve(); activeView.focus(); } @@ -184,6 +186,8 @@ export default ({ comment, top, commentId, recalculateTops }) => { onClickResolve={onClickResolve} onTextAreaBlur={onTextAreaBlur} recalculateTops={recalculateTops} + showTitle={showTitle} + title={comment.attrs.title} /> </ConnectedCommentStyled> ), diff --git a/wax-prosemirror-services/src/CommentsService/components/ui/comments/CommentBox.js b/wax-prosemirror-services/src/CommentsService/components/ui/comments/CommentBox.js index 910daf6a48a36efdd3ff2975a93215f8334e0c24..d93a906993f5392b402707d87738603673871e58 100644 --- a/wax-prosemirror-services/src/CommentsService/components/ui/comments/CommentBox.js +++ b/wax-prosemirror-services/src/CommentsService/components/ui/comments/CommentBox.js @@ -81,6 +81,8 @@ const CommentBox = props => { onClickPost, onClickResolve, onTextAreaBlur, + title, + showTitle, } = props; // send signal to make this comment active @@ -91,7 +93,6 @@ const CommentBox = props => { if (!active && (!commentData || commentData.length === 0)) return null; const { t, i18n } = useTranslation(); - return ( <Wrapper active={active} className={className} onClick={onClickWrapper}> {active && commentData.length > 0 && ( @@ -109,15 +110,14 @@ const CommentBox = props => { </Resolve> </Head> )} - - <CommentItemList active={active} data={commentData} /> - + <CommentItemList active={active} data={commentData} title={title} /> {active && ( <StyledReply isNewComment={commentData.length === 0} isReadOnly={isReadOnly} onClickPost={onClickPost} onTextAreaBlur={onTextAreaBlur} + showTitle={showTitle} /> )} </Wrapper> @@ -150,11 +150,14 @@ CommentBox.propTypes = { onClickResolve: PropTypes.func.isRequired, /** Function to run when text area loses focus */ onTextAreaBlur: PropTypes.func.isRequired, + title: PropTypes.string, + showTitle: PropTypes.bool.isRequired, }; CommentBox.defaultProps = { active: false, commentData: [], + title: null, }; export default CommentBox; diff --git a/wax-prosemirror-services/src/CommentsService/components/ui/comments/CommentItemList.js b/wax-prosemirror-services/src/CommentsService/components/ui/comments/CommentItemList.js index 2f1aff65926896758a2701085a2594f578506d4d..ef66d305fc864acd818380db699b3f34d1ae59ca 100644 --- a/wax-prosemirror-services/src/CommentsService/components/ui/comments/CommentItemList.js +++ b/wax-prosemirror-services/src/CommentsService/components/ui/comments/CommentItemList.js @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; import { clone, uniqueId } from 'lodash'; -import { override } from '@pubsweet/ui-toolkit'; +import { override, th } from '@pubsweet/ui-toolkit'; import CommentItem from './CommentItem'; @@ -15,6 +15,13 @@ const Wrapper = styled.div` ${override('Wax.CommentItemWrapper')} `; +const CommentTitle = styled.span` + font-weight: bold; + font-size: ${th('fontSizeBase')}; + + ${override('Wax.CommentItemTitle')} +`; + const More = styled.span` background: gray; border-radius: 3px; @@ -27,7 +34,7 @@ const More = styled.span` `; const CommentItemList = props => { - const { active, className, data } = props; + const { active, className, data, title } = props; if (!data || data.length === 0) return null; const [items, setItems] = useState(data); @@ -49,6 +56,7 @@ const CommentItemList = props => { return ( <Wrapper active={active} className={className}> + {title && <CommentTitle>{title}</CommentTitle>} {items.map(item => ( <CommentItem active={active} @@ -79,11 +87,13 @@ CommentItemList.propTypes = { timestamp: PropTypes.number.isRequired, }), ), + title: PropTypes.string, }; CommentItemList.defaultProps = { active: false, data: [], + title: null, }; export default CommentItemList; diff --git a/wax-prosemirror-services/src/CommentsService/components/ui/comments/CommentReply.js b/wax-prosemirror-services/src/CommentsService/components/ui/comments/CommentReply.js index d419b4a14ba41c661b1d0ed87cb03ad9480f7d1c..dc7cd2ca9a955eb22acf39537117daac5b49447f 100644 --- a/wax-prosemirror-services/src/CommentsService/components/ui/comments/CommentReply.js +++ b/wax-prosemirror-services/src/CommentsService/components/ui/comments/CommentReply.js @@ -4,6 +4,7 @@ import styled, { css } from 'styled-components'; import { isEmpty } from 'lodash'; import { useTranslation } from 'react-i18next'; import { grid, th, override } from '@pubsweet/ui-toolkit'; +import { useOnClickOutside } from 'wax-prosemirror-core'; const Wrapper = styled.div` background: ${th('colorBackgroundHue')}; @@ -14,6 +15,21 @@ const Wrapper = styled.div` const TextWrapper = styled.div``; +const CommentTitle = styled.input` + background: ${th('colorBackgroundHue')}; + border: 3px solid ${th('colorBackgroundTabs')}; + font-family: ${th('fontWriting')}; + margin-bottom: 10px; + position: relative; + width: 100%; + + &:focus { + outline: 1px solid ${th('colorPrimary')}; + } + + ${override('Wax.CommentTitle')} +`; + const ReplyTextArea = styled.textarea` background: ${th('colorBackgroundHue')}; border: 3px solid ${th('colorBackgroundTabs')}; @@ -66,40 +82,61 @@ const CommentReply = props => { onClickPost, isReadOnly, onTextAreaBlur, + showTitle, } = props; const { t, i18n } = useTranslation(); const commentInput = useRef(null); + const commentTitle = useRef(null); const [commentValue, setCommentValue] = useState(''); + const [title, setTitle] = useState(''); + + const ref = useRef(null); + + useOnClickOutside(ref, onTextAreaBlur); useEffect(() => { setTimeout(() => { - if (commentInput.current && isNewComment) commentInput.current.focus(); + if (commentTitle.current && isNewComment) commentTitle.current.focus(); + if (commentInput.current && !isNewComment) commentInput.current.focus(); }); }, []); const handleSubmit = e => { e.preventDefault(); e.stopPropagation(); - onClickPost(commentValue); + onClickPost({ title, commentValue }); setCommentValue(''); + setTitle(''); }; const resetValue = e => { e.preventDefault(); setCommentValue(''); - }; - - const onBlur = content => { - onTextAreaBlur(content, isNewComment); + setTitle(''); }; return ( - <Wrapper className={className}> + <Wrapper className={className} ref={ref}> <form onSubmit={handleSubmit}> <TextWrapper> + {isNewComment && showTitle && ( + <CommentTitle + name="title" + onChange={e => { + setTitle(e.target.value); + }} + placeholder={`${ + !isEmpty(i18n) && i18n.exists(`Wax.Comments.Write title`) + ? t(`Wax.Comments.Write title`) + : 'Write title' + }...`} + ref={commentTitle} + type="text" + value={title} + /> + )} <ReplyTextArea cols="5" - onBlur={() => onBlur(commentInput.current.value)} onChange={() => setCommentValue(commentInput.current.value)} onKeyDown={e => { if (e.keyCode === 13 && !e.shiftKey) { @@ -155,6 +192,7 @@ CommentReply.propTypes = { onClickPost: PropTypes.func.isRequired, isReadOnly: PropTypes.bool.isRequired, onTextAreaBlur: PropTypes.func.isRequired, + showTitle: PropTypes.bool.isRequired, }; CommentReply.defaultProps = {}; diff --git a/wax-prosemirror-services/src/CommentsService/schema/commentMark.js b/wax-prosemirror-services/src/CommentsService/schema/commentMark.js index 15d92a34c466f8995036ddc979d136decacba9b7..d311f56a3008bedf07ac4bd7e99540fa555edcc8 100644 --- a/wax-prosemirror-services/src/CommentsService/schema/commentMark.js +++ b/wax-prosemirror-services/src/CommentsService/schema/commentMark.js @@ -1,41 +1,61 @@ -const commentMark = { - attrs: { - class: { default: 'comment' }, - id: { default: '' }, - group: { default: '' }, - viewid: { default: '' }, - conversation: [], - }, - inclusive: false, - excludes: '', - parseDOM: [ - { - tag: 'span.comment', - getAttrs(hook, next) { - Object.assign(hook, { - class: hook.dom.getAttribute('class'), - id: hook.dom.dataset.id, - group: hook.dom.dataset.group, - viewid: hook.dom.dataset.viewid, - conversation: JSON.parse(hook.dom.dataset.conversation), - }); - next(); - }, +/* eslint-disable no-param-reassign */ +const commentMark = showTitle => { + const comment = { + attrs: { + class: { default: 'comment' }, + id: { default: '' }, + group: { default: '' }, + viewid: { default: '' }, + conversation: [], }, - ], - toDOM(hook, next) { - hook.value = [ - 'span', + inclusive: false, + excludes: '', + parseDOM: [ { - class: hook.node.attrs.class, - 'data-id': hook.node.attrs.id, - 'data-conversation': JSON.stringify(hook.node.attrs.conversation), - 'data-viewid': hook.node.attrs.viewid, - 'data-group': hook.node.attrs.group, + tag: 'span.comment', + getAttrs(hook, next) { + const parsedDom = { + class: hook.dom.getAttribute('class'), + id: hook.dom.dataset.id, + group: hook.dom.dataset.group, + viewid: hook.dom.dataset.viewid, + conversation: JSON.parse(hook.dom.dataset.conversation), + }; + + if (showTitle) { + parsedDom.title = hook.dom.dataset.title; + } + + Object.assign(hook, parsedDom); + next(); + }, }, - ]; - next(); - }, + ], + toDOM(hook, next) { + hook.value = [ + 'span', + { + class: hook.node.attrs.class, + 'data-id': hook.node.attrs.id, + 'data-conversation': JSON.stringify(hook.node.attrs.conversation), + 'data-viewid': hook.node.attrs.viewid, + 'data-group': hook.node.attrs.group, + }, + ]; + + if (showTitle) { + hook.value[1]['data-title'] = hook.node.attrs.title; + } + + next(); + }, + }; + + if (showTitle) { + comment.attrs.title = { default: '' }; + } + + return comment; }; export default commentMark; diff --git a/wax-prosemirror-services/src/YjsService/YjsService.js b/wax-prosemirror-services/src/YjsService/YjsService.js index b15284f4815d5ab1684036d0cc67ab3efa1de408..72ed63d39b14236ee98cbda06063a3a33f0ca714 100644 --- a/wax-prosemirror-services/src/YjsService/YjsService.js +++ b/wax-prosemirror-services/src/YjsService/YjsService.js @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import { Service } from 'wax-prosemirror-core'; import { yCursorPlugin, ySyncPlugin, yUndoPlugin } from 'y-prosemirror'; import { WebsocketProvider } from 'y-websocket'; @@ -7,10 +8,22 @@ import './yjs.css'; class YjsService extends Service { name = 'YjsService'; boot() { - const { connectionUrl, docIdentifier } = this.config; - const ydoc = new Y.Doc(); - // const provider = new WebsocketProvider('wss://demos.yjs.dev', 'prosemirror-demo', ydoc) - const provider = new WebsocketProvider(connectionUrl, docIdentifier, ydoc); + const { + connectionUrl, + docIdentifier, + cursorBuilder, + provider: configProvider, + ydoc: configYdoc, + } = this.config; + + let provider = configProvider ? configProvider() : null; + let ydoc = configYdoc ? configYdoc() : null; + + if (!configProvider || !configYdoc) { + ydoc = new Y.Doc(); + provider = new WebsocketProvider(connectionUrl, docIdentifier, ydoc); + } + provider.on('sync', args => { console.log({ sync: args }); }); @@ -23,9 +36,23 @@ class YjsService extends Service { provider.on('connection-error', args => { console.log({ connectioError: args }); }); + const type = ydoc.getXmlFragment('prosemirror'); + this.app.PmPlugins.add('ySyncPlugin', ySyncPlugin(type)); - this.app.PmPlugins.add('yCursorPlugin', yCursorPlugin(provider.awareness)); + + if (cursorBuilder) { + this.app.PmPlugins.add( + 'yCursorPlugin', + yCursorPlugin(provider.awareness, { cursorBuilder }), + ); + } else { + this.app.PmPlugins.add( + 'yCursorPlugin', + yCursorPlugin(provider.awareness), + ); + } + this.app.PmPlugins.add('yUndoPlugin', yUndoPlugin()); } }