diff --git a/CHANGELOG.md b/CHANGELOG.md index c132707..179bfeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## Version 2.1.0 +* [feature] Added custom category plugin and categories navigation section +* [feature] Added active navigation (url hash updates on scroll) +* [fix] Fixed missing navigation items if one of sections omitted and item should be visible in other section + ## Version 2.0.2 * [feature] added noURLEncode option to not url encode links in the menu for unicode texts ## Version 2.0.1 diff --git a/README.md b/README.md index 74383b7..519dafe 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ +# Docdash Extended +Clement Moron excellent Docdash template extended with support for @category plugin and some navigation improvements. + # Docdash -[![Build Status](https://api.travis-ci.org/clenemt/docdash.png?branch=master)](https://travis-ci.org/clenemt/docdash) [![npm version](https://badge.fury.io/js/docdash.svg)](https://badge.fury.io/js/docdash) [![license](https://img.shields.io/npm/l/docdash.svg)](LICENSE.md) +[![license](https://img.shields.io/npm/l/docdash.svg)](LICENSE.md) A clean, responsive documentation template theme for JSDoc 4. @@ -13,14 +16,14 @@ See http://clenemt.github.io/docdash/ for a sample demo. :rocket: ## Install ```bash -$ npm install docdash +$ npm install docdash-extended ``` ## Usage Clone repository to your designated `jsdoc` template directory, then: ```bash -$ jsdoc entry-file.js -t path/to/docdash +$ jsdoc entry-file.js -t path/to/docdash-extended ``` ## Usage (npm) @@ -36,7 +39,7 @@ In your `jsdoc.json` file, add a template option. ```json "opts": { - "template": "node_modules/docdash" + "template": "node_modules/docdash-extended" } ``` @@ -54,10 +57,11 @@ See the config file for the [fixtures](fixtures/fixtures.conf.json) or the sampl "excludePattern": "(node_modules/|docs)" }, "plugins": [ - "plugins/markdown" + "plugins/markdown", + "node_modules/docdash/categories", ], "opts": { - "template": "assets/template/docdash/", + "template": "assets/template/docdash-extended/", "encoding": "utf8", "destination": "docs/", "recurse": true, @@ -66,7 +70,8 @@ See the config file for the [fixtures](fixtures/fixtures.conf.json) or the sampl "templates": { "cleverLinks": false, "monospaceLinks": false - } + }, + "categoriesFile": "./categories.json" } ``` @@ -79,6 +84,7 @@ Docdash supports the following options: "static": [false|true], // Display the static members inside the navbar "sort": [false|true], // Sort the methods in the navbar "sectionOrder": [ // Order the main section in the navbar (default order shown here) + "Categories", "Classes", "Modules", "Externals", @@ -134,9 +140,41 @@ Docdash supports the following options: Place them anywhere inside your `jsdoc.json` file. +## Categories +Docdash supports custom categories through jsdoc plugin - plugin is available from docdash package. To use categories you need to load the plugin in your jsdoc.json and point location of your categories file: + +```json +"plugins": [ + "node_modules/docdash-extended/categories", +], +"categoriesFile": "./categories.json" +``` + +Next you need to create your json with your category definitions where key is your category keyword - example categories.json +```json +{ + "model" : { + "displayName" : "Models and Collections" + }, + "component" : { + "displayName" : "Components" + } +} +``` + +Now you can use @category tag in your docs. +```js +/** + * @category model + */ +``` + +**If you have custom sectionOrder you need to add "Categories".** It is best to add Categories before all other sections because of how Docdash generates navigation elements. + + ## Contributors -Thanks to [lodash](https://lodash.com) and [minami](https://github.com/nijikokun/minami). +Thanks to [docdash](https://github.com/clenemt/docdash), [lodash](https://lodash.com) and [minami](https://github.com/nijikokun/minami). ## License Licensed under the Apache License, version 2.0. (see [Apache-2.0](LICENSE.md)). diff --git a/categories.js b/categories.js new file mode 100644 index 0000000..cfbd749 --- /dev/null +++ b/categories.js @@ -0,0 +1,38 @@ +var logger = require('jsdoc/util/logger'); + +exports.defineTags = function(dictionary) { + dictionary.defineTag('category', { + mustHaveValue: true, + onTagged: function(doclet, tag) { + const category = tag.value; + if (!category) return; + if (env.conf.categories[category]) { + doclet.category = category; + } else { + logger.error(`Undefined category "${category}"`); + } + } + }); +}; + +exports.handlers = { + beforeParse: function() { + loadConfiguration(); + }, +}; + +function loadConfiguration () { + try { + const fs = require('jsdoc/fs'); + const confFileContents = fs.readFileSync(env.conf.categoriesFile, 'utf8'); + env.conf.categories = JSON.parse( (confFileContents || "{}" ) ); + env.conf.categoryList = Object.keys(env.conf.categories); + } catch (e) { + throw 'Could not load category file'; + } +} + +exports.getMembers = (data, category) => { + const doclets = data({category: category}).get(); + return doclets; +}; \ No newline at end of file diff --git a/package.json b/package.json index 91bd27f..de78421 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "docdash", - "version": "2.0.2", - "description": "A clean, responsive documentation template theme for JSDoc 3 inspired by lodash and minami", + "name": "docdash-extended", + "version": "1.0.2", + "description": "JSDoc 4 Docdash template extended with category plugin support", "main": "publish.js", "scripts": { "test": "jsdoc -c fixtures/fixtures.conf.json", @@ -10,25 +10,32 @@ }, "repository": { "type": "git", - "url": "https://github.com/clenemt/docdash.git" + "url": "git+https://github.com/ThomasK0lasa/docdash-extended.git" }, "devDependencies": { "browser-sync": "latest", "jsdoc": "latest", "watch-run": "latest" }, - "author": "Clement Moron ", + "author": "Tomasz Kolasa", "license": "Apache-2.0", "keywords": [ "jsdoc", - "template" + "template", + "category", + "categories" ], "files": [ "publish.js", + "categories.js", "static", "tmpl" ], "dependencies": { "@jsdoc/salty": "^0.2.1" - } + }, + "bugs": { + "url": "https://github.com/ThomasK0lasa/docdash-extended/issues" + }, + "homepage": "https://github.com/ThomasK0lasa/docdash-extended#readme" } diff --git a/publish.js b/publish.js index 5894d83..fa16884 100644 --- a/publish.js +++ b/publish.js @@ -9,11 +9,11 @@ var path = require('jsdoc/path'); var taffy = require('@jsdoc/salty').taffy; var template = require('jsdoc/template'); var util = require('util'); +var categories = require('docdash-extended/categories.js'); var htmlsafe = helper.htmlsafe; var linkto = helper.linkto; var resolveAuthorLinks = helper.resolveAuthorLinks; -var scopeToPunc = helper.scopeToPunc; var hasOwnProp = Object.prototype.hasOwnProperty; var data; @@ -322,9 +322,18 @@ function attachModuleSymbols(doclets, modules) { }); } -function buildMemberNav(items, itemHeading, itemsSeen, linktoFn) { +function buildCategoriesNav(items, itemsSeen, linktoFn) { var nav = ''; + for (var [category, members] of Object.entries(items)) { + var name = env.conf.categories[category].displayName; + nav += buildMemberNav(members, name, itemsSeen, linktoFn); + } + return nav; +} +function buildMemberNav(items, itemHeading, itemsSeen, linktoFn) { + var nav = ''; + if (items && items.length) { var itemsNav = ''; var docdash = env && env.conf && env.conf.docdash || {}; @@ -403,7 +412,7 @@ function buildMemberNav(items, itemHeading, itemsSeen, linktoFn) { itemsNav += ''; }); - if (itemsNav !== '') { + if (itemsNav !== '
  • ') { if(docdash.collapse === "top") { nav += '

    ' + itemHeading + '

    '; } @@ -439,7 +448,7 @@ function linktoExternal(longName, name) { * @return {string} The HTML for the navigation sidebar. */ -function buildNav(members) { +function buildNav(members, categoriesMembers) { var nav = '

    Home

    '; var seen = {}; var seenTutorials = {}; @@ -483,21 +492,22 @@ function buildNav(members) { return ret; } var defaultOrder = [ - 'Classes', 'Modules', 'Externals', 'Events', 'Namespaces', 'Mixins', 'Tutorials', 'Interfaces', 'Global' + 'Categories', 'Classes', 'Modules', 'Externals', 'Events', 'Namespaces', 'Mixins', 'Tutorials', 'Interfaces', 'Global' ]; var order = docdash.sectionOrder || defaultOrder; var sections = { - Classes: buildMemberNav(members.classes, 'Classes', seen, linkto), - Modules: buildMemberNav(members.modules, 'Modules', {}, linkto), - Externals: buildMemberNav(members.externals, 'Externals', seen, linktoExternal), - Events: buildMemberNav(members.events, 'Events', seen, linkto), - Namespaces: buildMemberNav(members.namespaces, 'Namespaces', seen, linkto), - Mixins: buildMemberNav(members.mixins, 'Mixins', seen, linkto), - Tutorials: buildMemberNav(members.tutorials, 'Tutorials', seenTutorials, linktoTutorial), - Interfaces: buildMemberNav(members.interfaces, 'Interfaces', seen, linkto), - Global: buildMemberNavGlobal() + Categories: () => buildCategoriesNav(categoriesMembers, seen, linkto), + Classes: () => buildMemberNav(members.classes, 'Classes', seen, linkto), + Modules: () => buildMemberNav(members.modules, 'Modules', {}, linkto), + Externals: () => buildMemberNav(members.externals, 'Externals', seen, linktoExternal), + Events: () => buildMemberNav(members.events, 'Events', seen, linkto), + Namespaces: () => buildMemberNav(members.namespaces, 'Namespaces', seen, linkto), + Mixins: () => buildMemberNav(members.mixins, 'Mixins', seen, linkto), + Tutorials: () => buildMemberNav(members.tutorials, 'Tutorials', seenTutorials, linktoTutorial), + Interfaces: () => buildMemberNav(members.interfaces, 'Interfaces', seen, linkto), + Global: () => buildMemberNavGlobal() }; - order.forEach(member => nav += sections[member]); + order.forEach(section => nav += sections[section]()); return nav; } @@ -719,6 +729,12 @@ exports.publish = function(taffyData, opts, tutorials) { }); var members = helper.getMembers(data); + var categoriesMembers = {}; + if (env.conf.categoryList) { + for (var category of env.conf.categoryList) { + categoriesMembers[category] = categories.getMembers(data, category); + } + } members.tutorials = tutorials.children; // output pretty-printed source files by default @@ -735,7 +751,7 @@ exports.publish = function(taffyData, opts, tutorials) { view.outputSourceFiles = outputSourceFiles; // once for all - view.nav = buildNav(members); + view.nav = buildNav(members, categoriesMembers); attachModuleSymbols( find({ longname: {left: 'module:'} }), members.modules ); // generate the pretty-printed source files first so other pages can link to them diff --git a/static/scripts/nav.js b/static/scripts/nav.js index 6dd8313..33d64f2 100644 --- a/static/scripts/nav.js +++ b/static/scripts/nav.js @@ -1,12 +1,82 @@ -function scrollToNavItem() { - var path = window.location.href.split('/').pop().replace(/\.html/, ''); - document.querySelectorAll('nav a').forEach(function(link) { - var href = link.attributes.href.value.replace(/\.html/, ''); - if (path === href) { - link.scrollIntoView({block: 'center'}); - return; +function initNavigation() { + + // get current url data + const pathname = window.location.pathname.split('/'); + let file = pathname.pop(); + if (!file) file = "index.html"; + const hash = window.location.hash; + const id = hash.substring(1); + + setActiveItem(id); + setActiveParentItem(); + scrollToCurrentItem(); + + // bind to scroll to set id live + window.addEventListener('scroll', function () { + debounceActiveHash(); + }); + + var hashTimeout; + function debounceActiveHash() { + clearTimeout(hashTimeout); + hashTimeout = setTimeout(findActiveHeader, 25); + } + + function findActiveHeader() { + const scrollTop = window.pageYOffset || document.documentElement.scrollTop; + const windowHeight = window.innerHeight; + const headers = Array.from(document.querySelectorAll("h4.name")); + const currentHeader = headers.find(header => { + const offsetTop = header.offsetTop; + const height = header.offsetHeight; + if (scrollTop <= offsetTop && (height + offsetTop) < (scrollTop + windowHeight)) { + return true; } - }) + }); + if (currentHeader) setActiveHash(currentHeader.id); + } + + function setActiveHash(id) { + removeActiveClass(); + setActiveItem(id); + setActiveParentItem(id); + setWindowHash(id); + } + + function removeActiveClass() { + const items = Array.from(document.querySelectorAll(`nav li`)); + items.forEach(item => item.classList.remove('active')); } - scrollToNavItem(); + function setActiveItem(id) { + const currentLink = document.querySelector(`a[href='${file}#${id}']`); + if (!currentLink) return; + const item = currentLink.closest('li'); + item.classList.add('active'); + } + + function setActiveParentItem() { + const currentLink = document.querySelector(`a[href='${file}']`); + if (!currentLink) return; + const item = currentLink.closest('li'); + if (item) item.classList.add('active'); + } + + function setWindowHash(id) { + const link = document.querySelector(`a[href='${file}#${id}']`); + const hash = link ? `#${id}` : ' '; + window.history.replaceState(null, null, hash); + } + + function scrollToCurrentItem() { + let item = document.querySelector(`li.active li.active`); + if (!item) item = document.querySelector(`li.active`); + if (!item) return; // index + document.addEventListener("DOMContentLoaded", function () { + item.scrollIntoView({ block: 'center' }); + }); + } + +} + +initNavigation(); diff --git a/static/styles/jsdoc.css b/static/styles/jsdoc.css index 0fe6d3c..0b73f22 100644 --- a/static/styles/jsdoc.css +++ b/static/styles/jsdoc.css @@ -171,10 +171,6 @@ tt, code, kbd, samp{ padding: 1px 5px; } -pre { - padding-bottom: 1em; -} - .class-description { font-size: 130%; line-height: 140%; @@ -220,6 +216,7 @@ nav { overflow: auto; position: fixed; height: 100%; + overflow: auto; } nav #nav-search{ @@ -306,6 +303,23 @@ nav > h2 > a { color: #606 !important; } +nav .active { + position: relative; +} + +nav .active:before { + content: "▶"; + position: absolute; + font-size: 22px; + line-height: 15px; + left: -18px; +} + +nav .methods .active:before { + left: 8px; + font-size: 13px; +} + footer { color: hsl(0, 0%, 28%); margin-left: 250px; @@ -772,5 +786,4 @@ html[data-search-mode] .level-hide { url('../fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg#source_sans_prolight') format('svg'); font-weight: 300; font-style: normal; - }