Yuki Takei 3 лет назад
Родитель
Сommit
e4e665d6e7

+ 2 - 2
packages/app/src/components/Page/DisplaySwitcher.tsx

@@ -18,7 +18,7 @@ import ContentLinkButtons from '../ContentLinkButtons';
 import HashChanged from '../EventListeneres/HashChanged';
 import PageListIcon from '../Icons/PageListIcon';
 import Page from '../Page';
-import Editor from '../PageEditor';
+import PageEditor from '../PageEditor';
 import EditorNavbarBottom from '../PageEditor/EditorNavbarBottom';
 import PageEditorByHackmd from '../PageEditorByHackmd';
 import TableOfContents from '../TableOfContents';
@@ -117,7 +117,7 @@ const DisplaySwitcher = (): JSX.Element => {
         { isEditable && (
           <TabPane tabId={EditorMode.Editor}>
             <div data-testid="page-editor" id="page-editor">
-              <Editor />
+              <PageEditor />
             </div>
           </TabPane>
         ) }

+ 301 - 349
packages/app/src/components/PageEditor.tsx

@@ -1,4 +1,8 @@
-import React, { useEffect, useRef } from 'react';
+import React, {
+  useCallback, useEffect, useMemo, useRef, useState,
+} from 'react';
+
+import EventEmitter from 'events';
 
 import { envUtils } from '@growi/core';
 import detectIndent from 'detect-indent';
@@ -30,127 +34,129 @@ import { withUnstatedContainers } from './UnstatedUtils';
 
 const logger = loggerFactory('growi:PageEditor');
 
-class PageEditor extends React.Component {
 
-  constructor(props) {
-    super(props);
+declare let window: {
+    globalEmitter: EventEmitter,
+};
 
-    this.previewElement = React.createRef();
+type EditorRef = {
+  setCaretLine: (line: number) => void,
+  forceToFocus: () => void,
+}
 
-    const config = this.props.appContainer.getConfig();
-    const isUploadable = config.upload.image || config.upload.file;
-    const isUploadableFile = config.upload.file;
-    const isMathJaxEnabled = !!config.env.MATHJAX;
+type Props = {
+  appContainer: AppContainer,
+  pageContainer: PageContainer,
+  editorContainer: EditorContainer,
 
-    this.state = {
-      markdown: this.props.pageContainer.state.markdown,
-      isUploadable,
-      isUploadableFile,
-      isMathJaxEnabled,
-    };
+  isEditable: boolean,
 
-    this.setCaretLine = this.setCaretLine.bind(this);
-    this.focusToEditor = this.focusToEditor.bind(this);
-    this.onMarkdownChanged = this.onMarkdownChanged.bind(this);
-    this.onSaveWithShortcut = this.onSaveWithShortcut.bind(this);
-    this.onUpload = this.onUpload.bind(this);
-    this.onEditorScroll = this.onEditorScroll.bind(this);
-    this.onEditorScrollCursorIntoView = this.onEditorScrollCursorIntoView.bind(this);
-    this.onPreviewScroll = this.onPreviewScroll.bind(this);
-    this.saveDraft = this.saveDraft.bind(this);
-    this.clearDraft = this.clearDraft.bind(this);
-
-    // for scrolling
-    this.lastScrolledDateWithCursor = null;
-    this.isOriginOfScrollSyncEditor = false;
-    this.isOriginOfScrollSyncEditor = false;
-
-    // create throttled function
-    this.scrollPreviewByEditorLineWithThrottle = throttle(20, this.scrollPreviewByEditorLine);
-    this.scrollPreviewByCursorMovingWithThrottle = throttle(20, this.scrollPreviewByCursorMoving);
-    this.scrollEditorByPreviewScrollWithThrottle = throttle(20, this.scrollEditorByPreviewScroll);
-    this.setMarkdownStateWithDebounce = debounce(50, throttle(100, (value) => {
-      this.setState({ markdown: value });
-    }));
-    this.saveDraftWithDebounce = debounce(800, this.saveDraft);
-
-    // Detect indent size from contents (only when users are allowed to change it)
-    // TODO: https://youtrack.weseek.co.jp/issue/GW-5368
-    if (!props.isIndentSizeForced && this.state.markdown) {
-      const detectedIndent = detectIndent(this.state.markdown);
-      if (detectedIndent.type === 'space' && new Set([2, 4]).has(detectedIndent.amount)) {
-        props.mutateCurrentIndentSize(detectedIndent.amount);
-      }
-    }
-  }
+  editorMode: string,
+  isSlackEnabled: boolean,
+  slackChannels: string,
+  isMobile?: boolean,
 
-  componentWillMount() {
-    this.props.appContainer.registerComponentInstance('PageEditor', this);
-  }
+  grant: number,
+  grantGroupId?: string,
+  grantGroupName?: string,
+  mutateGrant: (grant: number) => void,
 
-  getMarkdown() {
-    return this.state.markdown;
-  }
+  isTextlintEnabled?: boolean,
+  isIndentSizeForced?: boolean,
+  indentSize?: number,
+  mutateCurrentIndentSize: (indent: number) => void,
+};
 
-  updateEditorValue(markdown) {
-    this.editor.setValue(markdown);
-  }
+// for scrolling
+const lastScrolledDateWithCursor: Date | null = null;
+let isOriginOfScrollSyncEditor = false;
+let isOriginOfScrollSyncPreview = false;
 
-  focusToEditor() {
-    if (this.editor != null) {
-      this.editor.forceToFocus();
-    }
-  }
+const PageEditor = (props: Props): JSX.Element => {
+  const {
+    appContainer, pageContainer, editorContainer,
+  } = props;
 
-  /**
-   * set caret position of editor
-   * @param {number} line
-   */
-  setCaretLine(line) {
-    this.editor.setCaretLine(line);
-    scrollSyncHelper.scrollPreview(this.previewElement, line);
-  }
+  const { data: isEditable } = useIsEditable();
+  const { data: editorMode } = useEditorMode();
+  const { data: isMobile } = useIsMobile();
+  const { data: isSlackEnabled } = useIsSlackEnabled();
+  const { data: slackChannels } = useSlackChannels();
+  const { data: grant, mutate: mutateGrant } = useSelectedGrant();
+  const { data: grantGroupId } = useSelectedGrantGroupId();
+  const { data: grantGroupName } = useSelectedGrantGroupName();
+  const { data: isTextlintEnabled } = useIsTextlintEnabled();
+  const { data: isIndentSizeForced } = useIsIndentSizeForced();
+  const { data: indentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
 
-  /**
-   * the change event handler for `markdown` state
-   * @param {string} value
-   */
-  onMarkdownChanged(value) {
-    const { pageContainer } = this.props;
-    this.setMarkdownStateWithDebounce(value);
+  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+  const [markdown, setMarkdown] = useState<string>(pageContainer.state.markdown!);
+  const [lastScrolledDateWithCursor, setLastScrolledDateWithCursor] = useState<Date|null>(null);
+
+
+  const editorRef = useRef<EditorRef>(null);
+  const previewRef = useRef<HTMLDivElement>(null);
+
+
+  // componentWillMount() {
+  //   this.props.appContainer.registerComponentInstance('PageEditor', this);
+  // }
+
+  // getMarkdown() {
+  //   return this.state.markdown;
+  // }
+
+  // updateEditorValue(markdown) {
+  //   this.editor.setValue(markdown);
+  // }
+
+  // focusToEditor() {
+  //   if (this.editor != null) {
+  //     this.editor.forceToFocus();
+  //   }
+  // }
+
+  // /**
+  //  * set caret position of editor
+  //  * @param {number} line
+  //  */
+  // setCaretLine(line) {
+  //   this.editor.setCaretLine(line);
+  //   scrollSyncHelper.scrollPreview(this.previewElement, line);
+  // }
+
+
+  const setMarkdownWithDebounce = useMemo(() => debounce(50, throttle(100, value => setMarkdown(value))), []);
+  const saveDraftWithDebounce = useMemo(() => debounce(800, () => {
+    editorContainer.saveDraft(pageContainer.state.path, markdown);
+  }), [editorContainer, markdown, pageContainer.state.path]);
+
+  // clearDraft() {
+  //   this.props.editorContainer.clearDraft(this.props.pageContainer.state.path);
+  // }
+
+  const markdownChangedHandler = useCallback((value: string): void => {
+    setMarkdownWithDebounce(value);
     // only when the first time to edit
     if (!pageContainer.state.revisionId) {
-      this.saveDraftWithDebounce();
+      saveDraftWithDebounce();
     }
-  }
+  }, [pageContainer.state.revisionId, saveDraftWithDebounce, setMarkdownWithDebounce]);
 
-  // Displays an alert if there is a difference with pageContainer's markdown
-  componentDidUpdate(prevProps, prevState) {
-    const { pageContainer, editorContainer } = this.props;
 
-    if (this.state.markdown !== prevState.markdown) {
-      if (pageContainer.state.markdown !== this.state.markdown) {
-        editorContainer.enableUnsavedWarning();
-      }
+  const saveWithShortcut = useCallback(async() => {
+    if (grant == null) {
+      return;
     }
-  }
 
-  /**
-   * save and update state of containers
-   */
-  async onSaveWithShortcut() {
-    const {
-      isSlackEnabled, slackChannels, grant, grantGroupId, grantGroupName, editorContainer, pageContainer,
-    } = this.props;
-
-    const optionsToSave = getOptionsToSave(isSlackEnabled, slackChannels, grant, grantGroupId, grantGroupName, editorContainer);
+    const optionsToSave = getOptionsToSave(isSlackEnabled ?? false, slackChannels, grant, grantGroupId, grantGroupName, editorContainer);
 
     try {
       // disable unsaved warning
       editorContainer.disableUnsavedWarning();
 
       // eslint-disable-next-line no-unused-vars
-      const { page, tags } = await pageContainer.save(this.state.markdown, this.props.editorMode, optionsToSave);
+      const { page, tags } = await pageContainer.save(markdown, editorMode, optionsToSave);
       logger.debug('success to save');
 
       pageContainer.showSuccessToastr();
@@ -162,62 +168,81 @@ class PageEditor extends React.Component {
       logger.error('failed to save', error);
       pageContainer.showErrorToastr(error);
     }
-  }
-
-  /**
-   * the upload event handler
-   * @param {any} file
-   */
-  async onUpload(file) {
-    const {
-      appContainer, pageContainer, mutateGrant,
-    } = this.props;
-
-    try {
-      let res = await apiGet('/attachments.limit', {
-        fileSize: file.size,
-      });
-
-      if (!res.isUploadable) {
-        throw new Error(res.errorMessage);
-      }
-
-      const formData = new FormData();
-      const { pageId, path } = pageContainer.state;
-      formData.append('_csrf', appContainer.csrfToken);
-      formData.append('file', file);
-      formData.append('path', path);
-      if (pageId != null) {
-        formData.append('page_id', pageContainer.state.pageId);
-      }
-
-      res = await apiPost('/attachments.add', formData);
-      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);
-
-      // when if created newly
-      if (res.pageCreated) {
-        logger.info('Page is created', res.page._id);
-        pageContainer.updateStateAfterSave(res.page, res.tags, res.revision, this.props.editorMode);
-        mutateGrant(res.page.grant);
-      }
-    }
-    catch (e) {
-      logger.error('failed to upload', e);
-      pageContainer.showErrorToastr(e);
+  }, [editorContainer, editorMode, grant, grantGroupId, grantGroupName, isSlackEnabled, markdown, pageContainer, slackChannels]);
+
+
+  // /**
+  //  * the upload event handler
+  //  * @param {any} file
+  //  */
+  // async onUpload(file) {
+  //   const {
+  //     appContainer, pageContainer, mutateGrant,
+  //   } = this.props;
+
+  //   try {
+  //     let res = await apiGet('/attachments.limit', {
+  //       fileSize: file.size,
+  //     });
+
+  //     if (!res.isUploadable) {
+  //       throw new Error(res.errorMessage);
+  //     }
+
+  //     const formData = new FormData();
+  //     const { pageId, path } = pageContainer.state;
+  //     formData.append('_csrf', appContainer.csrfToken);
+  //     formData.append('file', file);
+  //     formData.append('path', path);
+  //     if (pageId != null) {
+  //       formData.append('page_id', pageContainer.state.pageId);
+  //     }
+
+  //     res = await apiPost('/attachments.add', formData);
+  //     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);
+
+  //     // when if created newly
+  //     if (res.pageCreated) {
+  //       logger.info('Page is created', res.page._id);
+  //       pageContainer.updateStateAfterSave(res.page, res.tags, res.revision, this.props.editorMode);
+  //       mutateGrant(res.page.grant);
+  //     }
+  //   }
+  //   catch (e) {
+  //     logger.error('failed to upload', e);
+  //     pageContainer.showErrorToastr(e);
+  //   }
+  //   finally {
+  //     this.editor.terminateUploadingState();
+  //   }
+  // }
+
+
+  const scrollPreviewByEditorLine = useCallback((line: number) => {
+    if (previewRef.current == null) {
+      return;
     }
-    finally {
-      this.editor.terminateUploadingState();
+
+    // prevent circular invocation
+    if (isOriginOfScrollSyncPreview) {
+      isOriginOfScrollSyncPreview = false; // turn off the flag
+      return;
     }
-  }
+
+    // turn on the flag
+    isOriginOfScrollSyncEditor = true;
+    scrollSyncHelper.scrollPreview(previewRef.current, line);
+  }, []);
+  const scrollPreviewByEditorLineWithThrottle = useMemo(() => throttle(20, scrollPreviewByEditorLine), [scrollPreviewByEditorLine]);
 
   /**
    * the scroll event handler from codemirror
@@ -226,202 +251,80 @@ class PageEditor extends React.Component {
    *                    And data.line is also available that is added by Editor component
    * @see https://codemirror.net/doc/manual.html#events
    */
-  onEditorScroll(data) {
+  const editorScrolledHandler = useCallback(({ line }: { line: number }) => {
     // prevent scrolling
     //   if the elapsed time from last scroll with cursor is shorter than 40ms
-    const now = new Date();
-    if (now - this.lastScrolledDateWithCursor < 40) {
-      return;
-    }
-
-    this.scrollPreviewByEditorLineWithThrottle(data.line);
-  }
-
-  /**
-   * the scroll event handler from codemirror
-   * @param {number} line
-   * @see https://codemirror.net/doc/manual.html#events
-   */
-  onEditorScrollCursorIntoView(line) {
-    // record date
-    this.lastScrolledDateWithCursor = new Date();
-    this.scrollPreviewByCursorMovingWithThrottle(line);
-  }
-
-  /**
-   * scroll Preview element by scroll event
-   * @param {number} line
-   */
-  scrollPreviewByEditorLine(line) {
-    if (this.previewElement == null) {
-      return;
-    }
-
-    // prevent circular invocation
-    if (this.isOriginOfScrollSyncPreview) {
-      this.isOriginOfScrollSyncPreview = false; // turn off the flag
-      return;
-    }
-
-    // turn on the flag
-    this.isOriginOfScrollSyncEditor = true;
-    scrollSyncHelper.scrollPreview(this.previewElement, line);
-  }
-
-  /**
-   * scroll Preview element by cursor moving
-   * @param {number} line
-   */
-  scrollPreviewByCursorMoving(line) {
-    if (this.previewElement == null) {
-      return;
-    }
-
-    // prevent circular invocation
-    if (this.isOriginOfScrollSyncPreview) {
-      this.isOriginOfScrollSyncPreview = false; // turn off the flag
-      return;
-    }
-
-    // turn on the flag
-    this.isOriginOfScrollSyncEditor = true;
-    scrollSyncHelper.scrollPreviewToRevealOverflowing(this.previewElement, line);
-  }
-
-  /**
-   * the scroll event handler from Preview component
-   * @param {number} offset
-   */
-  onPreviewScroll(offset) {
-    this.scrollEditorByPreviewScrollWithThrottle(offset);
-  }
+    // const now = new Date();
+    // if (now - lastScrolledDateWithCursor < 40) {
+    //   return;
+    // }
+
+    scrollPreviewByEditorLineWithThrottle(line);
+  }, [scrollPreviewByEditorLineWithThrottle]);
+
+  // /**
+  //  * the scroll event handler from codemirror
+  //  * @param {number} line
+  //  * @see https://codemirror.net/doc/manual.html#events
+  //  */
+  // onEditorScrollCursorIntoView(line) {
+  //   // record date
+  //   this.lastScrolledDateWithCursor = new Date();
+  //   this.scrollPreviewByCursorMovingWithThrottle(line);
+  // }
+
+  // /**
+  //  * scroll Preview element by cursor moving
+  //  * @param {number} line
+  //  */
+  // scrollPreviewByCursorMoving(line) {
+  //   if (this.previewElement == null) {
+  //     return;
+  //   }
+
+  //   // prevent circular invocation
+  //   if (this.isOriginOfScrollSyncPreview) {
+  //     this.isOriginOfScrollSyncPreview = false; // turn off the flag
+  //     return;
+  //   }
+
+  //   // turn on the flag
+  //   this.isOriginOfScrollSyncEditor = true;
+  //   scrollSyncHelper.scrollPreviewToRevealOverflowing(this.previewElement, line);
+  // }
 
   /**
    * scroll Editor component by scroll event of Preview component
    * @param {number} offset
    */
-  scrollEditorByPreviewScroll(offset) {
-    if (this.editor == null || this.previewElement == null) {
+  const scrollEditorByPreviewScroll = useCallback((offset: number) => {
+    if (editorRef.current == null || previewRef.current == null) {
       return;
     }
 
     // prevent circular invocation
-    if (this.isOriginOfScrollSyncEditor) {
-      this.isOriginOfScrollSyncEditor = false; // turn off the flag
+    if (isOriginOfScrollSyncEditor) {
+      isOriginOfScrollSyncEditor = false; // turn off the flag
       return;
     }
 
     // turn on the flag
-    this.isOriginOfScrollSyncPreview = true;
-    scrollSyncHelper.scrollEditor(this.editor, this.previewElement, offset);
-  }
-
-  saveDraft() {
-    const { pageContainer, editorContainer } = this.props;
-    editorContainer.saveDraft(pageContainer.state.path, this.state.markdown);
-  }
-
-  clearDraft() {
-    this.props.editorContainer.clearDraft(this.props.pageContainer.state.path);
-  }
-
-  render() {
-    if (!this.props.isEditable) {
-      return null;
-    }
-
-    const config = this.props.appContainer.getConfig();
-    const noCdn = envUtils.toBoolean(config.env.NO_CDN);
-
-    return (
-      <div className="d-flex flex-wrap">
-        <div className="page-editor-editor-container flex-grow-1 flex-basis-0 mw-0">
-          <Editor
-            ref={(c) => { this.editor = c }}
-            value={this.state.markdown}
-            noCdn={noCdn}
-            isMobile={this.props.isMobile}
-            isUploadable={this.state.isUploadable}
-            isUploadableFile={this.state.isUploadableFile}
-            isTextlintEnabled={this.props.isTextlintEnabled}
-            indentSize={this.props.indentSize}
-            onScroll={this.onEditorScroll}
-            onScrollCursorIntoView={this.onEditorScrollCursorIntoView}
-            onChange={this.onMarkdownChanged}
-            onUpload={this.onUpload}
-            onSave={this.onSaveWithShortcut}
-          />
-        </div>
-        <div className="d-none d-lg-block page-editor-preview-container flex-grow-1 flex-basis-0 mw-0">
-          <Preview
-            markdown={this.state.markdown}
-            // eslint-disable-next-line no-return-assign
-            inputRef={(el) => { return this.previewElement = el }}
-            isMathJaxEnabled={this.state.isMathJaxEnabled}
-            renderMathJaxOnInit={false}
-            onScroll={this.onPreviewScroll}
-          />
-        </div>
-        <ConflictDiffModal
-          isOpen={this.props.pageContainer.state.isConflictDiffModalOpen}
-          onClose={() => this.props.pageContainer.setState({ isConflictDiffModalOpen: false })}
-          appContainer={this.props.appContainer}
-          pageContainer={this.props.pageContainer}
-          markdownOnEdit={this.state.markdown}
-        />
-      </div>
-    );
-  }
-
-}
-
-PageEditor.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-  editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
-
-  isEditable: PropTypes.bool.isRequired,
-
-  // TODO: remove this when omitting unstated is completed
-  editorMode: PropTypes.string.isRequired,
-  isMobile: PropTypes.bool,
-  isSlackEnabled: PropTypes.bool.isRequired,
-  slackChannels: PropTypes.string.isRequired,
-  grant: PropTypes.number.isRequired,
-  grantGroupId: PropTypes.string,
-  grantGroupName: PropTypes.string,
-  mutateGrant: PropTypes.func,
-  isTextlintEnabled: PropTypes.bool,
-  isIndentSizeForced: PropTypes.bool,
-  indentSize: PropTypes.number,
-  mutateCurrentIndentSize: PropTypes.func,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const PageEditorHOCWrapper = withUnstatedContainers(PageEditor, [AppContainer, PageContainer, EditorContainer]);
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    isOriginOfScrollSyncPreview = true;
 
-const PageEditorWrapper = (props) => {
-  const { data: isEditable } = useIsEditable();
-  const { data: editorMode } = useEditorMode();
-  const { data: isMobile } = useIsMobile();
-  const { data: isSlackEnabled } = useIsSlackEnabled();
-  const { data: slackChannels } = useSlackChannels();
-  const { data: grant, mutate: mutateGrant } = useSelectedGrant();
-  const { data: grantGroupId } = useSelectedGrantGroupId();
-  const { data: grantGroupName } = useSelectedGrantGroupName();
-  const { data: isTextlintEnabled } = useIsTextlintEnabled();
-  const { data: isIndentSizeForced } = useIsIndentSizeForced();
-  const { data: indentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
+    scrollSyncHelper.scrollEditor(editorRef.current, previewRef.current, offset);
+  }, []);
+  const scrollEditorByPreviewScrollWithThrottle = useMemo(() => throttle(20, scrollEditorByPreviewScroll), [scrollEditorByPreviewScroll]);
 
-  const pageEditorRef = useRef(null);
 
   // set handler to set caret line
   useEffect(() => {
     const handler = (line) => {
-      if (pageEditorRef.current != null) {
-        pageEditorRef.current.setCaretLine(line);
+      if (editorRef.current != null) {
+        editorRef.current.setCaretLine(line);
+      }
+      if (previewRef.current != null) {
+        scrollSyncHelper.scrollPreview(previewRef.current, line);
       }
     };
     window.globalEmitter.on('setCaretLine', handler);
@@ -433,35 +336,84 @@ const PageEditorWrapper = (props) => {
 
   // set handler to focus
   useEffect(() => {
-    if (pageEditorRef.current != null && editorMode === EditorMode.Editor) {
-      pageEditorRef.current.focusToEditor();
+    if (editorRef.current != null && editorMode === EditorMode.Editor) {
+      editorRef.current.forceToFocus();
     }
   }, [editorMode]);
 
-  if (isEditable == null || editorMode == null) {
-    return null;
+  // Displays an alert if there is a difference with pageContainer's markdown
+  useEffect(() => {
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    if (pageContainer.state.markdown! !== markdown) {
+      editorContainer.enableUnsavedWarning();
+    }
+  }, [editorContainer, markdown, pageContainer.state.markdown]);
+
+
+  //   // Detect indent size from contents (only when users are allowed to change it)
+  //   // TODO: https://youtrack.weseek.co.jp/issue/GW-5368
+  //   if (!props.isIndentSizeForced && this.state.markdown) {
+  //     const detectedIndent = detectIndent(this.state.markdown);
+  //     if (detectedIndent.type === 'space' && new Set([2, 4]).has(detectedIndent.amount)) {
+  //       props.mutateCurrentIndentSize(detectedIndent.amount);
+  //     }
+  //   }
+
+
+  if (!isEditable) {
+    return <></>;
   }
 
+  const config = props.appContainer.getConfig();
+  const isUploadable = config.upload.image || config.upload.file;
+  const isUploadableFile = config.upload.file;
+  const isMathJaxEnabled = !!config.env.MATHJAX;
+
+  const noCdn = envUtils.toBoolean(config.env.NO_CDN);
+
   return (
-    <PageEditorHOCWrapper
-      {...props}
-      ref={pageEditorRef}
-      isEditable={isEditable}
-      editorMode={editorMode}
-      isMobile={isMobile}
-      isSlackEnabled={isSlackEnabled}
-      slackChannels={slackChannels}
-      grant={grant}
-      grantGroupId={grantGroupId}
-      grantGroupName={grantGroupName}
-      mutateGrant={mutateGrant}
-      isTextlintEnabled={isTextlintEnabled}
-      isIndentSizeForced={isIndentSizeForced}
-      indentSize={indentSize}
-      mutateCurrentIndentSize={mutateCurrentIndentSize}
-
-    />
+    <div className="d-flex flex-wrap">
+      <div className="page-editor-editor-container flex-grow-1 flex-basis-0 mw-0">
+        <Editor
+          ref={editorRef}
+          value={markdown}
+          noCdn={noCdn}
+          isMobile={isMobile}
+          isUploadable={isUploadable}
+          isUploadableFile={isUploadableFile}
+          isTextlintEnabled={isTextlintEnabled}
+          indentSize={indentSize}
+          onScroll={editorScrolledHandler}
+          // onScrollCursorIntoView={this.onEditorScrollCursorIntoView}
+          onChange={markdownChangedHandler}
+          // onUpload={this.onUpload}
+          onSave={() => saveWithShortcut()}
+        />
+      </div>
+      <div className="d-none d-lg-block page-editor-preview-container flex-grow-1 flex-basis-0 mw-0">
+        <Preview
+          markdown={markdown}
+          // eslint-disable-next-line no-return-assign
+          inputRef={previewRef}
+          isMathJaxEnabled={isMathJaxEnabled}
+          renderMathJaxOnInit={false}
+          onScroll={offset => scrollEditorByPreviewScrollWithThrottle(offset)}
+        />
+      </div>
+      {/* <ConflictDiffModal
+          isOpen={pageContainer.state.isConflictDiffModalOpen}
+          onClose={() => pageContainer.setState({ isConflictDiffModalOpen: false })}
+          appContainer={appContainer}
+          pageContainer={pageContainer}
+          markdownOnEdit={this.state.markdown}
+        /> */}
+    </div>
   );
 };
 
+/**
+   * Wrapper component for using unstated
+   */
+const PageEditorWrapper = withUnstatedContainers(PageEditor, [AppContainer, PageContainer, EditorContainer]);
+
 export default PageEditorWrapper;