bit-docs-html-toc
Advanced tools
Comparing version 1.0.0 to 1.0.1
@@ -66,3 +66,4 @@ module.exports = function makeTree(elements) { | ||
var text = element.textContent; | ||
var id = makeHeadingId(text); | ||
var id = element.id || makeHeadingId(text); | ||
element.id = id; | ||
var level = getElementLevel(element); | ||
@@ -69,0 +70,0 @@ |
{ | ||
"name": "bit-docs-html-toc", | ||
"version": "1.0.0", | ||
"version": "1.0.1", | ||
"description": "table of contents bit-docs plugin", | ||
@@ -30,2 +30,5 @@ "main": "toc.js", | ||
"can-assign": "^1.3.1", | ||
"can-define-lazy-value": "^1.1.0", | ||
"can-dom-mutate": "^1.3.6", | ||
"can-string": "^1.0.0", | ||
"can-view-target": "^4.1.2" | ||
@@ -32,0 +35,0 @@ }, |
@@ -32,9 +32,15 @@ # bit-docs-html-toc | ||
In your template add a class **on-this-page-container**: | ||
In your template add a `<bit-toc>` element: | ||
```html | ||
<div class="on-this-page-container"></div> | ||
<bit-toc></bit-toc> | ||
``` | ||
By default, all heading tags children of the first `article` tag on the page will | ||
## Attributes | ||
`<bit-toc>` supports the following attributes: | ||
### heading-container-selector | ||
By default, all heading tags children of the first `article` tag on the page will | ||
be collected to create the table of contents; if you want to use a different element | ||
@@ -44,7 +50,6 @@ just do: | ||
```html | ||
<div | ||
class="on-this-page-container" | ||
data-heading-container-selector="#my-custom-selector" | ||
<bit-toc | ||
heading-container-selector="#my-custom-selector" | ||
> | ||
</div> | ||
</bit-toc> | ||
``` | ||
@@ -54,4 +59,12 @@ | ||
### depth | ||
To control the number of child headers that will be included in the TOC, use the | ||
By default, only `h2` elements are collected. You | ||
can change to include `<h3>` elements by setting depth like: | ||
```html | ||
<bit-toc depth="2"></bit-toc> | ||
``` | ||
Alternatively, the number of child headers that will be included in the TOC, use the | ||
`@outline` tag like so: | ||
@@ -63,2 +76,21 @@ | ||
This will include `<h2>` and `<h3>` elements, rather than the normal `<h2>` only. | ||
### child-tag | ||
If you want `<li>`'s to be within an `<ol>` instead of a `<ul>`, this | ||
can be configured like: | ||
```html | ||
<bit-toc child-tag="ol"></bit-toc> | ||
``` | ||
## Methods | ||
Call `.highlight()` to force an update of the `active` or `completed` | ||
class names on the `<li>` elements: | ||
```js | ||
document.querySelector("bit-toc").highlight() | ||
``` | ||
This happens automatically when the `heading-container-selector` | ||
element is scrolled. |
@@ -36,2 +36,3 @@ var $ = require("jquery"); | ||
var $items = $("#toc-container ul"); | ||
$("#toc-container li").removeAttr("class"); | ||
assert.equal( | ||
@@ -46,3 +47,8 @@ $items.html(), | ||
); | ||
var ids = $.makeArray($("article h2").map(function(i, element) { | ||
return element.id; | ||
})); | ||
assert.deepEqual(ids, ["usage","install","configure"]); | ||
// remove headings container from the DOM | ||
@@ -52,37 +58,2 @@ $("article").remove(); | ||
it("reads the headings container selector from the element", function() { | ||
// set the container selector as a data-* attribute | ||
$el.attr( | ||
"data-headings-container-selector", | ||
".my-custom-container" | ||
); | ||
var headings = [ | ||
'<div class="my-custom-container">', | ||
"<h2>Usage</h2>", | ||
"<h2>Install</h2>", | ||
"<h2>Configure</h2>", | ||
"<h2>Configure</h2>", | ||
"</div>" | ||
]; | ||
// append the headings to the DOM and then instantiate the control | ||
$("body").append(headings.join("")); | ||
new TOCContainer($el.get(0)); | ||
var $items = $("#toc-container ul"); | ||
assert.equal( | ||
$items.html(), | ||
[ | ||
'<li><a href="#usage">Usage</a></li>', | ||
'<li><a href="#install">Install</a></li>', | ||
'<li><a href="#configure">Configure</a></li>', | ||
'<li><a href="#configure-1">Configure</a></li>' | ||
].join(""), | ||
"should create table of contents from the headings inside container" | ||
); | ||
// remove headings container from the DOM | ||
$(".my-custom-container").remove(); | ||
}); | ||
}); |
var $ = require("jquery"); | ||
var assert = require("chai/chai").assert; | ||
var TableOfContents = require("../toc-control"); | ||
var safeCustomElement = require("../safe-custom-element"); | ||
@@ -8,14 +9,15 @@ require("steal-mocha"); | ||
describe("TableOfContents", function() { | ||
if(!safeCustomElement.supported) { | ||
return; | ||
} | ||
var $el, $testArea; | ||
beforeEach(function() { | ||
$("body").append("<ul id=\"toc-test\"></ul>"); | ||
$testArea = $("#test-area"); | ||
$el = $("#toc-test"); | ||
$testArea = $("#test-area"); | ||
}); | ||
afterEach(function() { | ||
$el.remove(); | ||
$testArea.empty(); | ||
}); | ||
@@ -31,11 +33,12 @@ | ||
$testArea.html(headings.join("")); | ||
$testArea.html("<article id='article'>"+headings.join("")+"</article>"+ | ||
"<bit-toc child-tag='ul' headings-container-selector='#article' depth='1'>"); | ||
new TableOfContents($el.get(0), { | ||
/*new TableOfContents($el.get(0), { | ||
tagName: "ul", | ||
depth: 1, | ||
headingsContainerSelector: "#test-area" | ||
}); | ||
assert.equal($el.html(), [ | ||
});*/ | ||
$('bit-toc li').removeAttr("class"); // remove for nice html | ||
assert.equal(document.querySelector('bit-toc ul').innerHTML, [ | ||
'<li><a href="#usage">Usage</a></li>', | ||
@@ -58,10 +61,8 @@ '<li><a href="#install">Install</a></li>', | ||
$testArea.html(headings.join("")); | ||
new TableOfContents($el.get(0), { | ||
tagName: "ul", | ||
depth: 3, | ||
headingsContainerSelector: "#test-area" | ||
}); | ||
$testArea.html("<article id='article'>"+headings.join("")+"</article>"+ | ||
"<bit-toc child-tag='ul' headings-container-selector='#article' depth='3'>"); | ||
var $el = $('bit-toc>ul'); | ||
assert.equal($el.find(">li:eq(0) ul").length, 1, "bower has a nested list"); | ||
@@ -82,10 +83,9 @@ assert.equal($el.find(">li:eq(1) ul").length, 2, "npm two nested lists"); | ||
$testArea.html(headings.join("")); | ||
$testArea.html("<article id='article'>"+headings.join("")+"</article>"+ | ||
"<bit-toc child-tag='ul' headings-container-selector='#article' depth='1'>"); | ||
new TableOfContents($el.get(0), { | ||
tagName: "ul", | ||
depth: 1, | ||
headingsContainerSelector: "#test-area" | ||
}); | ||
var $el = $('bit-toc>ul'); | ||
$('bit-toc li').removeAttr("class"); // remove for nice html | ||
assert.equal($el.html(), [ | ||
@@ -97,2 +97,64 @@ '<li><a href="#bower">Bower</a></li>', | ||
}); | ||
it("highlights what has been completed", function(){ | ||
var headings = [ | ||
"<h2>Bower</h2>", | ||
"<p>Install</p>", | ||
"<h2>NPM</h2>", | ||
"<p>Install</p>", | ||
"<h2>Configure</h2>", | ||
"<p>xyz</p>", | ||
"<h2>Writing Modules</h2>", | ||
"<p>writing modules</p>", | ||
"<h2>Extra</h2>", | ||
"<p>final</p>" | ||
]; | ||
$testArea.html("<article id='article'>"+headings.join("")+"</article>"+ | ||
"<bit-toc headings-container-selector='#article'></bit-toc>"); | ||
$("article").css({ | ||
position: "fixed", | ||
top: 0, | ||
height: 200, | ||
left: 200, | ||
width: 600, | ||
backgroundColor: "gray", | ||
overflowY: "auto" | ||
}); | ||
$("article p").css({ | ||
height: "500px", | ||
border: "solid 1px red" | ||
}); | ||
$("bit-toc")[0].highlight(); | ||
function getCompletedAndActive(){ | ||
var result = {completed: [], active: []} | ||
$("bit-toc li").each(function(i, node){ | ||
if(node.classList.contains("completed")) { | ||
result.completed.push(node.textContent) | ||
} | ||
if(node.classList.contains("active")) { | ||
result.active.push(node.textContent) | ||
} | ||
}); | ||
return result; | ||
} | ||
assert.deepEqual(getCompletedAndActive(), { | ||
active: ["Bower"], | ||
completed: [] | ||
}, "initialized correctly"); | ||
$("#article").scrollTop(600); | ||
$("bit-toc")[0].highlight(); // so we don't have to wait for the throttling | ||
assert.deepEqual(getCompletedAndActive(), { | ||
active: ["NPM"], | ||
completed: ["Bower"] | ||
}, "initialized correctly"); | ||
}); | ||
}); |
@@ -1,49 +0,18 @@ | ||
var assign = require("can-assign"); | ||
var TableOfContents = require("./toc-control"); | ||
require("./toc-control"); | ||
// this is legacy | ||
var TocControl = function(el){ | ||
el.style.display = "none"; | ||
var depth = this.getOutlineDepth(); | ||
var tagName = this.getOutlineTagName(); | ||
var selector = this.getHeadingsContainerSelector(el); | ||
var toc = document.createElement(tagName); | ||
var toc = document.createElement("bit-toc"); | ||
toc.className = "on-this-page"; | ||
el.appendChild(toc); | ||
new TableOfContents(toc, { | ||
depth: depth, | ||
tagName: tagName, | ||
headingsContainerSelector: selector | ||
}); | ||
if(el.append) { | ||
el.append(toc); | ||
} else { | ||
el.appendChild(toc); | ||
} | ||
}; | ||
assign(TocControl.prototype, { | ||
getDocObject: function() { | ||
return window.docObject || {}; | ||
}, | ||
getOutlineTagName: function() { | ||
var docObject = this.getDocObject(); | ||
var outline = docObject.outline || {}; | ||
return (outline.tag === "ol") ? "ol" : "ul"; | ||
}, | ||
getOutlineDepth: function() { | ||
var docObject = this.getDocObject(); | ||
var depth = docObject.outline && docObject.outline.depth; | ||
return (typeof depth === "number" ? Math.min(depth, 6) : 1); | ||
}, | ||
getHeadingsContainerSelector: function(el) { | ||
var selector = el.dataset.headingsContainerSelector; | ||
return selector ? selector : "article"; | ||
} | ||
}); | ||
module.exports = TocControl; |
var viewTarget = require("can-view-target"); | ||
var makeTree = require("./make-tree"); | ||
var assign = require("can-assign"); | ||
var lazy = require("can-define-lazy-value"); | ||
var safeCustomElement = require("./safe-custom-element"); | ||
var attributeConnect = require("./attribute-connect"); | ||
// data { tagName: tagName, node: node } | ||
function debounce(func, wait) { | ||
var timeout; | ||
return function executedFunction() { | ||
var context = this; | ||
var args = arguments; | ||
var later = function() { | ||
timeout = null; | ||
func.apply(context, args); | ||
}; | ||
clearTimeout(timeout); | ||
timeout = setTimeout(later, wait); | ||
if (!timeout) { | ||
func.apply(context, args); | ||
} | ||
}; | ||
} | ||
// data { childTag: childTag, node: node } | ||
var renderNodeTarget = viewTarget([{ | ||
@@ -19,5 +43,5 @@ tag: "li", | ||
function(data){ | ||
var container = document.createElement(data.tagName); | ||
var container = document.createElement(data.childTag); | ||
data.node.children.forEach(function(node){ | ||
container.appendChild(renderNodeTarget.hydrate({node: node, tagName: data.tagName})); | ||
container.appendChild(renderNodeTarget.hydrate({node: node, childTag: data.childTag})); | ||
}); | ||
@@ -31,22 +55,8 @@ if(data.node.children.length) { | ||
}]); | ||
/* | ||
var renderNode = stache( | ||
"<li>" + | ||
"<a href='#{{node.id}}'>{{node.text}}</a>" + | ||
"{{#if node.children.length}}" + | ||
"{{#is tagName 'ul'}}" + | ||
"<ul>{{#each node.children}}{{renderNode tagName .}}{{/each}}</ul>" + | ||
"{{else}}" + | ||
"<ol>{{#each node.children}}{{renderNode tagName .}}{{/each}}</ol>" + | ||
"{{/is}}" + | ||
"{{/if}}" + | ||
"</li>" | ||
); | ||
*/ | ||
// data - { nodes: titles, tagName: this.tagName } | ||
// data - { nodes: titles, childTag: this.childTag } | ||
var template = function(data){ | ||
var container = document.createDocumentFragment(); | ||
var container = document.createElement(data.childTag); | ||
data.nodes.forEach(function(node){ | ||
container.appendChild(renderNodeTarget.hydrate({node: node, tagName: data.tagName})); | ||
container.appendChild(renderNodeTarget.hydrate({node: node, childTag: data.childTag})); | ||
}); | ||
@@ -56,49 +66,64 @@ return container; | ||
var connectAttribute = attributeConnect(); | ||
//var template = stache("{{#each nodes}}{{renderNode tagName .}}{{/each}}"); | ||
/*stache.registerSimpleHelper("renderNode", function(tagName, node) { | ||
return renderNode({ tagName: tagName, node: node }); | ||
});*/ | ||
var BitToc = function(){ | ||
console.log("initialized"); | ||
connectAttribute.initialize(this); | ||
this.teardowns = []; | ||
var TocControl = function(el, options){ | ||
this.depth = options.depth; | ||
this.tagName = options.tagName; | ||
this.headingsContainerSelector = options.headingsContainerSelector; | ||
}; | ||
var prototype = { | ||
connectedCallback: function() { | ||
var titles = this.collectTitles(); | ||
var titles = this.titleTree; | ||
// If there are no titles, bail | ||
if (!titles.length) { | ||
el.parentNode.removeChild(el); | ||
return; | ||
} else { | ||
el.parentNode.style.display = 'block'; | ||
} | ||
// If there are no titles, bail | ||
if (!titles.length) { | ||
this.parentNode.removeChild(this); | ||
return; | ||
} else { | ||
this.parentNode.style.display = 'block'; | ||
} | ||
// Append our template | ||
el.appendChild(template({ | ||
nodes: titles, | ||
tagName: this.tagName | ||
})); | ||
}; | ||
// Append our template | ||
this.appendChild(template({ | ||
nodes: titles, | ||
childTag: this.outlineTagName | ||
})); | ||
assign(TocControl.prototype, { | ||
this.setupHighlighting(); | ||
}, | ||
get docObject() { | ||
return window.docObject || {}; | ||
}, | ||
get outlineTagName(){ | ||
if(this.childTag) { | ||
return this.childTag; | ||
} else { | ||
var docObject = this.docObject; | ||
var outline = docObject.outline || {}; | ||
return (outline.tag === "ol") ? "ol" : "ul"; | ||
} | ||
}, | ||
get outlineDepth(){ | ||
if(this.depth) { | ||
return parseInt(this.depth); | ||
} else { | ||
var docObject = this.docObject; | ||
var depth = docObject.outline && docObject.outline.depth; | ||
return (typeof depth === "number" ? Math.min(depth, 6) : 1); | ||
} | ||
}, | ||
get containerSelector(){ | ||
return this.headingsContainerSelector || "article"; | ||
}, | ||
makeSelector: function(tagName) { | ||
var container = this.headingsContainerSelector; | ||
var container = this.containerSelector; | ||
return container + " " + tagName; | ||
}, | ||
collectTitles: function() { | ||
var selector = this.getHeadings() | ||
.map(this.makeSelector.bind(this)) | ||
.join(","); | ||
var titles = selector ? document.querySelectorAll(selector) : []; | ||
return makeTree(titles); | ||
}, | ||
getHeadings: function() { | ||
var headings = []; | ||
for(var i = 0; i < this.depth; i++) { | ||
for(var i = 0; i < this.outlineDepth; i++) { | ||
headings.push("h" + (i + 2)); | ||
@@ -108,4 +133,92 @@ } | ||
return headings; | ||
} | ||
}, | ||
setupHighlighting: function(){ | ||
this.article = document.querySelector(this.containerSelector); | ||
if(this.article) { | ||
var highlight = debounce(this.highlight.bind(this),50); | ||
this.article.addEventListener("scroll",highlight); | ||
this.teardowns.push(function(){ | ||
this.article.removeEventListener("scroll", highlight); | ||
}.bind(this)); | ||
this.highlight(); | ||
} | ||
}, | ||
// completed only once the one after it is in the page and 1/2 way visible. | ||
// | ||
highlight: function(){ | ||
var articleRect = this.article.getBoundingClientRect(); | ||
var buttons = this.buttons; | ||
var positions = this.titles.map(function(header, i){ | ||
return { | ||
header: header, | ||
rect: header.getBoundingClientRect(), | ||
button: buttons[i] | ||
}; | ||
}); | ||
// this simulates a header at the end of the page | ||
positions.push({ | ||
rect: { | ||
top: articleRect.top + this.article.scrollHeight - this.article.scrollTop | ||
} | ||
}); | ||
// loop through the actual headers and their positions | ||
positions.slice(0, positions.length - 1).forEach(function(position, index){ | ||
position.button.classList.remove("completed","active"); | ||
var curRect = position.rect; | ||
var curDistance = curRect.top - articleRect.top; | ||
var nextRect = positions[index+1].rect; | ||
var nextDistance = nextRect.top - articleRect.top; | ||
// =====[CUR=NEXT]=== | ||
if( nextDistance >= 0 && nextDistance <= articleRect.height && curDistance >= 0 && curDistance <= articleRect.height ) { | ||
position.button.classList.add("active"); | ||
} | ||
// =======CUR=====[=NEXT=|===]====== | ||
// =======CUR=======NEXT=====[===|===]=== | ||
else if( nextDistance < (articleRect.height / 2) ) { | ||
position.button.classList.add("completed"); | ||
} | ||
// ======[=CUR=|=NEXT=]====== | ||
// ======[=CUR=|===]=NEXT==== | ||
// ====CUR=[===|=NEXT]======= | ||
// ====CUR=[===|===]=NEXT==== | ||
else if( nextDistance >= (articleRect.height / 2) && curDistance < (articleRect.height / 2) ) { | ||
position.button.classList.add("active"); | ||
} | ||
}); | ||
}, | ||
disconnectedCallback: function(){ | ||
this.teardowns.forEach(function(teardown){ | ||
teardown(); | ||
}); | ||
}, | ||
attributeChangedCallback: connectAttribute.attributeChangedCallback | ||
}; | ||
lazy(prototype,"titles", function(){ | ||
var selector = this.getHeadings() | ||
.map(this.makeSelector.bind(this)) | ||
.join(","); | ||
// we should save this ... | ||
return selector ? Array.from( document.querySelectorAll(selector) ) : []; | ||
}); | ||
module.exports = TocControl; | ||
lazy(prototype,"titleTree", function(){ | ||
return makeTree(this.titles); | ||
}); | ||
lazy(prototype,"buttons", function(){ | ||
return this.querySelectorAll("li"); | ||
}); | ||
BitToc = safeCustomElement("bit-toc",BitToc, prototype); | ||
module.exports = BitToc; |
22
toc.js
/** | ||
* @parent bit-docs-html-toc/static | ||
* @module {function} bit-docs-html-toc/toc.js | ||
* | ||
* | ||
* Main front end JavaScript file for static portion of this plugin. | ||
* | ||
* | ||
* @signature `TOCContainer(el)` | ||
* | ||
* | ||
* Hydrates the container element with class `on-this-page-container` with the | ||
* headers from the page. | ||
* | ||
* | ||
* If `DEPTH` was specified to the [bit-docs-html-toc/tags/outline] tag, then | ||
* only headers less than and including that depth will be hydrated. | ||
* | ||
* | ||
* @param {HTMLElement} el The HTML element to hydrate. | ||
* | ||
* | ||
* @body | ||
*/ | ||
var TOCContainer = require("./toc-container-control"); | ||
var el = document.getElementsByClassName("on-this-page-container"); | ||
if (el.length) { | ||
new TOCContainer(el.item(0)); | ||
} else { | ||
console.log("An element with class 'on-this-page-container' is required"); | ||
} | ||
require("./toc"); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
25319
20
623
93
0
5
+ Addedcan-define-lazy-value@^1.1.0
+ Addedcan-dom-mutate@^1.3.6
+ Addedcan-string@^1.0.0
+ Addedcan-define-lazy-value@1.1.1(transitive)
+ Addedcan-string@1.1.0(transitive)