diff --git a/README.md b/README.md
index c376347..a5afc14 100644
--- a/README.md
+++ b/README.md
@@ -78,6 +78,7 @@ Build a release zip:
- [Parsedown](https://github.com/erusev/parsedown/) - PHP markdown parser.
- [prism](http://prismjs.com/) - JavaScript syntax highlighter.
- [CodeMirror](https://codemirror.net/) - JavaScript in-browser code editor.
+- [Tocbot](http://tscanlin.github.io/tocbot/) - JavaScript table of contents generator.
- [Vanilla JS](http://vanilla-js.com/) - No jQuery. Instead standard DOM API in order to make things fast and slim.
diff --git a/src/client/js/app-view.js b/src/client/js/app-view.js
index 4777464..284a22e 100644
--- a/src/client/js/app-view.js
+++ b/src/client/js/app-view.js
@@ -1,4 +1,4 @@
-(function(document, slimwiki, Prism) {
+(function(document, slimwiki, Prism, tocbot) {
slimwiki.View = {
updateSyntaxHighlighting: updateSyntaxHighlighting
@@ -15,10 +15,40 @@
Prism.plugins.autoloader.languages_path = 'client/libs/prism/components/';
if (mode == 'view' || mode == 'edit') {
+ initToc();
updateSyntaxHighlighting();
}
}
+ function initToc() {
+ var headingSelector = 'h1, h2, h3',
+ nextId = 1,
+ headingsOffset = 80,
+ headings;
+
+ // tocbot needs ID attributes at the headings in order to function
+ headings = document.getElementById('content').querySelectorAll(headingSelector);
+ Array.prototype.forEach.call(headings, function (headingElem) {
+ headingElem.id = 'heading-' + (nextId++);
+ });
+
+ tocbot.init({
+ // Where to render the table of contents.
+ tocSelector: '.toc-wrapper',
+ // Where to grab the headings to build the table of contents.
+ contentSelector: '#content',
+ // Which headings to grab inside of the contentSelector element.
+ headingSelector: headingSelector,
+ // Headings offset between the headings and the top of the document.
+ headingsOffset: headingsOffset,
+ // smooth-scroll options object, see docs at:
+ // https://github.com/cferdinandi/smooth-scroll
+ smoothScrollOptions: {
+ offset: headingsOffset
+ }
+ });
+ }
+
function updateSyntaxHighlighting(parentElem) {
if (! parentElem) {
parentElem = document.getElementById('content');
@@ -30,4 +60,4 @@
});
}
-})(document, slimwiki, Prism);
+})(document, slimwiki, Prism, tocbot);
diff --git a/src/server/layout/page.php b/src/server/layout/page.php
index aeeefe6..a488854 100644
--- a/src/server/layout/page.php
+++ b/src/server/layout/page.php
@@ -32,6 +32,7 @@ include($themeDir . '/init-theme.php');
+
@@ -179,6 +180,7 @@ if ($mode == 'edit') {
+
diff --git a/src/server/theme/slim/less/_layout.less b/src/server/theme/slim/less/_layout.less
index 15efd29..366abbd 100644
--- a/src/server/theme/slim/less/_layout.less
+++ b/src/server/theme/slim/less/_layout.less
@@ -1,10 +1,20 @@
@contentMaxWidth: 800px;
@contentMinMarginX: 30px;
+@tocMinWidth: 200px;
+@tocMarginLeft: @contentMinMarginX;
-// We need a variable using brackets here, otherwise less won't calculate the width
-// See: https://github.com/less/less.js/issues/1903
-@small-screen-breakpoint: (@contentMaxWidth + 2 * @contentMinMarginX);
+// Screen sizes:
+// - small: Content takes full width, TOC is hidden
+// - medium: Content is left, TOC is right or hidden (if width < @screen-size-toc-hidden-max)
+// - large: Content is centered, TOC is right
+//
+// NOTE: We need a variable using brackets here, otherwise less won't calculate the width
+// See: https://github.com/less/less.js/issues/1903
+@screen-size-small-max: (@contentMaxWidth + 2 * @contentMinMarginX);
+@screen-size-toc-hidden-max: (@screen-size-small-max + @tocMinWidth);
+@screen-size-medium-min: (@screen-size-small-max + 1px);
+@screen-size-medium-max: (@contentMaxWidth + 2 * @tocMinWidth);
.main-column {
width: @contentMaxWidth;
@@ -15,7 +25,11 @@
margin: 0 @contentMinMarginX;
}
- @media (max-width: @small-screen-breakpoint) {
+ @media (min-width: @screen-size-medium-min) and (max-width: @screen-size-medium-max) {
+ width: @contentMaxWidth;
+ margin: 0 @contentMinMarginX;
+ }
+ @media (max-width: @screen-size-small-max) {
width: auto;
margin: 0 @contentMinMarginX;
}
@@ -32,6 +46,26 @@
}
}
+.toc-wrapper {
+ position: fixed;
+ left: 50%;
+ right: 10px;
+ top: 70px;
+ margin-left: @contentMaxWidth / 2 + @tocMarginLeft;
+
+ .mode-edit & {
+ display: none;
+ }
+
+ @media (min-width: @screen-size-medium-min) and (max-width: @screen-size-medium-max) {
+ left: @contentMinMarginX + @contentMaxWidth + @tocMarginLeft;
+ margin-left: 0;
+ }
+ @media (max-width: @screen-size-toc-hidden-max) {
+ display: none;
+ }
+}
+
#content {
padding: 30px 0 150px;
}
diff --git a/src/server/theme/slim/less/_view.less b/src/server/theme/slim/less/_view.less
index 1403c2f..1e184c3 100644
--- a/src/server/theme/slim/less/_view.less
+++ b/src/server/theme/slim/less/_view.less
@@ -26,6 +26,31 @@ body {
}
}
+.toc-wrapper {
+ ul {
+ list-style: none;
+ }
+ .toc-link {
+ display: block;
+ padding: 5px 0;
+ line-height: 1;
+ text-decoration: none;
+ cursor: pointer;
+
+ &::before {
+ margin-top: -5px;
+ }
+ }
+ .is-active-link {
+ font-weight: normal;
+ color: @brand-primary;
+
+ &::before {
+ background-color: @brand-primary;
+ }
+ }
+}
+
footer {
height: 80px;
padding-top: 30px;
diff --git a/src/server/theme/slim/page-header.php b/src/server/theme/slim/page-header.php
index 8dae564..4695ac0 100644
--- a/src/server/theme/slim/page-header.php
+++ b/src/server/theme/slim/page-header.php
@@ -19,3 +19,4 @@ foreach ($data['breadcrumbs'] as $item) {
$isFirst = false;
}
?>
+