// ==UserScript==
// @name Confluence Floating TOC
// @namespace http://tampermonkey.net/
// @version 2.6
// @description 在 Confluence 文章页面上浮动展示文章目录,并支持展开和折叠功能,自动适应暗色/亮色模式
// @author mkdir700
// @match https://*.atlassian.net/wiki/*
// @grant none
// @license MIT
// ==/UserScript==
// 递归处理已有的 TOC,重新生成新的 TOC
function genertateTOCFromExistingToc(toc) {
if (toc.textContent === '') {
return;
}
let currUl = document.createElement('ul');
currUl.id = 'floating-toc-ul';
for (let i = 0; i < toc.children.length; i++) {
// li > span > a
var a = toc.children[i].querySelector('span > a');
var headerTextElement = toc.children[i].querySelector('span > a > span > span');
if (!headerTextElement) {
continue;
}
var headerText = headerTextElement.textContent;
// 创建目录项
var tocItem = document.createElement('li');
// 创建链接
var tocLink = document.createElement('a');
tocLink.textContent = headerText;
// 使用标题的 id 作为 URL 片段
// 标题中的空格需要替换为 -,并且转为小写
tocLink.href = a.href;
tocItem.appendChild(tocLink);
// 如果有子目录,递归处理
var childUl = toc.children[i].querySelector('ul');
if (childUl) {
var newUl = genertateTOCFromExistingToc(childUl);
if (newUl) {
tocItem.appendChild(newUl);
}
}
currUl.appendChild(tocItem);
}
return currUl;
}
function getExistingToc() {
return document.querySelector('[data-testid="list-style-toc-level-container"]');
}
function generateTOCFormPage() {
// 创建目录列表
var tocList = document.createElement('ul');
tocList.id = 'floating-toc-ul';
// 获取所有标题
var headers = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
headers.forEach(function (header) {
// 过滤掉 id 为空的标题
if (header.textContent === '') {
return;
}
// 检查是否有属性 data-item-title
if (header.hasAttribute('data-item-title')) {
return;
}
// 检查属性 data-testid 是否等于 title-text
if (header.getAttribute('data-testid') === 'title-text') {
return;
}
if (header.id === 'floating-toc-title') {
return;
}
// class 为 'cc-te0214' 的标题不需要显示在目录中
if (header.className === 'cc-te0214') {
return;
}
// 排除特定的 h2 标签
if (header.tagName === 'H2' && header.closest('[data-vc="end-of-page-recommendation-component"]')) {
return;
}
// 排除 "快速入门" 标题
if (header.closest('[data-test-id="onboarding-quickstart-experience"]')) {
return;
}
if (header.closest('[data-test-id="flag-visibility-wrapper"]')) {
return;
}
if (header.tagName === 'H2' && header.closest('[class="atlaskit-portal-container"]')) {
return;
}
// 创建目录项
var tocItem = document.createElement('li');
tocItem.style.marginLeft = (parseInt(header.tagName[1]) - 1) * 10 + 'px'; // 根据标题级别缩进
// 创建链接
var tocLink = document.createElement('a');
tocLink.textContent = header.textContent;
// 使用标题作为 URL 片段
tocLink.href = '#' + header.textContent.replace(/\s/g, '-');
tocItem.appendChild(tocLink);
// 将目录项添加到目录列表中
tocList.appendChild(tocItem);
});
return tocList;
}
function buildToggleButton() {
// 检查当前颜色模式
const isDarkMode = document.documentElement.getAttribute('data-color-mode') === 'dark' ||
(document.documentElement.getAttribute('data-color-mode') === 'light' &&
document.documentElement.getAttribute('data-theme') &&
document.documentElement.getAttribute('data-theme').includes('light:dark'));
var toggleButton = document.createElement('div');
toggleButton.id = 'floating-toc-toggle';
toggleButton.innerHTML = '▶'; // 右箭头 Unicode 字符
toggleButton.style.position = 'fixed';
toggleButton.style.top = '200px';
toggleButton.style.right = '0';
toggleButton.style.backgroundColor = isDarkMode ? '#4688ec' : '#007bff';
toggleButton.style.color = '#fff';
toggleButton.style.width = '20px';
toggleButton.style.height = '40px';
toggleButton.style.display = 'flex';
toggleButton.style.justifyContent = 'center';
toggleButton.style.alignItems = 'center';
toggleButton.style.cursor = 'pointer';
toggleButton.style.userSelect = 'none';
toggleButton.style.borderRadius = '5px 0 0 5px';
toggleButton.style.zIndex = '1000';
toggleButton.style.transition = 'all 0.3s ease-in-out';
toggleButton.style.fontSize = '14px';
var isCollapsed = false;
toggleButton.addEventListener('click', function () {
var tocContainer = document.getElementById('floating-toc-container');
if (isCollapsed) {
tocContainer.style.right = '0';
toggleButton.innerHTML = '▶'; // 右箭头
toggleButton.style.right = '220px'; // 调整按钮位置
} else {
tocContainer.style.right = '-220px'; // 完全隐藏目录
toggleButton.innerHTML = '◀'; // 左箭头
toggleButton.style.right = '-10px';
}
isCollapsed = !isCollapsed;
});
toggleButton.addEventListener('mouseenter', function() {
if (isCollapsed) {
toggleButton.style.right = '0';
}
});
toggleButton.addEventListener('mouseleave', function() {
if (isCollapsed) {
toggleButton.style.right = '-10px';
}
});
return toggleButton;
}
function buildToc() {
// 检查当前颜色模式
const isDarkMode = document.documentElement.getAttribute('data-color-mode') === 'dark' ||
(document.documentElement.getAttribute('data-color-mode') === 'light' &&
document.documentElement.getAttribute('data-theme') &&
document.documentElement.getAttribute('data-theme').includes('light:dark'));
var tocContainer = document.createElement('div');
tocContainer.id = 'floating-toc-container';
tocContainer.style.width = '220px'; // 增加宽度以包含padding
tocContainer.style.backgroundColor = isDarkMode ? '#2c2c2e' : '#fff';
tocContainer.style.border = isDarkMode ? '1px solid #444' : '1px solid #ccc';
tocContainer.style.padding = '10px';
tocContainer.style.boxSizing = 'border-box'; // 确保padding包含在宽度内
tocContainer.style.boxShadow = isDarkMode ? '0 0 10px rgba(0,0,0,0.3)' : '0 0 10px rgba(0,0,0,0.1)';
tocContainer.style.position = 'fixed';
tocContainer.style.top = '200px';
tocContainer.style.right = '0';
tocContainer.style.maxHeight = 'calc(100vh - 300px)';
tocContainer.style.overflowY = 'auto';
tocContainer.style.transition = 'right 0.3s ease-in-out';
tocContainer.style.zIndex = '999';
tocContainer.style.scrollbarWidth = 'none';
tocContainer.style.msOverflowStyle = 'none';
var style = document.createElement('style');
style.textContent = `
#floating-toc-container::-webkit-scrollbar {
display: none;
}
`;
document.head.appendChild(style);
var tocTitle = document.createElement('div');
tocTitle.textContent = '目录';
tocTitle.style.marginTop = '0';
tocTitle.style.marginBottom = '10px';
tocTitle.style.textAlign = 'center';
tocTitle.style.fontSize = '16px';
tocTitle.style.fontWeight = 'bold';
tocTitle.style.color = isDarkMode ? '#bfc1c4' : '#000';
tocContainer.appendChild(tocTitle);
return tocContainer;
}
function generateTOC() {
// var existingTOC = getExistingToc();
// var toc;
// if (existingTOC) {
// toc = genertateTOCFromExistingToc(existingTOC);
// } else {
// toc = generateTOCFormPage();
// }
var toc = generateTOCFormPage();
if (!toc || toc.children.length === 0) {
var emptyMessage = document.createElement('div');
emptyMessage.id = 'floating-toc-empty-message';
emptyMessage.textContent = '当前页面没有可用的目录内容';
emptyMessage.style.color = '#666';
emptyMessage.style.fontStyle = 'italic';
emptyMessage.style.textAlign = 'center';
emptyMessage.style.padding = '20px 0';
return emptyMessage;
}
toc.style.listStyle = 'none';
toc.style.padding = '0';
toc.style.margin = '0';
// 检查当前颜色模式
const isDarkMode = document.documentElement.getAttribute('data-color-mode') === 'dark' ||
(document.documentElement.getAttribute('data-color-mode') === 'light' &&
document.documentElement.getAttribute('data-theme') &&
document.documentElement.getAttribute('data-theme').includes('light:dark'));
// 优化目录列表样式
var listItems = toc.querySelectorAll('li');
listItems.forEach(function(item, index) {
item.style.marginBottom = '5px';
var link = item.querySelector('a');
if (link) {
link.style.textDecoration = 'none';
link.style.color = isDarkMode ? '#a9abaf' : '#333';
link.style.display = 'block';
link.style.padding = '3px 5px';
link.style.borderRadius = '3px';
link.style.transition = 'background-color 0.2s';
link.style.whiteSpace = 'nowrap';
link.style.overflow = 'hidden';
link.style.textOverflow = 'ellipsis';
link.style.maxWidth = '180px'; // 减小最大宽度
// 设置标题完整内容为title属性
link.title = link.textContent;
// 截断长标题
if (link.textContent.length > 25) {
link.textContent = link.textContent.substring(0, 22) + '...';
}
// 根据颜色模式设置悬停效果
const hoverBgColor = isDarkMode ? '#3a3a3c' : '#f0f0f0';
link.addEventListener('mouseover', function() {
this.style.backgroundColor = hoverBgColor;
});
link.addEventListener('mouseout', function() {
this.style.backgroundColor = 'transparent';
});
// 优化缩进
var level = parseInt(item.style.marginLeft) / 10;
item.style.paddingLeft = (level * 15) + 'px'; // 使用 padding 代替 margin
item.style.marginLeft = '0'; // 移除左边距
// 为第三级及以下的标题添加折叠功能
if (level > 2) {
item.style.display = 'none';
var parentLi = item.parentElement.closest('li');
if (parentLi && !parentLi.classList.contains('has-submenu')) {
parentLi.classList.add('has-submenu');
var toggleBtn = document.createElement('span');
toggleBtn.textContent = '▶';
toggleBtn.style.cursor = 'pointer';
toggleBtn.style.marginRight = '5px';
toggleBtn.style.fontSize = '10px'; // 减小箭头大小
toggleBtn.style.color = isDarkMode ? '#a9abaf' : '#333'; // 根据颜色模式设置颜色
parentLi.insertBefore(toggleBtn, parentLi.firstChild);
toggleBtn.addEventListener('click', function(e) {
e.stopPropagation(); // 防止点击事件冒泡
var subItems = this.parentElement.querySelectorAll('li');
subItems.forEach(function(subItem) {
subItem.style.display = subItem.style.display === 'none' ? 'block' : 'none';
});
this.textContent = this.textContent === '▶' ? '▼' : '▶';
});
}
}
}
});
// 添加平滑滚动
toc.style.scrollBehavior = 'smooth';
return toc;
}
function updateMaxHeight(tocContainer) {
const viewportHeight = window.innerHeight;
const topOffset = parseFloat(tocContainer.style.top);
tocContainer.style.maxHeight = (viewportHeight - topOffset - 20) + 'px'; // 20px 为一些额外的间距
}
// 检测 Confluence 页面的颜色模式并相应地调整插件样式
function detectColorModeAndApplyStyles() {
// 检查 HTML 元素的 data-color-mode 属性
const isDarkMode = document.documentElement.getAttribute('data-color-mode') === 'dark' ||
(document.documentElement.getAttribute('data-color-mode') === 'light' &&
document.documentElement.getAttribute('data-theme') &&
document.documentElement.getAttribute('data-theme').includes('light:dark'));
// 获取插件元素
const tocContainer = document.getElementById('floating-toc-container');
const toggleButton = document.getElementById('floating-toc-toggle');
const backToTopButton = document.getElementById('back-to-top-button');
if (isDarkMode) {
// 暗色模式样式
if (tocContainer) {
tocContainer.style.backgroundColor = '#2c2c2e'; // 深色背景
tocContainer.style.border = '1px solid #444';
tocContainer.style.boxShadow = '0 0 10px rgba(0,0,0,0.3)';
// 修改目录标题和链接颜色
const tocTitle = tocContainer.querySelector('div');
if (tocTitle) {
tocTitle.style.color = '#bfc1c4'; // 使用 --ds-text 变量值
}
// 修改所有链接颜色
const links = tocContainer.querySelectorAll('a');
links.forEach(link => {
link.style.color = '#a9abaf'; // 使用 --ds-text-subtle 变量值
// 修改鼠标悬停效果
link.addEventListener('mouseover', function() {
this.style.backgroundColor = '#3a3a3c';
});
link.addEventListener('mouseout', function() {
this.style.backgroundColor = 'transparent';
});
});
}
if (toggleButton) {
toggleButton.style.backgroundColor = '#4688ec'; // 使用 --ds-icon-accent-blue 变量值
}
if (backToTopButton) {
backToTopButton.style.backgroundColor = '#4688ec'; // 使用 --ds-icon-accent-blue 变量值
}
} else {
// 亮色模式样式(恢复默认)
if (tocContainer) {
tocContainer.style.backgroundColor = '#fff';
tocContainer.style.border = '1px solid #ccc';
tocContainer.style.boxShadow = '0 0 10px rgba(0,0,0,0.1)';
// 恢复目录标题颜色
const tocTitle = tocContainer.querySelector('div');
if (tocTitle) {
tocTitle.style.color = '#000';
}
// 恢复所有链接颜色
const links = tocContainer.querySelectorAll('a');
links.forEach(link => {
link.style.color = '#333';
// 恢复鼠标悬停效果
link.addEventListener('mouseover', function() {
this.style.backgroundColor = '#f0f0f0';
});
link.addEventListener('mouseout', function() {
this.style.backgroundColor = 'transparent';
});
});
}
if (toggleButton) {
toggleButton.style.backgroundColor = '#007bff';
}
if (backToTopButton) {
backToTopButton.style.backgroundColor = '#007bff';
}
}
}
function buildBackToTopButton() {
// 检查当前颜色模式
const isDarkMode = document.documentElement.getAttribute('data-color-mode') === 'dark' ||
(document.documentElement.getAttribute('data-color-mode') === 'light' &&
document.documentElement.getAttribute('data-theme') &&
document.documentElement.getAttribute('data-theme').includes('light:dark'));
var backToTopButton = document.createElement('div');
backToTopButton.id = 'back-to-top-button';
backToTopButton.innerHTML = '▲'; // 上箭头 Unicode 字符
backToTopButton.style.position = 'fixed';
backToTopButton.style.bottom = '30px';
backToTopButton.style.right = '220px'; // 调整位置,使其位于目录左侧
backToTopButton.style.backgroundColor = isDarkMode ? '#4688ec' : '#007bff';
backToTopButton.style.color = '#fff';
backToTopButton.style.width = '40px';
backToTopButton.style.height = '40px';
backToTopButton.style.borderRadius = '50%';
backToTopButton.style.display = 'flex';
backToTopButton.style.justifyContent = 'center';
backToTopButton.style.alignItems = 'center';
backToTopButton.style.cursor = 'pointer';
backToTopButton.style.fontSize = '20px';
backToTopButton.style.boxShadow = isDarkMode ? '0 2px 5px rgba(0,0,0,0.3)' : '0 2px 5px rgba(0,0,0,0.2)';
backToTopButton.style.transition = 'opacity 0.3s';
backToTopButton.style.opacity = '0';
backToTopButton.style.zIndex = '1000';
backToTopButton.addEventListener('click', function() {
window.scrollTo({top: 0, behavior: 'smooth'});
});
window.addEventListener('scroll', function() {
if (window.pageYOffset > 100) {
backToTopButton.style.opacity = '1';
} else {
backToTopButton.style.opacity = '0';
}
});
return backToTopButton;
}
(function () {
'use strict';
var tocContainer = buildToc();
document.body.appendChild(tocContainer);
var toggleButton = buildToggleButton();
document.body.appendChild(toggleButton);
// 初始化按钮位置
toggleButton.style.right = '220px';
var backToTopButton = buildBackToTopButton();
document.body.appendChild(backToTopButton);
function updateTOC() {
var existingContent = document.getElementById('floating-toc-ul') || document.getElementById('floating-toc-empty-message');
if (existingContent) {
existingContent.remove();
}
var newContent = generateTOC();
tocContainer.appendChild(newContent);
}
// 使用防抖函数来限制更新频率
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 防抖处理的更新函数
const debouncedUpdateTOC = debounce(updateTOC, 300);
// 初始化目录
updateTOC();
// 监听 URL 变化
var lastUrl = location.href;
new MutationObserver(() => {
const url = location.href;
if (url !== lastUrl) {
lastUrl = url;
setTimeout(updateTOC, 1000); // 延迟 1 秒更新目录,确保页面内容已加载
}
}).observe(document, {subtree: true, childList: true});
// 监听页面内容变化,包括编辑状态下的变化
var contentObserver = new MutationObserver(function(mutations) {
let shouldUpdate = false;
mutations.forEach(function(mutation) {
if (mutation.type === 'childList') {
// 检查是否有新的标题元素被添加或删除
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1 && /^H[1-6]$/i.test(node.tagName)) {
shouldUpdate = true;
}
});
mutation.removedNodes.forEach(function(node) {
if (node.nodeType === 1 && /^H[1-6]$/i.test(node.tagName)) {
shouldUpdate = true;
}
});
} else if (mutation.type === 'characterData') {
// 检查文本内容的变化
let node = mutation.target.parentNode;
while (node && node !== document.body) {
if (/^H[1-6]$/i.test(node.tagName)) {
shouldUpdate = true;
break;
}
node = node.parentNode;
}
}
});
if (shouldUpdate) {
debouncedUpdateTOC();
}
});
contentObserver.observe(document.body, {
childList: true,
subtree: true,
characterData: true
});
// 检测颜色模式并应用样式
detectColorModeAndApplyStyles();
// 监听颜色模式变化
const colorModeObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'attributes' &&
(mutation.attributeName === 'data-color-mode' || mutation.attributeName === 'data-theme')) {
detectColorModeAndApplyStyles();
}
});
});
colorModeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-color-mode', 'data-theme']
});
// ... 其他现有代码 ...
})();