Nested Outline Headings

Adds nesting functionality to outline headings, in addition to right click menu option to fold/unfold up to desired level.

安装此脚本?
作者推荐脚本

您可能也喜欢Google Docs Outline Expander

安装此脚本
  1. // ==UserScript==
  2. // @name Nested Outline Headings
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.2
  5. // @description Adds nesting functionality to outline headings, in addition to right click menu option to fold/unfold up to desired level.
  6. // @match *://docs.google.com/document/*
  7. // @match https://docs.google.com/document/d/*
  8. // @grant none
  9. // @license MIT
  10. // @run-at document-end
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. // ----------------------------
  17. // Shared utility functions
  18. // ----------------------------
  19.  
  20. // Returns the heading level from an outline item element.
  21. function getHeadingLevel(item) {
  22. const content = item.querySelector('.navigation-item-content');
  23. if (!content) return null;
  24. for (const cls of content.classList) {
  25. if (cls.startsWith('navigation-item-level-')) {
  26. return parseInt(cls.split('-').pop(), 10);
  27. }
  28. }
  29. return null;
  30. }
  31.  
  32. // Returns the container that holds the headings (the updating-navigation-item-list)
  33. // for the currently selected chapter item (subtab).
  34. function getActiveHeadingsContainer() {
  35. // Find the chapter header that is marked as selected.
  36. const selectedHeader = document.querySelector('.chapter-item-label-and-buttons-container[aria-selected="true"]');
  37. if (selectedHeader) {
  38. // Locate the parent chapter item.
  39. const chapterItem = selectedHeader.closest('.chapter-item');
  40. if (chapterItem) {
  41. // Return its associated headings container.
  42. return chapterItem.querySelector('.updating-navigation-item-list');
  43. }
  44. }
  45. return null;
  46. }
  47.  
  48. // Updates the inherited selection highlight in the outline.
  49. function updateInheritedSelection() {
  50. document.querySelectorAll('.navigation-item.inherited-selected').forEach(item => {
  51. item.classList.remove('inherited-selected');
  52. });
  53. const selected = document.querySelector('.navigation-item.location-indicator-highlight');
  54. if (!selected) return;
  55. if (!selected.classList.contains('folded')) return;
  56. const selectedLevel = getHeadingLevel(selected);
  57. if (selectedLevel === null) return;
  58. const headings = Array.from(document.querySelectorAll('.navigation-item'));
  59. const selectedIndex = headings.indexOf(selected);
  60. let parentCandidate = null;
  61. for (let i = selectedIndex - 1; i >= 0; i--) {
  62. const candidate = headings[i];
  63. const candidateLevel = getHeadingLevel(candidate);
  64. if (candidateLevel !== null && candidateLevel < selectedLevel && !candidate.classList.contains('folded')) {
  65. parentCandidate = candidate;
  66. break;
  67. }
  68. }
  69. if (parentCandidate) {
  70. parentCandidate.classList.add('inherited-selected');
  71. }
  72. }
  73.  
  74. // (Legacy) This function still returns the container for top-level tabs.
  75. // It is kept here if needed for other purposes.
  76. function getActiveTabContainer() {
  77. const tabs = document.querySelectorAll('div.chapter-container[id^="chapter-container-"]');
  78. for (const tab of tabs) {
  79. const selected = tab.querySelector('.chapter-item-label-and-buttons-container[aria-selected="true"]');
  80. if (selected) return tab;
  81. }
  82. return null;
  83. }
  84.  
  85. // ----------------------------
  86. // Integration: Folding function
  87. // ----------------------------
  88. // Global function to fold (collapse) the outline to a given level.
  89. // All headings with a level greater than or equal to targetLevel (within the active subtab's headings list)
  90. // will be folded.
  91. window.foldToLevel = function(targetLevel) {
  92. const headingsContainer = getActiveHeadingsContainer();
  93. const headings = headingsContainer ? headingsContainer.querySelectorAll('.navigation-item') : [];
  94. headings.forEach(item => {
  95. const level = getHeadingLevel(item);
  96. if (level === null) return;
  97.  
  98. const toggle = item.querySelector('.custom-toggle-button');
  99.  
  100. // If this heading is exactly one level above the target,
  101. // update its toggle button state only.
  102. if (level === targetLevel - 1) {
  103. if (toggle) {
  104. // Mark the toggle as collapsed.
  105. toggle.dataset.expanded = 'false';
  106. const inner = toggle.querySelector('.chapterItemArrowContainer');
  107. inner.setAttribute('aria-expanded', 'false');
  108. inner.setAttribute('aria-label', 'Expand subheadings');
  109. const icon = inner.querySelector('.material-symbols-outlined');
  110. icon.style.display = 'inline-block';
  111. icon.style.transformOrigin = 'center center';
  112. icon.style.transform = 'rotate(-90deg)';
  113. item.classList.add("toggle-on");
  114. }
  115. return;
  116. }
  117.  
  118. // For all other headings, use the normal logic.
  119. const shouldExpand = level < targetLevel;
  120. if (shouldExpand) {
  121. item.classList.remove('folded');
  122. if (toggle) {
  123. toggle.dataset.expanded = 'true';
  124. const inner = toggle.querySelector('.chapterItemArrowContainer');
  125. inner.setAttribute('aria-expanded', 'true');
  126. inner.setAttribute('aria-label', 'Collapse subheadings');
  127. const icon = inner.querySelector('.material-symbols-outlined');
  128. icon.style.display = 'inline-block';
  129. icon.style.transformOrigin = 'center center';
  130. icon.style.transform = 'rotate(-45deg)';
  131. item.classList.remove("toggle-on");
  132. } else {
  133. item.classList.remove("toggle-on");
  134. }
  135. expandChildren(item, level);
  136. } else {
  137. item.classList.add('folded');
  138. if (toggle) {
  139. toggle.dataset.expanded = 'false';
  140. const inner = toggle.querySelector('.chapterItemArrowContainer');
  141. inner.setAttribute('aria-expanded', 'false');
  142. inner.setAttribute('aria-label', 'Expand subheadings');
  143. const icon = inner.querySelector('.material-symbols-outlined');
  144. icon.style.display = 'inline-block';
  145. icon.style.transformOrigin = 'center center';
  146. icon.style.transform = 'rotate(-90deg)';
  147. item.classList.add("toggle-on");
  148. } else {
  149. item.classList.remove("toggle-on");
  150. }
  151. collapseChildren(item, level);
  152. }
  153. });
  154. updateInheritedSelection();
  155. };
  156.  
  157. // ----------------------------
  158. // "Show headings" menu (First Script)
  159. // ----------------------------
  160. function isCorrectMenu(menu) {
  161. const labels = menu.querySelectorAll('.goog-menuitem-label');
  162. return Array.from(labels).some(label => label.textContent.trim() === "Choose emoji");
  163. }
  164.  
  165. function menuHasShowHeadings(menu) {
  166. const labels = menu.querySelectorAll('.goog-menuitem-label');
  167. return Array.from(labels).some(label => label.textContent.trim() === "Show headings");
  168. }
  169.  
  170. // Dynamically update the submenu items.
  171. function updateSubmenu(submenu) {
  172. while (submenu.firstChild) {
  173. submenu.removeChild(submenu.firstChild);
  174. }
  175. // Use the headings only from the active subtab's headings container.
  176. const headingsContainer = getActiveHeadingsContainer();
  177. const headings = headingsContainer ? headingsContainer.querySelectorAll('.navigation-item') : [];
  178. let maxDisplayLevel = 0;
  179. headings.forEach(heading => {
  180. const rawLevel = getHeadingLevel(heading);
  181. if (rawLevel !== null) {
  182. const displayLevel = rawLevel + 1; // adjust to get the correct display level
  183. if (displayLevel > maxDisplayLevel) {
  184. maxDisplayLevel = displayLevel;
  185. }
  186. }
  187. });
  188. if (maxDisplayLevel === 0) {
  189. const item = document.createElement('div');
  190. item.className = "goog-menuitem";
  191. item.style.userSelect = "none";
  192. item.style.fontStyle = "italic";
  193. item.style.color = "#9aa0a6";
  194. const contentDiv = document.createElement('div');
  195. contentDiv.className = "goog-menuitem-content";
  196. const innerDiv = document.createElement('div');
  197. innerDiv.textContent = "No headings";
  198. contentDiv.appendChild(innerDiv);
  199. item.appendChild(contentDiv);
  200. submenu.appendChild(item);
  201. } else {
  202. for (let i = 1; i <= maxDisplayLevel; i++) {
  203. const item = document.createElement('div');
  204. item.className = "goog-menuitem";
  205. item.setAttribute("role", "menuitem");
  206. item.style.userSelect = "none";
  207.  
  208. const contentDiv = document.createElement('div');
  209. contentDiv.className = "goog-menuitem-content";
  210.  
  211. const innerDiv = document.createElement('div');
  212. innerDiv.setAttribute("aria-label", `Level ${i}`);
  213. innerDiv.textContent = `Level ${i}`;
  214.  
  215. contentDiv.appendChild(innerDiv);
  216. item.appendChild(contentDiv);
  217.  
  218. item.addEventListener('mouseenter', function() {
  219. item.classList.add('goog-menuitem-highlight');
  220. });
  221. item.addEventListener('mouseleave', function() {
  222. item.classList.remove('goog-menuitem-highlight');
  223. });
  224.  
  225. item.addEventListener('click', function(e) {
  226. window.foldToLevel(i);
  227. submenu.style.display = "none";
  228. });
  229.  
  230. submenu.appendChild(item);
  231. }
  232. }
  233. }
  234.  
  235. // Create an initially empty submenu.
  236. function createSubmenu() {
  237. const submenu = document.createElement('div');
  238. submenu.className = "goog-menu goog-menu-vertical docs-material shell-menu shell-tight-menu goog-menu-noaccel goog-menu-noicon";
  239. submenu.setAttribute("role", "menu");
  240. submenu.style.userSelect = "none";
  241. submenu.style.position = "absolute";
  242. submenu.style.display = "none";
  243. submenu.style.zIndex = 1003;
  244. submenu.style.background = "#fff";
  245. submenu.style.border = "1px solid transparent";
  246. submenu.style.borderRadius = "4px";
  247. submenu.style.boxShadow = "0 2px 6px 2px rgba(60,64,67,.15)";
  248. submenu.style.padding = "6px 0";
  249. submenu.style.fontSize = "13px";
  250. submenu.style.margin = "0";
  251.  
  252. document.body.appendChild(submenu);
  253. return submenu;
  254. }
  255.  
  256. // Create the "Show headings" menu option and attach the dynamic submenu.
  257. function createShowHeadingsOption() {
  258. const menuItem = document.createElement('div');
  259. menuItem.className = "goog-menuitem apps-menuitem goog-submenu";
  260. menuItem.setAttribute("role", "menuitem");
  261. menuItem.setAttribute("aria-haspopup", "true");
  262. menuItem.style.userSelect = "none";
  263. menuItem.dataset.showheadings = "true";
  264.  
  265. const contentDiv = document.createElement('div');
  266. contentDiv.className = "goog-menuitem-content";
  267. contentDiv.style.userSelect = "none";
  268.  
  269. const iconDiv = document.createElement('div');
  270. iconDiv.className = "docs-icon goog-inline-block goog-menuitem-icon";
  271. iconDiv.setAttribute("aria-hidden", "true");
  272. iconDiv.style.userSelect = "none";
  273.  
  274. const innerIconDiv = document.createElement('div');
  275. innerIconDiv.className = "docs-icon-img-container docs-icon-img docs-icon-editors-ia-header-footer";
  276. innerIconDiv.style.userSelect = "none";
  277. iconDiv.appendChild(innerIconDiv);
  278.  
  279. const labelSpan = document.createElement('span');
  280. labelSpan.className = "goog-menuitem-label";
  281. labelSpan.style.userSelect = "none";
  282. labelSpan.textContent = "Show headings";
  283.  
  284. const arrowSpan = document.createElement('span');
  285. arrowSpan.className = "goog-submenu-arrow";
  286. arrowSpan.style.userSelect = "none";
  287. arrowSpan.textContent = "►";
  288.  
  289. contentDiv.appendChild(iconDiv);
  290. contentDiv.appendChild(labelSpan);
  291. contentDiv.appendChild(arrowSpan);
  292. menuItem.appendChild(contentDiv);
  293.  
  294. const submenu = createSubmenu();
  295. menuItem._submenu = submenu;
  296.  
  297. menuItem.addEventListener('mouseenter', function() {
  298. menuItem.classList.add('goog-menuitem-highlight');
  299. updateSubmenu(submenu);
  300. const rect = menuItem.getBoundingClientRect();
  301. submenu.style.left = `${rect.right}px`;
  302. submenu.style.top = `${rect.top}px`;
  303.  
  304. submenu.style.display = "block";
  305. });
  306.  
  307. document.addEventListener('click', function(e) {
  308. if (submenu.style.display === "block" && !submenu.contains(e.target)) {
  309. submenu.style.display = "none";
  310. menuItem.classList.remove('goog-menuitem-highlight');
  311. }
  312. });
  313.  
  314. return menuItem;
  315. }
  316.  
  317. function processMenu(menu) {
  318. if (!isCorrectMenu(menu)) return;
  319. if (menuHasShowHeadings(menu)) return;
  320.  
  321. const newMenuItem = createShowHeadingsOption();
  322.  
  323. const firstSeparator = menu.querySelector('.apps-hoverable-menu-separator-container');
  324. if (firstSeparator) {
  325. let lastItem = null;
  326. let sibling = firstSeparator.nextElementSibling;
  327. while (sibling && !sibling.matches('.apps-hoverable-menu-separator-container')) {
  328. if (sibling.matches('.goog-menuitem')) {
  329. lastItem = sibling;
  330. }
  331. sibling = sibling.nextElementSibling;
  332. }
  333. if (lastItem) {
  334. if (lastItem.nextElementSibling) {
  335. menu.insertBefore(newMenuItem, lastItem.nextElementSibling);
  336. } else {
  337. menu.appendChild(newMenuItem);
  338. }
  339. } else {
  340. if (firstSeparator.nextSibling) {
  341. menu.insertBefore(newMenuItem, firstSeparator.nextSibling);
  342. } else {
  343. menu.appendChild(newMenuItem);
  344. }
  345. }
  346. } else {
  347. menu.appendChild(newMenuItem);
  348. }
  349.  
  350. if (!menu.dataset.showHeadingsListener) {
  351. menu.addEventListener('mouseenter', function(e) {
  352. const targetMenuItem = e.target.closest('.goog-menuitem');
  353. if (targetMenuItem && targetMenuItem.dataset.showheadings !== "true") {
  354. newMenuItem._submenu.style.display = "none";
  355. newMenuItem.classList.remove('goog-menuitem-highlight');
  356. }
  357. }, true);
  358. menu.dataset.showHeadingsListener = "true";
  359. }
  360. }
  361.  
  362. const menuObserver = new MutationObserver(mutations => {
  363. mutations.forEach(mutation => {
  364. mutation.addedNodes.forEach(node => {
  365. if (node.nodeType === Node.ELEMENT_NODE) {
  366. if (node.matches && node.matches('.goog-menu.goog-menu-vertical.docs-material.goog-menu-noaccel')) {
  367. processMenu(node);
  368. } else {
  369. const menus = node.querySelectorAll && node.querySelectorAll('.goog-menu.goog-menu-vertical.docs-material.goog-menu-noaccel');
  370. if (menus && menus.length > 0) {
  371. menus.forEach(menu => processMenu(menu));
  372. }
  373. }
  374. }
  375. });
  376. });
  377. });
  378.  
  379. menuObserver.observe(document.body, {childList: true, subtree: true});
  380.  
  381. // ----------------------------
  382. // Outline Sidebar Modifications (Second Script)
  383. // ----------------------------
  384.  
  385. const materialLink = document.createElement('link');
  386. materialLink.rel = 'stylesheet';
  387. materialLink.href = 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200';
  388. document.head.appendChild(materialLink);
  389.  
  390. const style = document.createElement('style');
  391. style.textContent =
  392. `.custom-toggle-button {
  393. opacity: 0;
  394. pointer-events: none;
  395. transition: opacity 0.3s;
  396. position: absolute;
  397. top: 50%;
  398. transform: translateY(-50%);
  399. cursor: pointer;
  400. z-index: 3;
  401. }
  402. .custom-toggle-button .goog-flat-button {
  403. width: 22px !important;
  404. height: 22px !important;
  405. display: flex !important;
  406. align-items: center !important;
  407. justify-content: center !important;
  408. border-radius: 50% !important;
  409. }
  410. .custom-toggle-button .material-symbols-outlined {
  411. color: #5f6368 !important;
  412. }
  413. .navigation-item-content-container {
  414. position: relative !important;
  415. overflow: visible !important;
  416. z-index: 0 !important;
  417. }
  418. .navigation-item-content {
  419. position: relative;
  420. z-index: 1;
  421. }
  422. .folded {
  423. opacity: 0;
  424. height: 0 !important;
  425. overflow: hidden;
  426. pointer-events: none;
  427. margin: 0 !important;
  428. padding: 0 !important;
  429. }
  430. .navigation-item.inherited-selected .navigation-item-content {
  431. color: #1967d2 !important;
  432. font-weight: 500 !important;
  433. }
  434. .navigation-item.inherited-selected .navigation-item-vertical-line-middle {
  435. background-color: #1967d2 !important;
  436. }
  437. .navigation-item.toggle-on .navigation-item-content-container::before {
  438. content: "";
  439. position: absolute !important;
  440. top: 50% !important;
  441. left: 5px !important;
  442. right: -5px !important;
  443. transform: translateY(-50%) !important;
  444. height: 80% !important;
  445. background-color: #f0f4f9 !important;
  446. border-radius: 5px !important;
  447. z-index: -1 !important;
  448. }
  449. .navigation-item-vertical-line {
  450. position: relative;
  451. z-index: 1;
  452. }`;
  453. document.head.appendChild(style);
  454.  
  455. function stopEvent(e) {
  456. e.stopPropagation();
  457. e.preventDefault();
  458. e.stopImmediatePropagation();
  459. }
  460.  
  461. function createToggleButton(expanded = true) {
  462. const btn = document.createElement('div');
  463. btn.className = 'custom-toggle-button';
  464. btn.dataset.expanded = expanded ? 'true' : 'false';
  465.  
  466. const inner = document.createElement('div');
  467. inner.className = 'goog-inline-block goog-flat-button chapterItemArrowContainer';
  468. inner.setAttribute('role', 'button');
  469. inner.setAttribute('aria-expanded', expanded ? 'true' : 'false');
  470. inner.setAttribute('aria-label', expanded ? 'Collapse subheadings' : 'Expand subheadings');
  471.  
  472. const icon = document.createElement('span');
  473. icon.className = 'material-symbols-outlined';
  474. icon.textContent = 'arrow_drop_down';
  475. icon.style.display = 'inline-block';
  476. icon.style.transition = 'transform 0.3s';
  477. icon.style.transformOrigin = 'center center';
  478. icon.style.transform = expanded ? 'rotate(-45deg)' : 'rotate(-90deg)';
  479.  
  480. inner.appendChild(icon);
  481. btn.appendChild(inner);
  482. return btn;
  483. }
  484.  
  485. function expandChildren(item, level) {
  486. let sibling = item.nextElementSibling;
  487. while (sibling) {
  488. const sibLevel = getHeadingLevel(sibling);
  489. if (sibLevel === null) {
  490. sibling = sibling.nextElementSibling;
  491. continue;
  492. }
  493. if (sibLevel <= level) break;
  494. if (sibLevel === level + 1) {
  495. sibling.classList.remove('folded');
  496. const childToggle = sibling.querySelector('.custom-toggle-button');
  497. if (childToggle && childToggle.dataset.expanded === 'true') {
  498. expandChildren(sibling, sibLevel);
  499. }
  500. }
  501. sibling = sibling.nextElementSibling;
  502. }
  503. }
  504.  
  505. function collapseChildren(item, level) {
  506. let sibling = item.nextElementSibling;
  507. while (sibling) {
  508. const sibLevel = getHeadingLevel(sibling);
  509. if (sibLevel === null) {
  510. sibling = sibling.nextElementSibling;
  511. continue;
  512. }
  513. if (sibLevel <= level) break;
  514. sibling.classList.add('folded');
  515. sibling = sibling.nextElementSibling;
  516. }
  517. }
  518.  
  519. // --------------------------------------------
  520. // New: Recursively toggle descendant headings
  521. // --------------------------------------------
  522. function simulateToggleForDescendants(parentHeading, parentOldState) {
  523. const parentLevel = getHeadingLevel(parentHeading);
  524. let sibling = parentHeading.nextElementSibling;
  525. while (sibling) {
  526. const sibLevel = getHeadingLevel(sibling);
  527. if (sibLevel === null) {
  528. sibling = sibling.nextElementSibling;
  529. continue;
  530. }
  531. if (sibLevel <= parentLevel) break;
  532. const childToggle = sibling.querySelector('.custom-toggle-button');
  533. if (childToggle && childToggle.dataset.expanded === parentOldState.toString()) {
  534. // Dispatch a normal click event (without ctrl) on the child's toggle button.
  535. childToggle.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, ctrlKey: false }));
  536. }
  537. sibling = sibling.nextElementSibling;
  538. }
  539. }
  540.  
  541. // Update addToggleButtons to operate only on headings in the active headings container.
  542. function addToggleButtons() {
  543. const headingsContainer = getActiveHeadingsContainer();
  544. if (!headingsContainer) return;
  545. const headings = headingsContainer.querySelectorAll('.navigation-item');
  546. headings.forEach(heading => {
  547. const container = heading.querySelector('.navigation-item-content-container');
  548. if (!container) return;
  549. container.style.position = 'relative';
  550. const level = getHeadingLevel(heading);
  551. if (level === null) return;
  552.  
  553. let hasChildren = false;
  554. let sibling = heading.nextElementSibling;
  555. while (sibling) {
  556. const sibLevel = getHeadingLevel(sibling);
  557. if (sibLevel === null) {
  558. sibling = sibling.nextElementSibling;
  559. continue;
  560. }
  561. if (sibLevel > level) {
  562. hasChildren = true;
  563. break;
  564. } else break;
  565. }
  566. if (hasChildren && !container.querySelector('.custom-toggle-button')) {
  567. const toggleBtn = createToggleButton(true);
  568. const computedLeft = (-2 + level * 12) + "px";
  569. toggleBtn.style.left = computedLeft;
  570.  
  571. container.insertBefore(toggleBtn, container.firstChild);
  572. ['mousedown', 'pointerdown', 'touchstart'].forEach(evt => {
  573. toggleBtn.addEventListener(evt, stopEvent, true);
  574. });
  575. toggleBtn.addEventListener('click', (e) => {
  576. stopEvent(e);
  577. // Store the parent heading's old toggle state.
  578. const parentOldState = toggleBtn.dataset.expanded === 'true';
  579. // Toggle the parent's state.
  580. toggleBtn.dataset.expanded = (!parentOldState).toString();
  581. const inner = toggleBtn.querySelector('.chapterItemArrowContainer');
  582. inner.setAttribute('aria-expanded', (!parentOldState).toString());
  583. inner.setAttribute('aria-label', !parentOldState ? 'Collapse subheadings' : 'Expand subheadings');
  584. const icon = inner.querySelector('.material-symbols-outlined');
  585. icon.style.display = 'inline-block';
  586. icon.style.transformOrigin = 'center center';
  587. if (!parentOldState) {
  588. icon.style.transform = 'rotate(-45deg)';
  589. heading.classList.remove("toggle-on");
  590. expandChildren(heading, level);
  591. } else {
  592. icon.style.transform = 'rotate(-90deg)';
  593. heading.classList.add("toggle-on");
  594. collapseChildren(heading, level);
  595. }
  596. updateInheritedSelection();
  597. // If ctrl key is pressed, simulate a click on all descendant toggles
  598. // that have the same state as the parent's old state.
  599. if (e.ctrlKey) {
  600. simulateToggleForDescendants(heading, parentOldState);
  601. }
  602. }, true);
  603. }
  604. });
  605. }
  606.  
  607. // Update updateVerticalLineWidth to only update headings from the active headings container.
  608. function updateVerticalLineWidth() {
  609. const headingsContainer = getActiveHeadingsContainer();
  610. if (!headingsContainer) return;
  611. const navigationItems = headingsContainer.querySelectorAll('.navigation-item');
  612. navigationItems.forEach(item => {
  613. const verticalLine = item.querySelector('.navigation-item-vertical-line');
  614. if (verticalLine) {
  615. const width = verticalLine.offsetWidth;
  616. item.style.setProperty('--vertical-line-width', width + 'px');
  617. }
  618. });
  619. }
  620.  
  621. function setupToggleVisibility() {
  622. function init() {
  623. const widget = document.querySelector('.outlines-widget');
  624. if (!widget) {
  625. setTimeout(init, 1000);
  626. return;
  627. }
  628. let hideTimer;
  629. widget.addEventListener('mouseenter', () => {
  630. if (hideTimer) clearTimeout(hideTimer);
  631. widget.querySelectorAll('.custom-toggle-button').forEach(btn => {
  632. btn.style.opacity = '1';
  633. btn.style.pointerEvents = 'auto';
  634. });
  635. });
  636. widget.addEventListener('mouseleave', () => {
  637. hideTimer = setTimeout(() => {
  638. widget.querySelectorAll('.custom-toggle-button').forEach(btn => {
  639. if (btn.dataset.expanded === 'true') {
  640. btn.style.opacity = '0';
  641. btn.style.pointerEvents = 'none';
  642. }
  643. });
  644. }, 3000);
  645. });
  646. }
  647. init();
  648. }
  649.  
  650. let debounceTimer;
  651. function debounceUpdate() {
  652. if (debounceTimer) clearTimeout(debounceTimer);
  653. debounceTimer = setTimeout(() => {
  654. addToggleButtons();
  655. updateInheritedSelection();
  656. updateVerticalLineWidth();
  657. }, 100);
  658. }
  659.  
  660. const outlineObserver = new MutationObserver(debounceUpdate);
  661. outlineObserver.observe(document.body, { childList: true, subtree: true });
  662.  
  663. // Initial outline setup.
  664. addToggleButtons();
  665. updateInheritedSelection();
  666. updateVerticalLineWidth();
  667. setupToggleVisibility();
  668.  
  669. const readyObserver = new MutationObserver((mutations, obs) => {
  670. if (document.querySelector('#kix-outlines-widget-header-text-chaptered')) {
  671. obs.disconnect();
  672. addToggleButtons();
  673. updateInheritedSelection();
  674. updateVerticalLineWidth();
  675. setupToggleVisibility();
  676. }
  677. });
  678. readyObserver.observe(document.body, { childList: true, subtree: true });
  679.  
  680. // ----------------------------
  681. // Adjust Chapter Overflow Menu Position
  682. // ----------------------------
  683. // This function continuously checks the position of the chapter overflow menu
  684. // and, if it is visible and its top is above the top of the navigation widget hat,
  685. // resets its top so that it is never displayed above the hat.
  686. function adjustChapterOverflowMenuPosition() {
  687. const menu = document.querySelector('div.chapter-overflow-menu');
  688. if (!menu) return;
  689. if (window.getComputedStyle(menu).display === 'none') return;
  690. const menuRect = menu.getBoundingClientRect();
  691. const hat = document.querySelector('.navigation-widget-hat');
  692. if (!hat) return;
  693. const hatRect = hat.getBoundingClientRect();
  694. if (menuRect.top < hatRect.top) {
  695. menu.style.top = `${hatRect.top}px`;
  696. }
  697. }
  698. setInterval(adjustChapterOverflowMenuPosition, 100);
  699.  
  700. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址