Nested Outline Headings

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

目前為 2025-03-31 提交的版本,檢視 最新版本

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