Edit Comment UI

This commit is contained in:
Benjamin Goering
2017-05-01 16:58:24 -05:00
parent 84016e362d
commit 00a2a65d31
4 changed files with 254 additions and 120 deletions
+2 -1
View File
@@ -29,7 +29,8 @@
right: 0px;
}
.topRight .popoverMenuOpen .link {
.topRight .link.active,
.topRight .active .link {
padding-bottom: 0.125em;
border-bottom: 2px solid currentColor;
}
+29 -62
View File
@@ -23,8 +23,8 @@ import Slot from 'coral-framework/components/Slot';
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
import {TopRightMenu} from './TopRightMenu';
import {getActionSummary, getTotalActionCount, iPerformedThisAction} from 'coral-framework/utils';
import {Button} from 'coral-ui';
import classnames from 'classnames';
import {EditableCommentContent} from './EditableCommentContent';
import styles from './Comment.css';
@@ -39,7 +39,13 @@ class Comment extends React.Component {
constructor(props) {
super(props);
this.state = {replyBoxVisible: false};
this.onClickEdit = this.onClickEdit.bind(this);
this.state = {
// Whether the comment should be editable (e.g. after a commenter clicking the 'Edit' button on their own comment)
isEditing: false,
replyBoxVisible: false,
};
}
static propTypes = {
@@ -102,6 +108,11 @@ class Comment extends React.Component {
ignoreUser: React.PropTypes.func,
}
onClickEdit (e) {
e.preventDefault();
this.setState({isEditing: true});
}
render () {
const {
comment,
@@ -163,55 +174,6 @@ class Comment extends React.Component {
tag: BEST_TAG,
}), () => 'Failed to remove best comment tag');
class PopoverMenu extends React.Component {
static propTypes = {
children: PropTypes.node,
Popover: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
openClassName: PropTypes.string,
}
constructor(props) {
super(props);
this.toggle = this.toggle.bind(this);
this.close = this.close.bind(this);
this.state = {
isOpen: false
};
}
toggle() {
this.setState({isOpen: ! this.state.isOpen});
}
close() {
this.setState({isOpen: false});
}
render() {
const {isOpen} = this.state;
const {children, Popover, openClassName} = this.props;
return (
<span className={classnames({[openClassName]: isOpen})}>
<span onClick={this.toggle}>
{ children }
</span>
<span>
{ isOpen ? <Popover close={this.close} /> : null }
</span>
</span>
);
}
}
const DeleteCommentConfirmation = ({cancel, deleteComment}) => {
return (
<div className={classnames(styles.popover, styles.Wizard)}>
<header>Delete a comment</header>
<p>Are you sure you want to delete that comment</p>
<div className={styles.textAlignRight}>
<Button cStyle='cancel' onClick={cancel}>Cancel</Button>
<Button onClick={() => deleteComment()}>Delete comment</Button>
</div>
</div>
);
};
return (
<div
className={commentClass}
@@ -236,16 +198,9 @@ class Comment extends React.Component {
/* User can edit/delete their own comment for a short window after posting */
? <span className={classnames(styles.topRight)}>
<PopoverMenu
className={styles.popoverMenu}
openClassName={styles.popoverMenuOpen}
Popover={ ({close}) =>
<DeleteCommentConfirmation
cancel={close}
deleteComment={() => { /*console.log('delete comment', comment)*/ }}
/> }>
<a className={styles.link}>Delete</a>
</PopoverMenu>
<a
className={classnames(styles.link, {[styles.active]: this.state.isEditing})}
onClick={this.onClickEdit}>Edit</a>
</span>
/* TopRightMenu allows currentUser to ignore other users' comments */
@@ -257,7 +212,19 @@ class Comment extends React.Component {
</span>
}
<Content body={comment.body} />
{
this.state.isEditing
? <EditableCommentContent
addNotification={addNotification}
asset={asset}
comment={comment}
currentUser={currentUser}
maxCharCount={maxCharCount}
parentId={parentId}
/>
: <Content body={comment.body} />
}
<div className="commentActionsLeft comment__action-container">
<ActionButton>
{/* TODO implmement iPerformedThisAction for the like */}
@@ -0,0 +1,62 @@
import React, {PropTypes} from 'react';
import {CommentForm} from 'coral-plugin-commentbox/CommentBox';
/**
* Renders a Comment's body in such a way that the end-user can edit it and save changes
*/
export class EditableCommentContent extends React.Component {
// @TODO (bengo) make sure these are accurate wrt isRequired
static propTypes = {
// show notification to the user (e.g. for errors)
addNotification: PropTypes.func.isRequired,
asset: PropTypes.shape({
id: PropTypes.string.isRequired,
settings: PropTypes.shape({
charCountEnable: PropTypes.bool,
}),
}).isRequired,
// comment that is being edited
comment: PropTypes.shape({
body: PropTypes.string
}).isRequired,
// logged in user
currentUser: PropTypes.shape({
id: PropTypes.string.isRequired
}),
maxCharCount: PropTypes.number,
parentId: PropTypes.string,
}
constructor(props) {
super(props);
}
render() {
const saveComment = function () {
};
const originalBody = this.props.comment.body;
return (
<div style={{marginBottom: '10px'}}>
<CommentForm
defaultValue={this.props.comment.body}
charCountEnable={this.props.asset.settings.charCountEnable}
maxCharCount={this.props.maxCharCount}
saveCommentEnabled={(comment) => {
// should be disabled if user hasn't actually changed their
// original comment
return comment.body !== originalBody;
}}
saveComment={saveComment}
bodyLabel={'Edit this comment' /* @TODO (bengo) i18n */}
bodyPlaceholder=""
submitText={'Save changes' /* @TODO (bengo) i18n */}
saveButtonCStyle="green"
/>
</div>
);
}
}
+161 -57
View File
@@ -7,6 +7,132 @@ import {connect} from 'react-redux';
const name = 'coral-plugin-commentbox';
/**
* Common UI for Creating or Editing a Comment
*/
export class CommentForm extends Component {
static propTypes = {
// Initial value for underlying comment body textarea
defaultValue: PropTypes.string,
charCountEnable: PropTypes.bool.isRequired,
maxCharCount: PropTypes.number,
cancelButtonClicked: PropTypes.func,
// Save the comment in the form.
// Will be passed { body: String }
saveComment: PropTypes.func.isRequired,
// DOM ID for form input that edits comment body
bodyInputId: PropTypes.string,
// screen reader label for input that edits comment body
bodyLabel: PropTypes.string,
// Placeholder for input that edits comment body
bodyPlaceholder: PropTypes.string,
// render at start of button container (useful for extra buttons)
buttonContainerStart: PropTypes.node,
// render inside submit button
submitText: PropTypes.node,
styles: PropTypes.shape({
textarea: PropTypes.string
}),
// cStyle for enabled save <coral-ui/Button>
saveButtonCStyle: PropTypes.string,
// return whether the save button should be enabled for the provided
// comment ({ body }) (for reasons other than charCount)
saveCommentEnabled: PropTypes.func,
}
static get defaultProps() {
return {
bodyLabel: lang.t('comment'),
bodyPlaceholder: lang.t('comment'),
submitText: lang.t('post'),
saveButtonCStyle: 'darkGrey',
saveCommentEnabled: () => true,
};
}
constructor(props) {
super(props);
this.onBodyChange = this.onBodyChange.bind(this);
this.onClickSubmit = this.onClickSubmit.bind(this);
this.state = {
body: props.defaultValue || ''
};
}
onBodyChange(e) {
this.setState({body: e.target.value});
}
onClickSubmit(e) {
e.preventDefault();
const {saveComment} = this.props;
const {body} = this.state;
saveComment({body});
}
render() {
const {maxCharCount, styles, saveCommentEnabled} = this.props;
const body = this.state.body;
const length = body.length;
const isNotValidLength = (length) => !length || (maxCharCount && length > maxCharCount);
const disablePostComment = isNotValidLength(length) || ! saveCommentEnabled({body});
return <div>
<div className={`${name}-container`}>
<label
htmlFor={this.props.bodyInputId}
className="screen-reader-text"
aria-hidden={true}>
{this.props.bodyLabel}
</label>
<textarea
style={styles && styles.textarea}
className={`${name}-textarea`}
value={this.state.body}
placeholder={this.props.bodyPlaceholder}
id={this.props.bodyInputId}
onChange={this.onBodyChange}
rows={3}/>
</div>
{
this.props.charCountEnable &&
<div className={`${name}-char-count ${length > maxCharCount ? `${name}-char-max` : ''}`}>
{maxCharCount && `${maxCharCount - length} ${lang.t('characters-remaining')}`}
</div>
}
<div className={`${name}-button-container`}>
{ this.props.buttonContainerStart }
{
typeof this.props.cancelButtonClicked === 'function' && (
<Button
cStyle='darkGrey'
className={`${name}-cancel-button`}
onClick={this.props.cancelButtonClicked}>
{lang.t('cancel')}
</Button>
)
}
<Button
cStyle={disablePostComment ? 'lightGrey' : this.props.saveButtonCStyle}
className={`${name}-button`}
onClick={this.onClickSubmit}
disabled={disablePostComment ? 'disabled' : ''}>
{this.props.submitText}
</Button>
</div>
</div>;
}
}
/**
* Container for posting a new Comment
*/
class CommentBox extends Component {
constructor(props) {
@@ -14,15 +140,21 @@ class CommentBox extends Component {
this.state = {
username: '',
body: '',
// incremented on successful post to clear form
postedCount: 0,
hooks: {
preSubmit: [],
postSubmit: []
}
};
}
postComment = () => {
static get defaultProps() {
return {
updateCountCache: () => {}
};
}
postComment = ({body}) => {
const {
isReply,
assetId,
@@ -37,7 +169,7 @@ class CommentBox extends Component {
let comment = {
asset_id: assetId,
parent_id: parentId,
body: this.state.body,
body: body,
...this.props.commentBox
};
@@ -67,7 +199,7 @@ class CommentBox extends Component {
})
.catch((err) => console.error(err));
this.setState({body: ''});
this.setState({postedCount: this.state.postedCount + 1});
}
registerHook = (hookType = '', hook = () => {}) => {
@@ -124,68 +256,39 @@ class CommentBox extends Component {
const {styles, isReply, authorId, maxCharCount} = this.props;
let {cancelButtonClicked} = this.props;
const length = this.state.body.length;
const enablePostComment = !length || (maxCharCount && length > maxCharCount);
if (isReply && typeof cancelButtonClicked !== 'function') {
console.warn('the CommentBox component should have a cancelButtonClicked callback defined if it lives in a Reply');
cancelButtonClicked = () => {};
}
return <div>
<div
className={`${name}-container`}>
<label
htmlFor={ isReply ? 'replyText' : 'commentText'}
className="screen-reader-text"
aria-hidden={true}>
{isReply ? lang.t('reply') : lang.t('comment')}
</label>
<textarea
className={`${name}-textarea`}
style={styles && styles.textarea}
value={this.state.body}
placeholder={lang.t('comment')}
id={isReply ? 'replyText' : 'commentText'}
onChange={this.handleChange}
rows={3}/>
</div>
<div className={`${name}-char-count ${length > maxCharCount ? `${name}-char-max` : ''}`}>
{maxCharCount && `${maxCharCount - length} ${lang.t('characters-remaining')}`}
</div>
<div className={`${name}-button-container`}>
<Slot
fill="commentBoxDetail"
registerHook={this.registerHook}
unregisterHook={this.unregisterHook}
inline
/>
{
isReply && (
<Button
cStyle='darkGrey'
className={`${name}-cancel-button`}
onClick={() => cancelButtonClicked('')}>
{lang.t('cancel')}
</Button>
)
}
{ authorId && (
<Button
cStyle={enablePostComment ? 'lightGrey' : 'darkGrey'}
className={`${name}-button`}
onClick={this.postComment}
disabled={enablePostComment ? 'disabled' : ''}>
{lang.t('post')}
</Button>
)
}
</div>
<CommentForm
styles={styles}
key={this.state.postedCount}
defaultValue={this.props.defaultValue}
bodyInputId={isReply ? 'replyText' : 'commentText'}
bodyLabel={isReply ? lang.t('reply') : lang.t('comment')}
maxCharCount={maxCharCount}
charCountEnable={this.props.charCountEnable}
bodyPlaceholder={lang.t('comment')}
bodyInputId={isReply ? 'replyText' : 'commentText'}
saveComment={authorId && this.postComment}
buttonContainerStart={<Slot
fill="commentBoxDetail"
registerHook={this.registerHook}
unregisterHook={this.unregisterHook}
inline
/>}
cancelButtonClicked={cancelButtonClicked}
/>
</div>;
}
}
CommentBox.propTypes = {
// Initial value for underlying comment body textarea
defaultValue: PropTypes.string,
charCountEnable: PropTypes.bool.isRequired,
maxCharCount: PropTypes.number,
commentPostedHandler: PropTypes.func,
@@ -196,7 +299,8 @@ CommentBox.propTypes = {
authorId: PropTypes.string.isRequired,
isReply: PropTypes.bool.isRequired,
canPost: PropTypes.bool,
currentUser: PropTypes.object
currentUser: PropTypes.object,
updateCountCache: PropTypes.func,
};
const mapStateToProps = ({commentBox}) => ({commentBox});