Confluence Floating TOC

在 Confluence 文章页面上浮动展示文章目录,并支持展开和折叠功能

目前為 2024-07-09 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Confluence Floating TOC
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  在 Confluence 文章页面上浮动展示文章目录,并支持展开和折叠功能
// @author       mkdir700
// @match        https://*.atlassian.net/wiki/*
// @grant        none
// @license      MIT
// ==/UserScript==


// 递归处理已有的 TOC,重新生成新的 TOC
function genertateTOCFromExistingToc(toc) {
    if (!toc) {
        return;
    }
    let currUl = document.createElement('ul');
    for (let i = 0; i < toc.children.length; i++) {
        // li > span > a > span > span
        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 = '#' + headerText.replace(/\s/g, '-');
        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');
    // 获取所有标题
    var headers = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
    headers.forEach(function (header) {
        // 过滤掉 id 为空的标题
        if (!header.id) 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(tocList) {
    // 添加折叠/展开按钮
    var toggleButton = document.createElement('button');
    toggleButton.textContent = '折叠';
    toggleButton.style.position = 'absolute';
    toggleButton.style.top = '5px';
    toggleButton.style.right = '5px';
    toggleButton.style.backgroundColor = '#007bff';
    toggleButton.style.color = '#fff';
    toggleButton.style.border = 'none';
    toggleButton.style.padding = '5px';
    toggleButton.style.cursor = 'pointer';

    var isCollapsed = false;
    // 折叠和展开功能
    toggleButton.addEventListener('click', function () {
        if (isCollapsed) {
            tocList.style.display = 'block';
            toggleButton.textContent = '折叠';
        } else {
            tocList.style.display = 'none';
            toggleButton.textContent = '展开';
        }
        isCollapsed = !isCollapsed;
    });
    return toggleButton;
}


function buildToc() {
    // 创建浮动目录的容器
    var tocContainer = document.createElement('div');
    tocContainer.id = 'floating-toc-container';
    tocContainer.style.position = 'fixed';
    tocContainer.style.top = '200px'; // 设置为 200px
    tocContainer.style.width = '200px';
    tocContainer.style.overflowY = 'auto';
    tocContainer.style.backgroundColor = '#fff';
    tocContainer.style.border = '1px solid #ccc';
    tocContainer.style.padding = '10px';
    tocContainer.style.boxShadow = '0 0 10px rgba(0,0,0,0.1)';
    tocContainer.style.zIndex = '1000';
    tocContainer.style.fontSize = '14px';

    // 添加隐藏滚动条样式
    var style = document.createElement('style');
    style.innerHTML = `
        #floating-toc-container {
            scrollbar-width: none;
            -ms-overflow-style: none;
        }
        #floating-toc-container::-webkit-scrollbar {
            display: none;
        }
    `;
    document.head.appendChild(style);

    // 添加标题
    var tocTitle = document.createElement('h3');
    tocTitle.textContent = '目录';
    tocTitle.style.marginTop = '0';
    tocContainer.appendChild(tocTitle);

    return tocContainer;
}


function generateTOC(tocContainer) {
    // 清空现有目录
    var tocList = tocContainer.querySelector('ul');
    if (tocList) {
        tocList.remove();
    }

    // 获取 content-body 容器
    var contentBody = document.getElementById('content-body');
    if (!contentBody) {
        console.error('未找到 id 为 content-body 的元素');
        return;
    }

    // 设置浮动目录的位置
    tocContainer.style.left = contentBody.getBoundingClientRect().left + 'px';

    // 检查是否存在已有的 TOC
    var existingTOC = getExistingToc();

    var toc;
    if (existingTOC) {
        toc = genertateTOCFromExistingToc(existingTOC);
        if (!toc) {
            console.error('生成目录失败');
        }
    } else {
        toc = generateTOCFormPage();
    }
    tocContainer.appendChild(toc);

    // 添加折叠/展开按钮
    const toggleButton = buildToggleButton(toc);
    tocContainer.appendChild(toggleButton);

    // 动态计算最大高度
    updateMaxHeight(tocContainer);
}

function updateMaxHeight(tocContainer) {
    const viewportHeight = window.innerHeight;
    const topOffset = parseFloat(tocContainer.style.top);
    tocContainer.style.maxHeight = (viewportHeight - topOffset - 20) + 'px'; // 20px 为一些额外的间距
}


(function () {
    'use strict';

    var tocContainer = buildToc();
    document.body.appendChild(tocContainer);

    generateTOC(tocContainer);

    function onUrlChange() {
        generateTOC(tocContainer);
    }

    // 使用 history API 拦截 URL 变化
    (function (history) {
        var pushState = history.pushState;
        var replaceState = history.replaceState;

        history.pushState = function () {
            var ret = pushState.apply(history, arguments);
            onUrlChange();
            return ret;
        };

        history.replaceState = function () {
            var ret = replaceState.apply(history, arguments);
            onUrlChange();
            return ret;
        };

        window.addEventListener('popstate', onUrlChange);
    })(window.history);

    // 监听窗口大小变化,调整目录位置
    window.addEventListener('resize', function () {
        var contentBody = document.getElementById('content-body');
        if (contentBody) {
            tocContainer.style.left = contentBody.getBoundingClientRect().left + 'px';
        }
        updateMaxHeight(tocContainer);
    });

    // 确保目录在滚动时保持在视口内
    window.addEventListener('scroll', function () {
        updateMaxHeight(tocContainer);
    });
})();