Greasy Fork

Confluence Floating TOC

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

目前为 2024-07-10 提交的版本。查看 最新版本

// ==UserScript==
// @name         Confluence Floating TOC
// @namespace    http://tampermonkey.net/
// @version      2.1
// @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 > 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');
    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;
        }
        

        // 创建目录项
        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() {
    // 添加折叠/展开按钮
    var toggleButton = document.createElement('button');
    toggleButton.textContent = '折叠';
    toggleButton.style.position = 'fixed';
    toggleButton.style.top = '200px';
    toggleButton.style.right = '0';
    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 () {
        var tocContainer = document.getElementById('floating-toc-container');
        if (isCollapsed) {
            tocContainer.style.visibility = 'visible';
            toggleButton.textContent = '折叠';
        } else {
            tocContainer.style.visibility = 'hidden';
            toggleButton.textContent = '展开';
        }
        isCollapsed = !isCollapsed;
    });
    return toggleButton;
}


function buildToc() {
    // 创建浮动目录的容器
    var tocContainer = document.createElement('div');
    tocContainer.id = 'floating-toc-container';
    tocContainer.style.width = '200px';
    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 = '4';
    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);

    return tocContainer;
}


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

    var toc;
    if (existingTOC) {
        toc = genertateTOCFromExistingToc(existingTOC);
    }

    if (toc === undefined || !toc) {
        toc = generateTOCFormPage();
    }
    
    toc.style.position = 'relative';
    toc.style.listStyle = 'none';
    toc.style.padding = '0';

    return toc
}

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 container = document.createElement('div');
    container.id = 'floating-toc-div';
    container.style.position = 'fixed';
    container.style.right = '0';
    container.style.top = '200px'; // 设置为 200px
    container.style.maxHeight = 'calc(100vh - 400px)';
    container.style.overflowY = 'auto';
    
    // 添加隐藏滚动条样式
    var style = document.createElement('style');
    style.innerHTML = `
        #floating-toc-div {
            scrollbar-width: none;
            -ms-overflow-style: none;
        }
        #floating-toc-div::-webkit-scrollbar {
            display: none;
        }
    `;
    document.head.appendChild(style);
    document.body.appendChild(container);
    

    var tocContainer = buildToc();
    container.appendChild(tocContainer);

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

    function onChange() {
        var tocList;
        tocList = document.getElementById('floating-toc-ul');
        if (tocList) {
            tocList.remove();
        }

        tocList = generateTOC(tocContainer);
        tocContainer.appendChild(tocList);

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

    onChange();

    var latestMainContent;
    var latestEditorTextarea;

    window.addEventListener('load', function () {
        const checkMainContentExistence = setInterval(function() {
            const mainContent = document.getElementById('main-content');
            if (mainContent) {
                if (latestMainContent === mainContent) {
                    return;
                }
                onChange();
            }
        }, 1000);

        // 轮询检查 ak-editor-textarea 是否存在
        const checkTextareaExistence = setInterval(function() {
            const editorTextarea = document.getElementById('ak-editor-textarea');
            if (editorTextarea) {
                if (latestEditorTextarea === editorTextarea) {
                    return;
                }
                onChange();
            }
        }, 1000);

    });

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