| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427 |
- import React from 'react';
- import PropTypes from 'prop-types';
- import {
- Button,
- TabContent, TabPane,
- } from 'reactstrap';
- import * as toastr from 'toastr';
- import { UserPicture } from '@growi/ui';
- import AppContainer from '~/client/services/AppContainer';
- import PageContainer from '~/client/services/PageContainer';
- import CommentContainer from '~/client/services/CommentContainer';
- import EditorContainer from '~/client/services/EditorContainer';
- import GrowiRenderer from '~/client/util/GrowiRenderer';
- import { withUnstatedContainers } from '../UnstatedUtils';
- import Editor from '../PageEditor/Editor';
- import { SlackNotification } from '../SlackNotification';
- import CommentPreview from './CommentPreview';
- import NotAvailableForGuest from '../NotAvailableForGuest';
- import { CustomNavTab } from '../CustomNavigation/CustomNav';
- const navTabMapping = {
- comment_editor: {
- Icon: () => <i className="icon-settings" />,
- i18n: 'Write',
- index: 0,
- },
- comment_preview: {
- Icon: () => <i className="icon-settings" />,
- i18n: 'Preview',
- index: 1,
- },
- };
- /**
- *
- * @author Yuki Takei <yuki@weseek.co.jp>
- *
- * @extends {React.Component}
- */
- class CommentEditor extends React.Component {
- constructor(props) {
- super(props);
- const config = this.props.appContainer.getConfig();
- const isUploadable = config.upload.image || config.upload.file;
- const isUploadableFile = config.upload.file;
- this.state = {
- isReadyToUse: !this.props.isForNewComment,
- comment: this.props.commentBody || '',
- isMarkdown: true,
- html: '',
- activeTab: 'comment_editor',
- isUploadable,
- isUploadableFile,
- errorMessage: undefined,
- isSlackConfigured: config.isSlackConfigured,
- };
- this.updateState = this.updateState.bind(this);
- this.updateStateCheckbox = this.updateStateCheckbox.bind(this);
- this.cancelButtonClickedHandler = this.cancelButtonClickedHandler.bind(this);
- this.commentButtonClickedHandler = this.commentButtonClickedHandler.bind(this);
- this.ctrlEnterHandler = this.ctrlEnterHandler.bind(this);
- this.postComment = this.postComment.bind(this);
- this.uploadHandler = this.uploadHandler.bind(this);
- this.renderHtml = this.renderHtml.bind(this);
- this.handleSelect = this.handleSelect.bind(this);
- this.onSlackEnabledFlagChange = this.onSlackEnabledFlagChange.bind(this);
- this.onSlackChannelsChange = this.onSlackChannelsChange.bind(this);
- }
- updateState(value) {
- this.setState({ comment: value });
- }
- updateStateCheckbox(event) {
- const value = event.target.checked;
- this.setState({ isMarkdown: value });
- // changeMode
- this.editor.setGfmMode(value);
- }
- handleSelect(activeTab) {
- this.setState({ activeTab });
- this.renderHtml(this.state.comment);
- }
- onSlackEnabledFlagChange(isSlackEnabled) {
- this.props.commentContainer.setState({ isSlackEnabled });
- }
- onSlackChannelsChange(slackChannels) {
- this.props.commentContainer.setState({ slackChannels });
- }
- initializeEditor() {
- this.setState({
- comment: '',
- isMarkdown: true,
- html: '',
- activeTab: 'comment_editor',
- errorMessage: undefined,
- });
- // reset value
- this.editor.setValue('');
- }
- cancelButtonClickedHandler() {
- const { isForNewComment, onCancelButtonClicked } = this.props;
- // change state to not ready
- // when this editor is for the new comment mode
- if (isForNewComment) {
- this.setState({ isReadyToUse: false });
- }
- if (onCancelButtonClicked != null) {
- const { replyTo, currentCommentId } = this.props;
- onCancelButtonClicked(replyTo || currentCommentId);
- }
- }
- commentButtonClickedHandler() {
- this.postComment();
- }
- ctrlEnterHandler(event) {
- if (event != null) {
- event.preventDefault();
- }
- this.postComment();
- }
- /**
- * Post comment with CommentContainer and update state
- */
- async postComment() {
- const {
- commentContainer, replyTo, currentCommentId, commentCreator, onCommentButtonClicked,
- } = this.props;
- try {
- if (currentCommentId != null) {
- await commentContainer.putComment(
- this.state.comment,
- this.state.isMarkdown,
- currentCommentId,
- commentCreator,
- );
- }
- else {
- await this.props.commentContainer.postComment(
- this.state.comment,
- this.state.isMarkdown,
- replyTo,
- commentContainer.state.isSlackEnabled,
- commentContainer.state.slackChannels,
- );
- }
- this.initializeEditor();
- if (onCommentButtonClicked != null) {
- onCommentButtonClicked();
- }
- }
- catch (err) {
- const errorMessage = err.message || 'An unknown error occured when posting comment';
- this.setState({ errorMessage });
- }
- }
- uploadHandler(file) {
- this.props.commentContainer.uploadAttachment(file)
- .then((res) => {
- const attachment = res.attachment;
- const fileName = attachment.originalName;
- let insertText = `[${fileName}](${attachment.filePathProxied})`;
- // when image
- if (attachment.fileFormat.startsWith('image/')) {
- // modify to "" syntax
- insertText = `!${insertText}`;
- }
- this.editor.insertText(insertText);
- })
- .catch(this.apiErrorHandler)
- // finally
- .then(() => {
- this.editor.terminateUploadingState();
- });
- }
- apiErrorHandler(error) {
- toastr.error(error.message, 'Error occured', {
- closeButton: true,
- progressBar: true,
- newestOnTop: false,
- showDuration: '100',
- hideDuration: '100',
- timeOut: '3000',
- });
- }
- getCommentHtml() {
- return (
- <CommentPreview
- inputRef={(el) => { this.previewElement = el }}
- html={this.state.html}
- />
- );
- }
- renderHtml(markdown) {
- const context = {
- markdown,
- };
- const { growiRenderer } = this.props;
- const interceptorManager = this.props.appContainer.interceptorManager;
- interceptorManager.process('preRenderCommnetPreview', context)
- .then(() => { return interceptorManager.process('prePreProcess', context) })
- .then(() => {
- context.markdown = growiRenderer.preProcess(context.markdown, context);
- })
- .then(() => { return interceptorManager.process('postPreProcess', context) })
- .then(() => {
- const parsedHTML = growiRenderer.process(context.markdown, context);
- context.parsedHTML = parsedHTML;
- })
- .then(() => { return interceptorManager.process('prePostProcess', context) })
- .then(() => {
- context.parsedHTML = growiRenderer.postProcess(context.parsedHTML, context);
- })
- .then(() => { return interceptorManager.process('postPostProcess', context) })
- .then(() => { return interceptorManager.process('preRenderCommentPreviewHtml', context) })
- .then(() => {
- this.setState({ html: context.parsedHTML });
- })
- // process interceptors for post rendering
- .then(() => { return interceptorManager.process('postRenderCommentPreviewHtml', context) });
- }
- generateInnerHtml(html) {
- return { __html: html };
- }
- renderBeforeReady() {
- return (
- <div className="text-center">
- <NotAvailableForGuest>
- <button
- type="button"
- className="btn btn-lg btn-link"
- onClick={() => this.setState({ isReadyToUse: true })}
- >
- <i className="icon-bubble"></i> Add Comment
- </button>
- </NotAvailableForGuest>
- </div>
- );
- }
- renderReady() {
- const { appContainer, commentContainer } = this.props;
- const { activeTab } = this.state;
- const commentPreview = this.state.isMarkdown ? this.getCommentHtml() : null;
- const errorMessage = <span className="text-danger text-right mr-2">{this.state.errorMessage}</span>;
- const cancelButton = (
- <Button outline color="danger" size="xs" className="btn btn-outline-danger rounded-pill" onClick={this.cancelButtonClickedHandler}>
- Cancel
- </Button>
- );
- const submitButton = (
- <Button
- outline
- color="primary"
- className="btn btn-outline-primary rounded-pill"
- onClick={this.commentButtonClickedHandler}
- >
- Comment
- </Button>
- );
- return (
- <>
- <div className="comment-write">
- <CustomNavTab activeTab={activeTab} navTabMapping={navTabMapping} onNavSelected={this.handleSelect} hideBorderBottom />
- <TabContent activeTab={activeTab}>
- <TabPane tabId="comment_editor">
- <Editor
- ref={(c) => { this.editor = c }}
- value={this.state.comment}
- isGfmMode={this.state.isMarkdown}
- lineNumbers={false}
- isMobile={appContainer.isMobile}
- isUploadable={this.state.isUploadable}
- isUploadableFile={this.state.isUploadableFile}
- onChange={this.updateState}
- onUpload={this.uploadHandler}
- onCtrlEnter={this.ctrlEnterHandler}
- />
- {/*
- Note: <OptionsSelector /> is not optimized for ComentEditor in terms of responsive design.
- See a review comment in https://github.com/weseek/growi/pull/3473
- */}
- </TabPane>
- <TabPane tabId="comment_preview">
- <div className="comment-form-preview">
- {commentPreview}
- </div>
- </TabPane>
- </TabContent>
- </div>
- <div className="comment-submit">
- <div className="d-flex">
- <label className="mr-2">
- {activeTab === 'comment_editor' && (
- <span className="custom-control custom-checkbox">
- <input
- type="checkbox"
- className="custom-control-input"
- id="comment-form-is-markdown"
- name="isMarkdown"
- checked={this.state.isMarkdown}
- value="1"
- onChange={this.updateStateCheckbox}
- />
- <label
- className="ml-2 custom-control-label"
- htmlFor="comment-form-is-markdown"
- >
- Markdown
- </label>
- </span>
- ) }
- </label>
- <span className="flex-grow-1" />
- <span className="d-none d-sm-inline">{ this.state.errorMessage && errorMessage }</span>
- { this.state.isSlackConfigured
- && (
- <div className="form-inline align-self-center mr-md-2">
- <SlackNotification
- isSlackEnabled={commentContainer.state.isSlackEnabled}
- slackChannels={commentContainer.state.slackChannels}
- onEnabledFlagChange={this.onSlackEnabledFlagChange}
- onChannelChange={this.onSlackChannelsChange}
- id="idForComment"
- />
- </div>
- )
- }
- <div className="d-none d-sm-block">
- <span className="mr-2">{cancelButton}</span><span>{submitButton}</span>
- </div>
- </div>
- <div className="d-block d-sm-none mt-2">
- <div className="d-flex justify-content-end">
- { this.state.errorMessage && errorMessage }
- <span className="mr-2">{cancelButton}</span><span>{submitButton}</span>
- </div>
- </div>
- </div>
- </>
- );
- }
- render() {
- const { appContainer } = this.props;
- const { isReadyToUse } = this.state;
- return (
- <div className="form page-comment-form">
- <div className="comment-form">
- <div className="comment-form-user">
- <UserPicture user={appContainer.currentUser} noLink noTooltip />
- </div>
- <div className="comment-form-main">
- { !isReadyToUse
- ? this.renderBeforeReady()
- : this.renderReady()
- }
- </div>
- </div>
- </div>
- );
- }
- }
- CommentEditor.propTypes = {
- appContainer: PropTypes.instanceOf(AppContainer).isRequired,
- pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
- editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
- commentContainer: PropTypes.instanceOf(CommentContainer).isRequired,
- growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
- isForNewComment: PropTypes.bool,
- replyTo: PropTypes.string,
- currentCommentId: PropTypes.string,
- commentBody: PropTypes.string,
- commentCreator: PropTypes.string,
- onCancelButtonClicked: PropTypes.func,
- onCommentButtonClicked: PropTypes.func,
- };
- /**
- * Wrapper component for using unstated
- */
- const CommentEditorWrapper = withUnstatedContainers(CommentEditor, [AppContainer, PageContainer, EditorContainer, CommentContainer]);
- export default CommentEditorWrapper;
|