Confluence Floating TOC

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

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

  1. // ==UserScript==
  2. // @name Confluence Floating TOC
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.0
  5. // @description 在 Confluence 文章页面上浮动展示文章目录,并支持展开和折叠功能
  6. // @author mkdir700
  7. // @match https://*.atlassian.net/wiki/*
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12.  
  13. // 递归处理已有的 TOC,重新生成新的 TOC
  14. function genertateTOCFromExistingToc(toc) {
  15. if (!toc) {
  16. return;
  17. }
  18. let currUl = document.createElement('ul');
  19. for (let i = 0; i < toc.children.length; i++) {
  20. // li > span > a > span > span
  21. var headerTextElement = toc.children[i].querySelector('span > a > span > span');
  22. if (!headerTextElement) {
  23. continue;
  24. }
  25.  
  26. var headerText = headerTextElement.textContent;
  27.  
  28. // 创建目录项
  29. var tocItem = document.createElement('li');
  30.  
  31. // 创建链接
  32. var tocLink = document.createElement('a');
  33. tocLink.textContent = headerText;
  34.  
  35. // 使用标题的 id 作为 URL 片段
  36. // 标题中的空格需要替换为 -,并且转为小写
  37. tocLink.href = '#' + headerText.replace(/\s/g, '-');
  38. tocItem.appendChild(tocLink);
  39.  
  40. // 如果有子目录,递归处理
  41. var childUl = toc.children[i].querySelector('ul');
  42. if (childUl) {
  43. var newUl = genertateTOCFromExistingToc(childUl);
  44. if (newUl) {
  45. tocItem.appendChild(newUl);
  46. }
  47. }
  48. currUl.appendChild(tocItem);
  49. }
  50.  
  51. return currUl;
  52. }
  53.  
  54.  
  55. function getExistingToc() {
  56. return document.querySelector('[data-testid="list-style-toc-level-container"]');
  57. }
  58.  
  59. function generateTOCFormPage() {
  60. // 创建目录列表
  61. var tocList = document.createElement('ul');
  62. // 获取所有标题
  63. var headers = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
  64. headers.forEach(function (header) {
  65. // 过滤掉 id 为空的标题
  66. if (!header.id) return;
  67.  
  68. // 创建目录项
  69. var tocItem = document.createElement('li');
  70. tocItem.style.marginLeft = (parseInt(header.tagName[1]) - 1) * 10 + 'px'; // 根据标题级别缩进
  71.  
  72. // 创建链接
  73. var tocLink = document.createElement('a');
  74. tocLink.textContent = header.textContent;
  75.  
  76. // 使用标题作为 URL 片段
  77. tocLink.href = '#' + header.textContent.replace(/\s/g, '-');
  78. tocItem.appendChild(tocLink);
  79.  
  80. // 将目录项添加到目录列表中
  81. tocList.appendChild(tocItem);
  82. });
  83. return tocList;
  84. }
  85.  
  86.  
  87. function buildToggleButton(tocList) {
  88. // 添加折叠/展开按钮
  89. var toggleButton = document.createElement('button');
  90. toggleButton.textContent = '折叠';
  91. toggleButton.style.position = 'absolute';
  92. toggleButton.style.top = '5px';
  93. toggleButton.style.right = '5px';
  94. toggleButton.style.backgroundColor = '#007bff';
  95. toggleButton.style.color = '#fff';
  96. toggleButton.style.border = 'none';
  97. toggleButton.style.padding = '5px';
  98. toggleButton.style.cursor = 'pointer';
  99.  
  100. var isCollapsed = false;
  101. // 折叠和展开功能
  102. toggleButton.addEventListener('click', function () {
  103. if (isCollapsed) {
  104. tocList.style.display = 'block';
  105. toggleButton.textContent = '折叠';
  106. } else {
  107. tocList.style.display = 'none';
  108. toggleButton.textContent = '展开';
  109. }
  110. isCollapsed = !isCollapsed;
  111. });
  112. return toggleButton;
  113. }
  114.  
  115.  
  116. function buildToc() {
  117. // 创建浮动目录的容器
  118. var tocContainer = document.createElement('div');
  119. tocContainer.id = 'floating-toc-container';
  120. tocContainer.style.position = 'fixed';
  121. tocContainer.style.top = '200px'; // 设置为 200px
  122. tocContainer.style.width = '200px';
  123. tocContainer.style.overflowY = 'auto';
  124. tocContainer.style.backgroundColor = '#fff';
  125. tocContainer.style.border = '1px solid #ccc';
  126. tocContainer.style.padding = '10px';
  127. tocContainer.style.boxShadow = '0 0 10px rgba(0,0,0,0.1)';
  128. tocContainer.style.zIndex = '1000';
  129. tocContainer.style.fontSize = '14px';
  130.  
  131. // 添加隐藏滚动条样式
  132. var style = document.createElement('style');
  133. style.innerHTML = `
  134. #floating-toc-container {
  135. scrollbar-width: none;
  136. -ms-overflow-style: none;
  137. }
  138. #floating-toc-container::-webkit-scrollbar {
  139. display: none;
  140. }
  141. `;
  142. document.head.appendChild(style);
  143.  
  144. // 添加标题
  145. var tocTitle = document.createElement('h3');
  146. tocTitle.textContent = '目录';
  147. tocTitle.style.marginTop = '0';
  148. tocContainer.appendChild(tocTitle);
  149.  
  150. return tocContainer;
  151. }
  152.  
  153.  
  154. function generateTOC(tocContainer) {
  155. // 清空现有目录
  156. var tocList = tocContainer.querySelector('ul');
  157. if (tocList) {
  158. tocList.remove();
  159. }
  160.  
  161. // 获取 content-body 容器
  162. var contentBody = document.getElementById('content-body');
  163. if (!contentBody) {
  164. console.error('未找到 id 为 content-body 的元素');
  165. return;
  166. }
  167.  
  168. // 设置浮动目录的位置
  169. tocContainer.style.left = contentBody.getBoundingClientRect().left + 'px';
  170.  
  171. // 检查是否存在已有的 TOC
  172. var existingTOC = getExistingToc();
  173.  
  174. var toc;
  175. if (existingTOC) {
  176. toc = genertateTOCFromExistingToc(existingTOC);
  177. if (!toc) {
  178. console.error('生成目录失败');
  179. }
  180. } else {
  181. toc = generateTOCFormPage();
  182. }
  183. tocContainer.appendChild(toc);
  184.  
  185. // 添加折叠/展开按钮
  186. const toggleButton = buildToggleButton(toc);
  187. tocContainer.appendChild(toggleButton);
  188.  
  189. // 动态计算最大高度
  190. updateMaxHeight(tocContainer);
  191. }
  192.  
  193. function updateMaxHeight(tocContainer) {
  194. const viewportHeight = window.innerHeight;
  195. const topOffset = parseFloat(tocContainer.style.top);
  196. tocContainer.style.maxHeight = (viewportHeight - topOffset - 20) + 'px'; // 20px 为一些额外的间距
  197. }
  198.  
  199.  
  200. (function () {
  201. 'use strict';
  202.  
  203. var tocContainer = buildToc();
  204. document.body.appendChild(tocContainer);
  205.  
  206. generateTOC(tocContainer);
  207.  
  208. function onUrlChange() {
  209. generateTOC(tocContainer);
  210. }
  211.  
  212. // 使用 history API 拦截 URL 变化
  213. (function (history) {
  214. var pushState = history.pushState;
  215. var replaceState = history.replaceState;
  216.  
  217. history.pushState = function () {
  218. var ret = pushState.apply(history, arguments);
  219. onUrlChange();
  220. return ret;
  221. };
  222.  
  223. history.replaceState = function () {
  224. var ret = replaceState.apply(history, arguments);
  225. onUrlChange();
  226. return ret;
  227. };
  228.  
  229. window.addEventListener('popstate', onUrlChange);
  230. })(window.history);
  231.  
  232. // 监听窗口大小变化,调整目录位置
  233. window.addEventListener('resize', function () {
  234. var contentBody = document.getElementById('content-body');
  235. if (contentBody) {
  236. tocContainer.style.left = contentBody.getBoundingClientRect().left + 'px';
  237. }
  238. updateMaxHeight(tocContainer);
  239. });
  240.  
  241. // 确保目录在滚动时保持在视口内
  242. window.addEventListener('scroll', function () {
  243. updateMaxHeight(tocContainer);
  244. });
  245. })();
  246.