Просмотр исходного кода

WIP: add CommentEditor component

Yuki Takei 6 лет назад
Родитель
Сommit
48bcfefbd8

+ 8 - 1
src/client/js/app.js

@@ -29,6 +29,7 @@ import Page from './components/Page';
 import PageHistory from './components/PageHistory';
 import PageComments from './components/PageComments';
 import CommentForm from './components/PageComment/CommentForm';
+import CommentEditor from './components/PageComment/CommentEditor';
 import PageAttachment from './components/PageAttachment';
 import PageStatusAlert from './components/PageStatusAlert';
 import RevisionPath from './components/Page/RevisionPath';
@@ -523,7 +524,13 @@ if (writeCommentElem) {
         onPostComplete={postCompleteHandler}
         replyTo={undefined}
         data={data}
-      />
+      >
+        <CommentEditor
+          replyTo={undefined}
+          data={data}
+        >
+        </CommentEditor>
+      </CommentForm>
     </I18nextProvider>,
     writeCommentElem,
   );

+ 2 - 1
src/client/js/components/PageComment/Comment.jsx

@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
 import Button from 'react-bootstrap/es/Button';
 import dateFnsFormat from 'date-fns/format';
 import CommentForm from './CommentForm';
+import CommentEditor from './CommentEditor';
 
 import RevisionBody from '../Page/RevisionBody';
 
@@ -174,7 +175,7 @@ export default class Comment extends React.Component {
         {
           this.state.showCommentForm
           && (
-          <CommentForm
+          <CommentEditor
             onPostComplete={this.props.onPostComplete}
             data={this.props.data}
             replyTo={comment._id.toString()}

+ 340 - 0
src/client/js/components/PageComment/CommentEditor.jsx

@@ -0,0 +1,340 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import Button from 'react-bootstrap/es/Button';
+import Tab from 'react-bootstrap/es/Tab';
+import Tabs from 'react-bootstrap/es/Tabs';
+import * as toastr from 'toastr';
+import UserPicture from '../User/UserPicture';
+import ReactUtils from '../ReactUtils';
+
+import GrowiRenderer from '../../util/GrowiRenderer';
+
+import Editor from '../PageEditor/Editor';
+import CommentPreview from './CommentPreview';
+import SlackNotification from '../SlackNotification';
+
+/**
+ *
+ * @author Yuki Takei <yuki@weseek.co.jp>
+ *
+ * @export
+ * @class Comment
+ * @extends {React.Component}
+ */
+
+export default class CommentEditor extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    const config = this.props.data.crowi.getConfig();
+    const isUploadable = config.upload.image || config.upload.file;
+    const isUploadableFile = config.upload.file;
+
+    this.state = {
+      isLayoutTypeGrowi: false,
+      isFormShown: false,
+      comment: '',
+      isMarkdown: true,
+      html: '',
+      key: 1,
+      isUploadable,
+      isUploadableFile,
+      errorMessage: undefined,
+      hasSlackConfig: config.hasSlackConfig,
+      isSlackEnabled: false,
+      slackChannels: this.props.data.slackChannels,
+    };
+
+    this.growiRenderer = new GrowiRenderer(this.props.data.crowi, this.props.data.crowiOriginRenderer, { mode: 'comment' });
+
+    this.updateState = this.updateState.bind(this);
+    this.updateStateCheckbox = this.updateStateCheckbox.bind(this);
+
+    this.onSubmit = this.onSubmit.bind(this);
+    this.onUpload = this.onUpload.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);
+    this.showCommentFormBtnClickHandler = this.showCommentFormBtnClickHandler.bind(this);
+  }
+
+  componentWillMount() {
+    this.init();
+  }
+
+  init() {
+    if (!this.props.data.pageId) {
+      return;
+    }
+
+    const layoutType = this.props.data.crowi.getConfig().layoutType;
+    this.setState({ isLayoutTypeGrowi: layoutType === 'crowi-plus' || layoutType === 'growi' });
+  }
+
+  updateState(value) {
+    this.setState({ comment: value });
+  }
+
+  updateStateCheckbox(event) {
+    const value = event.target.checked;
+    this.setState({ isMarkdown: value });
+    // changeMode
+    this.editor.setGfmMode(value);
+  }
+
+  handleSelect(key) {
+    this.setState({ key });
+    this.renderHtml(this.state.comment);
+  }
+
+  onSlackEnabledFlagChange(value) {
+    this.setState({ isSlackEnabled: value });
+  }
+
+  onSlackChannelsChange(value) {
+    this.setState({ slackChannels: value });
+  }
+
+  /**
+   * Load data of comments and rerender <PageComments />
+   */
+  onSubmit(event) {
+    if (event != null) {
+      event.preventDefault();
+    }
+  }
+
+  getCommentHtml() {
+    return (
+      <CommentPreview
+        inputRef={(el) => { this.previewElement = el }}
+        html={this.state.html}
+      />
+    );
+  }
+
+  renderHtml(markdown) {
+    const context = {
+      markdown,
+    };
+
+    const growiRenderer = this.growiRenderer;
+    const interceptorManager = this.props.data.crowi.interceptorManager;
+    interceptorManager.process('preRenderCommnetPreview', context)
+      .then(() => { return interceptorManager.process('prePreProcess', context) })
+      .then(() => {
+        context.markdown = growiRenderer.preProcess(context.markdown);
+      })
+      .then(() => { return interceptorManager.process('postPreProcess', context) })
+      .then(() => {
+        const parsedHTML = growiRenderer.process(context.markdown);
+        context.parsedHTML = parsedHTML;
+      })
+      .then(() => { return interceptorManager.process('prePostProcess', context) })
+      .then(() => {
+        context.parsedHTML = growiRenderer.postProcess(context.parsedHTML);
+      })
+      .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 };
+  }
+
+  onUpload(file) {
+    const endpoint = '/attachments.add';
+
+    /*
+    // create a FromData instance
+    const formData = new FormData();
+    formData.append('_csrf', this.props.data.crowi.csrfToken);
+    formData.append('file', file);
+    formData.append('path', this.props.data.pagePath);
+    formData.append('page_id', this.props.data.pageId || 0);
+
+    // post
+    this.props.data.crowi.apiPost(endpoint, formData)
+      .then((res) => {
+        const attachment = res.attachment;
+        const fileName = attachment.originalName;
+
+        let insertText = `[${fileName}](${attachment.filePathProxied})`;
+        // when image
+        if (attachment.fileFormat.startsWith('image/')) {
+          // modify to "![fileName](url)" 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',
+  //   });
+  // }
+
+  showCommentFormBtnClickHandler() {
+    this.setState({ isFormShown: true });
+  }
+
+  renderControls() {
+
+  }
+
+  render() {
+    const crowi = this.props.data.crowi;
+    const username = crowi.me;
+    const user = crowi.findUser(username);
+    const comment = this.state.comment;
+    const commentPreview = this.state.isMarkdown ? this.getCommentHtml() : ReactUtils.nl2br(comment);
+    const emojiStrategy = this.props.data.crowi.getEmojiStrategy();
+
+    const isLayoutTypeGrowi = this.state.isLayoutTypeGrowi;
+
+    const errorMessage = <span className="text-danger text-right mr-2">{this.state.errorMessage}</span>;
+    const submitButton = (
+      <Button bsStyle="primary" className="fcbtn btn btn-sm btn-primary btn-outline btn-rounded btn-1b" onClick={this.onSubmit}>
+        Comment
+      </Button>
+    );
+
+    return (
+      <div>
+
+        { username
+          && (
+          <div className="comment-form">
+            { isLayoutTypeGrowi
+              && (
+              <div className="comment-form-user">
+                <UserPicture user={user} />
+              </div>
+              )
+            }
+            <div className="comment-form-main">
+              {/* Add Comment Button */}
+              { !this.state.isFormShown
+                && (
+                <button
+                  type="button"
+                  className={`btn btn-lg ${isLayoutTypeGrowi ? 'btn-link' : 'btn-primary'} center-block`}
+                  onClick={this.showCommentFormBtnClickHandler}
+                >
+                  <i className="icon-bubble"></i> Add Comment
+                </button>
+                )
+              }
+              {/* Editor */}
+              { this.state.isFormShown
+                && (
+                <React.Fragment>
+                  <div className="comment-write">
+                    <Tabs activeKey={this.state.key} id="comment-form-tabs" onSelect={this.handleSelect} animation={false}>
+                      <Tab eventKey={1} title="Write">
+                        <Editor
+                          ref={(c) => { this.editor = c }}
+                          value={this.state.comment}
+                          isGfmMode={this.state.isMarkdown}
+                          editorOptions={this.props.data.editorOptions}
+                          lineNumbers={false}
+                          isMobile={this.props.data.crowi.isMobile}
+                          isUploadable={this.state.isUploadable && this.state.isLayoutTypeGrowi} // enable only when GROWI layout
+                          isUploadableFile={this.state.isUploadableFile}
+                          emojiStrategy={emojiStrategy}
+                          onChange={this.updateState}
+                          onUpload={this.onUpload}
+                          onCtrlEnter={this.onSubmit}
+                        />
+                      </Tab>
+                      { this.state.isMarkdown
+                        && (
+                        <Tab eventKey={2} title="Preview">
+                          <div className="comment-form-preview">
+                            {commentPreview}
+                          </div>
+                        </Tab>
+                        )
+                      }
+                    </Tabs>
+                  </div>
+                  <div className="comment-submit">
+                    <div className="d-flex">
+                      <label style={{ flex: 1 }}>
+                        { isLayoutTypeGrowi && this.state.key === 1
+                          && (
+                          <span>
+                            <input
+                              type="checkbox"
+                              id="comment-form-is-markdown"
+                              name="isMarkdown"
+                              checked={this.state.isMarkdown}
+                              value="1"
+                              onChange={this.updateStateCheckbox}
+                            />
+                            <span className="ml-2">Markdown</span>
+                          </span>
+                          )
+                      }
+                      </label>
+                      <span className="hidden-xs">{ this.state.errorMessage && errorMessage }</span>
+                      { this.state.hasSlackConfig
+                        && (
+                        <div className="form-inline align-self-center mr-md-2">
+                          <SlackNotification
+                            isSlackEnabled={this.state.isSlackEnabled}
+                            slackChannels={this.state.slackChannels}
+                            onEnabledFlagChange={this.onSlackEnabledFlagChange}
+                            onChannelChange={this.onSlackChannelsChange}
+                          />
+                        </div>
+                        )
+                      }
+                      <div className="hidden-xs">{submitButton}</div>
+                    </div>
+                    <div className="visible-xs mt-2">
+                      <div className="d-flex justify-content-end">
+                        { this.state.errorMessage && errorMessage }
+                        <div>{submitButton}</div>
+                      </div>
+                    </div>
+                  </div>
+                </React.Fragment>
+                )
+              }
+            </div>
+          </div>
+          )
+        }
+
+      </div>
+    );
+  }
+
+}
+
+CommentEditor.propTypes = {
+  replyTo: PropTypes.string,
+  data: PropTypes.object.isRequired,
+};

+ 71 - 295
src/client/js/components/PageComment/CommentForm.jsx

@@ -8,12 +8,6 @@ import * as toastr from 'toastr';
 import UserPicture from '../User/UserPicture';
 import ReactUtils from '../ReactUtils';
 
-import GrowiRenderer from '../../util/GrowiRenderer';
-
-import Editor from '../PageEditor/Editor';
-import CommentPreview from './CommentPreview';
-import SlackNotification from '../SlackNotification';
-
 /**
  *
  * @author Yuki Takei <yuki@weseek.co.jp>
@@ -28,37 +22,19 @@ export default class CommentForm extends React.Component {
   constructor(props) {
     super(props);
 
-    const config = this.props.data.crowi.getConfig();
-    const isUploadable = config.upload.image || config.upload.file;
-    const isUploadableFile = config.upload.file;
-
     this.state = {
-      isLayoutTypeGrowi: false,
-      isFormShown: false,
-      comment: '',
-      isMarkdown: true,
-      html: '',
-      key: 1,
-      isUploadable,
-      isUploadableFile,
-      errorMessage: undefined,
-      hasSlackConfig: config.hasSlackConfig,
-      isSlackEnabled: false,
-      slackChannels: this.props.data.slackChannels,
     };
 
-    this.growiRenderer = new GrowiRenderer(this.props.data.crowi, this.props.data.crowiOriginRenderer, { mode: 'comment' });
-
-    this.updateState = this.updateState.bind(this);
-    this.updateStateCheckbox = this.updateStateCheckbox.bind(this);
-    this.postComment = this.postComment.bind(this);
-    this.renderHtml = this.renderHtml.bind(this);
-    this.handleSelect = this.handleSelect.bind(this);
-    this.apiErrorHandler = this.apiErrorHandler.bind(this);
-    this.onUpload = this.onUpload.bind(this);
-    this.onSlackEnabledFlagChange = this.onSlackEnabledFlagChange.bind(this);
-    this.onSlackChannelsChange = this.onSlackChannelsChange.bind(this);
-    this.showCommentFormBtnClickHandler = this.showCommentFormBtnClickHandler.bind(this);
+    // this.updateState = this.updateState.bind(this);
+    // this.updateStateCheckbox = this.updateStateCheckbox.bind(this);
+    // this.postComment = this.postComment.bind(this);
+    // this.renderHtml = this.renderHtml.bind(this);
+    // this.handleSelect = this.handleSelect.bind(this);
+    // this.apiErrorHandler = this.apiErrorHandler.bind(this);
+    // this.onUpload = this.onUpload.bind(this);
+    // this.onSlackEnabledFlagChange = this.onSlackEnabledFlagChange.bind(this);
+    // this.onSlackChannelsChange = this.onSlackChannelsChange.bind(this);
+    // this.showCommentFormBtnClickHandler = this.showCommentFormBtnClickHandler.bind(this);
   }
 
   componentWillMount() {
@@ -69,33 +45,6 @@ export default class CommentForm extends React.Component {
     if (!this.props.data.pageId) {
       return;
     }
-
-    const layoutType = this.props.data.crowi.getConfig().layoutType;
-    this.setState({ isLayoutTypeGrowi: layoutType === 'crowi-plus' || layoutType === 'growi' });
-  }
-
-  updateState(value) {
-    this.setState({ comment: value });
-  }
-
-  updateStateCheckbox(event) {
-    const value = event.target.checked;
-    this.setState({ isMarkdown: value });
-    // changeMode
-    this.editor.setGfmMode(value);
-  }
-
-  handleSelect(key) {
-    this.setState({ key });
-    this.renderHtml(this.state.comment);
-  }
-
-  onSlackEnabledFlagChange(value) {
-    this.setState({ isSlackEnabled: value });
-  }
-
-  onSlackChannelsChange(value) {
-    this.setState({ slackChannels: value });
   }
 
   /**
@@ -106,113 +55,70 @@ export default class CommentForm extends React.Component {
       event.preventDefault();
     }
 
-    this.props.data.crowi.apiPost('/comments.add', {
-      commentForm: {
-        comment: this.state.comment,
-        _csrf: this.props.data.crowi.csrfToken,
-        page_id: this.props.data.pageId,
-        revision_id: this.props.data.revisionId,
-        is_markdown: this.state.isMarkdown,
-        replyTo: this.props.replyTo,
-      },
-      slackNotificationForm: {
-        isSlackEnabled: this.state.isSlackEnabled,
-        slackChannels: this.state.slackChannels,
-      },
-    })
-      .then((res) => {
-        if (this.props.onPostComplete != null) {
-          this.props.onPostComplete(res.comment);
-        }
-        this.setState({
-          comment: '',
-          isMarkdown: true,
-          html: '',
-          key: 1,
-          errorMessage: undefined,
-          isSlackEnabled: false,
-        });
-        // reset value
-        this.editor.setValue('');
-      })
-      .catch((err) => {
-        const errorMessage = err.message || 'An unknown error occured when posting comment';
-        this.setState({ errorMessage });
-      });
-  }
-
-  getCommentHtml() {
-    return (
-      <CommentPreview
-        inputRef={(el) => { this.previewElement = el }}
-        html={this.state.html}
-      />
-    );
-  }
-
-  renderHtml(markdown) {
-    const context = {
-      markdown,
-    };
-
-    const growiRenderer = this.growiRenderer;
-    const interceptorManager = this.props.data.crowi.interceptorManager;
-    interceptorManager.process('preRenderCommnetPreview', context)
-      .then(() => { return interceptorManager.process('prePreProcess', context) })
-      .then(() => {
-        context.markdown = growiRenderer.preProcess(context.markdown);
-      })
-      .then(() => { return interceptorManager.process('postPreProcess', context) })
-      .then(() => {
-        const parsedHTML = growiRenderer.process(context.markdown);
-        context.parsedHTML = parsedHTML;
-      })
-      .then(() => { return interceptorManager.process('prePostProcess', context) })
-      .then(() => {
-        context.parsedHTML = growiRenderer.postProcess(context.parsedHTML);
-      })
-      .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 };
+    // this.props.data.crowi.apiPost('/comments.add', {
+    //   commentForm: {
+    //     comment: this.state.comment,
+    //     _csrf: this.props.data.crowi.csrfToken,
+    //     page_id: this.props.data.pageId,
+    //     revision_id: this.props.data.revisionId,
+    //     is_markdown: this.state.isMarkdown,
+    //     replyTo: this.props.replyTo,
+    //   },
+    //   slackNotificationForm: {
+    //     isSlackEnabled: this.state.isSlackEnabled,
+    //     slackChannels: this.state.slackChannels,
+    //   },
+    // })
+    //   .then((res) => {
+    //     if (this.props.onPostComplete != null) {
+    //       this.props.onPostComplete(res.comment);
+    //     }
+    //     this.setState({
+    //       comment: '',
+    //       isMarkdown: true,
+    //       html: '',
+    //       key: 1,
+    //       errorMessage: undefined,
+    //       isSlackEnabled: false,
+    //     });
+    //     // reset value
+    //     this.editor.setValue('');
+    //   })
+    //   .catch((err) => {
+    //     const errorMessage = err.message || 'An unknown error occured when posting comment';
+    //     this.setState({ errorMessage });
+    //   });
   }
 
   onUpload(file) {
     const endpoint = '/attachments.add';
 
-    // create a FromData instance
-    const formData = new FormData();
-    formData.append('_csrf', this.props.data.crowi.csrfToken);
-    formData.append('file', file);
-    formData.append('path', this.props.data.pagePath);
-    formData.append('page_id', this.props.data.pageId || 0);
-
-    // post
-    this.props.data.crowi.apiPost(endpoint, formData)
-      .then((res) => {
-        const attachment = res.attachment;
-        const fileName = attachment.originalName;
-
-        let insertText = `[${fileName}](${attachment.filePathProxied})`;
-        // when image
-        if (attachment.fileFormat.startsWith('image/')) {
-          // modify to "![fileName](url)" syntax
-          insertText = `!${insertText}`;
-        }
-        this.editor.insertText(insertText);
-      })
-      .catch(this.apiErrorHandler)
-      // finally
-      .then(() => {
-        this.editor.terminateUploadingState();
-      });
+    // // create a FromData instance
+    // const formData = new FormData();
+    // formData.append('_csrf', this.props.data.crowi.csrfToken);
+    // formData.append('file', file);
+    // formData.append('path', this.props.data.pagePath);
+    // formData.append('page_id', this.props.data.pageId || 0);
+
+    // // post
+    // this.props.data.crowi.apiPost(endpoint, formData)
+    //   .then((res) => {
+    //     const attachment = res.attachment;
+    //     const fileName = attachment.originalName;
+
+    //     let insertText = `[${fileName}](${attachment.filePathProxied})`;
+    //     // when image
+    //     if (attachment.fileFormat.startsWith('image/')) {
+    //       // modify to "![fileName](url)" syntax
+    //       insertText = `!${insertText}`;
+    //     }
+    //     this.editor.insertText(insertText);
+    //   })
+    //   .catch(this.apiErrorHandler)
+    //   // finally
+    //   .then(() => {
+    //     this.editor.terminateUploadingState();
+    //   });
   }
 
   apiErrorHandler(error) {
@@ -226,141 +132,10 @@ export default class CommentForm extends React.Component {
     });
   }
 
-  showCommentFormBtnClickHandler() {
-    this.setState({ isFormShown: true });
-  }
-
-  renderControls() {
-
-  }
-
   render() {
-    const crowi = this.props.data.crowi;
-    const username = crowi.me;
-    const user = crowi.findUser(username);
-    const comment = this.state.comment;
-    const commentPreview = this.state.isMarkdown ? this.getCommentHtml() : ReactUtils.nl2br(comment);
-    const emojiStrategy = this.props.data.crowi.getEmojiStrategy();
-
-    const isLayoutTypeGrowi = this.state.isLayoutTypeGrowi;
-
-    const errorMessage = <span className="text-danger text-right mr-2">{this.state.errorMessage}</span>;
-    const submitButton = (
-      <Button type="submit" bsStyle="primary" className="fcbtn btn btn-sm btn-primary btn-outline btn-rounded btn-1b">
-        Comment
-      </Button>
-    );
-
     return (
       <div>
-
-        <form className="form page-comment-form" id="page-comment-form" onSubmit={this.postComment}>
-          { username
-            && (
-            <div className="comment-form">
-              { isLayoutTypeGrowi
-                && (
-                <div className="comment-form-user">
-                  <UserPicture user={user} />
-                </div>
-                )
-              }
-              <div className="comment-form-main">
-                {/* Add Comment Button */}
-                { !this.state.isFormShown
-                  && (
-                  <button
-                    type="button"
-                    className={`btn btn-lg ${isLayoutTypeGrowi ? 'btn-link' : 'btn-primary'} center-block`}
-                    onClick={this.showCommentFormBtnClickHandler}
-                  >
-                    <i className="icon-bubble"></i> Add Comment
-                  </button>
-                  )
-                }
-                {/* Editor */}
-                { this.state.isFormShown
-                  && (
-                  <React.Fragment>
-                    <div className="comment-write">
-                      <Tabs activeKey={this.state.key} id="comment-form-tabs" onSelect={this.handleSelect} animation={false}>
-                        <Tab eventKey={1} title="Write">
-                          <Editor
-                            ref={(c) => { this.editor = c }}
-                            value={this.state.comment}
-                            isGfmMode={this.state.isMarkdown}
-                            editorOptions={this.props.data.editorOptions}
-                            lineNumbers={false}
-                            isMobile={this.props.data.crowi.isMobile}
-                            isUploadable={this.state.isUploadable && this.state.isLayoutTypeGrowi} // enable only when GROWI layout
-                            isUploadableFile={this.state.isUploadableFile}
-                            emojiStrategy={emojiStrategy}
-                            onChange={this.updateState}
-                            onUpload={this.onUpload}
-                            onCtrlEnter={this.postComment}
-                          />
-                        </Tab>
-                        { this.state.isMarkdown
-                          && (
-                          <Tab eventKey={2} title="Preview">
-                            <div className="comment-form-preview">
-                              {commentPreview}
-                            </div>
-                          </Tab>
-                          )
-                        }
-                      </Tabs>
-                    </div>
-                    <div className="comment-submit">
-                      <div className="d-flex">
-                        <label style={{ flex: 1 }}>
-                          { isLayoutTypeGrowi && this.state.key === 1
-                            && (
-                            <span>
-                              <input
-                                type="checkbox"
-                                id="comment-form-is-markdown"
-                                name="isMarkdown"
-                                checked={this.state.isMarkdown}
-                                value="1"
-                                onChange={this.updateStateCheckbox}
-                              />
-                              <span className="ml-2">Markdown</span>
-                            </span>
-                            )
-                        }
-                        </label>
-                        <span className="hidden-xs">{ this.state.errorMessage && errorMessage }</span>
-                        { this.state.hasSlackConfig
-                          && (
-                          <div className="form-inline align-self-center mr-md-2">
-                            <SlackNotification
-                              isSlackEnabled={this.state.isSlackEnabled}
-                              slackChannels={this.state.slackChannels}
-                              onEnabledFlagChange={this.onSlackEnabledFlagChange}
-                              onChannelChange={this.onSlackChannelsChange}
-                            />
-                          </div>
-                          )
-                        }
-                        <div className="hidden-xs">{submitButton}</div>
-                      </div>
-                      <div className="visible-xs mt-2">
-                        <div className="d-flex justify-content-end">
-                          { this.state.errorMessage && errorMessage }
-                          <div>{submitButton}</div>
-                        </div>
-                      </div>
-                    </div>
-                  </React.Fragment>
-                  )
-                }
-              </div>
-            </div>
-            )
-          }
-        </form>
-
+        {this.props.children}
       </div>
     );
   }
@@ -368,6 +143,7 @@ export default class CommentForm extends React.Component {
 }
 
 CommentForm.propTypes = {
+  children: PropTypes.node.isRequired,
   onPostComplete: PropTypes.func,
   replyTo: PropTypes.string,
   data: PropTypes.object.isRequired,