2022-05-29 21:44:26 +02:00
|
|
|
/**
|
|
|
|
* Table of contents widget
|
|
|
|
* (c) Antonio Tejada 2022
|
|
|
|
*
|
2023-06-30 11:18:34 +02:00
|
|
|
* By design, there's no support for nonsensical or malformed constructs:
|
2023-01-15 21:04:17 +01:00
|
|
|
* - headings inside elements (e.g. Trilium allows headings inside tables, but
|
2022-05-29 21:44:26 +02:00
|
|
|
* not inside lists)
|
|
|
|
* - nested headings when using raw HTML <H2><H3></H3></H2>
|
|
|
|
* - malformed headings when using raw HTML <H2></H3></H2><H3>
|
|
|
|
* - etc.
|
|
|
|
*
|
2023-06-30 11:18:34 +02:00
|
|
|
* In those cases, the generated TOC may be incorrect, or the navigation may lead
|
2022-05-29 21:44:26 +02:00
|
|
|
* to the wrong heading (although what "right" means in those cases is not
|
|
|
|
* clear), but it won't crash.
|
|
|
|
*/
|
2024-10-15 15:12:09 +08:00
|
|
|
import { t } from "../services/i18n.js";
|
2022-05-29 21:44:26 +02:00
|
|
|
import attributeService from "../services/attributes.js";
|
2023-01-03 14:31:46 +01:00
|
|
|
import RightPanelWidget from "./right_panel_widget.js";
|
2022-07-16 00:15:45 +02:00
|
|
|
import options from "../services/options.js";
|
2023-01-24 16:24:51 +01:00
|
|
|
import OnClickButtonWidget from "./buttons/onclick_button.js";
|
2025-02-02 19:44:18 +02:00
|
|
|
import appContext, { type EventData } from "../components/app_context.js";
|
2025-05-17 10:31:31 +03:00
|
|
|
import katex from "../services/math.js";
|
2025-02-02 19:44:18 +02:00
|
|
|
import type FNote from "../entities/fnote.js";
|
2022-05-29 21:44:26 +02:00
|
|
|
|
2025-04-01 23:24:21 +03:00
|
|
|
const TPL = /*html*/`<div class="toc-widget">
|
2022-05-29 21:44:26 +02:00
|
|
|
<style>
|
|
|
|
.toc-widget {
|
|
|
|
padding: 10px;
|
2025-02-02 18:33:58 +02:00
|
|
|
contain: none;
|
2022-10-09 22:20:11 +02:00
|
|
|
overflow: auto;
|
2023-01-24 16:24:51 +01:00
|
|
|
position: relative;
|
2025-05-16 21:25:10 +08:00
|
|
|
padding-left:0px !important;
|
2022-05-29 21:44:26 +02:00
|
|
|
}
|
2025-02-02 18:33:58 +02:00
|
|
|
|
2022-05-29 21:44:26 +02:00
|
|
|
.toc ol {
|
2025-05-16 21:25:10 +08:00
|
|
|
position: relative;
|
|
|
|
overflow: hidden;
|
|
|
|
padding-left: 0px;
|
2025-05-17 17:33:29 +08:00
|
|
|
transition: max-height 0.3s ease;
|
2025-05-16 21:25:10 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
.toc li.collapsed + ol {
|
|
|
|
display:none;
|
|
|
|
}
|
|
|
|
|
|
|
|
.toc li + ol:before {
|
|
|
|
content: "";
|
|
|
|
position: absolute;
|
|
|
|
height: 100%;
|
|
|
|
border-left: 1px solid var(--main-border-color);
|
2025-05-17 17:33:29 +08:00
|
|
|
z-index: 10;
|
2022-10-09 22:20:11 +02:00
|
|
|
}
|
2025-02-02 18:33:58 +02:00
|
|
|
|
2022-10-09 22:20:11 +02:00
|
|
|
.toc li {
|
2025-05-16 21:25:10 +08:00
|
|
|
display: flex;
|
|
|
|
position: relative;
|
|
|
|
list-style: none;
|
2025-05-17 10:31:31 +03:00
|
|
|
align-items: center;
|
2025-05-16 21:25:10 +08:00
|
|
|
padding-left: 7px;
|
2022-10-09 22:20:11 +02:00
|
|
|
cursor: pointer;
|
2023-06-03 14:43:20 +08:00
|
|
|
text-align: justify;
|
|
|
|
word-wrap: break-word;
|
|
|
|
hyphens: auto;
|
2022-10-09 22:20:11 +02:00
|
|
|
}
|
2025-02-02 18:33:58 +02:00
|
|
|
|
2025-05-17 17:33:29 +08:00
|
|
|
.toc > ol {
|
|
|
|
--toc-depth-level: 1;
|
|
|
|
}
|
|
|
|
.toc > ol > ol {
|
|
|
|
--toc-depth-level: 2;
|
|
|
|
}
|
|
|
|
.toc > ol > ol > ol {
|
|
|
|
--toc-depth-level: 3;
|
|
|
|
}
|
|
|
|
.toc > ol > ol > ol > ol {
|
|
|
|
--toc-depth-level: 4;
|
|
|
|
}
|
|
|
|
.toc > ol > ol > ol > ol > ol {
|
|
|
|
--toc-depth-level: 5;
|
|
|
|
}
|
|
|
|
|
|
|
|
.toc > ol ol::before {
|
|
|
|
left: calc((var(--toc-depth-level) - 2) * 20px + 14px);
|
|
|
|
}
|
|
|
|
|
|
|
|
.toc li {
|
|
|
|
padding-left: calc((var(--toc-depth-level) - 1) * 20px + 4px);
|
|
|
|
}
|
|
|
|
|
2025-05-16 21:25:10 +08:00
|
|
|
.toc li .collapse-button {
|
|
|
|
display: flex;
|
|
|
|
position: relative;
|
2025-05-17 17:33:29 +08:00
|
|
|
width: 21px;
|
|
|
|
height: 21px;
|
2025-05-16 21:25:10 +08:00
|
|
|
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 {
|
2025-05-17 21:55:10 +08:00
|
|
|
margin-left: 25px;
|
2025-05-16 21:25:10 +08:00
|
|
|
flex: 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
.toc li .collapse-button + .item-content {
|
2025-05-17 17:33:29 +08:00
|
|
|
margin-left: 4px;
|
2025-05-16 21:25:10 +08:00
|
|
|
}
|
|
|
|
|
2022-10-09 22:20:11 +02:00
|
|
|
.toc li:hover {
|
|
|
|
font-weight: bold;
|
2022-05-29 21:44:26 +02:00
|
|
|
}
|
|
|
|
</style>
|
|
|
|
|
|
|
|
<span class="toc"></span>
|
|
|
|
</div>`;
|
|
|
|
|
2025-02-07 19:23:12 +02:00
|
|
|
interface Toc {
|
2025-03-02 20:47:57 +01:00
|
|
|
$toc: JQuery<HTMLElement>;
|
|
|
|
headingCount: number;
|
2025-02-07 19:23:12 +02:00
|
|
|
}
|
|
|
|
|
2023-01-24 16:24:51 +01:00
|
|
|
export default class TocWidget extends RightPanelWidget {
|
2025-02-02 19:44:18 +02:00
|
|
|
|
|
|
|
private $toc!: JQuery<HTMLElement>;
|
2025-02-07 19:23:12 +02:00
|
|
|
private tocLabelValue?: string | null;
|
2025-02-02 19:44:18 +02:00
|
|
|
|
2022-05-29 21:44:26 +02:00
|
|
|
get widgetTitle() {
|
2024-10-15 15:12:09 +08:00
|
|
|
return t("toc.table_of_contents");
|
2022-05-29 21:44:26 +02:00
|
|
|
}
|
|
|
|
|
2023-11-03 10:44:14 +01:00
|
|
|
get widgetButtons() {
|
|
|
|
return [
|
|
|
|
new OnClickButtonWidget()
|
2024-09-09 20:30:35 +03:00
|
|
|
.icon("bx-cog")
|
2024-10-15 15:12:09 +08:00
|
|
|
.title(t("toc.options"))
|
2023-11-03 10:44:14 +01:00
|
|
|
.titlePlacement("left")
|
2025-01-09 18:07:02 +02:00
|
|
|
.onClick(() => appContext.tabManager.openContextWithNote("_optionsTextNotes", { activate: true }))
|
2023-11-03 10:44:14 +01:00
|
|
|
.class("icon-action"),
|
|
|
|
new OnClickButtonWidget()
|
|
|
|
.icon("bx-x")
|
|
|
|
.titlePlacement("left")
|
2025-01-09 18:07:02 +02:00
|
|
|
.onClick((widget) => widget.triggerCommand("closeToc"))
|
2023-11-03 10:44:14 +01:00
|
|
|
.class("icon-action")
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2022-05-29 21:44:26 +02:00
|
|
|
isEnabled() {
|
2025-02-02 19:44:18 +02:00
|
|
|
if (!super.isEnabled() || !this.note) {
|
2025-02-02 18:33:58 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2025-03-02 20:47:57 +01:00
|
|
|
const isHelpNote = this.note.type === "doc" && this.note.noteId.startsWith("_help");
|
|
|
|
const isTextNote = this.note.type === "text";
|
2025-02-02 18:33:58 +02:00
|
|
|
const isNoteSupported = isTextNote || isHelpNote;
|
|
|
|
|
2025-02-02 19:44:18 +02:00
|
|
|
return isNoteSupported
|
|
|
|
&& !this.noteContext?.viewScope?.tocTemporarilyHidden
|
|
|
|
&& this.noteContext?.viewScope?.viewMode === "default";
|
2022-05-29 21:44:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async doRenderBody() {
|
|
|
|
this.$body.empty().append($(TPL));
|
2025-01-09 18:07:02 +02:00
|
|
|
this.$toc = this.$body.find(".toc");
|
2022-05-29 21:44:26 +02:00
|
|
|
}
|
|
|
|
|
2025-02-02 19:44:18 +02:00
|
|
|
async refreshWithNote(note: FNote) {
|
|
|
|
|
|
|
|
this.toggleInt(!!this.noteContext?.viewScope?.tocPreviousVisible);
|
2023-06-01 20:17:00 +08:00
|
|
|
|
2025-02-07 19:23:12 +02:00
|
|
|
this.tocLabelValue = note.getLabelValue("toc");
|
2022-07-19 23:56:29 +02:00
|
|
|
|
2025-02-07 19:23:12 +02:00
|
|
|
if (this.tocLabelValue === "hide") {
|
2022-07-19 23:56:29 +02:00
|
|
|
this.toggleInt(false);
|
2023-01-24 16:24:51 +01:00
|
|
|
this.triggerCommand("reEvaluateRightPaneVisibility");
|
2022-07-19 23:56:29 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-02-02 19:44:18 +02:00
|
|
|
if (!this.note || !this.noteContext?.viewScope) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-05-29 21:44:26 +02:00
|
|
|
// Check for type text unconditionally in case alwaysShowWidget is set
|
2025-01-09 18:07:02 +02:00
|
|
|
if (this.note.type === "text") {
|
2025-02-02 19:44:18 +02:00
|
|
|
const blob = await note.getBlob();
|
|
|
|
if (blob) {
|
2025-02-07 19:23:12 +02:00
|
|
|
const toc = await this.getToc(blob.content);
|
|
|
|
this.#updateToc(toc);
|
2025-02-02 18:33:58 +02:00
|
|
|
}
|
2025-02-07 19:23:12 +02:00
|
|
|
return;
|
2022-05-29 21:44:26 +02:00
|
|
|
}
|
|
|
|
|
2025-02-07 19:23:12 +02:00
|
|
|
if (this.note.type === "doc") {
|
|
|
|
/**
|
|
|
|
* For document note types, we obtain the content directly from the DOM since it allows us to obtain processed data without
|
|
|
|
* requesting data twice. However, when immediately navigating to a new note the new document is not yet attached to the hierarchy,
|
|
|
|
* resulting in an empty TOC. The fix is to simply wait for it to pop up.
|
|
|
|
*/
|
|
|
|
setTimeout(async () => {
|
|
|
|
const $contentEl = await this.noteContext?.getContentElement();
|
|
|
|
if ($contentEl) {
|
|
|
|
const content = $contentEl.html();
|
|
|
|
const toc = await this.getToc(content);
|
|
|
|
this.#updateToc(toc);
|
|
|
|
} else {
|
|
|
|
console.warn("Unable to get content element for doctype");
|
|
|
|
}
|
|
|
|
}, 10);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#updateToc({ $toc, headingCount }: Toc) {
|
2025-02-02 20:21:35 +02:00
|
|
|
this.$toc.empty();
|
2025-02-02 19:44:18 +02:00
|
|
|
if ($toc) {
|
|
|
|
this.$toc.append($toc);
|
|
|
|
}
|
|
|
|
|
2025-02-07 19:23:12 +02:00
|
|
|
const tocLabelValue = this.tocLabelValue;
|
|
|
|
|
2025-03-02 20:47:57 +01:00
|
|
|
const visible = tocLabelValue === "" || tocLabelValue === "show" || headingCount >= (options.getInt("minTocHeadings") ?? 0);
|
2025-02-07 19:23:12 +02:00
|
|
|
this.toggleInt(visible);
|
|
|
|
if (this.noteContext?.viewScope) {
|
|
|
|
this.noteContext.viewScope.tocPreviousVisible = visible;
|
2023-06-01 20:17:00 +08:00
|
|
|
}
|
2022-07-19 23:56:29 +02:00
|
|
|
|
2023-01-24 16:24:51 +01:00
|
|
|
this.triggerCommand("reEvaluateRightPaneVisibility");
|
2022-05-29 21:44:26 +02:00
|
|
|
}
|
|
|
|
|
2024-09-12 09:36:08 +08:00
|
|
|
/**
|
|
|
|
* Rendering formulas in strings using katex
|
|
|
|
*
|
2025-02-02 19:44:18 +02:00
|
|
|
* @param html Note's html content
|
|
|
|
* @returns The HTML content with mathematical formulas rendered by KaTeX.
|
2024-10-15 15:12:09 +08:00
|
|
|
*/
|
2025-02-02 19:44:18 +02:00
|
|
|
async replaceMathTextWithKatax(html: string) {
|
2024-09-12 09:36:08 +08:00
|
|
|
const mathTextRegex = /<span class="math-tex">\\\(([\s\S]*?)\\\)<\/span>/g;
|
|
|
|
var matches = [...html.matchAll(mathTextRegex)];
|
|
|
|
let modifiedText = html;
|
2024-10-15 15:12:09 +08:00
|
|
|
|
2024-09-12 09:36:08 +08:00
|
|
|
if (matches.length > 0) {
|
|
|
|
// Process all matches asynchronously
|
|
|
|
for (const match of matches) {
|
|
|
|
let latexCode = match[1];
|
|
|
|
let rendered;
|
2024-10-15 15:12:09 +08:00
|
|
|
|
2024-09-12 09:36:08 +08:00
|
|
|
try {
|
|
|
|
rendered = katex.renderToString(latexCode, {
|
|
|
|
throwOnError: false
|
|
|
|
});
|
|
|
|
} catch (e) {
|
2025-01-09 18:07:02 +02:00
|
|
|
if (e instanceof ReferenceError && e.message.includes("katex is not defined")) {
|
2024-09-12 09:36:08 +08:00
|
|
|
// Load KaTeX if it is not already loaded
|
|
|
|
try {
|
|
|
|
rendered = katex.renderToString(latexCode, {
|
|
|
|
throwOnError: false
|
|
|
|
});
|
|
|
|
} catch (renderError) {
|
|
|
|
console.error("KaTeX rendering error after loading library:", renderError);
|
|
|
|
rendered = match[0]; // Fall back to original if error persists
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.error("KaTeX rendering error:", e);
|
|
|
|
rendered = match[0]; // Fall back to original on error
|
|
|
|
}
|
|
|
|
}
|
2024-10-15 15:12:09 +08:00
|
|
|
|
2024-09-12 09:36:08 +08:00
|
|
|
// Replace the matched formula in the modified text
|
|
|
|
modifiedText = modifiedText.replace(match[0], rendered);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return modifiedText;
|
|
|
|
}
|
|
|
|
|
2022-05-29 21:44:26 +02:00
|
|
|
/**
|
|
|
|
* Builds a jquery table of contents.
|
|
|
|
*
|
2025-02-02 19:44:18 +02:00
|
|
|
* @param html Note's html content
|
|
|
|
* @returns ordered list table of headings, nested by heading level
|
2022-05-29 21:44:26 +02:00
|
|
|
* with an onclick event that will cause the document to scroll to
|
|
|
|
* the desired position.
|
|
|
|
*/
|
2025-02-07 19:23:12 +02:00
|
|
|
async getToc(html: string): Promise<Toc> {
|
2022-05-29 21:44:26 +02:00
|
|
|
// Regular expression for headings <h1>...</h1> using non-greedy
|
|
|
|
// matching and backreferences
|
2023-01-10 15:06:21 +01:00
|
|
|
const headingTagsRegex = /<h(\d+)[^>]*>(.*?)<\/h\1>/gi;
|
2022-05-29 21:44:26 +02:00
|
|
|
|
|
|
|
// Use jquery to build the table rather than html text, since it makes
|
|
|
|
// it easier to set the onclick event that will be executed with the
|
|
|
|
// right captured callback context
|
2023-10-31 00:18:03 +01:00
|
|
|
let $toc = $("<ol>");
|
2022-05-29 21:44:26 +02:00
|
|
|
// Note heading 2 is the first level Trilium makes available to the note
|
|
|
|
let curLevel = 2;
|
|
|
|
const $ols = [$toc];
|
2025-05-16 21:25:10 +08:00
|
|
|
let $previousLi: JQuery<HTMLElement> | undefined;
|
2025-05-17 10:31:31 +03:00
|
|
|
|
2025-05-16 21:25:10 +08:00
|
|
|
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
|
2025-05-17 10:31:31 +03:00
|
|
|
|
2025-02-02 19:44:18 +02:00
|
|
|
let headingCount = 0;
|
2025-01-09 18:07:02 +02:00
|
|
|
for (let m = null, headingIndex = 0; (m = headingTagsRegex.exec(html)) !== null; headingIndex++) {
|
2022-05-29 21:44:26 +02:00
|
|
|
//
|
|
|
|
// Nest/unnest whatever necessary number of ordered lists
|
|
|
|
//
|
2025-02-02 19:44:18 +02:00
|
|
|
const newLevel = parseInt(m[1]);
|
2022-05-29 21:44:26 +02:00
|
|
|
const levelDelta = newLevel - curLevel;
|
|
|
|
if (levelDelta > 0) {
|
|
|
|
// Open as many lists as newLevel - curLevel
|
|
|
|
for (let i = 0; i < levelDelta; i++) {
|
|
|
|
const $ol = $("<ol>");
|
|
|
|
$ols[$ols.length - 1].append($ol);
|
|
|
|
$ols.push($ol);
|
2025-05-16 21:25:10 +08:00
|
|
|
|
|
|
|
if ($previousLi) {
|
|
|
|
const headingKey = `h${newLevel}_${headingIndex}_${$previousLi?.text().trim()}`;
|
|
|
|
this.setupCollapsibleHeading($ol, $previousLi, headingKey, tocCollapsedHeadings, validHeadingKeys);
|
2025-05-17 10:31:31 +03:00
|
|
|
}
|
2022-05-29 21:44:26 +02:00
|
|
|
}
|
|
|
|
} else if (levelDelta < 0) {
|
|
|
|
// Close as many lists as curLevel - newLevel
|
2023-01-10 16:16:13 +01:00
|
|
|
// be careful not to empty $ols completely, the root element should stay (could happen with a rogue h1 element)
|
|
|
|
for (let i = 0; i < -levelDelta && $ols.length > 1; ++i) {
|
2022-05-29 21:44:26 +02:00
|
|
|
$ols.pop();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
curLevel = newLevel;
|
|
|
|
|
|
|
|
//
|
|
|
|
// Create the list item and set up the click callback
|
|
|
|
//
|
2022-07-14 23:39:16 +02:00
|
|
|
|
2025-01-09 18:07:02 +02:00
|
|
|
const headingText = await this.replaceMathTextWithKatax(m[2]);
|
2025-05-17 17:33:29 +08:00
|
|
|
const $itemContent = $('<div class="item-content">').html(headingText);
|
|
|
|
const $li = $("<li>").append($itemContent)
|
|
|
|
.on("click", () => this.jumpToHeading(headingIndex));
|
2022-05-29 21:44:26 +02:00
|
|
|
$ols[$ols.length - 1].append($li);
|
2022-05-30 17:45:59 +02:00
|
|
|
headingCount = headingIndex;
|
2025-05-16 21:25:10 +08:00
|
|
|
$previousLi = $li;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clean up unused entries in tocCollapsedHeadings
|
|
|
|
for (const key of tocCollapsedHeadings) {
|
|
|
|
if (!validHeadingKeys.has(key)) {
|
|
|
|
tocCollapsedHeadings.delete(key);
|
|
|
|
}
|
2022-05-29 21:44:26 +02:00
|
|
|
}
|
|
|
|
|
2023-10-31 00:18:03 +01:00
|
|
|
$toc = this.pullLeft($toc);
|
|
|
|
|
2022-05-30 17:45:59 +02:00
|
|
|
return {
|
2025-02-07 19:23:12 +02:00
|
|
|
$toc,
|
2022-05-30 17:45:59 +02:00
|
|
|
headingCount
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-10-31 00:18:03 +01:00
|
|
|
/**
|
|
|
|
* Reduce indent if a larger headings are not being used: https://github.com/zadam/trilium/issues/4363
|
|
|
|
*/
|
2025-02-02 19:44:18 +02:00
|
|
|
pullLeft($toc: JQuery<HTMLElement>) {
|
2023-10-31 00:18:03 +01:00
|
|
|
while (true) {
|
|
|
|
const $children = $toc.children();
|
|
|
|
|
|
|
|
if ($children.length !== 1) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
const $first = $toc.children(":first");
|
|
|
|
|
2025-05-16 21:25:10 +08:00
|
|
|
if ($first[0].tagName.toLowerCase() !== "ol") {
|
2023-10-31 00:18:03 +01:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
$toc = $first;
|
|
|
|
}
|
|
|
|
return $toc;
|
|
|
|
}
|
|
|
|
|
2025-02-02 19:44:18 +02:00
|
|
|
async jumpToHeading(headingIndex: number) {
|
|
|
|
if (!this.note || !this.noteContext) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-05-30 17:45:59 +02:00
|
|
|
// A readonly note can change state to "readonly disabled
|
|
|
|
// temporarily" (ie "edit this note" button) without any
|
|
|
|
// intervening events, do the readonly calculation at navigation
|
|
|
|
// time and not at outline creation time
|
|
|
|
// See https://github.com/zadam/trilium/issues/2828
|
2025-02-02 18:35:41 +02:00
|
|
|
const isDocNote = this.note.type === "doc";
|
2022-05-30 17:45:59 +02:00
|
|
|
const isReadOnly = await this.noteContext.isReadOnly();
|
|
|
|
|
2023-09-22 05:52:06 -04:00
|
|
|
let $container;
|
2025-02-02 18:35:41 +02:00
|
|
|
if (isReadOnly || isDocNote) {
|
2023-09-22 05:52:06 -04:00
|
|
|
$container = await this.noteContext.getContentElement();
|
2022-05-30 17:45:59 +02:00
|
|
|
} else {
|
|
|
|
const textEditor = await this.noteContext.getTextEditor();
|
2023-09-22 05:52:06 -04:00
|
|
|
$container = $(textEditor.sourceElement);
|
2022-05-30 17:45:59 +02:00
|
|
|
}
|
2023-09-22 05:52:06 -04:00
|
|
|
|
|
|
|
const headingElement = $container?.find(":header:not(section.include-note :header)")?.[headingIndex];
|
|
|
|
headingElement?.scrollIntoView({ behavior: "smooth" });
|
2022-05-29 21:44:26 +02:00
|
|
|
}
|
|
|
|
|
2025-05-16 21:25:10 +08:00
|
|
|
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");
|
|
|
|
}
|
|
|
|
|
2025-05-17 17:33:29 +08:00
|
|
|
$collapseButton.on("click", (event) => {
|
|
|
|
event.stopPropagation();
|
2025-05-16 21:25:10 +08:00
|
|
|
if ($previousLi.hasClass("animating")) return;
|
|
|
|
const willCollapse = !$previousLi.hasClass("collapsed");
|
|
|
|
$previousLi.addClass("animating");
|
|
|
|
|
2025-05-17 10:31:31 +03:00
|
|
|
if (willCollapse) { // Collapse
|
2025-05-16 21:25:10 +08:00
|
|
|
$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);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-24 16:24:51 +01:00
|
|
|
async closeTocCommand() {
|
2025-02-02 19:44:18 +02:00
|
|
|
if (this.noteContext?.viewScope) {
|
|
|
|
this.noteContext.viewScope.tocTemporarilyHidden = true;
|
|
|
|
}
|
2023-01-24 16:24:51 +01:00
|
|
|
await this.refresh();
|
2025-01-09 18:07:02 +02:00
|
|
|
this.triggerCommand("reEvaluateRightPaneVisibility");
|
2024-09-12 19:22:41 +08:00
|
|
|
appContext.triggerEvent("reEvaluateTocWidgetVisibility", { noteId: this.noteId });
|
2023-01-24 16:24:51 +01:00
|
|
|
}
|
|
|
|
|
2025-02-26 00:53:15 +01:00
|
|
|
async showTocWidgetEvent({ noteId }: EventData<"showTocWidget">) {
|
2024-09-12 19:22:41 +08:00
|
|
|
if (this.noteId === noteId) {
|
|
|
|
await this.refresh();
|
2025-01-09 18:07:02 +02:00
|
|
|
this.triggerCommand("reEvaluateRightPaneVisibility");
|
2024-09-12 19:22:41 +08:00
|
|
|
}
|
2023-01-24 16:24:51 +01:00
|
|
|
}
|
|
|
|
|
2025-02-02 19:44:18 +02:00
|
|
|
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
|
|
|
if (this.noteId && loadResults.isNoteContentReloaded(this.noteId)) {
|
2022-05-30 17:45:59 +02:00
|
|
|
await this.refresh();
|
2025-01-09 18:07:02 +02:00
|
|
|
} else if (
|
|
|
|
loadResults
|
|
|
|
.getAttributeRows()
|
2025-02-02 19:44:18 +02:00
|
|
|
.find((attr) => attr.type === "label" && ((attr.name ?? "").toLowerCase().includes("readonly") || attr.name === "toc") && attributeService.isAffecting(attr, this.note))
|
2025-01-09 18:07:02 +02:00
|
|
|
) {
|
2022-05-29 21:44:26 +02:00
|
|
|
await this.refresh();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|