Skip to content

Commit 53e2f23

Browse files
authored
Static table of contents (#701)
* Add showing/hiding submenus, fix sidebar styling, fix bug where includes wouldn't appear in ToC * Update ToC to highlight last header if page is scrolled to very bottom, fixes #280 * Set HTML title to current h1 section text, see #133 * Fix menu not opening on mobile * Add back increase toc item height on mobile * Fix padding bug * Add back in ToC sliding animation
1 parent e7f5144 commit 53e2f23

File tree

13 files changed

+202
-1697
lines changed

13 files changed

+202
-1697
lines changed

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ gem 'middleman-autoprefixer', '~> 2.7.0'
77
gem "middleman-sprockets", "~> 4.1.0"
88
gem 'rouge', '~> 2.0.5'
99
gem 'redcarpet', '~> 3.4.0'
10+
gem 'nokogiri', '~> 1.6.8'

Gemfile.lock

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,10 @@ GEM
7979
middleman-syntax (3.0.0)
8080
middleman-core (>= 3.2)
8181
rouge (~> 2.0)
82+
mini_portile2 (2.1.0)
8283
minitest (5.10.1)
84+
nokogiri (1.6.8.1)
85+
mini_portile2 (~> 2.1.0)
8386
padrino-helpers (0.13.3.3)
8487
i18n (~> 0.6, >= 0.6.7)
8588
padrino-support (= 0.13.3.3)
@@ -115,8 +118,9 @@ DEPENDENCIES
115118
middleman-autoprefixer (~> 2.7.0)
116119
middleman-sprockets (~> 4.1.0)
117120
middleman-syntax (~> 3.0.0)
121+
nokogiri (~> 1.6.8)
118122
redcarpet (~> 3.4.0)
119123
rouge (~> 2.0.5)
120124

121125
BUNDLED WITH
122-
1.14.3
126+
1.14.5

config.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,7 @@
4747
# Deploy Configuration
4848
# If you want Middleman to listen on a different port, you can set that below
4949
set :port, 4567
50+
51+
helpers do
52+
require './lib/toc_data.rb'
53+
end

lib/toc_data.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
require 'nokogiri'
2+
3+
def toc_data(page_content)
4+
html_doc = Nokogiri::HTML::DocumentFragment.parse(page_content)
5+
6+
# get a flat list of headers
7+
headers = []
8+
html_doc.css('h1, h2, h3').each do |header|
9+
headers.push({
10+
id: header.attribute('id').to_s,
11+
content: header.content,
12+
level: header.name[1].to_i,
13+
children: []
14+
})
15+
end
16+
17+
[3,2].each do |header_level|
18+
header_to_nest = nil
19+
headers = headers.reject do |header|
20+
if header[:level] == header_level
21+
header_to_nest[:children].push header if header_to_nest
22+
true
23+
else
24+
header_to_nest = header if header[:level] == (header_level - 1)
25+
false
26+
end
27+
end
28+
end
29+
headers
30+
end

source/javascripts/all.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,2 @@
1-
//= require ./lib/_energize
2-
//= require ./app/_lang
1+
//= require ./all_nosearch
32
//= require ./app/_search
4-
//= require ./app/_toc

source/javascripts/all_nosearch.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
11
//= require ./lib/_energize
2-
//= require ./app/_lang
32
//= require ./app/_toc
3+
//= require ./app/_lang
4+
5+
$(function() {
6+
loadToc($('#toc'), '.toc-link', '.toc-list-h2', 10);
7+
setupLanguages($('body').data('languages'));
8+
$('.content').imagesLoaded( function() {
9+
window.recacheHeights();
10+
window.refreshToc();
11+
});
12+
});
13+
14+
window.onpopstate = function() {
15+
activateLanguage(getLanguageFromQueryString());
16+
};

source/javascripts/app/_lang.js

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1515
License for the specific language governing permissions and limitations
1616
under the License.
1717
*/
18-
(function (global) {
18+
;(function () {
1919
'use strict';
2020

2121
var languages = [];
2222

23-
global.setupLanguages = setupLanguages;
24-
global.activateLanguage = activateLanguage;
23+
window.setupLanguages = setupLanguages;
24+
window.activateLanguage = activateLanguage;
25+
window.getLanguageFromQueryString = getLanguageFromQueryString;
2526

2627
function activateLanguage(language) {
2728
if (!language) return;
@@ -36,7 +37,7 @@ under the License.
3637
$(".highlight.tab-" + language).show();
3738
$(".lang-specific." + language).show();
3839

39-
global.toc.calculateHeights();
40+
window.recacheHeights();
4041

4142
// scroll to the new location of the position
4243
if ($(window.location.hash).get(0)) {
@@ -159,8 +160,5 @@ under the License.
159160
activateLanguage(language);
160161
return false;
161162
});
162-
window.onpopstate = function() {
163-
activateLanguage(getLanguageFromQueryString());
164-
};
165163
});
166-
})(window);
164+
})();

source/javascripts/app/_search.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//= require ../lib/_lunr
22
//= require ../lib/_jquery
33
//= require ../lib/_jquery.highlight
4-
(function () {
4+
;(function () {
55
'use strict';
66

77
var content, searchResults;

source/javascripts/app/_toc.js

Lines changed: 90 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,106 @@
11
//= require ../lib/_jquery
2-
//= require ../lib/_jquery_ui
3-
//= require ../lib/_jquery.tocify
42
//= require ../lib/_imagesloaded.min
5-
(function (global) {
3+
;(function () {
64
'use strict';
75

6+
var debounce = function(func, waitTime) {
7+
var timeout = false;
8+
return function() {
9+
if (timeout === false) {
10+
setTimeout(function() {
11+
func();
12+
timeout = false;
13+
}, waitTime);
14+
timeout = true;
15+
}
16+
};
17+
};
18+
819
var closeToc = function() {
920
$(".tocify-wrapper").removeClass('open');
1021
$("#nav-button").removeClass('open');
1122
};
1223

13-
var makeToc = function() {
14-
global.toc = $("#toc").tocify({
15-
selectors: 'h1, h2',
16-
extendPage: false,
17-
theme: 'none',
18-
smoothScroll: false,
19-
showEffectSpeed: 0,
20-
hideEffectSpeed: 180,
21-
ignoreSelector: '.toc-ignore',
22-
highlightOffset: 60,
23-
scrollTo: -1,
24-
scrollHistory: true,
25-
hashGenerator: function (text, element) {
26-
return element.prop('id');
24+
function loadToc($toc, tocLinkSelector, tocListSelector, scrollOffset) {
25+
var headerHeights = {};
26+
var pageHeight = 0;
27+
var windowHeight = 0;
28+
var originalTitle = document.title;
29+
30+
var recacheHeights = function() {
31+
headerHeights = {};
32+
pageHeight = $(document).height();
33+
windowHeight = $(window).height();
34+
35+
$toc.find(tocLinkSelector).each(function() {
36+
var targetId = $(this).attr('href');
37+
if (targetId[0] === "#") {
38+
headerHeights[targetId] = $(targetId).offset().top;
39+
}
40+
});
41+
};
42+
43+
var refreshToc = function() {
44+
var currentTop = $(document).scrollTop() + scrollOffset;
45+
46+
if (currentTop + windowHeight >= pageHeight) {
47+
// at bottom of page, so just select last header by making currentTop very large
48+
// this fixes the problem where the last header won't ever show as active if its content
49+
// is shorter than the window height
50+
currentTop = pageHeight + 1000;
51+
}
52+
53+
var best = null;
54+
for (var name in headerHeights) {
55+
if ((headerHeights[name] < currentTop && headerHeights[name] > headerHeights[best]) || best === null) {
56+
best = name;
57+
}
2758
}
28-
}).data('toc-tocify');
2959

30-
$("#nav-button").click(function() {
31-
$(".tocify-wrapper").toggleClass('open');
32-
$("#nav-button").toggleClass('open');
33-
return false;
34-
});
60+
var $best = $toc.find("[href='" + best + "']").first();
61+
if (!$best.hasClass("active")) {
62+
$toc.find(".active").removeClass("active");
63+
$best.addClass("active");
64+
$best.parents(tocListSelector).addClass("active");
65+
$best.siblings(tocListSelector).addClass("active");
66+
$toc.find(tocListSelector).filter(":not(.active)").slideUp(150);
67+
$toc.find(tocListSelector).filter(".active").slideDown(150);
68+
if (window.history.pushState) {
69+
window.history.pushState(null, "", best);
70+
}
71+
// TODO remove classnames
72+
document.title = $best.data("title") + " – " + originalTitle;
73+
}
74+
};
3575

36-
$(".page-wrapper").click(closeToc);
37-
$(".tocify-item").click(closeToc);
38-
};
76+
var makeToc = function() {
77+
recacheHeights();
78+
refreshToc();
3979

40-
// Hack to make already open sections to start opened,
41-
// instead of displaying an ugly animation
42-
function animate() {
43-
setTimeout(function() {
44-
toc.setOption('showEffectSpeed', 180);
45-
}, 50);
46-
}
80+
$("#nav-button").click(function() {
81+
$(".toc-wrapper").toggleClass('open');
82+
$("#nav-button").toggleClass('open');
83+
return false;
84+
});
85+
$(".page-wrapper").click(closeToc);
86+
$(".tocify-item").click(closeToc);
87+
88+
// reload immediately after scrolling on toc click
89+
$toc.find(tocLinkSelector).click(function() {
90+
setTimeout(function() {
91+
refreshToc();
92+
}, 0);
93+
});
94+
95+
$(window).scroll(debounce(refreshToc, 200));
96+
$(window).resize(debounce(recacheHeights, 200));
97+
};
4798

48-
$(function() {
4999
makeToc();
50-
animate();
51-
setupLanguages($('body').data('languages'));
52-
$('.content').imagesLoaded( function() {
53-
global.toc.calculateHeights();
54-
});
55-
});
56-
})(window);
57100

101+
window.recacheHeights = recacheHeights;
102+
window.refreshToc = refreshToc;
103+
}
104+
105+
window.loadToc = loadToc;
106+
})();

0 commit comments

Comments
 (0)