mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 20:06:08 +01:00 
			
		
		
		
	feat(toc): Collapsible TOC
This commit is contained in:
		@@ -58,6 +58,7 @@ export interface ViewScope {
 | 
				
			|||||||
     * toc will appear and then close immediately, because getToc(html) function will consume time
 | 
					     * toc will appear and then close immediately, because getToc(html) function will consume time
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    tocPreviousVisible?: boolean;
 | 
					    tocPreviousVisible?: boolean;
 | 
				
			||||||
 | 
					    tocCollapsedHeadings?:  Set<string>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface CreateLinkOptions {
 | 
					interface CreateLinkOptions {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,7 +13,7 @@ const TPL = /*html*/`
 | 
				
			|||||||
            height: 300px;
 | 
					            height: 300px;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        .open-full-button, .collapse-button {
 | 
					        .note-map-ribbon-widget .open-full-button, .note-map-ribbon-widget .collapse-button {
 | 
				
			||||||
            position: absolute;
 | 
					            position: absolute;
 | 
				
			||||||
            right: 5px;
 | 
					            right: 5px;
 | 
				
			||||||
            bottom: 5px;
 | 
					            bottom: 5px;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,23 +29,68 @@ const TPL = /*html*/`<div class="toc-widget">
 | 
				
			|||||||
            contain: none;
 | 
					            contain: none;
 | 
				
			||||||
            overflow: auto;
 | 
					            overflow: auto;
 | 
				
			||||||
            position: relative;
 | 
					            position: relative;
 | 
				
			||||||
 | 
					            padding-left:0px !important;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        .toc ol {
 | 
					        .toc ol {
 | 
				
			||||||
            padding-left: 25px;
 | 
					            position: relative;
 | 
				
			||||||
 | 
					            overflow: hidden;
 | 
				
			||||||
 | 
					            padding-left: 20px;
 | 
				
			||||||
 | 
					            transition: max-height 0.3s ease;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        .toc > ol {
 | 
					        .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 {
 | 
					        .toc li {
 | 
				
			||||||
 | 
					            display: flex;
 | 
				
			||||||
 | 
					            position: relative;
 | 
				
			||||||
 | 
					            list-style: none;
 | 
				
			||||||
 | 
					            align-items: center; 
 | 
				
			||||||
 | 
					            padding-left: 7px;
 | 
				
			||||||
            cursor: pointer;
 | 
					            cursor: pointer;
 | 
				
			||||||
            text-align: justify;
 | 
					            text-align: justify;
 | 
				
			||||||
            word-wrap: break-word;
 | 
					            word-wrap: break-word;
 | 
				
			||||||
            hyphens: auto;
 | 
					            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 {
 | 
					        .toc li:hover {
 | 
				
			||||||
            font-weight: bold;
 | 
					            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
 | 
					        // Note heading 2 is the first level Trilium makes available to the note
 | 
				
			||||||
        let curLevel = 2;
 | 
					        let curLevel = 2;
 | 
				
			||||||
        const $ols = [$toc];
 | 
					        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;
 | 
					        let headingCount = 0;
 | 
				
			||||||
        for (let m = null, headingIndex = 0; (m = headingTagsRegex.exec(html)) !== null; headingIndex++) {
 | 
					        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>");
 | 
					                    const $ol = $("<ol>");
 | 
				
			||||||
                    $ols[$ols.length - 1].append($ol);
 | 
					                    $ols[$ols.length - 1].append($ol);
 | 
				
			||||||
                    $ols.push($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) {
 | 
					            } else if (levelDelta < 0) {
 | 
				
			||||||
                // Close as many lists as curLevel - newLevel
 | 
					                // 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 headingText = await this.replaceMathTextWithKatax(m[2]);
 | 
				
			||||||
            const $li = $("<li>").html(headingText);
 | 
					            const $itemContent = $('<div class="item-content">').html(headingText).on("click", () => {
 | 
				
			||||||
            $li.on("click", () => this.jumpToHeading(headingIndex));
 | 
					                this.jumpToHeading(headingIndex);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            const $li = $("<li>").append($itemContent);
 | 
				
			||||||
            $ols[$ols.length - 1].append($li);
 | 
					            $ols[$ols.length - 1].append($li);
 | 
				
			||||||
            headingCount = headingIndex;
 | 
					            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);
 | 
					        $toc = this.pullLeft($toc);
 | 
				
			||||||
@@ -286,7 +354,7 @@ export default class TocWidget extends RightPanelWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            const $first = $toc.children(":first");
 | 
					            const $first = $toc.children(":first");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if ($first[0].tagName !== "OL") {
 | 
					            if ($first[0].tagName.toLowerCase() !== "ol") {
 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -320,6 +388,59 @@ export default class TocWidget extends RightPanelWidget {
 | 
				
			|||||||
        headingElement?.scrollIntoView({ behavior: "smooth" });
 | 
					        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() {
 | 
					    async closeTocCommand() {
 | 
				
			||||||
        if (this.noteContext?.viewScope) {
 | 
					        if (this.noteContext?.viewScope) {
 | 
				
			||||||
            this.noteContext.viewScope.tocTemporarilyHidden = true;
 | 
					            this.noteContext.viewScope.tocTemporarilyHidden = true;
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user