Browse Source

Merge pull request #6314 from weseek/imprv/100207-next-toc

Imprv/100207 next toc
Yuki Takei 3 years ago
parent
commit
2051a6aa90

+ 12 - 10
packages/app/src/components/Page/DisplaySwitcher.tsx

@@ -23,6 +23,8 @@ import { Page } from '../Page';
 import TableOfContents from '../TableOfContents';
 import UserInfo from '../User/UserInfo';
 
+import styles from '../TableOfContents.module.scss';
+
 
 const WIKI_HEADER_LINK = 120;
 
@@ -62,7 +64,14 @@ const DisplaySwitcher = (): JSX.Element => {
     <>
       <TabContent activeTab={editorMode}>
         <TabPane tabId={EditorMode.View}>
-          <div className="d-flex flex-column flex-lg-row-reverse">
+          <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 && !isNotCreatable && <NotFoundPage /> }
+              { isNotFound && isNotCreatable && <NotCreatablePage /> }
+            </div>
 
             { !isNotFound && !currentPage?.isEmpty && (
               <div className="grw-side-contents-container">
@@ -103,8 +112,8 @@ const DisplaySwitcher = (): JSX.Element => {
                   ) }
 
                   <div className="d-none d-lg-block">
-                    <div id="revision-toc" className="revision-toc">
-                      {/* <TableOfContents /> */}
+                    <div id="revision-toc" className={`revision-toc ${styles['revision-toc']}`}>
+                      <TableOfContents />
                     </div>
                     <ContentLinkButtons />
                   </div>
@@ -113,13 +122,6 @@ const DisplaySwitcher = (): JSX.Element => {
               </div>
             ) }
 
-            <div className="flex-grow-1 flex-basis-0 mw-0">
-              { isUserPage && <UserInfo pageUser={pageUser} />}
-              { !isNotFound && <Page /> }
-              { isNotFound && !isNotCreatable && <NotFoundPage /> }
-              { isNotFound && isNotCreatable && <NotCreatablePage /> }
-            </div>
-
           </div>
         </TabPane>
         { isEditable && (

+ 28 - 0
packages/app/src/components/TableOfContents.module.scss

@@ -0,0 +1,28 @@
+
+.revision-toc :global {
+  // to get on the Attachment row
+  z-index: 1;
+  padding: 5px;
+  font-size: 0.9em;
+
+  border-bottom: 1px solid transparent;
+
+  .revision-toc-content {
+    li {
+      margin: 6px;
+    }
+    > ul {
+      padding-left: 0;
+      ul {
+        padding-left: 1em;
+      }
+    }
+
+    // first level of li
+    > ul > li {
+      padding: 5px;
+      margin-right: 4px;
+      margin-left: 17px;
+    }
+  }
+}

+ 22 - 52
packages/app/src/components/TableOfContents.jsx → packages/app/src/components/TableOfContents.tsx

@@ -1,37 +1,35 @@
 import React, { useCallback, useEffect, useState } from 'react';
 
-import PropTypes from 'prop-types';
+import ReactMarkdown from 'react-markdown';
 
-
-import PageContainer from '~/client/services/PageContainer';
 import { blinkElem } from '~/client/util/blink-section-header';
 import { addSmoothScrollEvent } from '~/client/util/smooth-scroll';
+import { useIsUserPage } from '~/stores/context';
+import { useTocOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
 
 
 import { StickyStretchableScroller } from './StickyStretchableScroller';
-import { withUnstatedContainers } from './UnstatedUtils';
 
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:TableOfContents');
 
-/**
- * @author Yuki Takei <yuki@weseek.co.jp>
- *
- */
-const TableOfContents = (props) => {
+const TableOfContents = (): JSX.Element => {
 
-  const { pageContainer } = props;
-  const { pageUser } = pageContainer.state;
-  const isUserPage = pageUser != null;
+  const { data: isUserPage } = useIsUserPage();
 
   const [tocHtml, setTocHtml] = useState('');
 
+  const { data: rendererOptions } = useTocOptions();
+
   const calcViewHeight = useCallback(() => {
     // calculate absolute top of '#revision-toc' element
     const parentElem = document.querySelector('.grw-side-contents-container');
-    const parentBottom = parentElem.getBoundingClientRect().bottom;
     const containerElem = document.querySelector('#revision-toc');
+    if (parentElem == null || containerElem == null) {
+      return 0;
+    }
+    const parentBottom = parentElem.getBoundingClientRect().bottom;
     const containerTop = containerElem.getBoundingClientRect().top;
     const containerComputedStyle = getComputedStyle(containerElem);
     const containerPaddingTop = parseFloat(containerComputedStyle['padding-top']);
@@ -49,56 +47,28 @@ const TableOfContents = (props) => {
 
   useEffect(() => {
     const tocDom = document.getElementById('revision-toc-content');
+    if (tocDom == null) { return }
     const anchorsInToc = Array.from(tocDom.getElementsByTagName('a'));
     addSmoothScrollEvent(anchorsInToc, blinkElem);
   }, [tocHtml]);
 
-  // == TODO: render ToC without globalEmitter -- Yuki Takei
-  //
-  // set handler to render ToC
-  // useEffect(() => {
-  //   const handler = html => setTocHtml(html);
-  //   globalEmitter.on('renderTocHtml', handler);
-
-  //   return function cleanup() {
-  //     globalEmitter.removeListener('renderTocHtml', handler);
-  //   };
-  // }, [globalEmitter]);
-
   return (
     <StickyStretchableScroller
       stickyElemSelector=".grw-side-contents-sticky-container"
       calcViewHeight={calcViewHeight}
     >
-      { tocHtml !== ''
-        ? (
-          <div
-            id="revision-toc-content"
-            className="revision-toc-content mb-3"
-            // eslint-disable-next-line react/no-danger
-            dangerouslySetInnerHTML={{ __html: tocHtml }}
-          />
-        )
-        : (
-          <div
-            id="revision-toc-content"
-            className="revision-toc-content mb-2"
-          >
-          </div>
-        ) }
-
+      <div
+        id="revision-toc-content"
+        className="revision-toc-content mb-3"
+      >
+        {/* parse blank to show toc (https://github.com/weseek/growi/pull/6277) */}
+        <ReactMarkdown {...rendererOptions}>
+          {''}
+        </ReactMarkdown>
+      </div>
     </StickyStretchableScroller>
   );
 
 };
 
-/**
- * Wrapper component for using unstated
- */
-const TableOfContentsWrapper = withUnstatedContainers(TableOfContents, [PageContainer]);
-
-TableOfContents.propTypes = {
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-};
-
-export default TableOfContentsWrapper;
+export default TableOfContents;

+ 52 - 7
packages/app/src/services/renderer/renderer.tsx

@@ -2,7 +2,7 @@ import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
 import raw from 'rehype-raw';
 import sanitize, { defaultSchema } from 'rehype-sanitize';
 import slug from 'rehype-slug';
-// import toc, { HtmlElementNode } from 'rehype-toc';
+import toc, { HtmlElementNode } from 'rehype-toc';
 import breaks from 'remark-breaks';
 import emoji from 'remark-emoji';
 import footnotes from 'remark-footnotes';
@@ -233,7 +233,10 @@ const generateCommonOptions: ReactMarkdownOptionsGenerator = (config: RendererCo
   };
 };
 
-export const generateViewOptions: ReactMarkdownOptionsGenerator = (config: RendererConfig): RendererOptions => {
+export const generateViewOptions = (
+    config: RendererConfig,
+    storeTocNode: (node: HtmlElementNode) => void,
+): RendererOptions => {
 
   const options = generateCommonOptions(config);
 
@@ -248,11 +251,29 @@ export const generateViewOptions: ReactMarkdownOptionsGenerator = (config: Rende
     }
   }
 
-  // add rehypePlugins
-  // rehypePlugins.push([toc, {
-  //   headings: ['h1', 'h2', 'h3'],
-  //   customizeTOC: storeTocNode,
-  // }]);
+  // store toc node
+  if (rehypePlugins != null) {
+    rehypePlugins.push([toc, {
+      nav: false,
+      headings: ['h1', 'h2', 'h3'],
+      customizeTOC: (toc: HtmlElementNode) => {
+        // method for replace <ol> to <ul>
+        const replacer = (children) => {
+          children.forEach((child) => {
+            if (child.type === 'element' && child.tagName === 'ol') {
+              child.tagName = 'ul';
+            }
+            if (child.children) {
+              replacer(child.children);
+            }
+          });
+        };
+        replacer([toc]); // replace <ol> to <ul>
+        storeTocNode(toc); // store tocNode to global state with swr
+        return false; // not show toc in body
+      },
+    }]);
+  }
   // renderer.rehypePlugins.push([autoLinkHeadings, {
   //   behavior: 'append',
   // }]);
@@ -279,6 +300,30 @@ export const generateViewOptions: ReactMarkdownOptionsGenerator = (config: Rende
   return options;
 };
 
+export const generateTocOptions = (config: RendererConfig, tocNode: HtmlElementNode | undefined): RendererOptions => {
+
+  const options = generateCommonOptions(config);
+
+  const { remarkPlugins, rehypePlugins } = options;
+
+  // add remark plugins
+  if (remarkPlugins != null) {
+    remarkPlugins.push(emoji);
+  }
+  // set toc node
+  if (rehypePlugins != null) {
+    rehypePlugins.push([toc, {
+      headings: ['h1', 'h2', 'h3'],
+      customizeTOC: () => tocNode,
+    }]);
+  }
+  // renderer.rehypePlugins.push([autoLinkHeadings, {
+  //   behavior: 'append',
+  // }]);
+
+  return options;
+};
+
 export const generatePreviewOptions: ReactMarkdownOptionsGenerator = (config: RendererConfig): RendererOptions => {
   const options = generateCommonOptions(config);
 

+ 5 - 0
packages/app/src/stores/context.tsx

@@ -1,5 +1,6 @@
 import EventEmitter from 'events';
 
+import { HtmlElementNode } from 'rehype-toc';
 import { Key, SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
@@ -223,6 +224,10 @@ export const useRendererConfig = (initialData?: RendererConfig): SWRResponse<Ren
   return useStaticSWR('growiRendererConfig', initialData);
 };
 
+export const useCurrentPageTocNode = (): SWRResponse<HtmlElementNode, any> => {
+  return useStaticSWR('currentPageTocNode');
+};
+
 export const useIsBlinkedHeaderAtBoot = (initialData?: boolean): SWRResponse<boolean, Error> => {
   return useStaticSWR('isBlinkedAtBoot', initialData);
 };

+ 18 - 4
packages/app/src/stores/renderer.tsx

@@ -3,13 +3,17 @@ import useSWRImmutable from 'swr/immutable';
 
 import {
   ReactMarkdownOptionsGenerator, RendererOptions,
-  generateViewOptions, generatePreviewOptions, generateCommentPreviewOptions, generateOthersOptions,
+  generatePreviewOptions, generateCommentPreviewOptions, generateOthersOptions,
+  generateViewOptions, generateTocOptions,
 } from '~/services/renderer/renderer';
 
-import { useRendererConfig } from './context';
+
+import { useCurrentPageTocNode, useRendererConfig } from './context';
 
 // The base hook with common processes
-const _useOptionsBase = (rendererId: string, generator: ReactMarkdownOptionsGenerator): SWRResponse<RendererOptions, Error> => {
+const _useOptionsBase = (
+    rendererId: string, generator: ReactMarkdownOptionsGenerator,
+): SWRResponse<RendererOptions, Error> => {
   const { data: rendererConfig } = useRendererConfig();
 
   const isAllDataValid = rendererConfig != null;
@@ -31,7 +35,17 @@ const _useOptionsBase = (rendererId: string, generator: ReactMarkdownOptionsGene
 export const useViewOptions = (): SWRResponse<RendererOptions, Error> => {
   const key = 'viewOptions';
 
-  return _useOptionsBase(key, generateViewOptions);
+  const { mutate: storeTocNode } = useCurrentPageTocNode();
+
+  return _useOptionsBase(key, config => generateViewOptions(config, storeTocNode));
+};
+
+export const useTocOptions = (): SWRResponse<RendererOptions, Error> => {
+  const key = 'tocOptions';
+
+  const { data: tocNode } = useCurrentPageTocNode();
+
+  return _useOptionsBase(key, config => generateTocOptions(config, tocNode));
 };
 
 export const usePreviewOptions = (): SWRResponse<RendererOptions, Error> => {

+ 1 - 0
packages/app/src/styles/style-next.scss

@@ -8,6 +8,7 @@
 
 // import SimpleBar styles
 @import '~simplebar/dist/simplebar.min.css';
+
 // override simplebar-react styles
 @import 'override-simplebar';