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

+ 0 - 4
packages/app/src/client/services/AppContainer.js

@@ -143,10 +143,6 @@ export default class AppContainer extends Container {
       throw new Error('The specified instance must not be null');
     }
 
-    if (this.componentInstances[id] != null) {
-      throw new Error('The specified instance couldn\'t register because the same id has already been registered');
-    }
-
     this.componentInstances[id] = instance;
   }
 

+ 9 - 11
packages/app/src/client/services/PageContainer.js

@@ -217,13 +217,11 @@ export default class PageContainer extends Container {
     }
     this.setState(newState);
 
-    // PageEditor component
-    const pageEditor = this.appContainer.getComponentInstance('PageEditor');
-    if (pageEditor != null) {
-      if (editorMode !== EditorMode.Editor) {
-        pageEditor.updateEditorValue(newState.markdown);
-      }
+    // Update PageEditor component
+    if (editorMode !== EditorMode.Editor) {
+      window.globalEmitter.emit('updateEditorValue', newState.markdown);
     }
+
     // PageEditorByHackmd component
     const pageEditorByHackmd = this.appContainer.getComponentInstance('PageEditorByHackmd');
     if (pageEditorByHackmd != null) {
@@ -271,7 +269,7 @@ export default class PageContainer extends Container {
     let { revisionId } = this.state;
     const options = Object.assign({}, optionsToSave);
 
-    if (editorMode === 'hackmd') {
+    if (editorMode === EditorMode.HackMD) {
       // set option to sync
       options.isSyncRevisionToHackmd = true;
       revisionId = this.state.revisionIdHackmdSynced;
@@ -306,7 +304,7 @@ export default class PageContainer extends Container {
     const options = Object.assign({}, optionsToSave);
 
     let markdown;
-    if (editorMode === 'hackmd') {
+    if (editorMode === EditorMode.HackMD) {
       const pageEditorByHackmd = this.appContainer.getComponentInstance('PageEditorByHackmd');
       markdown = await pageEditorByHackmd.getMarkdown();
       // set option to sync
@@ -451,7 +449,6 @@ export default class PageContainer extends Container {
 
     const { pageId, remoteRevisionId, path } = this.state;
     const editorContainer = this.appContainer.getContainer('EditorContainer');
-    const pageEditor = this.appContainer.getComponentInstance('PageEditor');
     const options = editorContainer.getCurrentOptionsToSave();
     const optionsToSave = Object.assign({}, options);
 
@@ -460,8 +457,9 @@ export default class PageContainer extends Container {
     editorContainer.clearDraft(path);
     this.updateStateAfterSave(res.page, res.tags, res.revision, editorMode);
 
-    if (pageEditor != null) {
-      pageEditor.updateEditorValue(markdown);
+    // Update PageEditor component
+    if (editorMode !== EditorMode.Editor) {
+      window.globalEmitter.emit('updateEditorValue', markdown);
     }
 
     editorContainer.setState({ tags: res.tags });

+ 141 - 143
packages/app/src/components/PageEditor.tsx

@@ -6,7 +6,6 @@ import EventEmitter from 'events';
 
 import { envUtils } from '@growi/core';
 import detectIndent from 'detect-indent';
-import PropTypes from 'prop-types';
 import { throttle, debounce } from 'throttle-debounce';
 
 import AppContainer from '~/client/services/AppContainer';
@@ -36,12 +35,15 @@ const logger = loggerFactory('growi:PageEditor');
 
 
 declare let window: {
-    globalEmitter: EventEmitter,
+  globalEmitter: EventEmitter,
 };
 
 type EditorRef = {
+  setValue: (markdown: string) => void,
   setCaretLine: (line: number) => void,
+  insertText: (text: string) => void,
   forceToFocus: () => void,
+  terminateUploadingState: () => void,
 }
 
 type Props = {
@@ -68,7 +70,7 @@ type Props = {
 };
 
 // for scrolling
-const lastScrolledDateWithCursor: Date | null = null;
+let lastScrolledDateWithCursor: Date | null = null;
 let isOriginOfScrollSyncEditor = false;
 let isOriginOfScrollSyncPreview = false;
 
@@ -91,50 +93,16 @@ const PageEditor = (props: Props): JSX.Element => {
 
   // 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
@@ -171,60 +139,64 @@ const PageEditor = (props: Props): JSX.Element => {
   }, [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();
-  //   }
-  // }
+  /**
+   * the upload event handler
+   * @param {any} file
+   */
+  const uploadHandler = useCallback(async(file) => {
+    if (editorRef.current == null) {
+      return;
+    }
+
+    try {
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      let res: any = await apiGet('/attachments.limit', {
+        fileSize: file.size,
+      });
+
+      if (!res.isUploadable) {
+        throw new Error(res.errorMessage);
+      }
+
+      const formData = new FormData();
+      const { pageId, path } = pageContainer.state;
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      formData.append('_csrf', appContainer.csrfToken!);
+      formData.append('file', file);
+      if (path != null) {
+        formData.append('path', path);
+      }
+      if (pageId != null) {
+        formData.append('page_id', 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}`;
+      }
+      editorRef.current.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, editorMode);
+        mutateGrant(res.page.grant);
+      }
+    }
+    catch (e) {
+      logger.error('failed to upload', e);
+      pageContainer.showErrorToastr(e);
+    }
+    finally {
+      editorRef.current.terminateUploadingState();
+    }
+  }, [appContainer.csrfToken, editorMode, mutateGrant, pageContainer]);
 
 
   const scrollPreviewByEditorLine = useCallback((line: number) => {
@@ -254,44 +226,45 @@ const PageEditor = (props: Props): JSX.Element => {
   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 - lastScrolledDateWithCursor < 40) {
-    //   return;
-    // }
+    const now = new Date();
+    if (lastScrolledDateWithCursor != null && now.getTime() - lastScrolledDateWithCursor.getTime() < 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 Preview element by cursor moving
+   * @param {number} line
+   */
+  const scrollPreviewByCursorMoving = useCallback((line: number) => {
+    if (previewRef.current == null) {
+      return;
+    }
+
+    // prevent circular invocation
+    if (isOriginOfScrollSyncPreview) {
+      isOriginOfScrollSyncPreview = false; // turn off the flag
+      return;
+    }
+
+    // turn on the flag
+    isOriginOfScrollSyncEditor = true;
+    scrollSyncHelper.scrollPreviewToRevealOverflowing(previewRef.current, line);
+  }, []);
+  const scrollPreviewByCursorMovingWithThrottle = useMemo(() => throttle(20, scrollPreviewByCursorMoving), [scrollPreviewByCursorMoving]);
+
+  /**
+   * the scroll event handler from codemirror
+   * @param {number} line
+   * @see https://codemirror.net/doc/manual.html#events
+   */
+  const editorScrollCursorIntoViewHandler = useCallback((line: number) => {
+    // record date
+    lastScrolledDateWithCursor = new Date();
+    scrollPreviewByCursorMovingWithThrottle(line);
+  }, [scrollPreviewByCursorMovingWithThrottle]);
 
   /**
    * scroll Editor component by scroll event of Preview component
@@ -317,6 +290,30 @@ const PageEditor = (props: Props): JSX.Element => {
   const scrollEditorByPreviewScrollWithThrottle = useMemo(() => throttle(20, scrollEditorByPreviewScroll), [scrollEditorByPreviewScroll]);
 
 
+  // register dummy instance to get markdown
+  useEffect(() => {
+    const pageEditorInstance = {
+      getMarkdown: () => {
+        return markdown;
+      },
+    };
+    appContainer.registerComponentInstance('PageEditor', pageEditorInstance);
+  }, [appContainer, markdown]);
+
+  // set handler to update editor value
+  useEffect(() => {
+    const handler = (markdown) => {
+      if (editorRef.current != null) {
+        editorRef.current.setValue(markdown);
+      }
+    };
+    window.globalEmitter.on('updateEditorValue', handler);
+
+    return function cleanup() {
+      window.globalEmitter.removeListener('updateEditorValue', handler);
+    };
+  }, []);
+
   // set handler to set caret line
   useEffect(() => {
     const handler = (line) => {
@@ -349,15 +346,16 @@ const PageEditor = (props: Props): JSX.Element => {
     }
   }, [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);
-  //     }
-  //   }
+  // Detect indent size from contents (only when users are allowed to change it)
+  useEffect(() => {
+    const currentPageMarkdown = pageContainer.state.markdown;
+    if (!isIndentSizeForced && currentPageMarkdown != null) {
+      const detectedIndent = detectIndent(currentPageMarkdown);
+      if (detectedIndent.type === 'space' && new Set([2, 4]).has(detectedIndent.amount)) {
+        mutateCurrentIndentSize(detectedIndent.amount);
+      }
+    }
+  }, [isIndentSizeForced, mutateCurrentIndentSize, pageContainer.state.markdown]);
 
 
   if (!isEditable) {
@@ -384,9 +382,9 @@ const PageEditor = (props: Props): JSX.Element => {
           isTextlintEnabled={isTextlintEnabled}
           indentSize={indentSize}
           onScroll={editorScrolledHandler}
-          // onScrollCursorIntoView={this.onEditorScrollCursorIntoView}
+          onScrollCursorIntoView={editorScrollCursorIntoViewHandler}
           onChange={markdownChangedHandler}
-          // onUpload={this.onUpload}
+          onUpload={uploadHandler}
           onSave={() => saveWithShortcut()}
         />
       </div>
@@ -400,13 +398,13 @@ const PageEditor = (props: Props): JSX.Element => {
           onScroll={offset => scrollEditorByPreviewScrollWithThrottle(offset)}
         />
       </div>
-      {/* <ConflictDiffModal
-          isOpen={pageContainer.state.isConflictDiffModalOpen}
-          onClose={() => pageContainer.setState({ isConflictDiffModalOpen: false })}
-          appContainer={appContainer}
-          pageContainer={pageContainer}
-          markdownOnEdit={this.state.markdown}
-        /> */}
+      <ConflictDiffModal
+        isOpen={pageContainer.state.isConflictDiffModalOpen}
+        onClose={() => pageContainer.setState({ isConflictDiffModalOpen: false })}
+        appContainer={appContainer}
+        pageContainer={pageContainer}
+        markdownOnEdit={markdown}
+      />
     </div>
   );
 };