@docs-site/site-docsify
Version:
245 lines (213 loc) • 6.65 kB
JavaScript
/** =====================================================
* Copyright © hk. 2022-2025. All rights reserved.
* File name : docsify-toc.js
* Author : 苏木
* Date : 2025-07-18
* Version :
* Description: Add a Table of Contents to your site.
* https://github.com/mrpotatoes/docsify-toc
* ======================================================
*/
// 默认配置选项
var defaultOptions = {
headings: 'h1, h2', // 要包含的标题级别
scope: '.markdown-section', // 内容范围选择器
title: 'Contents', // 目录标题文本
listType: 'ul', // 列表类型(ul/ol)
minScreenWidth: 1200 // 显示目录的最小屏幕宽度
};
/**
* @brief 创建目录标题元素
* @param {string} title - 标题文本
* @return {HTMLElement} 标题元素
*/
var createTocHeading = function (title) {
return document.createElement('h2').appendChild(
document.createTextNode(title)
);
};
/**
* @brief 创建目录链接元素
* @param {HTMLElement} src - 源标题元素
* @return {HTMLElement} 链接元素
*/
var createTocLink = function (src) {
var a = document.createElement('a');
a.innerHTML = src.innerHTML;
a.href = src.firstChild.href || '#' + src.id;
a.onclick = handleTocClick;
a.setAttribute('class', 'anchor');
return a;
};
/**
* @brief 处理目录项点击事件
* @param {Event} e - 点击事件对象
*/
var handleTocClick = function (e) {
var activeItems = document.querySelectorAll('.page_toc .active');
[].forEach.call(activeItems, function (item) {
item.setAttribute('class', 'anchor');
});
e.currentTarget.setAttribute('class', 'active');
};
/**
* @brief 创建多级列表结构
* @param {HTMLElement} wrapper - 父级容器
* @param {number} depth - 嵌套深度
* @return {HTMLElement} 最内层列表元素
*/
var createNestedList = function (wrapper, depth) {
while (depth--) {
if (wrapper) {
wrapper = wrapper.appendChild(
document.createElement('ul')
);
}
if (depth) {
wrapper = wrapper.appendChild(
document.createElement('li')
);
}
}
return wrapper;
};
/**
* @brief 获取所有符合条件的标题元素
* @param {string} selector - 选择器
* @return {Array} 标题元素数组
*/
var getHeaders = function (selector) {
var headings = document.querySelectorAll(selector);
var ret = [];
[].forEach.call(headings, function (heading) {
ret = ret.concat(heading);
});
return ret;
};
/**
* @brief 获取标题级别
* @param {HTMLElement} header - 标题元素
* @return {number} 标题级别(1-6)
*/
var getHeaderLevel = function (header) {
var level = header.match(/\d/g);
return level ? Math.min.apply(null, level) : 1;
};
/**
* @brief 向上查找父级列表
* @param {HTMLElement} current - 当前元素
* @param {number} levels - 向上查找的层级数
* @return {HTMLElement} 目标父级元素
*/
var findParentList = function (current, levels) {
while (levels--) {
current = current.parentElement;
}
return current;
};
/**
* @brief 构建目录结构
* @param {object} options - 配置选项
* @return {HTMLElement} 完整的目录元素
*/
var buildTocStructure = function (options) {
var tocRoot = document.createElement('ul');
var currentWrapper = tocRoot;
var lastItem = null;
var selector = options.scope + ' ' + options.headings;
var headers = getHeaders(selector).filter(h => h.id);
headers.reduce(function (prevLevel, currentHeader, index) {
var currentLevel = getHeaderLevel(currentHeader.tagName);
var levelDiff = currentLevel - prevLevel;
currentWrapper = (levelDiff > 0)
? createNestedList(lastItem, levelDiff)
: findParentList(currentWrapper, -levelDiff * 2);
currentWrapper = currentWrapper || tocRoot;
var listItem = document.createElement('li');
currentWrapper.appendChild(listItem).appendChild(createTocLink(currentHeader));
lastItem = listItem;
return currentLevel;
}, getHeaderLevel(options.headings));
return tocRoot;
};
/**
* @brief 主插件函数
* @param {object} hook - Docsify钩子对象
* @param {object} vm - Docsify虚拟机实例
*/
function docsifyToc(hook, vm) {
var userOptions = Object.assign({}, defaultOptions, window.$docsify.toc);
hook.doneEach(function () {
// 小屏幕不显示目录
if (window.innerWidth < userOptions.minScreenWidth) {
const existingNav = document.querySelector('.nav');
if (existingNav) {
existingNav.parentNode?.removeChild(existingNav);
}
return;
}
// 检查是否有标题
const selector = userOptions.scope + ' ' + userOptions.headings;
const headers = getHeaders(selector).filter(h => h.id);
if (headers.length === 0) {
const existingNav = document.querySelector('.nav');
if (existingNav) {
existingNav.parentNode?.removeChild(existingNav);
}
return;
}
// 创建导航容器
let nav = document.querySelector('.nav');
if (!nav) {
const content = window.Docsify.dom.find(".content");
if (!content) return;
nav = window.Docsify.dom.create("aside", "");
window.Docsify.dom.toggleClass(nav, "add", "nav");
window.Docsify.dom.before(content, nav);
}
// 渲染目录
requestAnimationFrame(() => {
const toc = buildTocStructure(userOptions);
const title = document.createElement('p');
title.innerHTML = userOptions.title;
title.setAttribute('class', 'title');
const container = document.createElement('div');
container.setAttribute('class', 'page_toc');
container.appendChild(title);
container.appendChild(toc);
// 移除旧目录
const oldToc = document.querySelectorAll('.nav .page_toc');
if (oldToc.length > 0) {
oldToc[0].parentNode.removeChild(oldToc[0]);
}
nav.appendChild(container);
});
});
}
/**
* @brief 注册插件
* @description 安全地注册插件并合并配置
*/
(function registerPlugin() {
// 初始化docsify全局对象
window.$docsify = window.$docsify || {};
// 深度合并配置
window.$docsify.toc = Object.assign(
{},
defaultOptions,
window.$docsify.toc || {}
);
// 防止重复注册
if (!window.$docsify.plugins) {
window.$docsify.plugins = [];
}
// 检查是否已注册
const isRegistered = window.$docsify.plugins.some(
plugin => plugin.name === 'docsifyToc'
);
if (!isRegistered) {
// 为插件函数添加标识
docsifyToc.name = 'docsifyToc';
window.$docsify.plugins.unshift(docsifyToc);
}
})();