Explorar el Código

Merge pull request #6575 from weseek/imprv/editor-importing-dynamically

imprv: Editor rendering dynamically
Yuki Takei hace 3 años
padre
commit
696a759a63

+ 2 - 1
packages/app/src/components/CustomNavigation/CustomTabContent.tsx

@@ -1,4 +1,5 @@
 import React, { useEffect, useState } from 'react';
+
 import PropTypes from 'prop-types';
 import {
   TabContent, TabPane,
@@ -18,7 +19,7 @@ const CustomTabContent = (props: Props): JSX.Element => {
 
   const { activeTab, navTabMapping, additionalClassNames } = props;
 
-  const [activatedContent, setActivatedContent] = useState<Set<string>>(new Set<string>());
+  const [activatedContent, setActivatedContent] = useState(new Set([activeTab]));
 
   // add activated content to Set
   useEffect(() => {

+ 107 - 86
packages/app/src/components/Page/DisplaySwitcher.tsx

@@ -1,9 +1,8 @@
-import React from 'react';
+import React, { useMemo } from 'react';
 
 import { pagePathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
-import { TabContent, TabPane } from 'reactstrap';
 
 // import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
 import {
@@ -14,6 +13,7 @@ import { useSWRxCurrentPage } from '~/stores/page';
 import { EditorMode, useEditorMode } from '~/stores/ui';
 
 import CountBadge from '../Common/CountBadge';
+import CustomTabContent from '../CustomNavigation/CustomTabContent';
 import PageListIcon from '../Icons/PageListIcon';
 import { Page } from '../Page';
 // import PageEditorByHackmd from '../PageEditorByHackmd';
@@ -24,8 +24,6 @@ import UserInfo from '../User/UserInfo';
 import styles from './DisplaySwitcher.module.scss';
 
 
-const WIKI_HEADER_LINK = 120;
-
 const { isTopPage } = pagePathUtils;
 
 
@@ -35,109 +33,132 @@ const HashChanged = dynamic(() => import('../EventListeneres/HashChanged'), { ss
 const ContentLinkButtons = dynamic(() => import('../ContentLinkButtons'), { ssr: false });
 const NotFoundPage = dynamic(() => import('../NotFoundPage'), { ssr: false });
 
-const DisplaySwitcher = React.memo((): JSX.Element => {
-  const { t } = useTranslation();
 
-  // get element for smoothScroll
-  // const getCommentListDom = useMemo(() => { return document.getElementById('page-comments-list') }, []);
+const PageView = React.memo((): JSX.Element => {
+  const { t } = useTranslation();
 
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: shareLinkId } = useShareLinkId();
   const { data: isUserPage } = useIsUserPage();
-  const { data: isEditable } = useIsEditable();
   const { data: pageUser } = usePageUser();
   const { data: isNotFound } = useIsNotFound();
-  const { data: isNotCreatable } = useIsNotCreatable();
   const { data: currentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
-
-  const { data: editorMode } = useEditorMode();
-
   const { open: openDescendantPageListModal } = useDescendantsPageListModal();
 
-  const isViewMode = editorMode === EditorMode.View;
   const isTopPagePath = isTopPage(currentPagePath ?? '');
 
-  const revision = currentPage?.revision;
-
   return (
-    <>
-      <TabContent activeTab={editorMode}>
-        <TabPane tabId={EditorMode.View}>
-          <div className="d-flex flex-column flex-lg-row">
-
-            <div className="flex-grow-1 flex-basis-0 mw-0">
-              { isUserPage && <UserInfo pageUser={pageUser} />}
-              { !isNotFound && <Page /> }
-              { isNotFound && <NotFoundPage /> }
-            </div>
-
-            { !isNotFound && (
-              <div className="grw-side-contents-container">
-                <div className="grw-side-contents-sticky-container">
-
-                  {/* Page list */}
-                  <div className={`grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
-                    { currentPagePath != null && !isSharedUser && (
-                      <button
-                        type="button"
-                        className="btn btn-block btn-outline-secondary grw-btn-page-accessories rounded-pill d-flex justify-content-between align-items-center"
-                        onClick={() => openDescendantPageListModal(currentPagePath)}
-                        data-testid="pageListButton"
-                      >
-                        <div className="grw-page-accessories-control-icon">
-                          <PageListIcon />
-                        </div>
-                        {t('page_list')}
-                        <CountBadge count={currentPage?.descendantCount} offset={1} />
-                      </button>
-                    ) }
-                  </div>
-
-                  {/* Comments */}
-                  {/* { getCommentListDom != null && !isTopPagePath && ( */}
-                  { !isTopPagePath && (
-                    <div className={`mt-2 grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
-                      <button
-                        type="button"
-                        className="btn btn-block btn-outline-secondary grw-btn-page-accessories rounded-pill d-flex justify-content-between align-items-center"
-                        // onClick={() => smoothScrollIntoView(getCommentListDom, WIKI_HEADER_LINK)}
-                      >
-                        <i className="icon-fw icon-bubbles grw-page-accessories-control-icon"></i>
-                        <span>Comments</span>
-                        <CountBadge count={currentPage?.commentCount} />
-                      </button>
-                    </div>
-                  ) }
-
-                  <div className="d-none d-lg-block">
-                    <TableOfContents />
-                    <ContentLinkButtons />
+    <div className="d-flex flex-column flex-lg-row">
+
+      <div className="flex-grow-1 flex-basis-0 mw-0">
+        { isUserPage && <UserInfo pageUser={pageUser} />}
+        { !isNotFound && <Page /> }
+        { isNotFound && <NotFoundPage /> }
+      </div>
+
+      { !isNotFound && (
+        <div className="grw-side-contents-container">
+          <div className="grw-side-contents-sticky-container">
+
+            {/* Page list */}
+            <div className={`grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
+              { currentPagePath != null && !isSharedUser && (
+                <button
+                  type="button"
+                  className="btn btn-block btn-outline-secondary grw-btn-page-accessories rounded-pill d-flex justify-content-between align-items-center"
+                  onClick={() => openDescendantPageListModal(currentPagePath)}
+                  data-testid="pageListButton"
+                >
+                  <div className="grw-page-accessories-control-icon">
+                    <PageListIcon />
                   </div>
+                  {t('page_list')}
+                  <CountBadge count={currentPage?.descendantCount} offset={1} />
+                </button>
+              ) }
+            </div>
 
-                </div>
+            {/* Comments */}
+            {/* { getCommentListDom != null && !isTopPagePath && ( */}
+            { !isTopPagePath && (
+              <div className={`mt-2 grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
+                <button
+                  type="button"
+                  className="btn btn-block btn-outline-secondary grw-btn-page-accessories rounded-pill d-flex justify-content-between align-items-center"
+                  // onClick={() => smoothScrollIntoView(getCommentListDom, WIKI_HEADER_LINK)}
+                >
+                  <i className="icon-fw icon-bubbles grw-page-accessories-control-icon"></i>
+                  <span>Comments</span>
+                  <CountBadge count={currentPage?.commentCount} />
+                </button>
               </div>
             ) }
 
-          </div>
-        </TabPane>
-        { isEditable && (
-          <TabPane tabId={EditorMode.Editor}>
-            <div data-testid="page-editor" id="page-editor">
-              <PageEditor />
-            </div>
-          </TabPane>
-        ) }
-        { isEditable && (
-          <TabPane tabId={EditorMode.HackMD}>
-            <div id="page-editor-with-hackmd">
-              {/* <PageEditorByHackmd /> */}
+            <div className="d-none d-lg-block">
+              <TableOfContents />
+              <ContentLinkButtons />
             </div>
-          </TabPane>
-        ) }
-      </TabContent>
-      { isEditable && !isViewMode && <EditorNavbarBottom /> }
 
+          </div>
+        </div>
+      ) }
+    </div>
+  );
+});
+PageView.displayName = 'PageView';
+
+
+const DisplaySwitcher = React.memo((): JSX.Element => {
+  // get element for smoothScroll
+  // const getCommentListDom = useMemo(() => { return document.getElementById('page-comments-list') }, []);
+
+  const { data: isEditable } = useIsEditable();
+
+  const { data: editorMode = EditorMode.View } = useEditorMode();
+
+  const isViewMode = editorMode === EditorMode.View;
+
+  const navTabMapping = useMemo(() => {
+    return {
+      [EditorMode.View]: {
+        Content: () => (
+          <div data-testid="page-view" id="page-view">
+            <PageView />
+          </div>
+        ),
+      },
+      [EditorMode.Editor]: {
+        Content: () => (
+          isEditable
+            ? (
+              <div data-testid="page-editor" id="page-editor">
+                <PageEditor />
+              </div>
+            )
+            : <></>
+        ),
+      },
+      [EditorMode.HackMD]: {
+        Content: () => (
+          isEditable
+            ? (
+              <div id="page-editor-with-hackmd">
+                {/* <PageEditorByHackmd /> */}
+              </div>
+            )
+            : <></>
+        ),
+      },
+    };
+  }, [isEditable]);
+
+
+  return (
+    <>
+      <CustomTabContent activeTab={editorMode} navTabMapping={navTabMapping} />
+
+      { isEditable && !isViewMode && <EditorNavbarBottom /> }
       { isEditable && <HashChanged></HashChanged> }
     </>
   );

+ 2 - 6
packages/app/src/components/PageComment/CommentEditor.tsx

@@ -9,6 +9,7 @@ import {
 import * as toastr from 'toastr';
 
 import { apiPostForm } from '~/client/util/apiv1-client';
+import { IEditorMethods } from '~/interfaces/editor-methods';
 import { RendererOptions } from '~/services/renderer/renderer';
 import { useSWRxPageComment } from '~/stores/comment';
 import {
@@ -50,11 +51,6 @@ export type CommentEditorProps = {
   onCommentButtonClicked?: () => void,
 }
 
-type EditorRef = {
-  setValue: (value: string) => void,
-  insertText: (text: string) => void,
-  terminateUploadingState: () => void,
-}
 
 export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
 
@@ -80,7 +76,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
   const [error, setError] = useState();
   const [slackChannels, setSlackChannels] = useState(slackChannelsData?.toString());
 
-  const editorRef = useRef<EditorRef>(null);
+  const editorRef = useRef<IEditorMethods>(null);
 
   const handleSelect = useCallback((activeTab: string) => {
     setActiveTab(activeTab);

+ 2 - 9
packages/app/src/components/PageEditor.tsx

@@ -11,6 +11,7 @@ import { throttle, debounce } from 'throttle-debounce';
 import { saveOrUpdate } from '~/client/services/page-operation';
 import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
 import { getOptionsToSave } from '~/client/util/editor';
+import { IEditorMethods } from '~/interfaces/editor-methods';
 import {
   useIsEditable, useIsIndentSizeForced, useCurrentPagePath, useCurrentPathname, useCurrentPageId, useIsUploadableFile, useIsUploadableImage,
 } from '~/stores/context';
@@ -39,14 +40,6 @@ const logger = loggerFactory('growi:PageEditor');
 declare const globalEmitter: EventEmitter;
 
 
-type EditorRef = {
-  setValue: (markdown: string) => void,
-  setCaretLine: (line: number) => void,
-  insertText: (text: string) => void,
-  forceToFocus: () => void,
-  terminateUploadingState: () => void,
-}
-
 // for scrolling
 let lastScrolledDateWithCursor: Date | null = null;
 let isOriginOfScrollSyncEditor = false;
@@ -84,7 +77,7 @@ const PageEditor = React.memo((): JSX.Element => {
   const slackChannels = useMemo(() => (slackChannelsData ? slackChannelsData.toString() : ''), [slackChannelsData]);
 
 
-  const editorRef = useRef<EditorRef>(null);
+  const editorRef = useRef<IEditorMethods>(null);
   const previewRef = useRef<HTMLDivElement>(null);
 
   const setMarkdownWithDebounce = useMemo(() => debounce(100, throttle(150, (value: string, isClean: boolean) => {

+ 27 - 45
packages/app/src/components/PageEditor/Editor.tsx

@@ -1,5 +1,5 @@
 import React, {
-  useState, useRef, useImperativeHandle, useCallback, useMemo,
+  useState, useRef, useImperativeHandle, useCallback, ForwardRefRenderFunction, forwardRef,
 } from 'react';
 
 import Dropzone from 'react-dropzone';
@@ -22,7 +22,7 @@ import TextAreaEditor from './TextAreaEditor';
 
 import styles from './Editor.module.scss';
 
-type EditorPropsType = {
+export type EditorPropsType = {
   value?: string,
   isGfmMode?: boolean,
   noCdn?: boolean,
@@ -44,7 +44,7 @@ type DropzoneRef = {
   open: () => void
 }
 
-const Editor = React.forwardRef((props: EditorPropsType, ref): JSX.Element => {
+const Editor: ForwardRefRenderFunction<IEditorMethods, EditorPropsType> = (props, ref): JSX.Element => {
   const {
     onUpload, isUploadable, isUploadableFile, indentSize, isGfmMode = true,
   } = props;
@@ -66,45 +66,29 @@ const Editor = React.forwardRef((props: EditorPropsType, ref): JSX.Element => {
     return isMobile ? taEditorRef.current : cmEditorRef.current;
   }, [isMobile]);
 
-  const methods: Partial<IEditorMethods> = useMemo(() => {
-    return {
-      forceToFocus: () => {
-        editorSubstance()?.forceToFocus();
-      },
-      setValue: (newValue: string) => {
-        editorSubstance()?.setValue(newValue);
-      },
-      setGfmMode: (bool: boolean) => {
-        editorSubstance()?.setGfmMode(bool);
-      },
-      setCaretLine: (line: number) => {
-        editorSubstance()?.setCaretLine(line);
-      },
-      setScrollTopByLine: (line: number) => {
-        editorSubstance()?.setScrollTopByLine(line);
-      },
-      insertText: (text: string) => {
-        editorSubstance()?.insertText(text);
-      },
-      getNavbarItems: (): JSX.Element[] => {
-        // concat common items and items specific to CodeMirrorEditor or TextAreaEditor
-        const navbarItems = editorSubstance()?.getNavbarItems() ?? [];
-        return navbarItems;
-      },
-    };
-  }, [editorSubstance]);
-
   // methods for ref
   useImperativeHandle(ref, () => ({
-    forceToFocus: methods.forceToFocus,
-    setValue: methods.setValue,
-    setGfmMode: methods.setGfmMode,
-    setCaretLine: methods.setCaretLine,
-    setScrollTopByLine: methods.setScrollTopByLine,
-    insertText: methods.insertText,
+    forceToFocus: () => {
+      editorSubstance()?.forceToFocus();
+    },
+    setValue: (newValue: string) => {
+      editorSubstance()?.setValue(newValue);
+    },
+    setGfmMode: (bool: boolean) => {
+      editorSubstance()?.setGfmMode(bool);
+    },
+    setCaretLine: (line: number) => {
+      editorSubstance()?.setCaretLine(line);
+    },
+    setScrollTopByLine: (line: number) => {
+      editorSubstance()?.setScrollTopByLine(line);
+    },
+    insertText: (text: string) => {
+      editorSubstance()?.insertText(text);
+    },
     /**
-   * remove overlay and set isUploading to false
-   */
+     * remove overlay and set isUploading to false
+     */
     terminateUploadingState: () => {
       setDropzoneActive(false);
       setIsUploading(false);
@@ -239,14 +223,14 @@ const Editor = React.forwardRef((props: EditorPropsType, ref): JSX.Element => {
     return (
       <div className="m-0 navbar navbar-default navbar-editor" style={{ minHeight: 'unset' }}>
         <ul className="pl-2 nav nav-navbar">
-          { methods.getNavbarItems?.().map((item, idx) => {
+          { (editorSubstance()?.getNavbarItems() ?? []).map((item, idx) => {
             // eslint-disable-next-line react/no-array-index-key
             return <li key={`navbarItem-${idx}`}>{item}</li>;
           }) }
         </ul>
       </div>
     );
-  }, [methods]);
+  }, [editorSubstance]);
 
   const renderCheatsheetModal = useCallback(() => {
     const hideCheatsheetModal = () => {
@@ -355,8 +339,6 @@ const Editor = React.forwardRef((props: EditorPropsType, ref): JSX.Element => {
       </div>
     </>
   );
-});
-
-Editor.displayName = 'Editor';
+};
 
-export default Editor;
+export default forwardRef(Editor);

+ 5 - 2
packages/app/src/interfaces/editor-methods.ts

@@ -1,15 +1,18 @@
 export interface IEditorMethods {
   forceToFocus: () => void,
   setValue: (newValue: string) => void,
-  setGfmMode: (bool: boolean) => void,
   setCaretLine: (line: number) => void,
   setScrollTopByLine: (line: number) => void,
+  insertText: (text: string) => void,
+  terminateUploadingState: () => void,
+}
+
+export interface IEditorInnerMethods {
   getStrFromBol(): void,
   getStrToEol: () => void,
   getStrFromBolToSelectedUpperPos: () => void,
   replaceBolToCurrentPos: (text: string) => void,
   replaceLine: (text: string) => void,
-  insertText: (text: string) => void,
   insertLinebreak: () => void,
   dispatchSave: () => void,
   dispatchPasteFiles: (event: Event) => void,

+ 1 - 1
packages/app/src/interfaces/ui.ts

@@ -12,7 +12,7 @@ export type SidebarContentsType = typeof SidebarContentsType[keyof typeof Sideba
 
 export type ICustomTabContent = {
   Content: () => JSX.Element,
-  i18n: string,
+  i18n?: string,
   Icon?: () => JSX.Element,
   index?: number,
   isLinkEnabled?: boolean | ((content: ICustomTabContent) => boolean),