Confluence Floating TOC

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

当前为 2024-07-10 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Confluence Floating TOC
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.2
  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.textContent === '') {
  16. return;
  17. }
  18. let currUl = document.createElement('ul');
  19. currUl.id = 'floating-toc-ul';
  20. for (let i = 0; i < toc.children.length; i++) {
  21. // li > span > a
  22. var a = toc.children[i].querySelector('span > a');
  23. var headerTextElement = toc.children[i].querySelector('span > a > span > span');
  24. if (!headerTextElement) {
  25. continue;
  26. }
  27.  
  28. var headerText = headerTextElement.textContent;
  29.  
  30. // 创建目录项
  31. var tocItem = document.createElement('li');
  32.  
  33. // 创建链接
  34. var tocLink = document.createElement('a');
  35. tocLink.textContent = headerText;
  36.  
  37. // 使用标题的 id 作为 URL 片段
  38. // 标题中的空格需要替换为 -,并且转为小写
  39. tocLink.href = a.href;
  40. tocItem.appendChild(tocLink);
  41.  
  42. // 如果有子目录,递归处理
  43. var childUl = toc.children[i].querySelector('ul');
  44. if (childUl) {
  45. var newUl = genertateTOCFromExistingToc(childUl);
  46. if (newUl) {
  47. tocItem.appendChild(newUl);
  48. }
  49. }
  50. currUl.appendChild(tocItem);
  51. }
  52.  
  53. return currUl;
  54. }
  55.  
  56.  
  57. function getExistingToc() {
  58. return document.querySelector('[data-testid="list-style-toc-level-container"]');
  59. }
  60.  
  61. function generateTOCFormPage() {
  62. // 创建目录列表
  63. var tocList = document.createElement('ul');
  64. tocList.id = 'floating-toc-ul';
  65. // 获取所有标题
  66. var headers = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
  67. headers.forEach(function (header) {
  68. // 过滤掉 id 为空的标题
  69. if (header.textContent === '') {
  70. return;
  71. }
  72. // 检查是否有属性 data-item-title
  73. if (header.hasAttribute('data-item-title')) {
  74. return;
  75. }
  76. // 检查属性 data-testid 是否等于 title-text
  77. if (header.getAttribute('data-testid') === 'title-text') {
  78. return;
  79. }
  80. if (header.id === 'floating-toc-title') {
  81. return;
  82. }
  83. // class 为 'cc-te0214' 的标题不需要显示在目录中
  84. if (header.className === 'cc-te0214') {
  85. return;
  86. }
  87.  
  88. // 创建目录项
  89. var tocItem = document.createElement('li');
  90. tocItem.style.marginLeft = (parseInt(header.tagName[1]) - 1) * 10 + 'px'; // 根据标题级别缩进
  91.  
  92. // 创建链接
  93. var tocLink = document.createElement('a');
  94. tocLink.textContent = header.textContent;
  95.  
  96. // 使用标题作为 URL 片段
  97. tocLink.href = '#' + header.textContent.replace(/\s/g, '-');
  98. tocItem.appendChild(tocLink);
  99.  
  100. // 将目录项添加到目录列表中
  101. tocList.appendChild(tocItem);
  102. });
  103. return tocList;
  104. }
  105.  
  106.  
  107. function buildToggleButton() {
  108. // 添加折叠/展开按钮
  109. var toggleButton = document.createElement('button');
  110. toggleButton.textContent = '折叠';
  111. toggleButton.style.position = 'fixed';
  112. toggleButton.style.top = '200px';
  113. toggleButton.style.right = '0';
  114. toggleButton.style.backgroundColor = '#007bff';
  115. toggleButton.style.color = '#fff';
  116. toggleButton.style.border = 'none';
  117. toggleButton.style.padding = '5px';
  118. toggleButton.style.cursor = 'pointer';
  119.  
  120. var isCollapsed = false;
  121. // 折叠和展开功能
  122. toggleButton.addEventListener('click', function () {
  123. var tocContainer = document.getElementById('floating-toc-container');
  124. if (isCollapsed) {
  125. tocContainer.style.visibility = 'visible';
  126. toggleButton.textContent = '折叠';
  127. } else {
  128. tocContainer.style.visibility = 'hidden';
  129. toggleButton.textContent = '展开';
  130. }
  131. isCollapsed = !isCollapsed;
  132. });
  133. return toggleButton;
  134. }
  135.  
  136.  
  137. function buildToc() {
  138. // 创建浮动目录的容器
  139. var tocContainer = document.createElement('div');
  140. tocContainer.id = 'floating-toc-container';
  141. tocContainer.style.width = '200px';
  142. tocContainer.style.backgroundColor = '#fff';
  143. tocContainer.style.border = '1px solid #ccc';
  144. tocContainer.style.padding = '10px';
  145. tocContainer.style.boxShadow = '0 0 10px rgba(0,0,0,0.1)';
  146. tocContainer.style.zIndex = '4';
  147. tocContainer.style.fontSize = '14px';
  148.  
  149. // 添加隐藏滚动条样式
  150. var style = document.createElement('style');
  151. style.innerHTML = `
  152. #floating-toc-container {
  153. scrollbar-width: none;
  154. -ms-overflow-style: none;
  155. }
  156. #floating-toc-container::-webkit-scrollbar {
  157. display: none;
  158. }
  159. `;
  160. document.head.appendChild(style);
  161.  
  162. return tocContainer;
  163. }
  164.  
  165.  
  166. function generateTOC() {
  167. // 检查是否存在已有的 TOC
  168. var existingTOC = getExistingToc();
  169.  
  170. var toc;
  171. if (existingTOC) {
  172. toc = genertateTOCFromExistingToc(existingTOC);
  173. }
  174.  
  175. if (toc === undefined || !toc) {
  176. toc = generateTOCFormPage();
  177. }
  178. toc.style.position = 'relative';
  179. toc.style.listStyle = 'none';
  180. toc.style.padding = '0';
  181.  
  182. return toc
  183. }
  184.  
  185. function updateMaxHeight(tocContainer) {
  186. const viewportHeight = window.innerHeight;
  187. const topOffset = parseFloat(tocContainer.style.top);
  188. tocContainer.style.maxHeight = (viewportHeight - topOffset - 20) + 'px'; // 20px 为一些额外的间距
  189. }
  190.  
  191.  
  192. (function () {
  193. 'use strict';
  194. var container = document.createElement('div');
  195. container.id = 'floating-toc-div';
  196. container.style.position = 'fixed';
  197. container.style.right = '0';
  198. container.style.top = '200px'; // 设置为 200px
  199. container.style.maxHeight = 'calc(100vh - 400px)';
  200. container.style.overflowY = 'auto';
  201. // 添加隐藏滚动条样式
  202. var style = document.createElement('style');
  203. style.innerHTML = `
  204. #floating-toc-div {
  205. scrollbar-width: none;
  206. -ms-overflow-style: none;
  207. }
  208. #floating-toc-div::-webkit-scrollbar {
  209. display: none;
  210. }
  211. `;
  212. document.head.appendChild(style);
  213. document.body.appendChild(container);
  214.  
  215. var tocContainer = buildToc();
  216. container.appendChild(tocContainer);
  217.  
  218. // 添加折叠/展开按钮
  219. const toggleButton = buildToggleButton();
  220. container.appendChild(toggleButton);
  221.  
  222. function onChange() {
  223. var tocList;
  224. tocList = document.getElementById('floating-toc-ul');
  225. if (tocList) {
  226. tocList.remove();
  227. }
  228.  
  229. tocList = generateTOC(tocContainer);
  230. tocContainer.appendChild(tocList);
  231.  
  232. // 动态计算最大高度
  233. updateMaxHeight(tocContainer);
  234. }
  235.  
  236. onChange();
  237.  
  238. var latestMainContent;
  239. var latestEditorTextarea;
  240.  
  241. window.addEventListener('load', function () {
  242. const checkMainContentExistence = setInterval(function() {
  243. const mainContent = document.getElementById('main-content');
  244. if (mainContent) {
  245. if (latestMainContent === mainContent) {
  246. return;
  247. }
  248. onChange();
  249. }
  250. }, 1000);
  251.  
  252. // 轮询检查 ak-editor-textarea 是否存在
  253. const checkTextareaExistence = setInterval(function() {
  254. const editorTextarea = document.getElementById('ak-editor-textarea');
  255. if (editorTextarea) {
  256. if (latestEditorTextarea === editorTextarea) {
  257. return;
  258. }
  259. onChange();
  260. }
  261. }, 1000);
  262.  
  263. });
  264.  
  265. // 确保目录在滚动时保持在视口内
  266. window.addEventListener('scroll', function () {
  267. updateMaxHeight(tocContainer);
  268. });
  269. })();
  270.