mirror of
https://github.com/wassname/talk.git
synced 2026-07-04 01:25:19 +08:00
Edit Comment UI
This commit is contained in:
@@ -29,7 +29,8 @@
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.topRight .popoverMenuOpen .link {
|
||||
.topRight .link.active,
|
||||
.topRight .active .link {
|
||||
padding-bottom: 0.125em;
|
||||
border-bottom: 2px solid currentColor;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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});
|
||||
|
||||
Reference in New Issue
Block a user