Refactor draft area api step 1

This commit is contained in:
Chi Vinh Le
2018-03-21 15:41:27 +01:00
parent f6e661090c
commit 65778c31ff
12 changed files with 378 additions and 318 deletions
@@ -16,7 +16,7 @@ import mapValues from 'lodash/mapValues';
import get from 'lodash/get';
import LoadMore from './LoadMore';
import { getEditableUntilDate } from './util';
import { getEditableUntilDate } from '../util';
import { findCommentWithId } from '../../../graphql/utils';
import CommentContent from 'coral-framework/components/CommentContent';
import Slot from 'coral-framework/components/Slot';
@@ -37,15 +37,15 @@ class CommentForm extends React.Component {
submitButtonCStyle: PropTypes.string,
// return whether the submit button should be enabled for the provided
// comment ({ body }) (for reasons other than charCount)
// input (for reasons other than charCount)
submitEnabled: PropTypes.func,
// className to add to buttons
submitButtonClassName: PropTypes.string,
cancelButtonClassName: PropTypes.string,
body: PropTypes.string.isRequired,
onBodyChange: PropTypes.func.isRequired,
input: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
onCancel: PropTypes.func,
state: PropTypes.string,
@@ -53,6 +53,7 @@ class CommentForm extends React.Component {
registerHook: PropTypes.func,
unregisterHook: PropTypes.func,
isReply: PropTypes.bool,
isEdit: PropTypes.bool,
root: PropTypes.object.isRequired,
comment: PropTypes.object,
};
@@ -90,20 +91,20 @@ class CommentForm extends React.Component {
cancelButtonClassName,
submitButtonClassName,
charCountEnable,
body,
input,
loadingState,
comment,
root,
} = this.props;
const length = body.length;
const length = input.body.length;
const isRespectingMaxCount = length =>
charCountEnable && maxCharCount && length > maxCharCount;
const disableSubmitButton =
!length ||
body.trim().length === 0 ||
input.body.trim().length === 0 ||
isRespectingMaxCount(length) ||
!submitEnabled({ body }) ||
!submitEnabled(input) ||
loadingState === 'loading';
const disableCancelButton = loadingState === 'loading';
const disableTextArea = loadingState === 'loading';
@@ -115,15 +116,16 @@ class CommentForm extends React.Component {
comment={comment}
id={this.props.bodyInputId}
label={this.props.bodyLabel}
value={body}
input={input}
placeholder={this.props.bodyPlaceholder}
onChange={this.props.onBodyChange}
onInputChange={this.props.onInputChange}
disabled={disableTextArea}
charCountEnable={this.props.charCountEnable}
maxCharCount={this.props.maxCharCount}
registerHook={this.props.registerHook}
unregisterHook={this.props.unregisterHook}
isReply={this.props.isReply}
isEdit={this.props.isEdit}
/>
<div className={cn(styles.buttonContainer, `${name}-button-container`)}>
{this.props.buttonContainerStart}
@@ -13,17 +13,17 @@ import styles from './DraftArea.css';
*/
export default class DraftArea extends React.Component {
renderCharCount() {
const { value, maxCharCount } = this.props;
const { input, maxCharCount } = this.props;
const className = cn(
styles.charCount,
'talk-plugin-commentbox-char-count',
{
[`${styles.charMax} talk-plugin-commentbox-char-max`]:
value.length > maxCharCount,
input.body.length > maxCharCount,
}
);
const remaining = maxCharCount - value.length;
const remaining = maxCharCount - input.body.length;
return (
<div className={className}>
@@ -34,7 +34,7 @@ export default class DraftArea extends React.Component {
render() {
const {
value,
input,
placeholder,
id,
disabled,
@@ -42,8 +42,9 @@ export default class DraftArea extends React.Component {
label,
charCountEnable,
maxCharCount,
onChange,
onInputChange,
isReply,
isEdit,
registerHook,
unregisterHook,
root,
@@ -51,7 +52,7 @@ export default class DraftArea extends React.Component {
} = this.props;
return (
<div>
<div id={id}>
<div
className={cn(styles.container, 'talk-plugin-commentbox-container')}
>
@@ -67,13 +68,14 @@ export default class DraftArea extends React.Component {
comment,
registerHook,
unregisterHook,
value,
input,
placeholder,
id,
onChange,
onInputChange,
rows,
disabled,
isReply,
isEdit,
}}
/>
<Slot fill="commentInputArea" />
@@ -92,10 +94,10 @@ DraftArea.propTypes = {
charCountEnable: PropTypes.bool,
maxCharCount: PropTypes.number,
id: PropTypes.string,
value: PropTypes.string,
input: PropTypes.object,
placeholder: PropTypes.string,
label: PropTypes.string,
onChange: PropTypes.func,
onInputChange: PropTypes.func,
disabled: PropTypes.bool,
rows: PropTypes.number,
root: PropTypes.object.isRequired,
@@ -103,4 +105,5 @@ DraftArea.propTypes = {
registerHook: PropTypes.func,
unregisterHook: PropTypes.func,
isReply: PropTypes.bool,
isEdit: PropTypes.bool,
};
@@ -4,19 +4,19 @@ import cn from 'classnames';
import styles from './DraftAreaContent.css';
const DraftAreaContent = ({
value,
input,
placeholder,
id,
onChange,
onInputChange,
rows,
disabled,
}) => (
<textarea
className={cn(styles.content, 'talk-plugin-commentbox-textarea')}
value={value}
value={input.body}
placeholder={placeholder}
id={id}
onChange={e => onChange(e.target.value)}
onChange={e => onInputChange({ body: e.target.value })}
rows={rows}
disabled={disabled}
/>
@@ -28,9 +28,9 @@ DraftAreaContent.defaultProps = {
DraftAreaContent.propTypes = {
id: PropTypes.string,
value: PropTypes.string,
input: PropTypes.object,
placeholder: PropTypes.string,
onChange: PropTypes.func,
onInputChange: PropTypes.func,
disabled: PropTypes.bool,
rows: PropTypes.number,
};
@@ -1,12 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { notifyForNewCommentStatus } from '../helpers';
import CommentForm from '../containers/CommentForm';
import styles from './Comment.css';
import { CountdownSeconds } from './CountdownSeconds';
import { getEditableUntilDate } from './util';
import { can } from 'coral-framework/services/perms';
import { Icon } from 'coral-ui';
import t from 'coral-framework/services/i18n';
@@ -15,187 +12,83 @@ import t from 'coral-framework/services/i18n';
* Renders a Comment's body in such a way that the end-user can edit it and save changes
*/
class EditableCommentContent extends React.Component {
static propTypes = {
// show notification to the user (e.g. for errors)
notify: PropTypes.func.isRequired,
root: PropTypes.object.isRequired,
// comment that is being edited
comment: PropTypes.shape({
id: PropTypes.string,
body: PropTypes.string,
editing: PropTypes.shape({
edited: PropTypes.bool,
// ISO8601
editableUntil: PropTypes.string,
}),
}).isRequired,
// logged in user
currentUser: PropTypes.shape({
id: PropTypes.string.isRequired,
}),
charCountEnable: PropTypes.bool,
maxCharCount: PropTypes.number,
// edit a comment, passed {{ body }}
editComment: PropTypes.func,
// called when editing should be stopped
stopEditing: PropTypes.func,
};
unmounted = false;
constructor(props) {
super(props);
this.editWindowExpiryTimeout = null;
this.state = {
body: props.comment.body,
loadingState: '',
// data: {@object} contains data that might be useful for plugins, metadata, etc
data: {},
};
}
componentDidMount() {
const editableUntil = getEditableUntilDate(this.props.comment);
const now = new Date();
const editWindowRemainingMs = editableUntil && editableUntil - now;
if (editWindowRemainingMs > 0) {
this.editWindowExpiryTimeout = setTimeout(() => {
this.forceUpdate();
}, editWindowRemainingMs);
}
}
componentWillUnmount() {
this.unmounted = true;
if (this.editWindowExpiryTimeout) {
this.editWindowExpiryTimeout = clearTimeout(this.editWindowExpiryTimeout);
}
}
handleBodyChange = (body, data) => {
this.setState(state => ({
body,
data: {
...state.data,
...data,
},
}));
};
handleSubmit = async () => {
if (!can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) {
this.props.notify('error', t('error.NOT_AUTHORIZED'));
return;
}
this.setState({ loadingState: 'loading' });
const { editComment, stopEditing } = this.props;
if (typeof editComment !== 'function') {
return;
}
let input = {
body: this.state.body,
...this.state.data,
};
let response;
try {
response = await editComment(input);
if (!this.unmounted) {
this.setState({ loadingState: 'success' });
}
const status = response.data.editComment.comment.status;
notifyForNewCommentStatus(this.props.notify, status);
if (typeof stopEditing === 'function') {
stopEditing();
}
} catch (error) {
this.setState({ loadingState: 'error' });
}
};
getEditableUntil = (props = this.props) => {
return getEditableUntilDate(props.comment);
};
isEditWindowExpired = (props = this.props) => {
return this.getEditableUntil(props) - new Date() < 0;
};
isSubmitEnabled = comment => {
// should be disabled if user hasn't actually changed their
// original comment
renderButtonContainerStart() {
return (
comment.body !== this.props.comment.body && !this.isEditWindowExpired()
<div className={styles.buttonContainerLeft}>
<span className={styles.editWindowRemaining}>
{this.props.editWindowExpired ? (
<span>
{t('edit_comment.edit_window_expired')}
<span>
&nbsp;<a className={styles.link} onClick={this.props.onCancel}>
{t('edit_comment.edit_window_expired_close')}
</a>
</span>
</span>
) : (
<span>
<Icon name="timer" className={styles.timerIcon} />{' '}
{t('edit_comment.edit_window_timer_prefix')}
<CountdownSeconds
until={this.props.editableUntil}
classNameForMsRemaining={remainingMs =>
remainingMs <= 10 * 1000 ? styles.editWindowAlmostOver : ''
}
/>
</span>
)}
</span>
</div>
);
};
}
render() {
const id = `edit-draft_${this.props.comment.id}`;
return (
<div className={styles.editCommentForm}>
<CommentForm
isEdit
root={this.props.root}
comment={this.props.comment}
defaultValue={this.props.comment.body}
bodyInputId={id}
charCountEnable={this.props.charCountEnable}
maxCharCount={this.props.maxCharCount}
submitEnabled={this.isSubmitEnabled}
body={this.state.body}
onBodyChange={this.handleBodyChange}
onSubmit={this.handleSubmit}
submitEnabled={this.props.submitEnabled}
input={this.props.input}
onInputChange={this.props.onInputChange}
onSubmit={this.props.onSubmit}
onCancel={this.props.onCancel}
loadingState={this.props.loadingState}
registerHook={this.props.registerHook}
unregisterHook={this.props.unregisterHook}
buttonContainerStart={this.renderButtonContainerStart()}
submitButtonClassName={styles.button}
cancelButtonClassName={styles.button}
bodyLabel={t('edit_comment.body_input_label')}
bodyPlaceholder=""
submitText={<span>{t('edit_comment.save_button')}</span>}
submitButtonCStyle="green"
onCancel={this.props.stopEditing}
submitButtonClassName={styles.button}
cancelButtonClassName={styles.button}
loadingState={this.state.loadingState}
buttonContainerStart={
<div className={styles.buttonContainerLeft}>
<span className={styles.editWindowRemaining}>
{this.isEditWindowExpired() ? (
<span>
{t('edit_comment.edit_window_expired')}
{typeof this.props.stopEditing === 'function' ? (
<span>
&nbsp;<a
className={styles.link}
onClick={this.props.stopEditing}
>
{t('edit_comment.edit_window_expired_close')}
</a>
</span>
) : null}
</span>
) : (
<span>
<Icon name="timer" className={styles.timerIcon} />{' '}
{t('edit_comment.edit_window_timer_prefix')}
<CountdownSeconds
until={this.getEditableUntil()}
classNameForMsRemaining={remainingMs =>
remainingMs <= 10 * 1000
? styles.editWindowAlmostOver
: ''
}
/>
</span>
)}
</span>
</div>
}
bodyInputId={id}
/>
</div>
);
}
}
EditableCommentContent.propTypes = {
charCountEnable: PropTypes.bool,
submitEnabled: PropTypes.func,
maxCharCount: PropTypes.number,
root: PropTypes.object.isRequired,
comment: PropTypes.object.isRequired,
input: PropTypes.object.isRequired,
registerHook: PropTypes.func.isRequired,
unregisterHook: PropTypes.func.isRequired,
onInputChange: PropTypes.func,
onSubmit: PropTypes.func,
onCancel: PropTypes.func,
loadingState: PropTypes.string,
editWindowExpired: PropTypes.bool,
editableUntil: PropTypes.object,
};
export default EditableCommentContent;
@@ -7,6 +7,8 @@ import Slot from 'coral-framework/components/Slot';
import { connect } from 'react-redux';
import CommentForm from '../containers/CommentForm';
import { notifyForNewCommentStatus } from '../helpers';
import withHooks from '../hocs/withHooks';
import { compose } from 'recompose';
// TODO: (kiwi) Need to adapt CSS classes post refactor to match the rest.
export const name = 'talk-plugin-commentbox';
@@ -19,13 +21,9 @@ class CommentBox extends React.Component {
super(props);
this.state = {
body: '',
loadingState: '',
// data: {@object} contains data that might be useful for plugins
data: {},
hooks: {
preSubmit: [],
postSubmit: [],
input: {
body: '',
},
};
}
@@ -59,13 +57,12 @@ class CommentBox extends React.Component {
let input = {
asset_id: assetId,
parent_id: parentId,
body: this.state.body,
tags: this.props.tags,
...this.state.data,
...this.state.input,
};
// Execute preSubmit Hooks
this.state.hooks.preSubmit.forEach(hook => {
this.props.forEachHook('preSubmit', hook => {
const result = hook(input);
if (result) {
input = result;
@@ -75,13 +72,13 @@ class CommentBox extends React.Component {
postComment(input, 'comments')
.then(({ data }) => {
this.setState({ loadingState: 'success', body: '' });
this.setState({ loadingState: 'success', input: { body: '' } });
const postedComment = data.createComment.comment;
const actions = data.createComment.actions;
// Execute postSubmit Hooks
this.state.hooks.postSubmit.forEach(hook =>
hook(data, this.handleBodyChange)
this.props.forEachHook('postSubmit', hook =>
hook(data, this.handleInputChange)
);
notifyForNewCommentStatus(notify, postedComment.status, actions);
@@ -95,62 +92,29 @@ class CommentBox extends React.Component {
});
};
handleBodyChange = (body, data) => {
handleInputChange = input => {
this.setState(state => ({
body,
data: {
...state.data,
...data,
input: {
...state.input,
...input,
},
}));
};
registerHook = (hookType = '', hook = () => {}) => {
if (typeof hook !== 'function') {
return console.warn(
`Hooks must be functions. Please check your ${hookType} hooks`
);
} else if (typeof hookType === 'string') {
this.setState(state => ({
hooks: {
...state.hooks,
[hookType]: [...state.hooks[hookType], hook],
},
}));
return {
hookType,
hook,
};
} else {
return console.warn(
'hookTypes must be a string. Please check your hooks'
);
}
};
unregisterHook = hookData => {
const { hookType, hook } = hookData;
this.setState(state => {
let newHooks = state.hooks[newHooks];
const idx = state.hooks[hookType].indexOf(hook);
if (idx !== -1) {
newHooks = [
...state.hooks[hookType].slice(0, idx),
...state.hooks[hookType].slice(idx + 1),
];
}
return {
hooks: {
...state.hooks,
[hookType]: newHooks,
},
};
});
};
renderButtonContainerStart() {
const { isReply, registerHook, unregisterHook } = this.props;
return (
<Slot
fill="commentInputDetailArea"
passthrough={{
registerHook: registerHook,
unregisterHook: unregisterHook,
isReply,
}}
inline
/>
);
}
render() {
const {
@@ -160,6 +124,8 @@ class CommentBox extends React.Component {
parentId,
comment,
root,
registerHook,
unregisterHook,
} = this.props;
let { onCancel } = this.props;
@@ -186,22 +152,12 @@ class CommentBox extends React.Component {
charCountEnable={this.props.charCountEnable}
bodyPlaceholder={t('comment.comment')}
bodyInputId={id}
body={this.state.body}
registerHook={this.registerHook}
unregisterHook={this.unregisterHook}
input={this.state.input}
registerHook={registerHook}
unregisterHook={unregisterHook}
isReply={isReply}
buttonContainerStart={
<Slot
fill="commentInputDetailArea"
passthrough={{
registerHook: this.registerHook,
unregisterHook: this.unregisterHook,
isReply,
}}
inline
/>
}
onBodyChange={this.handleBodyChange}
buttonContainerStart={this.renderButtonContainerStart()}
onInputChange={this.handleInputChange}
loadingState={this.state.loadingState}
onCancel={onCancel}
onSubmit={this.handleSubmit}
@@ -228,6 +184,9 @@ CommentBox.propTypes = {
tags: PropTypes.array,
root: PropTypes.object.isRequired,
comment: PropTypes.object,
registerHook: PropTypes.func.isRequired,
unregisterHook: PropTypes.func.isRequired,
forEachHook: PropTypes.func.isRequired,
};
CommentBox.fragments = CommentForm.fragments;
@@ -236,4 +195,9 @@ const mapStateToProps = state => ({
tags: state.stream.commentBoxTags,
});
export default connect(mapStateToProps, null)(CommentBox);
const enhance = compose(
withHooks(['preSubmit', 'postSubmit']),
connect(mapStateToProps, null)
);
export default enhance(CommentBox);
@@ -17,9 +17,17 @@ class DraftAreaContainer extends React.Component {
}
async initValue() {
const value = await this.context.pymSessionStorage.getItem(this.getPath());
if (value && this.props.onChange) {
this.props.onChange(value);
const input = await this.context.pymSessionStorage.getItem(this.getPath());
if (input && this.props.onInputChange) {
let parsed = '';
// Older version saved a normal string, catch those and ignore them.
try {
parsed = JSON.parse(input);
} catch (_e) {}
if (typeof parsed === 'object') {
this.props.onInputChange(parsed);
}
}
}
@@ -27,14 +35,13 @@ class DraftAreaContainer extends React.Component {
return `${STORAGE_PATH}_${this.props.id}`;
};
onChange = (body, data) => {
this.props.onChange && this.props.onChange(body, data);
};
componentWillReceiveProps(nextProps) {
if (this.props.value !== nextProps.value) {
if (nextProps.value) {
this.context.pymSessionStorage.setItem(this.getPath(), nextProps.value);
if (this.props.input !== nextProps.input) {
if (nextProps.input) {
this.context.pymSessionStorage.setItem(
this.getPath(),
JSON.stringify(nextProps.input)
);
} else {
this.context.pymSessionStorage.removeItem(this.getPath());
}
@@ -46,10 +53,10 @@ class DraftAreaContainer extends React.Component {
<DraftArea
root={this.props.root}
comment={this.props.comment}
value={this.props.value}
input={this.props.input}
placeholder={this.props.placeholder}
id={this.props.id}
onChange={this.onChange}
onInputChange={this.props.onInputChange}
rows={this.props.rows}
disabled={this.props.disabled}
charCountEnable={this.props.charCountEnable}
@@ -58,6 +65,7 @@ class DraftAreaContainer extends React.Component {
registerHook={this.props.registerHook}
unregisterHook={this.props.unregisterHook}
isReply={this.props.isReply}
isEdit={this.props.isEdit}
/>
);
}
@@ -73,15 +81,16 @@ DraftAreaContainer.propTypes = {
charCountEnable: PropTypes.bool,
maxCharCount: PropTypes.number,
id: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
input: PropTypes.object.isRequired,
placeholder: PropTypes.string,
onChange: PropTypes.func.isRequired,
onInputChange: PropTypes.func.isRequired,
disabled: PropTypes.bool,
rows: PropTypes.number,
label: PropTypes.string.isRequired,
registerHook: PropTypes.func,
unregisterHook: PropTypes.func,
isReply: PropTypes.bool,
isEdit: PropTypes.bool,
root: PropTypes.object.isRequired,
comment: PropTypes.object,
};
@@ -1,11 +1,150 @@
import React from 'react';
import PropTypes from 'prop-types';
import { notifyForNewCommentStatus } from '../helpers';
import { getEditableUntilDate } from '../util';
import { can } from 'coral-framework/services/perms';
import t from 'coral-framework/services/i18n';
import EditableCommentContent from '../components/EditableCommentContent';
import CommentForm from './CommentForm';
import withHooks from '../hocs/withHooks';
import { compose } from 'recompose';
const EditableCommentContentContainer = props => (
<EditableCommentContent {...props} />
);
/**
* Renders a Comment's body in such a way that the end-user can edit it and save changes
*/
class EditableCommentContentContainer extends React.Component {
unmounted = false;
editWindowExpiryTimeout = null;
state = {
loadingState: '',
submitEnabled: false,
input: {
body: this.props.comment.body,
},
};
componentDidMount() {
const editableUntil = getEditableUntilDate(this.props.comment);
const now = new Date();
const editWindowRemainingMs = editableUntil && editableUntil - now;
if (editWindowRemainingMs > 0) {
this.editWindowExpiryTimeout = setTimeout(() => {
this.forceUpdate();
}, editWindowRemainingMs);
}
}
componentWillUnmount() {
this.unmounted = true;
if (this.editWindowExpiryTimeout) {
this.editWindowExpiryTimeout = clearTimeout(this.editWindowExpiryTimeout);
}
}
handleInputChange = input => {
this.setState(state => ({
submitEnabled: true,
input: {
...state.input,
...input,
},
}));
};
handleSubmit = async () => {
if (!can(this.props.currentUser, 'INTERACT_WITH_COMMUNITY')) {
this.props.notify('error', t('error.NOT_AUTHORIZED'));
return;
}
this.setState({ loadingState: 'loading' });
const { editComment, stopEditing } = this.props;
if (typeof editComment !== 'function') {
return;
}
let input = this.state.input;
// Execute preSubmit Hooks
this.props.forEachHook('preSubmit', hook => {
const result = hook(input);
if (result) {
input = result;
}
});
let response;
try {
response = await editComment(input);
// Execute postSubmit Hooks
this.props.forEachHook('postSubmit', hook =>
hook(response, this.handleInputChange)
);
if (!this.unmounted) {
this.setState({ loadingState: 'success' });
}
const status = response.data.editComment.comment.status;
notifyForNewCommentStatus(this.props.notify, status);
if (typeof stopEditing === 'function') {
stopEditing();
}
} catch (error) {
this.setState({ loadingState: 'error' });
}
};
getEditableUntil = (props = this.props) => {
return getEditableUntilDate(props.comment);
};
isEditWindowExpired = (props = this.props) => {
return this.getEditableUntil(props) - new Date() < 0;
};
isSubmitEnabled = () => this.state.submitEnabled;
render() {
return (
<EditableCommentContent
charCountEnable={this.props.charCountEnable}
submitEnabled={this.isSubmitEnabled}
maxCharCount={this.props.maxCharCount}
root={this.props.root}
comment={this.props.comment}
input={this.state.input}
onInputChange={this.handleInputChange}
onSubmit={this.handleSubmit}
onCancel={this.props.stopEditing}
loadingState={this.state.loadingState}
editWindowExpired={this.isEditWindowExpired()}
editableUntil={this.getEditableUntil()}
registerHook={this.props.registerHook}
unregisterHook={this.props.unregisterHook}
/>
);
}
}
EditableCommentContentContainer.propTypes = {
notify: PropTypes.func.isRequired,
root: PropTypes.object.isRequired,
comment: PropTypes.object.isRequired,
currentUser: PropTypes.object,
charCountEnable: PropTypes.bool,
maxCharCount: PropTypes.number,
editComment: PropTypes.func,
stopEditing: PropTypes.func,
registerHook: PropTypes.func.isRequired,
unregisterHook: PropTypes.func.isRequired,
forEachHook: PropTypes.func.isRequired,
};
EditableCommentContentContainer.fragments = CommentForm.fragments;
export default EditableCommentContentContainer;
const enhance = compose(withHooks(['preSubmit', 'postSubmit']));
export default enhance(EditableCommentContentContainer);
@@ -0,0 +1,57 @@
import React from 'react';
import hoistStatics from 'recompose/hoistStatics';
/**
* WithHooks provides a property `hooks` to the wrapped component.
*/
export default hooks =>
hoistStatics(WrappedComponent => {
class WithHooks extends React.Component {
hooks = hooks.reduce((map, key) => {
map[key] = [];
return map;
}, {});
registerHook = (hookType = '', hook) => {
if (typeof hook !== 'function') {
return console.warn(
`Hooks must be functions. Please check your ${hookType} hooks`
);
}
if (!hooks.includes(hookType)) {
throw new Error(`Unknown hookType ${hookType}`);
}
this.hooks[hookType].push(hook);
return {
hookType,
hook,
};
};
unregisterHook = hookData => {
const { hookType, hook } = hookData;
const idx = this.hooks[hookType].indexOf(hook);
if (idx !== -1) {
this.hooks[hookType].splice(idx, 1);
}
};
forEachHook = (hookType, callback) => {
this.hooks[hookType].forEach(callback);
};
render() {
return (
<WrappedComponent
{...this.props}
registerHook={this.registerHook}
unregisterHook={this.unregisterHook}
forEachHook={this.forEachHook}
/>
);
}
}
return WithHooks;
});
@@ -12,42 +12,38 @@ import bowser from 'bowser';
class Editor extends React.Component {
ref = null;
handleRef = ref => (this.ref = ref);
state = {
html:
!this.props.isReply && this.props.comment
? this.props.comment.richTextBody || this.props.comment.body || ''
: '',
};
handleChange = evt => {
const html = evt.target.value;
this.setState({ html });
this.props.onChange(this.ref.htmlEl.innerText, {
richTextBody: htmlNormalizer(html),
this.props.onInputChange({
body: this.ref.htmlEl.innerText,
richTextBody: evt.target.value,
});
};
componentDidMount() {
if (this.props.registerHook) {
this.clearInputHook = this.props.registerHook(
'postSubmit',
(res, handleBodyChange) => {
this.setState({ html: '' });
handleBodyChange('', { richTextBody: '' });
}
);
getHTML(props = this.props) {
if (props.input.richTextBody) {
return props.input.richTextBody;
}
return (
(props.isEdit && (props.comment.richTextBody || props.comment.body)) || ''
);
}
shouldComponentUpdate(nextProps) {
if (this.props.value !== nextProps.value) {
return false;
componentDidMount() {
if (this.props.registerHook) {
this.normalizeHook = this.props.registerHook('preSubmit', input => {
if (input.richTextBody) {
return {
...input,
richTextBody: htmlNormalizer(input.richTextBody),
};
}
});
}
return true;
}
componentWillUnmount() {
this.props.unregisterHook(this.clearInputHook);
this.props.unregisterHook(this.normalizeHook);
}
getCurrentTagName() {
@@ -90,7 +86,6 @@ class Editor extends React.Component {
};
render() {
const { id } = this.props;
return (
<div className={cn(styles.root, `${PLUGIN_NAME}-container`)}>
<Toolbar>
@@ -109,9 +104,8 @@ class Editor extends React.Component {
<ContentEditable
onKeyPress={this.outdentOnEnter}
className={styles.contentEditable}
id={id}
ref={this.handleRef}
html={this.state.html}
html={this.getHTML()}
disabled={false}
onChange={this.handleChange}
/>
@@ -121,17 +115,16 @@ class Editor extends React.Component {
}
Editor.propTypes = {
rows: PropTypes.number, // TODO: should not be passed.
id: PropTypes.string, // TODO: should not be passed.
value: PropTypes.string,
input: PropTypes.object,
placeholder: PropTypes.string,
onChange: PropTypes.func,
onInputChange: PropTypes.func,
disabled: PropTypes.bool,
comment: PropTypes.object,
classNames: PropTypes.object,
registerHook: PropTypes.func,
unregisterHook: PropTypes.func,
isReply: PropTypes.bool,
isEdit: PropTypes.bool,
};
export default Editor;