chore(react/type_widget): add code block & image integration

This commit is contained in:
Elian Doran 2025-09-21 20:47:40 +03:00
parent fb46e09428
commit 2947682783
No known key found for this signature in database
4 changed files with 79 additions and 100 deletions

View File

@ -11,11 +11,12 @@ import "@triliumnext/ckeditor5";
import FNote from "../../../entities/fnote"; import FNote from "../../../entities/fnote";
import { getLocaleById } from "../../../services/i18n"; import { getLocaleById } from "../../../services/i18n";
import { getMermaidConfig } from "../../../services/mermaid"; import { getMermaidConfig } from "../../../services/mermaid";
import { loadIncludedNote, refreshIncludedNote } from "./utils"; import { loadIncludedNote, refreshIncludedNote, setupImageOpening } from "./utils";
import { renderMathInElement } from "../../../services/math"; import { renderMathInElement } from "../../../services/math";
import link from "../../../services/link"; import link from "../../../services/link";
import { formatCodeBlocks } from "../../../services/syntax_highlight";
export default function ReadOnlyText({ note }: TypeWidgetProps) { export default function ReadOnlyText({ note, ntxId }: TypeWidgetProps) {
const blob = useNoteBlob(note); const blob = useNoteBlob(note);
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
const { isRtl } = useNoteLanguage(note); const { isRtl } = useNoteLanguage(note);
@ -29,6 +30,8 @@ export default function ReadOnlyText({ note }: TypeWidgetProps) {
applyIncludedNotes(container); applyIncludedNotes(container);
applyMath(container); applyMath(container);
applyReferenceLinks(container); applyReferenceLinks(container);
formatCodeBlocks($(container));
setupImageOpening(container, true);
}, [ blob ]); }, [ blob ]);
// React to included note changes. // React to included note changes.
@ -37,6 +40,12 @@ export default function ReadOnlyText({ note }: TypeWidgetProps) {
refreshIncludedNote(contentRef.current, noteId); refreshIncludedNote(contentRef.current, noteId);
}); });
// Search integration.
useTriliumEvent("executeWithContentElement", ({ resolve, ntxId: eventNtxId }) => {
if (eventNtxId !== ntxId || !contentRef.current) return;
resolve($(contentRef.current));
});
return ( return (
<div <div
className="note-detail-readonly-text note-detail-printable" className="note-detail-readonly-text note-detail-printable"

View File

@ -1,6 +1,8 @@
import appContext from "../../../components/app_context";
import content_renderer from "../../../services/content_renderer"; import content_renderer from "../../../services/content_renderer";
import froca from "../../../services/froca"; import froca from "../../../services/froca";
import link from "../../../services/link"; import link from "../../../services/link";
import utils from "../../../services/utils";
export async function loadIncludedNote(noteId: string, $el: JQuery<HTMLElement>) { export async function loadIncludedNote(noteId: string, $el: JQuery<HTMLElement>) {
const note = await froca.getNote(noteId); const note = await froca.getNote(noteId);
@ -25,3 +27,69 @@ export function refreshIncludedNote(container: HTMLDivElement, noteId: string) {
loadIncludedNote(noteId, $(includedNote as HTMLElement)); loadIncludedNote(noteId, $(includedNote as HTMLElement));
} }
} }
export function setupImageOpening(container: HTMLDivElement, singleClickOpens: boolean) {
const $container = $(container);
$container.on("dblclick", "img", (e) => openImageInCurrentTab($(e.target)));
$container.on("click", "img", (e) => {
e.stopPropagation();
const isLeftClick = e.which === 1;
const isMiddleClick = e.which === 2;
const ctrlKey = utils.isCtrlKey(e);
const activate = (isLeftClick && ctrlKey && e.shiftKey) || (isMiddleClick && e.shiftKey);
if ((isLeftClick && ctrlKey) || isMiddleClick) {
openImageInNewTab($(e.target), activate);
} else if (isLeftClick && singleClickOpens) {
openImageInCurrentTab($(e.target));
}
});
}
async function openImageInCurrentTab($img: JQuery<HTMLElement>) {
const parsedImage = await parseFromImage($img);
if (parsedImage) {
appContext.tabManager.getActiveContext()?.setNote(parsedImage.noteId, { viewScope: parsedImage.viewScope });
} else {
window.open($img.prop("src"), "_blank");
}
}
async function openImageInNewTab($img: JQuery<HTMLElement>, activate: boolean = false) {
const parsedImage = await parseFromImage($img);
if (parsedImage) {
appContext.tabManager.openTabWithNoteWithHoisting(parsedImage.noteId, { activate, viewScope: parsedImage.viewScope });
} else {
window.open($img.prop("src"), "_blank");
}
}
async function parseFromImage($img: JQuery<HTMLElement>) {
const imgSrc = $img.prop("src");
const imageNoteMatch = imgSrc.match(/\/api\/images\/([A-Za-z0-9_]+)\//);
if (imageNoteMatch) {
return {
noteId: imageNoteMatch[1],
viewScope: {}
};
}
const attachmentMatch = imgSrc.match(/\/api\/attachments\/([A-Za-z0-9_]+)\/image\//);
if (attachmentMatch) {
const attachmentId = attachmentMatch[1];
const attachment = await froca.getAttachment(attachmentId);
return {
noteId: attachment?.ownerId,
viewScope: {
viewMode: "attachments",
attachmentId: attachmentId
}
};
}
return null;
}

View File

@ -13,71 +13,6 @@ export default class AbstractTextTypeWidget extends TypeWidget {
this.refreshCodeBlockOptions(); this.refreshCodeBlockOptions();
} }
setupImageOpening(singleClickOpens: boolean) {
this.$widget.on("dblclick", "img", (e) => this.openImageInCurrentTab($(e.target)));
this.$widget.on("click", "img", (e) => {
e.stopPropagation();
const isLeftClick = e.which === 1;
const isMiddleClick = e.which === 2;
const ctrlKey = utils.isCtrlKey(e);
const activate = (isLeftClick && ctrlKey && e.shiftKey) || (isMiddleClick && e.shiftKey);
if ((isLeftClick && ctrlKey) || isMiddleClick) {
this.openImageInNewTab($(e.target), activate);
} else if (isLeftClick && singleClickOpens) {
this.openImageInCurrentTab($(e.target));
}
});
}
async openImageInCurrentTab($img: JQuery<HTMLElement>) {
const parsedImage = await this.parseFromImage($img);
if (parsedImage) {
appContext.tabManager.getActiveContext()?.setNote(parsedImage.noteId, { viewScope: parsedImage.viewScope });
} else {
window.open($img.prop("src"), "_blank");
}
}
async openImageInNewTab($img: JQuery<HTMLElement>, activate: boolean = false) {
const parsedImage = await this.parseFromImage($img);
if (parsedImage) {
appContext.tabManager.openTabWithNoteWithHoisting(parsedImage.noteId, { activate, viewScope: parsedImage.viewScope });
} else {
window.open($img.prop("src"), "_blank");
}
}
async parseFromImage($img: JQuery<HTMLElement>) {
const imgSrc = $img.prop("src");
const imageNoteMatch = imgSrc.match(/\/api\/images\/([A-Za-z0-9_]+)\//);
if (imageNoteMatch) {
return {
noteId: imageNoteMatch[1],
viewScope: {}
};
}
const attachmentMatch = imgSrc.match(/\/api\/attachments\/([A-Za-z0-9_]+)\/image\//);
if (attachmentMatch) {
const attachmentId = attachmentMatch[1];
const attachment = await froca.getAttachment(attachmentId);
return {
noteId: attachment?.ownerId,
viewScope: {
viewMode: "attachments",
attachmentId: attachmentId
}
};
}
return null;
}
async loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string | null = null) { async loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string | null = null) {
await linkService.loadReferenceLinkTitle($el, href); await linkService.loadReferenceLinkTitle($el, href);

View File

@ -5,39 +5,6 @@ import type { CommandListenerData, EventData } from "../../components/app_contex
import appContext from "../../components/app_context.js"; import appContext from "../../components/app_context.js";
export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget { export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
private $content!: JQuery<HTMLElement>;
static getType() {
return "readOnlyText";
}
doRender() {
this.$content = this.$widget.find(".note-detail-readonly-text-content");
this.setupImageOpening(true);
super.doRender();
}
cleanup() {
this.$content.html("");
}
async doRefresh(note: FNote) {
await formatCodeBlocks(this.$content);
}
async executeWithContentElementEvent({ resolve, ntxId }: EventData<"executeWithContentElement">) {
if (!this.isNoteContext(ntxId)) {
return;
}
await this.initialized;
resolve(this.$content);
}
buildTouchBarCommand({ TouchBar, buildIcon }: CommandListenerData<"buildTouchBar">) { buildTouchBarCommand({ TouchBar, buildIcon }: CommandListenerData<"buildTouchBar">) {
return [ return [
new TouchBar.TouchBarSpacer({ size: "flexible" }), new TouchBar.TouchBarSpacer({ size: "flexible" }),