forked from afeld/bootstrap-toc
-
Notifications
You must be signed in to change notification settings - Fork 0
/
bootstrap-toc.js
180 lines (152 loc) · 6.08 KB
/
bootstrap-toc.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
/*!
* Bootstrap Table of Contents v<%= version %> (http://afeld.github.io/bootstrap-toc/)
* Copyright 2015 Aidan Feldman
* Licensed under MIT (https://github.com/afeld/bootstrap-toc/blob/gh-pages/LICENSE.md) */
(function($) {
"use strict";
window.Toc = {
helpers: {
// return all matching elements in the set, or their descendants
findOrFilter: function($el, selector) {
// http://danielnouri.org/notes/2011/03/14/a-jquery-find-that-also-finds-the-root-element/
// http://stackoverflow.com/a/12731439/358804
var $descendants = $el.find(selector);
return $el
.filter(selector)
.add($descendants)
.filter(":not([data-toc-skip])");
},
generateUniqueIdBase: function(el) {
var text = $(el).text();
// adapted from
// https://github.com/bryanbraun/anchorjs/blob/65fede08d0e4a705f72f1e7e6284f643d5ad3cf3/anchor.js#L237-L257
// Regex for finding the non-safe URL characters (many need escaping): & +$,:;=?@"#{}|^~[`%!'<>]./()*\ (newlines, tabs, backspace, & vertical tabs)
var nonsafeChars = /[& +$,:;=?@"#{}|^~[`%!'<>\]\.\/\(\)\*\\\n\t\b\v]/g,
urlText;
// Note: we trim hyphens after truncating because truncating can cause dangling hyphens.
// Example string: // " ⚡⚡ Don't forget: URL fragments should be i18n-friendly, hyphenated, short, and clean."
urlText = text
.trim() // "⚡⚡ Don't forget: URL fragments should be i18n-friendly, hyphenated, short, and clean."
.replace(/\'/gi, "") // "⚡⚡ Dont forget: URL fragments should be i18n-friendly, hyphenated, short, and clean."
.replace(nonsafeChars, "-") // "⚡⚡-Dont-forget--URL-fragments-should-be-i18n-friendly--hyphenated--short--and-clean-"
.replace(/-{2,}/g, "-") // "⚡⚡-Dont-forget-URL-fragments-should-be-i18n-friendly-hyphenated-short-and-clean-"
.substring(0, 64) // "⚡⚡-Dont-forget-URL-fragments-should-be-i18n-friendly-hyphenated-"
.replace(/^-+|-+$/gm, "") // "⚡⚡-Dont-forget-URL-fragments-should-be-i18n-friendly-hyphenated"
.toLowerCase(); // "⚡⚡-dont-forget-url-fragments-should-be-i18n-friendly-hyphenated"
return urlText || el.tagName.toLowerCase();
},
generateUniqueId: function(el) {
var anchorBase = this.generateUniqueIdBase(el);
for (var i = 0; ; i++) {
var anchor = anchorBase;
if (i > 0) {
// add suffix
anchor += "-" + i;
}
// check if ID already exists
if (!document.getElementById(anchor)) {
return anchor;
}
}
},
generateAnchor: function(el) {
if (el.id) {
return el.id;
} else {
var anchor = this.generateUniqueId(el);
el.id = anchor;
return anchor;
}
},
createNavList: function() {
return $('<ul class="nav navbar-nav"></ul>');
},
createChildNavList: function($parent) {
var $childList = this.createNavList();
$parent.append($childList);
return $childList;
},
generateNavEl: function(anchor, text) {
var $a = $('<a class="nav-link"></a>');
$a.attr("href", "#" + anchor);
$a.text(text);
var $li = $("<li></li>");
$li.append($a);
return $li;
},
generateNavItem: function(headingEl) {
var anchor = this.generateAnchor(headingEl);
var $heading = $(headingEl);
var text = $heading.data("toc-text") || $heading.text();
return this.generateNavEl(anchor, text);
},
// Find the first heading level (`<h1>`, then `<h2>`, etc.) that has more than one element. Defaults to 1 (for `<h1>`).
getTopLevel: function($scope) {
for (var i = 1; i <= 6; i++) {
var $headings = this.findOrFilter($scope, "h" + i);
if ($headings.length > 1) {
return i;
}
}
return 1;
},
// returns the elements for the top level, and the next below it
getHeadings: function($scope, topLevel) {
var topSelector = "h" + topLevel;
var secondaryLevel = topLevel + 1;
var secondarySelector = "h" + secondaryLevel;
return this.findOrFilter($scope, topSelector + "," + secondarySelector);
},
getNavLevel: function(el) {
return parseInt(el.tagName.charAt(1), 10);
},
populateNav: function($topContext, topLevel, $headings) {
var $context = $topContext;
var $prevNav;
var helpers = this;
$headings.each(function(i, el) {
var $newNav = helpers.generateNavItem(el);
var navLevel = helpers.getNavLevel(el);
// determine the proper $context
if (navLevel === topLevel) {
// use top level
$context = $topContext;
} else if ($prevNav && $context === $topContext) {
// create a new level of the tree and switch to it
$context = helpers.createChildNavList($prevNav);
} // else use the current $context
$context.append($newNav);
$prevNav = $newNav;
});
},
parseOps: function(arg) {
var opts;
if (arg.jquery) {
opts = {
$nav: arg
};
} else {
opts = arg;
}
opts.$scope = opts.$scope || $(document.body);
return opts;
}
},
// accepts a jQuery object, or an options object
init: function(opts) {
opts = this.helpers.parseOps(opts);
// ensure that the data attribute is in place for styling
opts.$nav.attr("data-toggle", "toc");
var $topContext = this.helpers.createChildNavList(opts.$nav);
var topLevel = this.helpers.getTopLevel(opts.$scope);
var $headings = this.helpers.getHeadings(opts.$scope, topLevel);
this.helpers.populateNav($topContext, topLevel, $headings);
}
};
$(function() {
$('nav[data-toggle="toc"]').each(function(i, el) {
var $nav = $(el);
Toc.init($nav);
});
});
})(jQuery);