mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-10-31 21:11:30 +08:00 
			
		
		
		
	feat(toc): Collapsible TOC
This commit is contained in:
		
							parent
							
								
									e946bde939
								
							
						
					
					
						commit
						c80d7a3ec3
					
				| @ -58,6 +58,7 @@ export interface ViewScope { | ||||
|      * toc will appear and then close immediately, because getToc(html) function will consume time | ||||
|      */ | ||||
|     tocPreviousVisible?: boolean; | ||||
|     tocCollapsedHeadings?:  Set<string>; | ||||
| } | ||||
| 
 | ||||
| interface CreateLinkOptions { | ||||
|  | ||||
| @ -13,7 +13,7 @@ const TPL = /*html*/` | ||||
|             height: 300px; | ||||
|         } | ||||
| 
 | ||||
|         .open-full-button, .collapse-button { | ||||
|         .note-map-ribbon-widget .open-full-button, .note-map-ribbon-widget .collapse-button { | ||||
|             position: absolute; | ||||
|             right: 5px; | ||||
|             bottom: 5px; | ||||
|  | ||||
| @ -29,23 +29,68 @@ const TPL = /*html*/`<div class="toc-widget"> | ||||
|             contain: none; | ||||
|             overflow: auto; | ||||
|             position: relative; | ||||
|             padding-left:0px !important; | ||||
|         } | ||||
| 
 | ||||
|         .toc ol { | ||||
|             padding-left: 25px; | ||||
|             position: relative; | ||||
|             overflow: hidden; | ||||
|             padding-left: 20px; | ||||
|             transition: max-height 0.3s ease; | ||||
|         } | ||||
| 
 | ||||
|         .toc > ol { | ||||
|             padding-left: 20px; | ||||
|             padding-left: 0px; | ||||
|         } | ||||
| 
 | ||||
|         .toc li.collapsed + ol { | ||||
|             display:none; | ||||
|         } | ||||
| 
 | ||||
|         .toc li + ol:before { | ||||
|             content: ""; | ||||
|             position: absolute; | ||||
|             height: 100%; | ||||
|             left: 17px; | ||||
|             border-left: 1px solid var(--main-border-color); | ||||
|         } | ||||
| 
 | ||||
|         .toc li { | ||||
|             display: flex; | ||||
|             position: relative; | ||||
|             list-style: none; | ||||
|             align-items: center;  | ||||
|             padding-left: 7px; | ||||
|             cursor: pointer; | ||||
|             text-align: justify; | ||||
|             word-wrap: break-word; | ||||
|             hyphens: auto; | ||||
|         } | ||||
| 
 | ||||
|         .toc li .collapse-button { | ||||
|             display: flex; | ||||
|             position: relative; | ||||
|             width: 20px; | ||||
|             height: 20px; | ||||
|             flex-shrink: 0; | ||||
|             align-items: center; | ||||
|             justify-content: center; | ||||
|             transition: transform 0.3s ease; | ||||
|         } | ||||
| 
 | ||||
|         .toc li.collapsed .collapse-button { | ||||
|             transform: rotate(-90deg); | ||||
|         } | ||||
| 
 | ||||
|         .toc li .item-content { | ||||
|             margin-left: 28px; | ||||
|             flex: 1; | ||||
|         } | ||||
| 
 | ||||
|         .toc li .collapse-button + .item-content { | ||||
|             margin-left: 8px; | ||||
|         } | ||||
| 
 | ||||
|         .toc li:hover { | ||||
|             font-weight: bold; | ||||
|         } | ||||
| @ -231,6 +276,14 @@ export default class TocWidget extends RightPanelWidget { | ||||
|         // Note heading 2 is the first level Trilium makes available to the note
 | ||||
|         let curLevel = 2; | ||||
|         const $ols = [$toc]; | ||||
|         let $previousLi: JQuery<HTMLElement> | undefined; | ||||
|          | ||||
|         if (!(this.noteContext?.viewScope?.tocCollapsedHeadings instanceof Set)) { | ||||
|             this.noteContext!.viewScope!.tocCollapsedHeadings = new Set<string>(); | ||||
|         } | ||||
|         const tocCollapsedHeadings = this.noteContext!.viewScope!.tocCollapsedHeadings as Set<string>; | ||||
|         const validHeadingKeys = new Set<string>(); // Used to clean up obsolete entries in tocCollapsedHeadings
 | ||||
|          | ||||
|         let headingCount = 0; | ||||
|         for (let m = null, headingIndex = 0; (m = headingTagsRegex.exec(html)) !== null; headingIndex++) { | ||||
|             //
 | ||||
| @ -244,6 +297,11 @@ export default class TocWidget extends RightPanelWidget { | ||||
|                     const $ol = $("<ol>"); | ||||
|                     $ols[$ols.length - 1].append($ol); | ||||
|                     $ols.push($ol); | ||||
| 
 | ||||
|                     if ($previousLi) { | ||||
|                         const headingKey = `h${newLevel}_${headingIndex}_${$previousLi?.text().trim()}`; | ||||
|                         this.setupCollapsibleHeading($ol, $previousLi, headingKey, tocCollapsedHeadings, validHeadingKeys); | ||||
|                     }                     | ||||
|                 } | ||||
|             } else if (levelDelta < 0) { | ||||
|                 // Close as many lists as curLevel - newLevel
 | ||||
| @ -259,10 +317,20 @@ export default class TocWidget extends RightPanelWidget { | ||||
|             //
 | ||||
| 
 | ||||
|             const headingText = await this.replaceMathTextWithKatax(m[2]); | ||||
|             const $li = $("<li>").html(headingText); | ||||
|             $li.on("click", () => this.jumpToHeading(headingIndex)); | ||||
|             const $itemContent = $('<div class="item-content">').html(headingText).on("click", () => { | ||||
|                 this.jumpToHeading(headingIndex); | ||||
|             }); | ||||
|             const $li = $("<li>").append($itemContent); | ||||
|             $ols[$ols.length - 1].append($li); | ||||
|             headingCount = headingIndex; | ||||
|             $previousLi = $li; | ||||
|         } | ||||
| 
 | ||||
|         //  Clean up unused entries in tocCollapsedHeadings
 | ||||
|         for (const key of tocCollapsedHeadings) { | ||||
|             if (!validHeadingKeys.has(key)) { | ||||
|                 tocCollapsedHeadings.delete(key); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         $toc = this.pullLeft($toc); | ||||
| @ -286,7 +354,7 @@ export default class TocWidget extends RightPanelWidget { | ||||
| 
 | ||||
|             const $first = $toc.children(":first"); | ||||
| 
 | ||||
|             if ($first[0].tagName !== "OL") { | ||||
|             if ($first[0].tagName.toLowerCase() !== "ol") { | ||||
|                 break; | ||||
|             } | ||||
| 
 | ||||
| @ -320,6 +388,59 @@ export default class TocWidget extends RightPanelWidget { | ||||
|         headingElement?.scrollIntoView({ behavior: "smooth" }); | ||||
|     } | ||||
| 
 | ||||
|     async setupCollapsibleHeading($ol: JQuery<HTMLElement>, $previousLi: JQuery<HTMLElement>, headingKey: string, tocCollapsedHeadings: Set<string>, validHeadingKeys: Set<string>) { | ||||
|         if ($previousLi && $previousLi.find(".collapse-button").length === 0) { | ||||
|             const $collapseButton = $('<div class="collapse-button bx bx-chevron-down"></div>'); | ||||
|             $previousLi.prepend($collapseButton); | ||||
| 
 | ||||
|             // Restore the previous collapsed state
 | ||||
|             if (tocCollapsedHeadings?.has(headingKey)) { | ||||
|                 $previousLi.addClass("collapsed"); | ||||
|                 validHeadingKeys.add(headingKey); | ||||
|             } else { | ||||
|                 $previousLi.removeClass("collapsed"); | ||||
|             } | ||||
| 
 | ||||
|             $collapseButton.on("click", () => { | ||||
|                 if ($previousLi.hasClass("animating")) return; | ||||
|                 const willCollapse  = !$previousLi.hasClass("collapsed"); | ||||
|                 $previousLi.addClass("animating"); | ||||
| 
 | ||||
|                 if (willCollapse) { // Collapse 
 | ||||
|                     $ol.css("maxHeight", `${$ol.prop("scrollHeight")}px`); | ||||
|                     requestAnimationFrame(() => { | ||||
|                         requestAnimationFrame(() => { | ||||
|                             $ol.css("maxHeight", "0px"); | ||||
|                             $collapseButton.css("transform", "rotate(-90deg)"); | ||||
|                         }); | ||||
|                     }); | ||||
|                     setTimeout(() => { | ||||
|                         $ol.css("maxHeight", ""); | ||||
|                         $previousLi.addClass("collapsed"); | ||||
|                         $previousLi.removeClass("animating"); | ||||
|                     }, 300); | ||||
|                 } else { // Expand
 | ||||
|                     $previousLi.removeClass("collapsed"); | ||||
|                     $ol.css("maxHeight", "0px"); | ||||
|                     requestAnimationFrame(() => { | ||||
|                         $ol.css("maxHeight", `${$ol.prop("scrollHeight")}px`); | ||||
|                         $collapseButton.css("transform", ""); | ||||
|                     }); | ||||
|                     setTimeout(() => { | ||||
|                         $ol.css("maxHeight", ""); | ||||
|                         $previousLi.removeClass("animating"); | ||||
|                     }, 300); | ||||
|                 } | ||||
| 
 | ||||
|                 if (willCollapse) { // Store collapsed headings
 | ||||
|                     tocCollapsedHeadings!.add(headingKey); | ||||
|                 } else { | ||||
|                     tocCollapsedHeadings!.delete(headingKey); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async closeTocCommand() { | ||||
|         if (this.noteContext?.viewScope) { | ||||
|             this.noteContext.viewScope.tocTemporarilyHidden = true; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 SiriusXT
						SiriusXT