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

Merge pull request #1139 from weseek/imprv/toc

reactify ToC
Yuki Takei 6 лет назад
Родитель
Сommit
5c8e09ccae

+ 1 - 0
CHANGES.md

@@ -5,6 +5,7 @@
 * Feature: Support S3-compatible object storage (e.g. MinIO)
 * Feature: Support S3-compatible object storage (e.g. MinIO)
 * Feature: Enable/Disable ID/Password Authentication
 * Feature: Enable/Disable ID/Password Authentication
 * Improvement: Login Mechanism with HTTP Basic Authentication header
 * Improvement: Login Mechanism with HTTP Basic Authentication header
+* Improvement: Reactify Table Of Contents
 * Fix: Profile images are broken in User Management
 * Fix: Profile images are broken in User Management
 * Fix: Template page under root page doesn't work
 * Fix: Template page under root page doesn't work
 * Support: Upgrade libs
 * Support: Upgrade libs

+ 2 - 0
src/client/js/app.jsx

@@ -31,6 +31,7 @@ import RecentCreated from './components/RecentCreated/RecentCreated';
 import StaffCredit from './components/StaffCredit/StaffCredit';
 import StaffCredit from './components/StaffCredit/StaffCredit';
 import MyDraftList from './components/MyDraftList/MyDraftList';
 import MyDraftList from './components/MyDraftList/MyDraftList';
 import UserPictureList from './components/User/UserPictureList';
 import UserPictureList from './components/User/UserPictureList';
+import TableOfContents from './components/TableOfContents';
 
 
 import CustomCssEditor from './components/Admin/CustomCssEditor';
 import CustomCssEditor from './components/Admin/CustomCssEditor';
 import CustomScriptEditor from './components/Admin/CustomScriptEditor';
 import CustomScriptEditor from './components/Admin/CustomScriptEditor';
@@ -106,6 +107,7 @@ if (pageContainer.state.pageId != null) {
     'page-comments-list': <PageComments />,
     'page-comments-list': <PageComments />,
     'page-attachment':  <PageAttachment />,
     'page-attachment':  <PageAttachment />,
     'page-comment-write':  <CommentEditorLazyRenderer />,
     'page-comment-write':  <CommentEditorLazyRenderer />,
+    'revision-toc': <TableOfContents />,
     'like-button': <LikeButton pageId={pageContainer.state.pageId} isLiked={pageContainer.state.isLiked} />,
     'like-button': <LikeButton pageId={pageContainer.state.pageId} isLiked={pageContainer.state.isLiked} />,
     'seen-user-list': <UserPictureList userIds={pageContainer.state.seenUserIds} />,
     'seen-user-list': <UserPictureList userIds={pageContainer.state.seenUserIds} />,
     'liker-list': <UserPictureList userIds={pageContainer.state.likerUserIds} />,
     'liker-list': <UserPictureList userIds={pageContainer.state.likerUserIds} />,

+ 116 - 0
src/client/js/components/TableOfContents.jsx

@@ -0,0 +1,116 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { withTranslation } from 'react-i18next';
+
+import { debounce } from 'throttle-debounce';
+
+import AppContainer from '../services/AppContainer';
+import PageContainer from '../services/PageContainer';
+
+import { createSubscribedElement } from './UnstatedUtils';
+
+// get these value with
+//   document.querySelector('.revision-toc').getBoundingClientRect().top
+const DEFAULT_REVISION_TOC_TOP_FOR_GROWI_LAYOUT = 190;
+const DEFAULT_REVISION_TOC_TOP_FOR_KIBELA_LAYOUT = 105;
+
+/**
+ * @author Yuki Takei <yuki@weseek.co.jp>
+ *
+ * @export
+ * @class TableOfContents
+ * @extends {React.Component}
+ */
+class TableOfContents extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.resetScrollbarDebounced = debounce(100, this.resetScrollbar);
+  }
+
+  componentDidUpdate() {
+    const { layoutType } = this.props.appContainer.config;
+    if (layoutType === 'crowi') {
+      return;
+    }
+
+    let defaultRevisionTocTop = DEFAULT_REVISION_TOC_TOP_FOR_GROWI_LAYOUT;
+    if (layoutType === 'kibela') {
+      defaultRevisionTocTop = DEFAULT_REVISION_TOC_TOP_FOR_KIBELA_LAYOUT;
+    }
+
+    // initialize
+    this.resetScrollbar(defaultRevisionTocTop);
+
+    /*
+     * set event listener
+     */
+    // resize
+    window.addEventListener('resize', (event) => {
+      this.resetScrollbarDebounced(defaultRevisionTocTop);
+    });
+    // affix on
+    $('#revision-toc').on('affixed.bs.affix', () => {
+      this.resetScrollbar(this.getCurrentRevisionTocTop());
+    });
+    // affix off
+    $('#revision-toc').on('affixed-top.bs.affix', () => {
+      this.resetScrollbar(defaultRevisionTocTop);
+    });
+  }
+
+  getCurrentRevisionTocTop() {
+    // calculate absolute top of '#revision-toc' element
+    const revisionTocElem = document.querySelector('.revision-toc');
+    return revisionTocElem.getBoundingClientRect().top;
+  }
+
+  resetScrollbar(revisionTocTop) {
+    const tocContentElem = document.querySelector('.revision-toc .markdownIt-TOC');
+
+    // window height - revisionTocTop - .system-version height
+    let h = window.innerHeight - revisionTocTop - 20;
+
+    const tocContentHeight = tocContentElem.getBoundingClientRect().height + 15; // add margin
+
+    h = Math.min(h, tocContentHeight);
+
+    $('#revision-toc-content').slimScroll({
+      railVisible: true,
+      position: 'right',
+      height: h,
+    });
+  }
+
+  render() {
+    const { tocHtml } = this.props.pageContainer.state;
+
+    return (
+      <div
+        id="revision-toc-content"
+        className="revision-toc-content"
+        // eslint-disable-next-line react/no-danger
+        dangerouslySetInnerHTML={{
+          __html: tocHtml,
+        }}
+      />
+    );
+  }
+
+}
+
+/**
+ * Wrapper component for using unstated
+ */
+const TableOfContentsWrapper = (props) => {
+  return createSubscribedElement(TableOfContents, props, [AppContainer, PageContainer]);
+};
+
+TableOfContents.propTypes = {
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+};
+
+export default withTranslation()(TableOfContentsWrapper);

+ 0 - 65
src/client/js/legacy/crowi.js

@@ -5,8 +5,6 @@ import ReactDOM from 'react-dom';
 
 
 import { Provider } from 'unstated';
 import { Provider } from 'unstated';
 
 
-import { debounce } from 'throttle-debounce';
-
 import { pathUtils } from 'growi-commons';
 import { pathUtils } from 'growi-commons';
 
 
 import GrowiRenderer from '../util/GrowiRenderer';
 import GrowiRenderer from '../util/GrowiRenderer';
@@ -26,14 +24,6 @@ if (!window) {
 }
 }
 window.Crowi = Crowi;
 window.Crowi = Crowi;
 
 
-/**
- * render Table Of Contents
- * @param {string} tocHtml
- */
-Crowi.renderTocContent = (tocHtml) => {
-  $('#revision-toc-content').html(tocHtml);
-};
-
 /**
 /**
  * set 'data-caret-line' attribute that will be processed when 'shown.bs.tab' event fired
  * set 'data-caret-line' attribute that will be processed when 'shown.bs.tab' event fired
  * @param {number} line
  * @param {number} line
@@ -157,60 +147,6 @@ Crowi.initAffix = () => {
   }
   }
 };
 };
 
 
-Crowi.initSlimScrollForRevisionToc = () => {
-  const revisionTocElem = document.querySelector('.growi .revision-toc');
-  const tocContentElem = document.querySelector('.growi .revision-toc .markdownIt-TOC');
-
-  // growi layout only
-  if (revisionTocElem == null || tocContentElem == null) {
-    return;
-  }
-
-  function getCurrentRevisionTocTop() {
-    // calculate absolute top of '#revision-toc' element
-    return revisionTocElem.getBoundingClientRect().top;
-  }
-
-  function resetScrollbar(revisionTocTop) {
-    // window height - revisionTocTop - .system-version height
-    let h = window.innerHeight - revisionTocTop - 20;
-
-    const tocContentHeight = tocContentElem.getBoundingClientRect().height + 15; // add margin
-
-    h = Math.min(h, tocContentHeight);
-
-    $('#revision-toc-content').slimScroll({
-      railVisible: true,
-      position: 'right',
-      height: h,
-    });
-  }
-
-  const resetScrollbarDebounced = debounce(100, resetScrollbar);
-
-  // initialize
-  const revisionTocTop = getCurrentRevisionTocTop();
-  resetScrollbar(revisionTocTop);
-
-  /*
-   * set event listener
-   */
-  // resize
-  window.addEventListener('resize', (event) => {
-    resetScrollbarDebounced(getCurrentRevisionTocTop());
-  });
-  // affix on
-  $('#revision-toc').on('affixed.bs.affix', () => {
-    resetScrollbar(getCurrentRevisionTocTop());
-  });
-  // affix off
-  $('#revision-toc').on('affixed-top.bs.affix', () => {
-    // calculate sum of height (.navbar-header + .bg-title) + margin-top of .main
-    const sum = 138;
-    resetScrollbar(sum);
-  });
-};
-
 Crowi.initClassesByOS = function() {
 Crowi.initClassesByOS = function() {
   // add classes to cmd-key by OS
   // add classes to cmd-key by OS
   const platform = navigator.platform.toLowerCase();
   const platform = navigator.platform.toLowerCase();
@@ -769,7 +705,6 @@ window.addEventListener('load', (e) => {
 
 
   Crowi.highlightSelectedSection(window.location.hash);
   Crowi.highlightSelectedSection(window.location.hash);
   Crowi.modifyScrollTop();
   Crowi.modifyScrollTop();
-  Crowi.initSlimScrollForRevisionToc();
   Crowi.initAffix();
   Crowi.initAffix();
   Crowi.initClassesByOS();
   Crowi.initClassesByOS();
 });
 });

+ 7 - 0
src/client/js/services/PageContainer.js

@@ -36,6 +36,7 @@ export default class PageContainer extends Container {
       revisionId,
       revisionId,
       revisionCreatedAt: +mainContent.getAttribute('data-page-revision-created'),
       revisionCreatedAt: +mainContent.getAttribute('data-page-revision-created'),
       path: mainContent.getAttribute('data-path'),
       path: mainContent.getAttribute('data-path'),
+      tocHtml: '',
       isLiked: false,
       isLiked: false,
       seenUserIds: [],
       seenUserIds: [],
       likerUserIds: [],
       likerUserIds: [],
@@ -55,6 +56,7 @@ export default class PageContainer extends Container {
     this.initStateMarkdown();
     this.initStateMarkdown();
     this.initStateOthers();
     this.initStateOthers();
 
 
+    this.setTocHtml = this.setTocHtml.bind(this);
     this.save = this.save.bind(this);
     this.save = this.save.bind(this);
     this.addWebSocketEventHandlers = this.addWebSocketEventHandlers.bind(this);
     this.addWebSocketEventHandlers = this.addWebSocketEventHandlers.bind(this);
     this.addWebSocketEventHandlers();
     this.addWebSocketEventHandlers();
@@ -110,6 +112,11 @@ export default class PageContainer extends Container {
     });
     });
   }
   }
 
 
+  setTocHtml(tocHtml) {
+    if (this.state.tocHtml !== tocHtml) {
+      this.setState({ tocHtml });
+    }
+  }
 
 
   /**
   /**
    * save success handler
    * save success handler

+ 2 - 2
src/client/js/util/GrowiRenderer.js

@@ -74,11 +74,11 @@ export default class GrowiRenderer {
     // add configurers according to mode
     // add configurers according to mode
     switch (mode) {
     switch (mode) {
       case 'page': {
       case 'page': {
-        const renderToc = appContainer.getCrowiForJquery().renderTocContent;
+        const pageContainer = appContainer.getContainer('PageContainer');
 
 
         this.markdownItConfigurers = this.markdownItConfigurers.concat([
         this.markdownItConfigurers = this.markdownItConfigurers.concat([
           new FooternoteConfigurer(appContainer),
           new FooternoteConfigurer(appContainer),
-          new TocAndAnchorConfigurer(appContainer, renderToc),
+          new TocAndAnchorConfigurer(appContainer, pageContainer.setTocHtml),
           new HeaderLineNumberConfigurer(appContainer),
           new HeaderLineNumberConfigurer(appContainer),
           new HeaderWithEditLinkConfigurer(appContainer),
           new HeaderWithEditLinkConfigurer(appContainer),
           new TableWithHandsontableButtonConfigurer(appContainer),
           new TableWithHandsontableButtonConfigurer(appContainer),

+ 4 - 4
src/client/js/util/markdown-it/toc-and-anchor.js

@@ -1,8 +1,8 @@
 export default class TocAndAnchorConfigurer {
 export default class TocAndAnchorConfigurer {
 
 
-  constructor(crowi, renderToc) {
+  constructor(crowi, setHtml) {
     this.crowi = crowi;
     this.crowi = crowi;
-    this.renderToc = renderToc;
+    this.setHtml = setHtml;
   }
   }
 
 
   configure(md) {
   configure(md) {
@@ -15,10 +15,10 @@ export default class TocAndAnchorConfigurer {
     });
     });
 
 
     // set toc render function
     // set toc render function
-    if (this.renderToc != null) {
+    if (this.setHtml != null) {
       md.set({
       md.set({
         tocCallback: (tocMarkdown, tocArray, tocHtml) => {
         tocCallback: (tocMarkdown, tocArray, tocHtml) => {
-          this.renderToc(tocHtml);
+          this.setHtml(tocHtml);
         },
         },
       });
       });
     }
     }