Rolled the reply field into CommentWidget, where it belongs

This commit is contained in:
Rainer Simon 2020-04-04 20:47:04 +02:00
parent dcef9d5cab
commit 0e70226aaa
5 changed files with 65 additions and 36 deletions

View File

@ -1,7 +1,6 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import setPosition from './setPosition'; import setPosition from './setPosition';
import TagWidget from './widgets/tag/TagWidget'; import TagWidget from './widgets/tag/TagWidget';
import TypeSelectorWidget from './widgets/type/TypeSelectorWidget';
import CommentWidget from './widgets/comment/CommentWidget'; import CommentWidget from './widgets/comment/CommentWidget';
/** /**
@ -15,7 +14,6 @@ const Editor = props => {
// The current state of the edited annotation vs. original // The current state of the edited annotation vs. original
const [ currentAnnotation, setCurrentAnnotation ] = useState(); const [ currentAnnotation, setCurrentAnnotation ] = useState();
const [ currentReply, setCurrentReply ] = useState('');
// Reference to the DOM element, so we can set position // Reference to the DOM element, so we can set position
const element = useRef(); const element = useRef();
@ -23,7 +21,6 @@ const Editor = props => {
// Re-render: set derived annotation state & position the editor // Re-render: set derived annotation state & position the editor
useEffect(() => { useEffect(() => {
setCurrentAnnotation(props.annotation); setCurrentAnnotation(props.annotation);
setCurrentReply('');
if (element.current) if (element.current)
setPosition(props.containerEl, element.current, props.bounds); setPosition(props.containerEl, element.current, props.bounds);
@ -48,25 +45,24 @@ const Editor = props => {
); );
const onOk = _ => { const onOk = _ => {
// If there is a non-empty reply, append it as a comment body // Removes the 'draft' flag from all bodies
const updated = currentReply.trim() ? const undraft = annotation => annotation.clone({
currentAnnotation.clone({ body : annotation.bodies.map(({ draft, ...rest }) => rest)
body: [ ...currentAnnotation.bodies, { type: 'TextualBody', value: currentReply.trim() } ] });
}) : currentAnnotation;
// Current annotation is either a selection (if it was created from // Current annotation is either a selection (if it was created from
// scratch just now) or an annotation (if it existed already and was // scratch just now) or an annotation (if it existed already and was
// opened for editing) // opened for editing)
if (updated.bodies.length === 0) { if (currentAnnotation.bodies.length === 0) {
if (updated.isSelection) if (currentAnnotation.isSelection)
props.onCancel(); props.onCancel();
else else
props.onAnnotationDeleted(props.annotation); props.onAnnotationDeleted(props.annotation);
} else { } else {
if (updated.isSelection) if (currentAnnotation.isSelection)
props.onAnnotationCreated(updated.toAnnotation()); props.onAnnotationCreated(undraft(currentAnnotation).toAnnotation());
else else
props.onAnnotationUpdated(updated, props.annotation); props.onAnnotationUpdated(undraft(currentAnnotation), props.annotation);
} }
}; };
@ -76,16 +72,15 @@ const Editor = props => {
<div className="inner"> <div className="inner">
<CommentWidget <CommentWidget
annotation={currentAnnotation} annotation={currentAnnotation}
currentReply={currentReply} onAppendBody={onAppendBody}
onUpdateComment={onUpdateBody} onUpdateBody={onUpdateBody}
onDeleteComment={onRemoveBody} onRemoveBody={onRemoveBody}
onUpdateReply={evt => setCurrentReply(evt.target.value.trim())} onSaveAndClose={onOk} />
onOk={onOk} />
<TagWidget <TagWidget
annotation={currentAnnotation} annotation={currentAnnotation}
onAddTag={onAppendBody} onAppendBody={onAppendBody}
onRemoveTag={onRemoveBody} /> onRemoveBody={onRemoveBody} />
{ props.readOnly ? ( { props.readOnly ? (
<div className="footer"> <div className="footer">

View File

@ -2,10 +2,8 @@ import Editor from './Editor';
import CommentWidget from './widgets/comment/CommentWidget'; import CommentWidget from './widgets/comment/CommentWidget';
import TagWidget from './widgets/tag/TagWidget'; import TagWidget from './widgets/tag/TagWidget';
import TypeSelectorWidget from './widgets/type/TypeSelectorWidget';
Editor.CommentWidget = CommentWidget; Editor.CommentWidget = CommentWidget;
Editor.TagWidget = TagWidget; Editor.TagWidget = TagWidget;
Editor.TypeSelectorWidget = TypeSelectorWidget;
export { Editor }; export { Editor };

View File

@ -32,7 +32,7 @@ const Comment = props => {
editable={isEditable} editable={isEditable}
content={props.body.value} content={props.body.value}
onChange={onUpdateComment} onChange={onUpdateComment}
onOk={props.onOk} onSaveAndClose={props.onSaveAndClose}
/> />
<div <div

View File

@ -2,36 +2,72 @@ import React from 'react';
import Comment from './Comment'; import Comment from './Comment';
import TextEntryField from './TextEntryField'; import TextEntryField from './TextEntryField';
/**
* Comments are TextualBodies where the purpose field is either
* blank or 'commenting' or 'replying'
*/
const isComment = body =>
body.type === 'TextualBody' && (
!body.hasOwnProperty('purpose') || body.purpose === 'commenting' || body.purpose === 'replying'
);
/**
* The draft reply is a comment body with a 'draft' flag
*/
const getDraftReply = (existingDraft, isReply) => {
return existingDraft ? existingDraft : {
type: 'TextualBody', value: '', purpose: isReply ? 'replying' : 'commenting', draft: true
};
};
/** /**
* Renders a list of comment bodies, followed by a 'reply' field. * Renders a list of comment bodies, followed by a 'reply' field.
*/ */
const CommentWidget = props => { const CommentWidget = props => {
const commentBodies = props.annotation ? // All comments (draft + non-draft)
props.annotation.bodies.filter(b => // No purpose or 'commenting', 'replying' const all = props.annotation ?
!b.hasOwnProperty('purpose') || b.purpose === 'commenting' || b.purpose === 'replying' props.annotation.bodies.filter(isComment) : [];
) : [];
// Non-draft comments
const comments = all.filter(b => !b.draft);
// Draft reply
const draftReply = getDraftReply(all.find(b => b.draft), comments.length > 0);
const onEditReply = evt => {
const prev = draftReply.value.trim();
const updated = evt.target.value.trim();
if (prev.length === 0 && updated.length > 0) {
props.onAppendBody({ ...draftReply, value: updated });
} else if (prev.length > 0 && updated.length === 0) {
props.onRemoveBody(draftReply);
} else {
props.onUpdateBody(draftReply, { ...draftReply, value: updated });
}
}
return ( return (
<> <>
{ commentBodies.map((body, idx) => { comments.map((body, idx) =>
<Comment <Comment
key={idx} key={idx}
readOnly={props.readOnly} readOnly={props.readOnly}
body={body} body={body}
onUpdate={props.onUpdateComment} onUpdate={props.onUpdateBody}
onDelete={props.onDeleteComment} onDelete={props.onRemoveBody}
onOk={props.onOk} /> onSaveAndClose={props.onSaveAndClose} />
)} )}
{ !props.readOnly && props.annotation && { !props.readOnly && props.annotation &&
<div className="r6o-widget comment editable"> <div className="r6o-widget comment editable">
<TextEntryField <TextEntryField
content={props.currentReply} content={draftReply.value}
editable={true} editable={true}
placeholder={commentBodies.length > 0 ? 'Add a reply...' : 'Add a comment...'} placeholder={comments.length > 0 ? 'Add a reply...' : 'Add a comment...'}
onChange={props.onUpdateReply} onChange={onEditReply}
onOk={() => props.onOk()} onSaveAndClose={() => props.onSaveAndClose()}
/> />
</div> </div>
} }

View File

@ -13,7 +13,7 @@ export default class TextEntryField extends Component {
// CTRL+Enter functions as Ok // CTRL+Enter functions as Ok
onKeyDown = evt => { onKeyDown = evt => {
if (evt.which === 13 && evt.ctrlKey) if (evt.which === 13 && evt.ctrlKey)
this.props.onOk(); this.props.onSaveAndClose();
} }
// Focus on render // Focus on render