chore(client/ts): port toc

This commit is contained in:
Elian Doran 2025-02-02 19:44:18 +02:00
parent d901a0f787
commit d0317f4bb6
No known key found for this signature in database
4 changed files with 71 additions and 32 deletions

View File

@ -77,6 +77,7 @@ export type CommandMappings = {
searchString?: string; searchString?: string;
ancestorNoteId?: string | null; ancestorNoteId?: string | null;
}; };
closeTocCommand: CommandData;
showLaunchBarSubtree: CommandData; showLaunchBarSubtree: CommandData;
showOptions: CommandData & { showOptions: CommandData & {
section: string; section: string;
@ -206,6 +207,8 @@ export type CommandMappings = {
zoomFactor: string; zoomFactor: string;
} }
reEvaluateRightPaneVisibility: CommandData;
// Geomap // Geomap
deleteFromMap: { noteId: string }, deleteFromMap: { noteId: string },
openGeoLocation: { noteId: string, event: JQuery.MouseDownEvent } openGeoLocation: { noteId: string, event: JQuery.MouseDownEvent }
@ -266,6 +269,9 @@ type EventMappings = {
reEvaluateHighlightsListWidgetVisibility: { reEvaluateHighlightsListWidgetVisibility: {
noteId: string | undefined; noteId: string | undefined;
}; };
reEvaluateTocWidgetVisibility: {
noteId: string | undefined;
};
showHighlightsListWidget: { showHighlightsListWidget: {
noteId: string; noteId: string;
}; };
@ -301,7 +307,10 @@ type EventMappings = {
}; };
refreshNoteList: { refreshNoteList: {
noteId: string; noteId: string;
} };
showToc: {
noteId: string;
};
}; };
export type EventListener<T extends EventNames> = { export type EventListener<T extends EventNames> = {

View File

@ -33,6 +33,14 @@ export interface ViewScope {
readOnlyTemporarilyDisabled?: boolean; readOnlyTemporarilyDisabled?: boolean;
highlightsListPreviousVisible?: boolean; highlightsListPreviousVisible?: boolean;
highlightsListTemporarilyHidden?: boolean; highlightsListTemporarilyHidden?: boolean;
tocTemporarilyHidden?: boolean;
/*
* The reason for adding tocPreviousVisible is to record whether the previous state of the toc is hidden or displayed,
* and then let it be displayed/hidden at the initial time. If there is no such value,
* when the right panel needs to display highlighttext but not toc, every time the note content is changed,
* toc will appear and then close immediately, because getToc(html) function will consume time
*/
tocPreviousVisible?: boolean;
} }
interface CreateLinkOptions { interface CreateLinkOptions {

View File

@ -239,6 +239,7 @@ declare global {
}, },
getData(): string; getData(): string;
setData(data: string): void; setData(data: string): void;
sourceElement: HTMLElement;
} }
interface MentionItem { interface MentionItem {

View File

@ -18,8 +18,9 @@ import attributeService from "../services/attributes.js";
import RightPanelWidget from "./right_panel_widget.js"; import RightPanelWidget from "./right_panel_widget.js";
import options from "../services/options.js"; import options from "../services/options.js";
import OnClickButtonWidget from "./buttons/onclick_button.js"; import OnClickButtonWidget from "./buttons/onclick_button.js";
import appContext from "../components/app_context.js"; import appContext, { type EventData } from "../components/app_context.js";
import libraryLoader from "../services/library_loader.js"; import libraryLoader from "../services/library_loader.js";
import type FNote from "../entities/fnote.js";
const TPL = `<div class="toc-widget"> const TPL = `<div class="toc-widget">
<style> <style>
@ -54,6 +55,9 @@ const TPL = `<div class="toc-widget">
</div>`; </div>`;
export default class TocWidget extends RightPanelWidget { export default class TocWidget extends RightPanelWidget {
private $toc!: JQuery<HTMLElement>;
get widgetTitle() { get widgetTitle() {
return t("toc.table_of_contents"); return t("toc.table_of_contents");
} }
@ -75,7 +79,7 @@ export default class TocWidget extends RightPanelWidget {
} }
isEnabled() { isEnabled() {
if (!super.isEnabled()) { if (!super.isEnabled() || !this.note) {
return false; return false;
} }
@ -83,7 +87,9 @@ export default class TocWidget extends RightPanelWidget {
const isTextNote = (this.note.type === "text"); const isTextNote = (this.note.type === "text");
const isNoteSupported = isTextNote || isHelpNote; const isNoteSupported = isTextNote || isHelpNote;
return isNoteSupported && !this.noteContext.viewScope.tocTemporarilyHidden && this.noteContext.viewScope.viewMode === "default"; return isNoteSupported
&& !this.noteContext?.viewScope?.tocTemporarilyHidden
&& this.noteContext?.viewScope?.viewMode === "default";
} }
async doRenderBody() { async doRenderBody() {
@ -91,12 +97,9 @@ export default class TocWidget extends RightPanelWidget {
this.$toc = this.$body.find(".toc"); this.$toc = this.$body.find(".toc");
} }
async refreshWithNote(note) { async refreshWithNote(note: FNote) {
/*The reason for adding tocPreviousVisible is to record whether the previous state of the toc is hidden or displayed,
* and then let it be displayed/hidden at the initial time. If there is no such value, this.toggleInt(!!this.noteContext?.viewScope?.tocPreviousVisible);
* when the right panel needs to display highlighttext but not toc, every time the note content is changed,
* toc will appear and then close immediately, because getToc(html) function will consume time*/
this.toggleInt(!!this.noteContext.viewScope.tocPreviousVisible);
const tocLabel = note.getLabel("toc"); const tocLabel = note.getLabel("toc");
@ -106,12 +109,19 @@ export default class TocWidget extends RightPanelWidget {
return; return;
} }
let $toc = "", if (!this.note || !this.noteContext?.viewScope) {
headingCount = 0; return;
}
let $toc: JQuery<HTMLElement> | null = null;
let headingCount = 0;
// Check for type text unconditionally in case alwaysShowWidget is set // Check for type text unconditionally in case alwaysShowWidget is set
if (this.note.type === "text") { if (this.note.type === "text") {
const { content } = await note.getBlob(); const blob = await note.getBlob();
({ $toc, headingCount } = await this.getToc(content)); if (blob) {
({ $toc, headingCount } = await this.getToc(blob.content));
}
} else if (this.note.type === "doc") { } else if (this.note.type === "doc") {
const $contentEl = await this.noteContext.getContentElement(); const $contentEl = await this.noteContext.getContentElement();
if ($contentEl) { if ($contentEl) {
@ -122,8 +132,13 @@ export default class TocWidget extends RightPanelWidget {
} }
} }
this.$toc.html($toc); if ($toc) {
if (["", "show"].includes(tocLabel?.value) || headingCount >= options.getInt("minTocHeadings")) { this.$toc.append($toc);
} else {
this.$toc.empty();
}
if (["", "show"].includes(tocLabel?.value ?? "") || headingCount >= (options.getInt("minTocHeadings") ?? 0)) {
this.toggleInt(true); this.toggleInt(true);
this.noteContext.viewScope.tocPreviousVisible = true; this.noteContext.viewScope.tocPreviousVisible = true;
} else { } else {
@ -137,10 +152,10 @@ export default class TocWidget extends RightPanelWidget {
/** /**
* Rendering formulas in strings using katex * Rendering formulas in strings using katex
* *
* @param {string} html Note's html content * @param html Note's html content
* @returns {string} The HTML content with mathematical formulas rendered by KaTeX. * @returns The HTML content with mathematical formulas rendered by KaTeX.
*/ */
async replaceMathTextWithKatax(html) { async replaceMathTextWithKatax(html: string) {
const mathTextRegex = /<span class="math-tex">\\\(([\s\S]*?)\\\)<\/span>/g; const mathTextRegex = /<span class="math-tex">\\\(([\s\S]*?)\\\)<\/span>/g;
var matches = [...html.matchAll(mathTextRegex)]; var matches = [...html.matchAll(mathTextRegex)];
let modifiedText = html; let modifiedText = html;
@ -183,12 +198,12 @@ export default class TocWidget extends RightPanelWidget {
/** /**
* Builds a jquery table of contents. * Builds a jquery table of contents.
* *
* @param {string} html Note's html content * @param html Note's html content
* @returns {$toc: jQuery, headingCount: integer} ordered list table of headings, nested by heading level * @returns ordered list table of headings, nested by heading level
* with an onclick event that will cause the document to scroll to * with an onclick event that will cause the document to scroll to
* the desired position. * the desired position.
*/ */
async getToc(html) { async getToc(html: string) {
// Regular expression for headings <h1>...</h1> using non-greedy // Regular expression for headings <h1>...</h1> using non-greedy
// matching and backreferences // matching and backreferences
const headingTagsRegex = /<h(\d+)[^>]*>(.*?)<\/h\1>/gi; const headingTagsRegex = /<h(\d+)[^>]*>(.*?)<\/h\1>/gi;
@ -200,12 +215,12 @@ 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 headingCount; 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++) {
// //
// Nest/unnest whatever necessary number of ordered lists // Nest/unnest whatever necessary number of ordered lists
// //
const newLevel = m[1]; const newLevel = parseInt(m[1]);
const levelDelta = newLevel - curLevel; const levelDelta = newLevel - curLevel;
if (levelDelta > 0) { if (levelDelta > 0) {
// Open as many lists as newLevel - curLevel // Open as many lists as newLevel - curLevel
@ -237,7 +252,7 @@ export default class TocWidget extends RightPanelWidget {
$toc = this.pullLeft($toc); $toc = this.pullLeft($toc);
return { return {
$toc, $toc: $toc,
headingCount headingCount
}; };
} }
@ -245,7 +260,7 @@ export default class TocWidget extends RightPanelWidget {
/** /**
* Reduce indent if a larger headings are not being used: https://github.com/zadam/trilium/issues/4363 * Reduce indent if a larger headings are not being used: https://github.com/zadam/trilium/issues/4363
*/ */
pullLeft($toc) { pullLeft($toc: JQuery<HTMLElement>) {
while (true) { while (true) {
const $children = $toc.children(); const $children = $toc.children();
@ -264,7 +279,11 @@ export default class TocWidget extends RightPanelWidget {
return $toc; return $toc;
} }
async jumpToHeading(headingIndex) { async jumpToHeading(headingIndex: number) {
if (!this.note || !this.noteContext) {
return;
}
// A readonly note can change state to "readonly disabled // A readonly note can change state to "readonly disabled
// temporarily" (ie "edit this note" button) without any // temporarily" (ie "edit this note" button) without any
// intervening events, do the readonly calculation at navigation // intervening events, do the readonly calculation at navigation
@ -286,26 +305,28 @@ export default class TocWidget extends RightPanelWidget {
} }
async closeTocCommand() { async closeTocCommand() {
this.noteContext.viewScope.tocTemporarilyHidden = true; if (this.noteContext?.viewScope) {
this.noteContext.viewScope.tocTemporarilyHidden = true;
}
await this.refresh(); await this.refresh();
this.triggerCommand("reEvaluateRightPaneVisibility"); this.triggerCommand("reEvaluateRightPaneVisibility");
appContext.triggerEvent("reEvaluateTocWidgetVisibility", { noteId: this.noteId }); appContext.triggerEvent("reEvaluateTocWidgetVisibility", { noteId: this.noteId });
} }
async showTocWidgetEvent({ noteId }) { async showTocWidgetEvent({ noteId }: EventData<"showToc">) {
if (this.noteId === noteId) { if (this.noteId === noteId) {
await this.refresh(); await this.refresh();
this.triggerCommand("reEvaluateRightPaneVisibility"); this.triggerCommand("reEvaluateRightPaneVisibility");
} }
} }
async entitiesReloadedEvent({ loadResults }) { async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isNoteContentReloaded(this.noteId)) { if (this.noteId && loadResults.isNoteContentReloaded(this.noteId)) {
await this.refresh(); await this.refresh();
} else if ( } else if (
loadResults loadResults
.getAttributeRows() .getAttributeRows()
.find((attr) => attr.type === "label" && (attr.name.toLowerCase().includes("readonly") || attr.name === "toc") && attributeService.isAffecting(attr, this.note)) .find((attr) => attr.type === "label" && ((attr.name ?? "").toLowerCase().includes("readonly") || attr.name === "toc") && attributeService.isAffecting(attr, this.note))
) { ) {
await this.refresh(); await this.refresh();
} }