Confluence Floating TOC

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

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

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