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

View File

@ -33,6 +33,14 @@ export interface ViewScope {
readOnlyTemporarilyDisabled?: boolean;
highlightsListPreviousVisible?: 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 {

View File

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

View File

@ -18,8 +18,9 @@ import attributeService from "../services/attributes.js";
import RightPanelWidget from "./right_panel_widget.js";
import options from "../services/options.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 type FNote from "../entities/fnote.js";
const TPL = `<div class="toc-widget">
<style>
@ -54,6 +55,9 @@ const TPL = `<div class="toc-widget">
</div>`;
export default class TocWidget extends RightPanelWidget {
private $toc!: JQuery<HTMLElement>;
get widgetTitle() {
return t("toc.table_of_contents");
}
@ -75,7 +79,7 @@ export default class TocWidget extends RightPanelWidget {
}
isEnabled() {
if (!super.isEnabled()) {
if (!super.isEnabled() || !this.note) {
return false;
}
@ -83,7 +87,9 @@ export default class TocWidget extends RightPanelWidget {
const isTextNote = (this.note.type === "text");
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() {
@ -91,12 +97,9 @@ export default class TocWidget extends RightPanelWidget {
this.$toc = this.$body.find(".toc");
}
async refreshWithNote(note) {
/*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*/
this.toggleInt(!!this.noteContext.viewScope.tocPreviousVisible);
async refreshWithNote(note: FNote) {
this.toggleInt(!!this.noteContext?.viewScope?.tocPreviousVisible);
const tocLabel = note.getLabel("toc");
@ -106,12 +109,19 @@ export default class TocWidget extends RightPanelWidget {
return;
}
let $toc = "",
headingCount = 0;
if (!this.note || !this.noteContext?.viewScope) {
return;
}
let $toc: JQuery<HTMLElement> | null = null;
let headingCount = 0;
// Check for type text unconditionally in case alwaysShowWidget is set
if (this.note.type === "text") {
const { content } = await note.getBlob();
({ $toc, headingCount } = await this.getToc(content));
const blob = await note.getBlob();
if (blob) {
({ $toc, headingCount } = await this.getToc(blob.content));
}
} else if (this.note.type === "doc") {
const $contentEl = await this.noteContext.getContentElement();
if ($contentEl) {
@ -122,8 +132,13 @@ export default class TocWidget extends RightPanelWidget {
}
}
this.$toc.html($toc);
if (["", "show"].includes(tocLabel?.value) || headingCount >= options.getInt("minTocHeadings")) {
if ($toc) {
this.$toc.append($toc);
} else {
this.$toc.empty();
}
if (["", "show"].includes(tocLabel?.value ?? "") || headingCount >= (options.getInt("minTocHeadings") ?? 0)) {
this.toggleInt(true);
this.noteContext.viewScope.tocPreviousVisible = true;
} else {
@ -137,10 +152,10 @@ export default class TocWidget extends RightPanelWidget {
/**
* Rendering formulas in strings using katex
*
* @param {string} html Note's html content
* @returns {string} The HTML content with mathematical formulas rendered by KaTeX.
* @param html Note's html content
* @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;
var matches = [...html.matchAll(mathTextRegex)];
let modifiedText = html;
@ -183,12 +198,12 @@ export default class TocWidget extends RightPanelWidget {
/**
* Builds a jquery table of contents.
*
* @param {string} html Note's html content
* @returns {$toc: jQuery, headingCount: integer} ordered list table of headings, nested by heading level
* @param html Note's html content
* @returns ordered list table of headings, nested by heading level
* with an onclick event that will cause the document to scroll to
* the desired position.
*/
async getToc(html) {
async getToc(html: string) {
// Regular expression for headings <h1>...</h1> using non-greedy
// matching and backreferences
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
let curLevel = 2;
const $ols = [$toc];
let headingCount;
let headingCount = 0;
for (let m = null, headingIndex = 0; (m = headingTagsRegex.exec(html)) !== null; headingIndex++) {
//
// Nest/unnest whatever necessary number of ordered lists
//
const newLevel = m[1];
const newLevel = parseInt(m[1]);
const levelDelta = newLevel - curLevel;
if (levelDelta > 0) {
// Open as many lists as newLevel - curLevel
@ -237,7 +252,7 @@ export default class TocWidget extends RightPanelWidget {
$toc = this.pullLeft($toc);
return {
$toc,
$toc: $toc,
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
*/
pullLeft($toc) {
pullLeft($toc: JQuery<HTMLElement>) {
while (true) {
const $children = $toc.children();
@ -264,7 +279,11 @@ export default class TocWidget extends RightPanelWidget {
return $toc;
}
async jumpToHeading(headingIndex) {
async jumpToHeading(headingIndex: number) {
if (!this.note || !this.noteContext) {
return;
}
// 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
@ -286,26 +305,28 @@ export default class TocWidget extends RightPanelWidget {
}
async closeTocCommand() {
this.noteContext.viewScope.tocTemporarilyHidden = true;
if (this.noteContext?.viewScope) {
this.noteContext.viewScope.tocTemporarilyHidden = true;
}
await this.refresh();
this.triggerCommand("reEvaluateRightPaneVisibility");
appContext.triggerEvent("reEvaluateTocWidgetVisibility", { noteId: this.noteId });
}
async showTocWidgetEvent({ noteId }) {
async showTocWidgetEvent({ noteId }: EventData<"showToc">) {
if (this.noteId === noteId) {
await this.refresh();
this.triggerCommand("reEvaluateRightPaneVisibility");
}
}
async entitiesReloadedEvent({ loadResults }) {
if (loadResults.isNoteContentReloaded(this.noteId)) {
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (this.noteId && loadResults.isNoteContentReloaded(this.noteId)) {
await this.refresh();
} else if (
loadResults
.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();
}