From 4ff2fe280ebd41c4b491936fdd4ae75b7805ed61 Mon Sep 17 00:00:00 2001
From: Elian Ibaj <elianibaj@gmail.com>
Date: Sun, 4 Mar 2018 06:23:59 +0100
Subject: [PATCH] Add separate on-page navigation sidebar (#475)

---
 lib/core/DocsLayout.js    |  8 ++++-
 lib/core/Site.js          |  7 +++-
 lib/core/getTOC.js        |  7 ++++
 lib/core/nav/OnPageNav.js | 40 +++++++++++++++++++++++
 lib/static/css/main.css   | 67 +++++++++++++++++++++++++++++++++++++++
 5 files changed, 127 insertions(+), 2 deletions(-)
 create mode 100644 lib/core/nav/OnPageNav.js

diff --git a/lib/core/DocsLayout.js b/lib/core/DocsLayout.js
index 0c6633ed3551..9ef37bf73df4 100644
--- a/lib/core/DocsLayout.js
+++ b/lib/core/DocsLayout.js
@@ -9,6 +9,7 @@ const React = require('react');
 const Container = require('./Container.js');
 const Doc = require('./Doc.js');
 const DocsSidebar = require('./DocsSidebar.js');
+const OnPageNav = require('./nav/OnPageNav.js');
 const Site = require('./Site.js');
 const translation = require('../server/translation.js');
 
@@ -25,7 +26,7 @@ class DocsLayout extends React.Component {
     return (
       <Site
         config={this.props.config}
-        className="sideNavVisible"
+        className="sideNavVisible doc"
         title={
           i18n
             ? translation[this.props.metadata.language]['localized-strings'][
@@ -90,6 +91,11 @@ class DocsLayout extends React.Component {
               )}
             </div>
           </Container>
+          {this.props.config.onPageNav == 'separate' && (
+            <nav className="onPageNav">
+              <OnPageNav rawContent={this.props.children} />
+            </nav>
+          )}
         </div>
       </Site>
     );
diff --git a/lib/core/Site.js b/lib/core/Site.js
index 6ac9a235b286..6506e0ed74a5 100644
--- a/lib/core/Site.js
+++ b/lib/core/Site.js
@@ -11,6 +11,7 @@ const HeaderNav = require('./nav/HeaderNav.js');
 const Head = require('./Head.js');
 const Footer = require(process.cwd() + '/core/Footer.js');
 const translation = require('../server/translation.js');
+const classNames = require('classnames');
 
 const CWD = process.cwd();
 
@@ -63,7 +64,11 @@ class Site extends React.Component {
           title={title}
           url={url}
         />
-        <body className={this.props.className}>
+        <body
+          className={classNames({
+            [this.props.className]: true,
+            separateOnPageNav: this.props.config.onPageNav == 'separate',
+          })}>
           <HeaderNav
             config={this.props.config}
             baseUrl={this.props.config.baseUrl}
diff --git a/lib/core/getTOC.js b/lib/core/getTOC.js
index 182d88058241..3d99813ccab7 100644
--- a/lib/core/getTOC.js
+++ b/lib/core/getTOC.js
@@ -1,3 +1,10 @@
+/**
+ * Copyright (c) 2017-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
 const Remarkable = require('remarkable');
 const toSlug = require('./toSlug');
 
diff --git a/lib/core/nav/OnPageNav.js b/lib/core/nav/OnPageNav.js
new file mode 100644
index 000000000000..1afd3079f468
--- /dev/null
+++ b/lib/core/nav/OnPageNav.js
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2017-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+const React = require('react');
+
+const siteConfig = require(process.cwd() + '/siteConfig.js');
+const getTOC = require('../getTOC');
+
+const Link = ({hashLink, text}) => <a href={`#${hashLink}`}>{text}</a>;
+
+const Headings = ({headings}) => {
+  if (!headings.length) return null;
+  return (
+    <ul className="toc-headings">
+      {headings.map((heading, i) => (
+        <li key={i}>
+          <Link hashLink={heading.hashLink} text={heading.text} />
+          <Headings headings={heading.children} />
+        </li>
+      ))}
+    </ul>
+  );
+};
+
+class OnPageNav extends React.Component {
+  render() {
+    const customTags = siteConfig.onPageNavHeadings;
+    const headings = customTags
+      ? getTOC(this.props.rawContent, customTags.topLevel, customTags.sub)
+      : getTOC(this.props.rawContent);
+
+    return <Headings headings={headings} />;
+  }
+}
+
+module.exports = OnPageNav;
diff --git a/lib/static/css/main.css b/lib/static/css/main.css
index 5bc67e47c375..3dd560e23300 100644
--- a/lib/static/css/main.css
+++ b/lib/static/css/main.css
@@ -1494,6 +1494,73 @@ nav.toc .toggleNav .navBreadcrumb h2 {
     padding: 0 10px;
   }
 }
+
+.onPageNav {
+  display: none;
+}
+.onPageNav::-webkit-scrollbar {
+  display: none;
+}
+
+@supports(position: sticky) {
+  @media only screen and (min-width: 1024px) {
+    .separateOnPageNav.doc .wrapper,
+    .separateOnPageNav .headerWrapper.wrapper {
+      max-width: 1400px;
+    }
+
+    .doc.separateOnPageNav .docsNavContainer {
+      flex: 0 0 240px;
+      margin: 40px 0 0;
+    }
+
+    .doc.separateOnPageNav nav.toc:last-child {
+      padding-bottom: 0;
+      width: auto;
+    }
+
+    .doc.separateOnPageNav.sideNavVisible .navPusher .mainContainer {
+      max-width: 100%;
+      flex: 1 auto;
+      padding: 0 40px 50px;
+      min-width: 0;
+    }
+
+    .onPageNav {
+      display: block;
+      position: -webkit-sticky;
+      position: sticky;
+      top: 110px;
+      flex: 0 0 240px;
+      overflow-y: auto;
+      max-height: calc(100vh - 110px);
+    }
+
+    .onPageNav > ul {
+      border-left: 1px solid #e0e0e0;
+      padding: 10px 0 2px 15px;
+    }
+
+    .onPageNav a {
+      color: #717171;
+    }
+
+    .onPageNav ul li {
+      font-size: 12px;
+      line-height: 14px;
+      padding-bottom: 12px;
+    }
+
+    .onPageNav ul ul {
+      padding: 8px 0 0 20px;
+    }
+
+    .onPageNav ul ul li {
+      padding-bottom: 8px;
+    }
+  }
+}
+
 table {
   background: #F8F8F8;
   border: 1px solid #B0B0B0;