203 lines
6.0 KiB
React
203 lines
6.0 KiB
React
/** @jsx h */
|
|
import { h, Component } from 'preact';
|
|
|
|
import { BASE_URL, API_BASE, DEFAULT_MAX_COMMENT_SIZE } from 'common/constants';
|
|
import { siteId, url, pageTitle } from 'common/settings';
|
|
|
|
import api from 'common/api';
|
|
import store from 'common/store';
|
|
import TextareaAutosize from 'components/input/textarea-autosize';
|
|
|
|
const RSS_THREAD_URL = `${BASE_URL}${API_BASE}/rss/post?site=${siteId}&url=${url}`;
|
|
const RSS_SITE_URL = `${BASE_URL}${API_BASE}/rss/site?site=${siteId}`;
|
|
const RSS_REPLIES_URL = `${BASE_URL}${API_BASE}/rss/reply?site=${siteId}&user=`;
|
|
|
|
export default class Input extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
const config = store.get('config') || {};
|
|
|
|
this.state = {
|
|
preview: null,
|
|
isErrorShown: false,
|
|
errorMessage: null,
|
|
isDisabled: false,
|
|
maxLength: config.max_comment_size || DEFAULT_MAX_COMMENT_SIZE,
|
|
text: props.value || '',
|
|
};
|
|
|
|
this.send = this.send.bind(this);
|
|
this.getPreview = this.getPreview.bind(this);
|
|
this.onInput = this.onInput.bind(this);
|
|
this.onKeyDown = this.onKeyDown.bind(this);
|
|
}
|
|
|
|
componentDidMount() {
|
|
store.onUpdate('config', config => {
|
|
this.setState({ maxLength: (config && config.max_comment_size) || DEFAULT_MAX_COMMENT_SIZE });
|
|
});
|
|
}
|
|
|
|
shouldComponentUpdate(nextProps, nextState) {
|
|
return (
|
|
nextProps.id !== this.props.id ||
|
|
nextProps.mods !== this.props.mods ||
|
|
nextProps.pid !== this.props.pid ||
|
|
nextProps.value !== this.props.value ||
|
|
nextProps.errorMessage !== this.props.errorMessage ||
|
|
nextState !== this.state
|
|
);
|
|
}
|
|
|
|
onKeyDown(e) {
|
|
// send on cmd+enter / ctrl+enter
|
|
if (e.keyCode === 13 && (e.metaKey || e.ctrlKey)) {
|
|
this.send();
|
|
}
|
|
}
|
|
|
|
onInput(e) {
|
|
this.setState({
|
|
preview: null,
|
|
isErrorShown: false,
|
|
errorMessage: null,
|
|
text: e.target.value,
|
|
});
|
|
}
|
|
|
|
send(e) {
|
|
const text = this.state.text;
|
|
const { mods = {}, pid, id } = this.props;
|
|
|
|
if (e) e.preventDefault();
|
|
|
|
if (!text || !text.trim()) return;
|
|
|
|
this.setState({ isDisabled: true, isErrorShown: false });
|
|
|
|
const request =
|
|
mods.mode === 'edit'
|
|
? api.updateComment({ text, id })
|
|
: api.addComment({ title: pageTitle || document.title, text, ...(pid ? { pid } : {}) });
|
|
|
|
request
|
|
.then(comment => {
|
|
if (this.props.onSubmit) {
|
|
this.props.onSubmit(comment);
|
|
}
|
|
|
|
this.setState({ preview: null, text: '' });
|
|
})
|
|
.catch(e => {
|
|
if (
|
|
e.response &&
|
|
e.response.data &&
|
|
typeof e.response.data.error === 'string' &&
|
|
e.response.data.error.indexOf("parent comment with reply can't be edited") === 0
|
|
) {
|
|
this.setState({ isErrorShown: true, errorMessage: 'Comment has reply, editing is not possible' });
|
|
return;
|
|
}
|
|
this.setState({ isErrorShown: true, errorMessage: null });
|
|
})
|
|
.finally(() => this.setState({ isDisabled: false }));
|
|
}
|
|
|
|
getPreview() {
|
|
const text = this.state.text;
|
|
|
|
if (!text || !text.trim()) return;
|
|
|
|
this.setState({ isErrorShown: false, errorMessage: null });
|
|
|
|
api
|
|
.getPreview({ text })
|
|
.then(preview => this.setState({ preview }))
|
|
.catch(() => {
|
|
this.setState({ isErrorShown: true, errorMessage: null });
|
|
});
|
|
}
|
|
|
|
render(props, { isDisabled, isErrorShown, errorMessage, preview, maxLength, text }) {
|
|
const charactersLeft = maxLength - text.length;
|
|
const { mods = {}, userId } = props;
|
|
errorMessage = props.errorMessage || errorMessage;
|
|
|
|
return (
|
|
<form className={b('input', props)} onSubmit={this.send} aria-label="New comment">
|
|
<div className="input__field-wrapper">
|
|
<TextareaAutosize
|
|
className="input__field"
|
|
placeholder="Your comment here"
|
|
value={text}
|
|
maxLength={maxLength}
|
|
onInput={this.onInput}
|
|
onKeyDown={this.onKeyDown}
|
|
disabled={isDisabled}
|
|
/>
|
|
|
|
{charactersLeft < 100 && <span className="input__counter">{charactersLeft}</span>}
|
|
</div>
|
|
|
|
{(isErrorShown || !!errorMessage) && (
|
|
<p className="input__error" role="alert">
|
|
{errorMessage || 'Something went wrong. Please try again a bit later.'}
|
|
</p>
|
|
)}
|
|
|
|
<div className="input__actions">
|
|
<button
|
|
className={b('input__button', {}, { type: 'preview' })}
|
|
type="button"
|
|
disabled={isDisabled}
|
|
onClick={this.getPreview}
|
|
>
|
|
Preview
|
|
</button>
|
|
|
|
<button className={b('input__button', {}, { type: 'send' })} type="submit" disabled={isDisabled}>
|
|
Send
|
|
</button>
|
|
|
|
{mods.type === 'main' && (
|
|
<div className="input__rss">
|
|
<div class="input__markdown">
|
|
Styling with{' '}
|
|
<a className="input__markdown-link" target="_blank" href="markdown-help.html" target="_blank">
|
|
Markdown
|
|
</a>{' '}
|
|
is supported
|
|
</div>
|
|
Subscribe to the{' '}
|
|
<a className="input__rss-link" href={RSS_THREAD_URL} target="_blank">
|
|
Thread
|
|
</a>
|
|
{', '}
|
|
<a className="input__rss-link" href={RSS_SITE_URL} target="_blank">
|
|
Site
|
|
</a>{' '}
|
|
or
|
|
<a className="input__rss-link" href={RSS_REPLIES_URL + userId} target="_blank">
|
|
Replies
|
|
</a>{' '}
|
|
by RSS
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{// TODO: it can be more elegant;
|
|
// for example it can render full comment component here (or above textarea on mobile)
|
|
!!preview && (
|
|
<div className="input__preview-wrapper">
|
|
<div
|
|
className={b('input__preview', { mix: b('raw-content', {}, { theme: mods.theme }) })}
|
|
dangerouslySetInnerHTML={{ __html: preview }}
|
|
/>
|
|
</div>
|
|
)}
|
|
</form>
|
|
);
|
|
}
|
|
}
|