Greasy Fork 还支持 简体中文。

Confluence Floating TOC

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

目前為 2024-10-15 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Confluence Floating TOC
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.4
  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. // 排除特定的 h2 标签
  88. if (header.tagName === 'H2' && header.closest('[data-vc="end-of-page-recommendation-component"]')) {
  89. return;
  90. }
  91. // 排除 "快速入门" 标题
  92. if (header.closest('[data-test-id="onboarding-quickstart-experience"]')) {
  93. return;
  94. }
  95.  
  96. if (header.closest('[data-test-id="flag-visibility-wrapper"]')) {
  97. return;
  98. }
  99.  
  100. // 创建目录项
  101. var tocItem = document.createElement('li');
  102. tocItem.style.marginLeft = (parseInt(header.tagName[1]) - 1) * 10 + 'px'; // 根据标题级别缩进
  103.  
  104. // 创建链接
  105. var tocLink = document.createElement('a');
  106. tocLink.textContent = header.textContent;
  107.  
  108. // 使用标题作为 URL 片段
  109. tocLink.href = '#' + header.textContent.replace(/\s/g, '-');
  110. tocItem.appendChild(tocLink);
  111.  
  112. // 将目录项添加到目录列表中
  113. tocList.appendChild(tocItem);
  114. });
  115.  
  116. return tocList;
  117. }
  118.  
  119. function buildToggleButton() {
  120. var toggleButton = document.createElement('div');
  121. toggleButton.id = 'floating-toc-toggle';
  122. toggleButton.innerHTML = '&#9654;'; // 右箭头 Unicode 字符
  123. toggleButton.style.position = 'fixed';
  124. toggleButton.style.top = '200px';
  125. toggleButton.style.right = '0';
  126. toggleButton.style.backgroundColor = '#007bff';
  127. toggleButton.style.color = '#fff';
  128. toggleButton.style.width = '20px';
  129. toggleButton.style.height = '40px';
  130. toggleButton.style.display = 'flex';
  131. toggleButton.style.justifyContent = 'center';
  132. toggleButton.style.alignItems = 'center';
  133. toggleButton.style.cursor = 'pointer';
  134. toggleButton.style.userSelect = 'none';
  135. toggleButton.style.borderRadius = '5px 0 0 5px';
  136. toggleButton.style.zIndex = '1000';
  137. toggleButton.style.transition = 'all 0.3s ease-in-out';
  138. toggleButton.style.fontSize = '14px';
  139.  
  140. var isCollapsed = false;
  141. toggleButton.addEventListener('click', function () {
  142. var tocContainer = document.getElementById('floating-toc-container');
  143. if (isCollapsed) {
  144. tocContainer.style.right = '0';
  145. toggleButton.innerHTML = '&#9654;'; // 右箭头
  146. toggleButton.style.right = '220px'; // 调整按钮位置
  147. } else {
  148. tocContainer.style.right = '-220px'; // 完全隐藏目录
  149. toggleButton.innerHTML = '&#9664;'; // 左箭头
  150. toggleButton.style.right = '-10px';
  151. }
  152. isCollapsed = !isCollapsed;
  153. });
  154.  
  155. toggleButton.addEventListener('mouseenter', function() {
  156. if (isCollapsed) {
  157. toggleButton.style.right = '0';
  158. }
  159. });
  160.  
  161. toggleButton.addEventListener('mouseleave', function() {
  162. if (isCollapsed) {
  163. toggleButton.style.right = '-10px';
  164. }
  165. });
  166.  
  167. return toggleButton;
  168. }
  169.  
  170.  
  171. function buildToc() {
  172. var tocContainer = document.createElement('div');
  173. tocContainer.id = 'floating-toc-container';
  174. tocContainer.style.width = '220px'; // 增加宽度以包含padding
  175. tocContainer.style.backgroundColor = '#fff';
  176. tocContainer.style.border = '1px solid #ccc';
  177. tocContainer.style.padding = '10px';
  178. tocContainer.style.boxSizing = 'border-box'; // 确保padding包含在宽度内
  179. tocContainer.style.boxShadow = '0 0 10px rgba(0,0,0,0.1)';
  180. tocContainer.style.position = 'fixed';
  181. tocContainer.style.top = '200px';
  182. tocContainer.style.right = '0';
  183. tocContainer.style.maxHeight = 'calc(100vh - 300px)';
  184. tocContainer.style.overflowY = 'auto';
  185. tocContainer.style.transition = 'right 0.3s ease-in-out';
  186. tocContainer.style.zIndex = '999';
  187. tocContainer.style.scrollbarWidth = 'none';
  188. tocContainer.style.msOverflowStyle = 'none';
  189.  
  190. var style = document.createElement('style');
  191. style.textContent = `
  192. #floating-toc-container::-webkit-scrollbar {
  193. display: none;
  194. }
  195. `;
  196. document.head.appendChild(style);
  197.  
  198. var tocTitle = document.createElement('div');
  199. tocTitle.textContent = '目录';
  200. tocTitle.style.marginTop = '0';
  201. tocTitle.style.marginBottom = '10px';
  202. tocTitle.style.textAlign = 'center';
  203. tocTitle.style.fontSize = '16px';
  204. tocTitle.style.fontWeight = 'bold';
  205. tocContainer.appendChild(tocTitle);
  206.  
  207. return tocContainer;
  208. }
  209.  
  210. function generateTOC() {
  211. // var existingTOC = getExistingToc();
  212. // var toc;
  213.  
  214. // if (existingTOC) {
  215. // toc = genertateTOCFromExistingToc(existingTOC);
  216. // } else {
  217. // toc = generateTOCFormPage();
  218. // }
  219. var toc = generateTOCFormPage();
  220.  
  221. if (!toc || toc.children.length === 0) {
  222. var emptyMessage = document.createElement('div');
  223. emptyMessage.id = 'floating-toc-empty-message';
  224. emptyMessage.textContent = '当前页面没有可用的目录内容';
  225. emptyMessage.style.color = '#666';
  226. emptyMessage.style.fontStyle = 'italic';
  227. emptyMessage.style.textAlign = 'center';
  228. emptyMessage.style.padding = '20px 0';
  229. return emptyMessage;
  230. }
  231.  
  232. toc.style.listStyle = 'none';
  233. toc.style.padding = '0';
  234. toc.style.margin = '0';
  235.  
  236. // 优化目录列表样式
  237. var listItems = toc.querySelectorAll('li');
  238. listItems.forEach(function(item, index) {
  239. item.style.marginBottom = '5px';
  240. var link = item.querySelector('a');
  241. if (link) {
  242. link.style.textDecoration = 'none';
  243. link.style.color = '#333';
  244. link.style.display = 'block';
  245. link.style.padding = '3px 5px';
  246. link.style.borderRadius = '3px';
  247. link.style.transition = 'background-color 0.2s';
  248. link.style.whiteSpace = 'nowrap';
  249. link.style.overflow = 'hidden';
  250. link.style.textOverflow = 'ellipsis';
  251. link.style.maxWidth = '180px'; // 减小最大宽度
  252.  
  253. // 设置标题完整内容为title属性
  254. link.title = link.textContent;
  255.  
  256. // 截断长标题
  257. if (link.textContent.length > 25) {
  258. link.textContent = link.textContent.substring(0, 22) + '...';
  259. }
  260.  
  261. link.addEventListener('mouseover', function() {
  262. this.style.backgroundColor = '#f0f0f0';
  263. });
  264. link.addEventListener('mouseout', function() {
  265. this.style.backgroundColor = 'transparent';
  266. });
  267.  
  268. // 优化缩进
  269. var level = parseInt(item.style.marginLeft) / 10;
  270. item.style.paddingLeft = (level * 15) + 'px'; // 使用 padding 代替 margin
  271. item.style.marginLeft = '0'; // 移除左边距
  272.  
  273. // 为第三级及以下的标题添加折叠功能
  274. if (level > 2) {
  275. item.style.display = 'none';
  276. var parentLi = item.parentElement.closest('li');
  277. if (parentLi && !parentLi.classList.contains('has-submenu')) {
  278. parentLi.classList.add('has-submenu');
  279. var toggleBtn = document.createElement('span');
  280. toggleBtn.textContent = '▶';
  281. toggleBtn.style.cursor = 'pointer';
  282. toggleBtn.style.marginRight = '5px';
  283. toggleBtn.style.fontSize = '10px'; // 减小箭头大小
  284. parentLi.insertBefore(toggleBtn, parentLi.firstChild);
  285.  
  286. toggleBtn.addEventListener('click', function(e) {
  287. e.stopPropagation(); // 防止点击事件冒泡
  288. var subItems = this.parentElement.querySelectorAll('li');
  289. subItems.forEach(function(subItem) {
  290. subItem.style.display = subItem.style.display === 'none' ? 'block' : 'none';
  291. });
  292. this.textContent = this.textContent === '▶' ? '▼' : '▶';
  293. });
  294. }
  295. }
  296. }
  297. });
  298.  
  299. // 添加平滑滚动
  300. toc.style.scrollBehavior = 'smooth';
  301.  
  302. return toc;
  303. }
  304.  
  305. function updateMaxHeight(tocContainer) {
  306. const viewportHeight = window.innerHeight;
  307. const topOffset = parseFloat(tocContainer.style.top);
  308. tocContainer.style.maxHeight = (viewportHeight - topOffset - 20) + 'px'; // 20px 为一些额外的间距
  309. }
  310.  
  311.  
  312. function buildBackToTopButton() {
  313. var backToTopButton = document.createElement('div');
  314. backToTopButton.id = 'back-to-top-button';
  315. backToTopButton.innerHTML = '&#9650;'; // 上箭头 Unicode 字符
  316. backToTopButton.style.position = 'fixed';
  317. backToTopButton.style.bottom = '30px';
  318. backToTopButton.style.right = '220px'; // 调整位置,使其位于目录左侧
  319. backToTopButton.style.backgroundColor = '#007bff';
  320. backToTopButton.style.color = '#fff';
  321. backToTopButton.style.width = '40px';
  322. backToTopButton.style.height = '40px';
  323. backToTopButton.style.borderRadius = '50%';
  324. backToTopButton.style.display = 'flex';
  325. backToTopButton.style.justifyContent = 'center';
  326. backToTopButton.style.alignItems = 'center';
  327. backToTopButton.style.cursor = 'pointer';
  328. backToTopButton.style.fontSize = '20px';
  329. backToTopButton.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
  330. backToTopButton.style.transition = 'opacity 0.3s';
  331. backToTopButton.style.opacity = '0';
  332. backToTopButton.style.zIndex = '1000';
  333.  
  334. backToTopButton.addEventListener('click', function() {
  335. window.scrollTo({top: 0, behavior: 'smooth'});
  336. });
  337.  
  338. window.addEventListener('scroll', function() {
  339. if (window.pageYOffset > 100) {
  340. backToTopButton.style.opacity = '1';
  341. } else {
  342. backToTopButton.style.opacity = '0';
  343. }
  344. });
  345.  
  346. return backToTopButton;
  347. }
  348.  
  349.  
  350. (function () {
  351. 'use strict';
  352.  
  353. var tocContainer = buildToc();
  354. document.body.appendChild(tocContainer);
  355.  
  356. var toggleButton = buildToggleButton();
  357. document.body.appendChild(toggleButton);
  358.  
  359. // 初始化按钮位置
  360. toggleButton.style.right = '220px';
  361.  
  362. var backToTopButton = buildBackToTopButton();
  363. document.body.appendChild(backToTopButton);
  364.  
  365. function updateTOC() {
  366. var existingContent = document.getElementById('floating-toc-ul') || document.getElementById('floating-toc-empty-message');
  367. if (existingContent) {
  368. existingContent.remove();
  369. }
  370.  
  371. var newContent = generateTOC();
  372. tocContainer.appendChild(newContent);
  373. }
  374.  
  375. // 使用防抖函数来限制更新频率
  376. function debounce(func, wait) {
  377. let timeout;
  378. return function executedFunction(...args) {
  379. const later = () => {
  380. clearTimeout(timeout);
  381. func(...args);
  382. };
  383. clearTimeout(timeout);
  384. timeout = setTimeout(later, wait);
  385. };
  386. }
  387.  
  388. // 防抖处理的更新函数
  389. const debouncedUpdateTOC = debounce(updateTOC, 300);
  390.  
  391. // 初始化目录
  392. updateTOC();
  393.  
  394. // 监听 URL 变化
  395. var lastUrl = location.href;
  396. new MutationObserver(() => {
  397. const url = location.href;
  398. if (url !== lastUrl) {
  399. lastUrl = url;
  400. setTimeout(updateTOC, 1000); // 延迟 1 秒更新目录,确保页面内容已加载
  401. }
  402. }).observe(document, {subtree: true, childList: true});
  403.  
  404. // 监听页面内容变化,包括编辑状态下的变化
  405. var contentObserver = new MutationObserver(function(mutations) {
  406. let shouldUpdate = false;
  407. mutations.forEach(function(mutation) {
  408. if (mutation.type === 'childList') {
  409. // 检查是否有新的标题元素被添加或删除
  410. mutation.addedNodes.forEach(function(node) {
  411. if (node.nodeType === 1 && /^H[1-6]$/i.test(node.tagName)) {
  412. shouldUpdate = true;
  413. }
  414. });
  415. mutation.removedNodes.forEach(function(node) {
  416. if (node.nodeType === 1 && /^H[1-6]$/i.test(node.tagName)) {
  417. shouldUpdate = true;
  418. }
  419. });
  420. } else if (mutation.type === 'characterData') {
  421. // 检查文本内容的变化
  422. let node = mutation.target.parentNode;
  423. while (node && node !== document.body) {
  424. if (/^H[1-6]$/i.test(node.tagName)) {
  425. shouldUpdate = true;
  426. break;
  427. }
  428. node = node.parentNode;
  429. }
  430. }
  431. });
  432.  
  433. if (shouldUpdate) {
  434. debouncedUpdateTOC();
  435. }
  436. });
  437.  
  438. contentObserver.observe(document.body, {
  439. childList: true,
  440. subtree: true,
  441. characterData: true
  442. });
  443.  
  444. // ... 其他现有代码 ...
  445. })();