mirror of
https://github.com/TriliumNext/Trilium.git
synced 2025-12-10 03:53:37 -06:00
Compare commits
33 Commits
f662b95dc9
...
cb3aced2ed
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb3aced2ed | ||
|
|
2f13a1ad21 | ||
|
|
045127adee | ||
|
|
db1a0c0362 | ||
|
|
dcaf91a878 | ||
|
|
a9209f5103 | ||
|
|
ea613986c2 | ||
|
|
1ed46bd47c | ||
|
|
f1ee79e75a | ||
|
|
cd27160905 | ||
|
|
9ddf4a1308 | ||
|
|
a1c5ed9eb5 | ||
|
|
7a4f19eada | ||
|
|
397fb785d6 | ||
|
|
75a1fcc933 | ||
|
|
292cbf1383 | ||
|
|
37a14fefb3 | ||
|
|
f424633d8c | ||
|
|
048258d2d1 | ||
|
|
f779108b6c | ||
|
|
522f3ae0a1 | ||
|
|
3fc7067c59 | ||
|
|
c600e8ef89 | ||
|
|
5ad267fe1b | ||
|
|
d8b3e438f8 | ||
|
|
2834af66e9 | ||
|
|
1bbf86fbeb | ||
|
|
f976dd8d30 | ||
|
|
2d3aa3a96e | ||
|
|
2666c1e196 | ||
|
|
4b8c8888ee | ||
|
|
ce1fd64aa9 | ||
|
|
5c9503732d |
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@ -8,6 +8,7 @@
|
||||
"redhat.vscode-yaml",
|
||||
"tobermory.es6-string-html",
|
||||
"vitest.explorer",
|
||||
"yzhang.markdown-all-in-one"
|
||||
"yzhang.markdown-all-in-one",
|
||||
"usernamehw.errorlens"
|
||||
]
|
||||
}
|
||||
|
||||
@ -34,6 +34,7 @@
|
||||
"@triliumnext/highlightjs": "workspace:*",
|
||||
"@triliumnext/share-theme": "workspace:*",
|
||||
"@triliumnext/split.js": "workspace:*",
|
||||
"@zumer/snapdom": "2.0.1",
|
||||
"autocomplete.js": "0.38.1",
|
||||
"bootstrap": "5.3.8",
|
||||
"boxicons": "2.1.4",
|
||||
|
||||
@ -34,6 +34,7 @@ import { AddLinkOpts } from "../widgets/dialogs/add_link.jsx";
|
||||
import { IncludeNoteOpts } from "../widgets/dialogs/include_note.jsx";
|
||||
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
|
||||
import type { MarkdownImportOpts } from "../widgets/dialogs/markdown_import.jsx";
|
||||
import type { InfoProps } from "../widgets/dialogs/info.jsx";
|
||||
|
||||
interface Layout {
|
||||
getRootWidget: (appContext: AppContext) => RootContainer;
|
||||
@ -124,7 +125,7 @@ export type CommandMappings = {
|
||||
isNewNote?: boolean;
|
||||
};
|
||||
showPromptDialog: PromptDialogOptions;
|
||||
showInfoDialog: ConfirmWithMessageOptions;
|
||||
showInfoDialog: InfoProps;
|
||||
showConfirmDialog: ConfirmWithMessageOptions;
|
||||
showRecentChanges: CommandData & { ancestorNoteId: string };
|
||||
showImportDialog: CommandData & { noteId: string };
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { Modal } from "bootstrap";
|
||||
import appContext from "../components/app_context.js";
|
||||
import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js";
|
||||
import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions, MessageType } from "../widgets/dialogs/confirm.js";
|
||||
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
||||
import { focusSavedElement, saveFocusedElement } from "./focus.js";
|
||||
import { InfoExtraProps } from "../widgets/dialogs/info.jsx";
|
||||
|
||||
export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true, config?: Partial<Modal.Options>) {
|
||||
if (closeActDialog) {
|
||||
@ -37,8 +38,8 @@ export function closeActiveDialog() {
|
||||
}
|
||||
}
|
||||
|
||||
async function info(message: string) {
|
||||
return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res }));
|
||||
async function info(message: MessageType, extraProps?: InfoExtraProps) {
|
||||
return new Promise((res) => appContext.triggerCommand("showInfoDialog", { ...extraProps, message, callback: res }));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -77,6 +77,10 @@ export interface Api {
|
||||
|
||||
/**
|
||||
* Entity whose event triggered this execution.
|
||||
*
|
||||
* <p>
|
||||
* For front-end scripts, generally there's no origin entity specified since the scripts are run by the user or automatically by the UI (widgets).
|
||||
* If there is an origin entity specified, then it's going to be a note entity.
|
||||
*/
|
||||
originEntity: unknown | null;
|
||||
|
||||
@ -278,12 +282,16 @@ export interface Api {
|
||||
getActiveContextNote(): FNote;
|
||||
|
||||
/**
|
||||
* @returns returns active context (split)
|
||||
* Obtains the currently active/focused split in the current tab.
|
||||
*
|
||||
* Note that this method does not return the note context of the "Quick edit" panel, it will return the note context behind it.
|
||||
*/
|
||||
getActiveContext(): NoteContext;
|
||||
|
||||
/**
|
||||
* @returns returns active main context (first split in a tab, represents the tab as a whole)
|
||||
* Obtains the main context of the current tab. This is the left-most split.
|
||||
*
|
||||
* Note that this method does not return the note context of the "Quick edit" panel, it will return the note context behind it.
|
||||
*/
|
||||
getActiveMainContext(): NoteContext;
|
||||
|
||||
|
||||
@ -9,6 +9,10 @@ export interface ToastOptions {
|
||||
message: string;
|
||||
timeout?: number;
|
||||
progress?: number;
|
||||
buttons?: {
|
||||
text: string;
|
||||
onClick: (api: { dismissToast: () => void }) => void;
|
||||
}[];
|
||||
}
|
||||
|
||||
export type ToastOptionsWithRequiredId = Omit<ToastOptions, "id"> & Required<Pick<ToastOptions, "id">>;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { dayjs } from "@triliumnext/commons";
|
||||
import type { ViewScope } from "./link.js";
|
||||
import FNote from "../entities/fnote";
|
||||
import { snapdom } from "@zumer/snapdom";
|
||||
|
||||
const SVG_MIME = "image/svg+xml";
|
||||
|
||||
@ -628,16 +629,69 @@ export function createImageSrcUrl(note: FNote) {
|
||||
return `api/images/${note.noteId}/${encodeURIComponent(note.title)}?timestamp=${Date.now()}`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Given a string representation of an SVG, triggers a download of the file on the client device.
|
||||
* Helper function to prepare an element for snapdom rendering.
|
||||
* Handles string parsing and temporary DOM attachment for style computation.
|
||||
*
|
||||
* @param source - Either an SVG/HTML string to be parsed, or an existing SVG/HTML element.
|
||||
* @returns An object containing the prepared element and a cleanup function.
|
||||
* The cleanup function removes temporarily attached elements from the DOM,
|
||||
* or is a no-op if the element was already in the DOM.
|
||||
*/
|
||||
function prepareElementForSnapdom(source: string | SVGElement | HTMLElement): {
|
||||
element: SVGElement | HTMLElement;
|
||||
cleanup: () => void;
|
||||
} {
|
||||
if (typeof source === 'string') {
|
||||
const parser = new DOMParser();
|
||||
|
||||
// Detect if content is SVG or HTML
|
||||
const isSvg = source.trim().startsWith('<svg');
|
||||
const mimeType = isSvg ? SVG_MIME : 'text/html';
|
||||
|
||||
const doc = parser.parseFromString(source, mimeType);
|
||||
const element = doc.documentElement;
|
||||
|
||||
// Temporarily attach to DOM for proper style computation
|
||||
element.style.position = 'absolute';
|
||||
element.style.left = '-9999px';
|
||||
element.style.top = '-9999px';
|
||||
document.body.appendChild(element);
|
||||
|
||||
return {
|
||||
element,
|
||||
cleanup: () => document.body.removeChild(element)
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
element: source,
|
||||
cleanup: () => {} // No-op for existing elements
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads an SVG using snapdom for proper rendering. Can accept either an SVG string, an SVG element, or an HTML element.
|
||||
*
|
||||
* @param nameWithoutExtension the name of the file. The .svg suffix is automatically added to it.
|
||||
* @param svgContent the content of the SVG file download.
|
||||
* @param svgSource either an SVG string, an SVGElement, or an HTMLElement to be downloaded.
|
||||
*/
|
||||
function downloadSvg(nameWithoutExtension: string, svgContent: string) {
|
||||
const filename = `${nameWithoutExtension}.svg`;
|
||||
const dataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`;
|
||||
triggerDownload(filename, dataUrl);
|
||||
async function downloadAsSvg(nameWithoutExtension: string, svgSource: string | SVGElement | HTMLElement) {
|
||||
const { element, cleanup } = prepareElementForSnapdom(svgSource);
|
||||
|
||||
try {
|
||||
const result = await snapdom(element, {
|
||||
backgroundColor: "transparent",
|
||||
scale: 2
|
||||
});
|
||||
triggerDownload(`${nameWithoutExtension}.svg`, result.url);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -658,62 +712,26 @@ function triggerDownload(fileName: string, dataUrl: string) {
|
||||
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string representation of an SVG, renders the SVG to PNG and triggers a download of the file on the client device.
|
||||
*
|
||||
* Note that the SVG must specify its width and height as attributes in order for it to be rendered.
|
||||
* Downloads an SVG as PNG using snapdom. Can accept either an SVG string, an SVG element, or an HTML element.
|
||||
*
|
||||
* @param nameWithoutExtension the name of the file. The .png suffix is automatically added to it.
|
||||
* @param svgContent the content of the SVG file download.
|
||||
* @returns a promise which resolves if the operation was successful, or rejects if it failed (permissions issue or some other issue).
|
||||
* @param svgSource either an SVG string, an SVGElement, or an HTMLElement to be converted to PNG.
|
||||
*/
|
||||
function downloadSvgAsPng(nameWithoutExtension: string, svgContent: string) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
// First, we need to determine the width and the height from the input SVG.
|
||||
const result = getSizeFromSvg(svgContent);
|
||||
if (!result) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
async function downloadAsPng(nameWithoutExtension: string, svgSource: string | SVGElement | HTMLElement) {
|
||||
const { element, cleanup } = prepareElementForSnapdom(svgSource);
|
||||
|
||||
// Convert the image to a blob.
|
||||
const { width, height } = result;
|
||||
|
||||
// Create an image element and load the SVG.
|
||||
const imageEl = new Image();
|
||||
imageEl.width = width;
|
||||
imageEl.height = height;
|
||||
imageEl.crossOrigin = "anonymous";
|
||||
imageEl.onload = () => {
|
||||
try {
|
||||
// Draw the image with a canvas.
|
||||
const canvasEl = document.createElement("canvas");
|
||||
canvasEl.width = imageEl.width;
|
||||
canvasEl.height = imageEl.height;
|
||||
document.body.appendChild(canvasEl);
|
||||
|
||||
const ctx = canvasEl.getContext("2d");
|
||||
if (!ctx) {
|
||||
reject();
|
||||
}
|
||||
|
||||
ctx?.drawImage(imageEl, 0, 0);
|
||||
|
||||
const imgUri = canvasEl.toDataURL("image/png")
|
||||
triggerDownload(`${nameWithoutExtension}.png`, imgUri);
|
||||
document.body.removeChild(canvasEl);
|
||||
resolve();
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
reject();
|
||||
}
|
||||
};
|
||||
imageEl.onerror = (e) => reject(e);
|
||||
imageEl.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`;
|
||||
});
|
||||
try {
|
||||
const result = await snapdom(element, {
|
||||
backgroundColor: "transparent",
|
||||
scale: 2
|
||||
});
|
||||
const pngImg = await result.toPng();
|
||||
await triggerDownload(`${nameWithoutExtension}.png`, pngImg.src);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
export function getSizeFromSvg(svgContent: string) {
|
||||
const svgDocument = (new DOMParser()).parseFromString(svgContent, SVG_MIME);
|
||||
|
||||
@ -925,8 +943,8 @@ export default {
|
||||
areObjectsEqual,
|
||||
copyHtmlToClipboard,
|
||||
createImageSrcUrl,
|
||||
downloadSvg,
|
||||
downloadSvgAsPng,
|
||||
downloadAsSvg,
|
||||
downloadAsPng,
|
||||
compareVersions,
|
||||
isUpdateAvailable,
|
||||
isLaunchBarConfig
|
||||
|
||||
@ -948,7 +948,7 @@
|
||||
"move-to-available-launchers": "نقل الى المشغلات المتوفرة",
|
||||
"duplicate-launcher": "تكرار المشغل <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"editable-text": {
|
||||
"editable_text": {
|
||||
"auto-detect-language": "تم اكتشافه تلقائيا"
|
||||
},
|
||||
"classic_editor_toolbar": {
|
||||
|
||||
@ -986,7 +986,9 @@
|
||||
"placeholder": "在这里输入您的代码笔记内容..."
|
||||
},
|
||||
"editable_text": {
|
||||
"placeholder": "在这里输入您的笔记内容..."
|
||||
"placeholder": "在这里输入您的笔记内容...",
|
||||
"auto-detect-language": "自动检测",
|
||||
"keeps-crashing": "编辑组件时持续崩溃。请尝试重启 Trilium。如果问题仍然存在,请考虑提交错误报告。"
|
||||
},
|
||||
"empty": {
|
||||
"open_note_instruction": "通过在下面的输入框中输入笔记标题或在树中选择笔记来打开笔记。",
|
||||
@ -1660,10 +1662,6 @@
|
||||
"move-to-available-launchers": "移动到可用启动器",
|
||||
"duplicate-launcher": "复制启动器 <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "自动检测",
|
||||
"keeps-crashing": "编辑组件时持续崩溃。请尝试重启 Trilium。如果问题仍然存在,请考虑提交错误报告。"
|
||||
},
|
||||
"highlighting": {
|
||||
"title": "代码块",
|
||||
"description": "控制文本笔记中代码块的语法高亮,代码笔记不会受到影响。",
|
||||
@ -2106,5 +2104,10 @@
|
||||
},
|
||||
"popup-editor": {
|
||||
"maximize": "切换至完整编辑器"
|
||||
},
|
||||
"server": {
|
||||
"unknown_http_error_title": "与服务器通讯错误",
|
||||
"unknown_http_error_content": "状态码: {{statusCode}}\n地址: {{method}} {{url}}\n信息: {{message}}",
|
||||
"traefik_blocks_requests": "如果您使用 Traefik 反向代理,它引入了一项影响与服务器的通信重大更改。"
|
||||
}
|
||||
}
|
||||
|
||||
@ -983,7 +983,9 @@
|
||||
"placeholder": "Gebe hier den Inhalt deiner Codenotiz ein..."
|
||||
},
|
||||
"editable_text": {
|
||||
"placeholder": "Gebe hier den Inhalt deiner Notiz ein..."
|
||||
"placeholder": "Gebe hier den Inhalt deiner Notiz ein...",
|
||||
"auto-detect-language": "Automatisch erkannt",
|
||||
"keeps-crashing": "Die Bearbeitungskomponente stürzt immer wieder ab. Bitte starten Sie Trilium neu. Wenn das Problem weiterhin besteht, erstellen Sie einen Fehlerbericht."
|
||||
},
|
||||
"empty": {
|
||||
"open_note_instruction": "Öffne eine Notiz, indem du den Titel der Notiz in die Eingabe unten eingibst oder eine Notiz in der Baumstruktur auswählst.",
|
||||
@ -1624,10 +1626,6 @@
|
||||
"move-to-available-launchers": "Zu verfügbaren Launchern verschieben",
|
||||
"duplicate-launcher": "Launcher duplizieren <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "Automatisch erkannt",
|
||||
"keeps-crashing": "Die Bearbeitungskomponente stürzt immer wieder ab. Bitte starten Sie Trilium neu. Wenn das Problem weiterhin besteht, erstellen Sie einen Fehlerbericht."
|
||||
},
|
||||
"highlighting": {
|
||||
"description": "Steuert die Syntaxhervorhebung für Codeblöcke in Textnotizen, Code-Notizen sind nicht betroffen.",
|
||||
"color-scheme": "Farbschema",
|
||||
|
||||
@ -205,7 +205,8 @@
|
||||
"info": {
|
||||
"modalTitle": "Info message",
|
||||
"closeButton": "Close",
|
||||
"okButton": "OK"
|
||||
"okButton": "OK",
|
||||
"copy_to_clipboard": "Copy to clipboard"
|
||||
},
|
||||
"jump_to_note": {
|
||||
"search_placeholder": "Search for note by its name or type > for commands...",
|
||||
@ -987,7 +988,14 @@
|
||||
"placeholder": "Type the content of your code note here..."
|
||||
},
|
||||
"editable_text": {
|
||||
"placeholder": "Type the content of your note here..."
|
||||
"placeholder": "Type the content of your note here...",
|
||||
"editor_crashed_title": "The text editor crashed",
|
||||
"editor_crashed_content": "Your content was recovered successfully, but a few of your most recent changes may not have been saved.",
|
||||
"editor_crashed_details_button": "View more details...",
|
||||
"editor_crashed_details_intro": "If you experience this error several times, consider reporting it on GitHub by pasting the information below.",
|
||||
"editor_crashed_details_title": "Technical information",
|
||||
"auto-detect-language": "Auto-detected",
|
||||
"keeps-crashing": "Editing component keeps crashing. Please try restarting Trilium. If problem persists, consider creating a bug report."
|
||||
},
|
||||
"empty": {
|
||||
"open_note_instruction": "Open a note by typing the note's title into the input below or choose a note in the tree.",
|
||||
@ -1826,10 +1834,6 @@
|
||||
"move-to-available-launchers": "Move to available launchers",
|
||||
"duplicate-launcher": "Duplicate launcher <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "Auto-detected",
|
||||
"keeps-crashing": "Editing component keeps crashing. Please try restarting Trilium. If problem persists, consider creating a bug report."
|
||||
},
|
||||
"highlighting": {
|
||||
"title": "Code Blocks",
|
||||
"description": "Controls the syntax highlighting for code blocks inside text notes, code notes will not be affected.",
|
||||
@ -1968,7 +1972,8 @@
|
||||
"button_title": "Export diagram as PNG"
|
||||
},
|
||||
"svg": {
|
||||
"export_to_png": "The diagram could not be exported to PNG."
|
||||
"export_to_png": "The diagram could not be exported to PNG.",
|
||||
"export_to_svg": "The diagram could not be exported to SVG."
|
||||
},
|
||||
"code_theme": {
|
||||
"title": "Appearance",
|
||||
|
||||
@ -987,7 +987,8 @@
|
||||
"placeholder": "Escriba el contenido de su nota de código aquí..."
|
||||
},
|
||||
"editable_text": {
|
||||
"placeholder": "Escribe aquí el contenido de tu nota..."
|
||||
"placeholder": "Escribe aquí el contenido de tu nota...",
|
||||
"auto-detect-language": "Detectado automáticamente"
|
||||
},
|
||||
"empty": {
|
||||
"open_note_instruction": "Abra una nota escribiendo el título de la nota en la entrada a continuación o elija una nota en el árbol.",
|
||||
@ -1812,9 +1813,6 @@
|
||||
"move-to-available-launchers": "Mover a lanzadores disponibles",
|
||||
"duplicate-launcher": "Duplicar lanzador <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "Detectado automáticamente"
|
||||
},
|
||||
"highlighting": {
|
||||
"title": "Bloques de código",
|
||||
"description": "Controla el resaltado de sintaxis para bloques de código dentro de las notas de texto, las notas de código no serán afectadas.",
|
||||
|
||||
@ -981,7 +981,8 @@
|
||||
"placeholder": "Saisir le contenu de votre note de code ici..."
|
||||
},
|
||||
"editable_text": {
|
||||
"placeholder": "Saisir le contenu de votre note ici..."
|
||||
"placeholder": "Saisir le contenu de votre note ici...",
|
||||
"auto-detect-language": "Détecté automatiquement"
|
||||
},
|
||||
"empty": {
|
||||
"open_note_instruction": "Ouvrez une note en tapant son titre dans la zone ci-dessous ou choisissez une note dans l'arborescence.",
|
||||
@ -1617,9 +1618,6 @@
|
||||
"move-to-available-launchers": "Déplacer vers les raccourcis disponibles",
|
||||
"duplicate-launcher": "Dupliquer le raccourci <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "Détecté automatiquement"
|
||||
},
|
||||
"highlighting": {
|
||||
"description": "Contrôle la coloration syntaxique des blocs de code à l'intérieur des notes texte, les notes de code ne seront pas affectées.",
|
||||
"color-scheme": "Jeu de couleurs",
|
||||
|
||||
@ -1491,7 +1491,9 @@
|
||||
"placeholder": "Digita qui il contenuto della tua nota di codice..."
|
||||
},
|
||||
"editable_text": {
|
||||
"placeholder": "Digita qui il contenuto della tua nota..."
|
||||
"placeholder": "Digita qui il contenuto della tua nota...",
|
||||
"auto-detect-language": "Rilevato automaticamente",
|
||||
"keeps-crashing": "Il componente di modifica continua a bloccarsi. Prova a riavviare Trilium. Se il problema persiste, valuta la possibilità di creare una segnalazione di bug."
|
||||
},
|
||||
"empty": {
|
||||
"open_note_instruction": "Apri una nota digitandone il titolo nel campo sottostante oppure scegli una nota nell'albero.",
|
||||
@ -1937,10 +1939,6 @@
|
||||
"move-to-available-launchers": "Passa ai launcher disponibili",
|
||||
"duplicate-launcher": "Duplica il launcher <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "Rilevato automaticamente",
|
||||
"keeps-crashing": "Il componente di modifica continua a bloccarsi. Prova a riavviare Trilium. Se il problema persiste, valuta la possibilità di creare una segnalazione di bug."
|
||||
},
|
||||
"highlighting": {
|
||||
"title": "Blocchi di codice",
|
||||
"description": "Controlla l'evidenziazione della sintassi per i blocchi di codice all'interno delle note di testo; le note di codice non saranno interessate.",
|
||||
|
||||
@ -1268,10 +1268,6 @@
|
||||
"duplicate-launcher": "ランチャーの複製 <kbd data-command=\"duplicateSubtree\">",
|
||||
"reset_launcher_confirm": "本当に「{{title}}」をリセットしますか? このノート(およびその子ノート)のすべてのデータと設定が失われ、ランチャーは元の場所に戻ります。"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "自動検出",
|
||||
"keeps-crashing": "編集コンポーネントがクラッシュし続けます。Trilium を再起動してください。問題が解決しない場合は、バグレポートの作成をご検討ください。"
|
||||
},
|
||||
"highlighting": {
|
||||
"title": "コードブロック",
|
||||
"description": "テキストノート内のコードブロックのシンタックスハイライトを制御します。コードノートには影響しません。",
|
||||
@ -1778,7 +1774,9 @@
|
||||
"placeholder": "ここにコードノートの内容を入力..."
|
||||
},
|
||||
"editable_text": {
|
||||
"placeholder": "ここにノートの内容を入力..."
|
||||
"placeholder": "ここにノートの内容を入力...",
|
||||
"auto-detect-language": "自動検出",
|
||||
"keeps-crashing": "編集コンポーネントがクラッシュし続けます。Trilium を再起動してください。問題が解決しない場合は、バグレポートの作成をご検討ください。"
|
||||
},
|
||||
"empty": {
|
||||
"open_note_instruction": "以下の入力欄にノートのタイトルを入力するか、ツリー内のノートを選択してノートを開きます。",
|
||||
|
||||
@ -1330,7 +1330,8 @@
|
||||
"placeholder": "Wpisz tutaj treść swojej notatki kodowej..."
|
||||
},
|
||||
"editable_text": {
|
||||
"placeholder": "Wpisz tutaj treść swojej notatki..."
|
||||
"placeholder": "Wpisz tutaj treść swojej notatki...",
|
||||
"auto-detect-language": "Wykryto automatycznie"
|
||||
},
|
||||
"empty": {
|
||||
"open_note_instruction": "Otwórz notatkę, wpisując jej tytuł w poniższe pole lub wybierz notatkę z drzewa.",
|
||||
@ -2014,9 +2015,6 @@
|
||||
"move-to-available-launchers": "Przenieś do dostępnych programów uruchamiających",
|
||||
"duplicate-launcher": "Duplikuj program uruchamiający <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "Wykryto automatycznie"
|
||||
},
|
||||
"highlighting": {
|
||||
"title": "Bloki kodu",
|
||||
"description": "Kontroluje podświetlanie składni dla bloków kodu w notatkach tekstowych, notatki kodowe nie będą miały wpływu.",
|
||||
|
||||
@ -954,7 +954,8 @@
|
||||
"placeholder": "Digite o conteúdo da sua nota de código aqui…"
|
||||
},
|
||||
"editable_text": {
|
||||
"placeholder": "Digite o conteúdo da sua nota aqui…"
|
||||
"placeholder": "Digite o conteúdo da sua nota aqui…",
|
||||
"auto-detect-language": "Detetado automaticamente"
|
||||
},
|
||||
"empty": {
|
||||
"open_note_instruction": "Abra uma nota a digitar o título da nota no campo abaixo ou escolha uma nota na árvore.",
|
||||
@ -1768,9 +1769,6 @@
|
||||
"move-to-available-launchers": "Mover para lançadores disponíveis",
|
||||
"duplicate-launcher": "Duplicar o lançador <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "Detetado automaticamente"
|
||||
},
|
||||
"highlighting": {
|
||||
"title": "Blocos de Código",
|
||||
"description": "Controla o destaque de sintaxe para blocos de código dentro de notas de texto, notas de código não serão afetadas.",
|
||||
|
||||
@ -1191,7 +1191,8 @@
|
||||
"placeholder": "Digite o conteúdo da sua nota de código aqui…"
|
||||
},
|
||||
"editable_text": {
|
||||
"placeholder": "Digite o conteúdo da sua nota aqui…"
|
||||
"placeholder": "Digite o conteúdo da sua nota aqui…",
|
||||
"auto-detect-language": "Detectado automaticamente"
|
||||
},
|
||||
"empty": {
|
||||
"search_placeholder": "buscar uma nota pelo nome",
|
||||
@ -1689,9 +1690,6 @@
|
||||
"move-to-available-launchers": "Mover para lançadores disponíveis",
|
||||
"duplicate-launcher": "Duplicar o lançador <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "Detectado automaticamente"
|
||||
},
|
||||
"highlighting": {
|
||||
"title": "Blocos de Código",
|
||||
"description": "Controla o destaque de sintaxe para blocos de código dentro de notas de texto, notas de código não serão afetadas.",
|
||||
|
||||
@ -491,7 +491,9 @@
|
||||
"placeholder": "Scrieți conținutul notiței de cod aici..."
|
||||
},
|
||||
"editable_text": {
|
||||
"placeholder": "Scrieți conținutul notiței aici..."
|
||||
"placeholder": "Scrieți conținutul notiței aici...",
|
||||
"auto-detect-language": "Automat",
|
||||
"keeps-crashing": "Componenta de editare se blochează în continuu. Încercați să reporniți Trilium. Dacă problema persistă, luați în considerare să raportați această problemă."
|
||||
},
|
||||
"edited_notes": {
|
||||
"deleted": "(șters)",
|
||||
@ -1626,10 +1628,6 @@
|
||||
"move-to-visible-launchers": "Mută în Lansatoare vizibile",
|
||||
"reset": "Resetează"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "Automat",
|
||||
"keeps-crashing": "Componenta de editare se blochează în continuu. Încercați să reporniți Trilium. Dacă problema persistă, luați în considerare să raportați această problemă."
|
||||
},
|
||||
"highlighting": {
|
||||
"color-scheme": "Temă de culori",
|
||||
"description": "Controlează evidențierea de sintaxă pentru blocurile de cod în interiorul notițelor text, notițele de tip cod nu vor fi afectate de aceste setări.",
|
||||
|
||||
@ -726,9 +726,6 @@
|
||||
"title": "Блоки кода",
|
||||
"description": "Управляет подсветкой синтаксиса для блоков кода внутри текстовых заметок. Заметки с типом \"Код\" не будут затронуты."
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "Определен автоматически"
|
||||
},
|
||||
"launcher_context_menu": {
|
||||
"reset": "Сбросить",
|
||||
"add-spacer": "Добавить разделитель",
|
||||
@ -2026,7 +2023,8 @@
|
||||
"placeholder": "Введите содержимое для заметки с кодом..."
|
||||
},
|
||||
"editable_text": {
|
||||
"placeholder": "Введите содержимое для заметки..."
|
||||
"placeholder": "Введите содержимое для заметки...",
|
||||
"auto-detect-language": "Определен автоматически"
|
||||
},
|
||||
"hoisted_note": {
|
||||
"confirm_unhoisting": "Запрошенная заметка «{{requestedNote}}» находится за пределами поддерева закрепленной заметки \"{{hoistedNote}}\", и для доступа к ней необходимо снять закрепление. Открепить заметку?"
|
||||
|
||||
@ -984,7 +984,9 @@
|
||||
"placeholder": "在這裡輸入您的程式碼筆記內容…"
|
||||
},
|
||||
"editable_text": {
|
||||
"placeholder": "在這裡輸入您的筆記內容…"
|
||||
"placeholder": "在這裡輸入您的筆記內容…",
|
||||
"auto-detect-language": "自動檢測",
|
||||
"keeps-crashing": "編輯元件持續發生崩潰。請嘗試重新啟動 Trilium。若問題仍存在,請考慮提交錯誤報告。"
|
||||
},
|
||||
"empty": {
|
||||
"open_note_instruction": "透過在下面的輸入框中輸入筆記標題或在樹中選擇筆記來打開筆記。",
|
||||
@ -1620,10 +1622,6 @@
|
||||
"move-to-available-launchers": "移動至可用啟動器",
|
||||
"duplicate-launcher": "複製啟動器 <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "自動檢測",
|
||||
"keeps-crashing": "編輯元件持續發生崩潰。請嘗試重新啟動 Trilium。若問題仍存在,請考慮提交錯誤報告。"
|
||||
},
|
||||
"highlighting": {
|
||||
"description": "控制文字筆記程式碼區塊中的語法高亮,程式碼筆記不會受到影響。",
|
||||
"color-scheme": "配色方案",
|
||||
|
||||
@ -130,9 +130,6 @@
|
||||
"move-to-available-launchers": "Перейти до доступних лаунчерів",
|
||||
"duplicate-launcher": "Дублікат програми запуску <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "Автовизначено"
|
||||
},
|
||||
"highlighting": {
|
||||
"color-scheme": "Схема кольорів",
|
||||
"title": "Блоки коду",
|
||||
@ -1076,7 +1073,8 @@
|
||||
"placeholder": "Введіть тут вміст вашої нотатки з кодом..."
|
||||
},
|
||||
"editable_text": {
|
||||
"placeholder": "Введіть тут вміст вашої нотатки..."
|
||||
"placeholder": "Введіть тут вміст вашої нотатки...",
|
||||
"auto-detect-language": "Автовизначено"
|
||||
},
|
||||
"empty": {
|
||||
"open_note_instruction": "Відкрийте нотатку, ввівши її заголовок в поле нижче, або виберіть нотатку в дереві.",
|
||||
|
||||
@ -48,12 +48,22 @@
|
||||
background-color: unset !important;
|
||||
}
|
||||
|
||||
.toast .toast-progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
background-color: var(--toast-text-color) !important;
|
||||
height: 4px;
|
||||
transition: width 0.1s linear;
|
||||
.toast {
|
||||
.toast-buttons {
|
||||
padding: 0 1em 1em 1em;
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.toast-progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
inset-inline-start: 0;
|
||||
inset-inline-end: 0;
|
||||
background-color: var(--toast-text-color) !important;
|
||||
height: 4px;
|
||||
transition: width 0.1s linear;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import { useEffect } from "preact/hooks";
|
||||
import { removeToastFromStore, ToastOptionsWithRequiredId, toasts } from "../services/toast";
|
||||
import Icon from "./react/Icon";
|
||||
import { RawHtmlBlock } from "./react/RawHtml";
|
||||
import Button from "./react/Button";
|
||||
|
||||
export default function ToastContainer() {
|
||||
return (
|
||||
@ -15,7 +16,7 @@ export default function ToastContainer() {
|
||||
)
|
||||
}
|
||||
|
||||
function Toast({ id, title, timeout, progress, message, icon }: ToastOptionsWithRequiredId) {
|
||||
function Toast({ id, title, timeout, progress, message, icon, buttons }: ToastOptionsWithRequiredId) {
|
||||
// Autohide.
|
||||
useEffect(() => {
|
||||
if (!timeout || timeout <= 0) return;
|
||||
@ -23,10 +24,14 @@ function Toast({ id, title, timeout, progress, message, icon }: ToastOptionsWith
|
||||
return () => clearTimeout(timerId);
|
||||
}, [ id, timeout ]);
|
||||
|
||||
function dismissToast() {
|
||||
removeToastFromStore(id);
|
||||
}
|
||||
|
||||
const closeButton = (
|
||||
<button
|
||||
type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"
|
||||
onClick={() => removeToastFromStore(id)}
|
||||
onClick={dismissToast}
|
||||
/>
|
||||
);
|
||||
const toastIcon = <Icon icon={icon.startsWith("bx ") ? icon : `bx bx-${icon}`} />;
|
||||
@ -52,6 +57,15 @@ function Toast({ id, title, timeout, progress, message, icon }: ToastOptionsWith
|
||||
<RawHtmlBlock className="toast-body" html={message} />
|
||||
|
||||
{!title && <div class="toast-header">{closeButton}</div>}
|
||||
|
||||
{buttons && (
|
||||
<div class="toast-buttons">
|
||||
{buttons.map(({ text, onClick }) => (
|
||||
<Button text={text} onClick={() => onClick({ dismissToast })} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
class="toast-progress"
|
||||
style={{ width: `${(progress ?? 0) * 100}%` }}
|
||||
|
||||
@ -4,10 +4,12 @@ import { t } from "../../services/i18n";
|
||||
import { useState } from "preact/hooks";
|
||||
import FormCheckbox from "../react/FormCheckbox";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import { isValidElement, type VNode } from "preact";
|
||||
import { RawHtmlBlock } from "../react/RawHtml";
|
||||
|
||||
interface ConfirmDialogProps {
|
||||
title?: string;
|
||||
message?: string | HTMLElement;
|
||||
message?: MessageType;
|
||||
callback?: ConfirmDialogCallback;
|
||||
isConfirmDeleteNoteBox?: boolean;
|
||||
}
|
||||
@ -20,7 +22,7 @@ export default function ConfirmDialog() {
|
||||
function showDialog(title: string | null, message: MessageType, callback: ConfirmDialogCallback, isConfirmDeleteNoteBox: boolean) {
|
||||
setOpts({
|
||||
title: title ?? undefined,
|
||||
message: (typeof message === "object" && "length" in message ? message[0] : message),
|
||||
message,
|
||||
callback,
|
||||
isConfirmDeleteNoteBox
|
||||
});
|
||||
@ -57,9 +59,10 @@ export default function ConfirmDialog() {
|
||||
show={shown}
|
||||
stackable
|
||||
>
|
||||
{!opts?.message || typeof opts?.message === "string"
|
||||
? <div>{(opts?.message as string) ?? ""}</div>
|
||||
: <div dangerouslySetInnerHTML={{ __html: opts?.message.outerHTML ?? "" }} />}
|
||||
{isValidElement(opts?.message)
|
||||
? opts?.message
|
||||
: <RawHtmlBlock html={opts?.message} />
|
||||
}
|
||||
|
||||
{opts?.isConfirmDeleteNoteBox && (
|
||||
<FormCheckbox
|
||||
@ -74,7 +77,7 @@ export default function ConfirmDialog() {
|
||||
|
||||
export type ConfirmDialogResult = false | ConfirmDialogOptions;
|
||||
export type ConfirmDialogCallback = (val?: ConfirmDialogResult) => void;
|
||||
type MessageType = string | HTMLElement | JQuery<HTMLElement>;
|
||||
export type MessageType = string | HTMLElement | JQuery<HTMLElement> | VNode;
|
||||
|
||||
export interface ConfirmDialogOptions {
|
||||
confirmed: boolean;
|
||||
|
||||
11
apps/client/src/widgets/dialogs/info.css
Normal file
11
apps/client/src/widgets/dialogs/info.css
Normal file
@ -0,0 +1,11 @@
|
||||
.modal.info-dialog {
|
||||
user-select: text;
|
||||
|
||||
h3 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,26 @@
|
||||
import { EventData } from "../../components/app_context";
|
||||
import Modal from "../react/Modal";
|
||||
import Modal, { type ModalProps } from "../react/Modal";
|
||||
import { t } from "../../services/i18n";
|
||||
import Button from "../react/Button";
|
||||
import { useRef, useState } from "preact/hooks";
|
||||
import { RawHtmlBlock } from "../react/RawHtml";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import { isValidElement } from "preact";
|
||||
import { ConfirmWithMessageOptions } from "./confirm";
|
||||
import "./info.css";
|
||||
import server from "../../services/server";
|
||||
import { ToMarkdownResponse } from "@triliumnext/commons";
|
||||
import { copyTextWithToast } from "../../services/clipboard_ext";
|
||||
|
||||
export interface InfoExtraProps extends Partial<Pick<ModalProps, "size" | "title">> {
|
||||
/** Adds a button in the footer that allows easily copying the content of the infobox to clipboard. */
|
||||
copyToClipboardButton?: boolean;
|
||||
}
|
||||
|
||||
export type InfoProps = ConfirmWithMessageOptions & InfoExtraProps;
|
||||
|
||||
export default function InfoDialog() {
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const [ opts, setOpts ] = useState<EventData<"showInfoDialog">>();
|
||||
const [ shown, setShown ] = useState(false);
|
||||
const okButtonRef = useRef<HTMLButtonElement>(null);
|
||||
@ -18,21 +32,42 @@ export default function InfoDialog() {
|
||||
|
||||
return (<Modal
|
||||
className="info-dialog"
|
||||
size="sm"
|
||||
title={t("info.modalTitle")}
|
||||
size={opts?.size ?? "sm"}
|
||||
title={opts?.title ?? t("info.modalTitle")}
|
||||
onHidden={() => {
|
||||
opts?.callback?.();
|
||||
setShown(false);
|
||||
}}
|
||||
onShown={() => okButtonRef.current?.focus?.()}
|
||||
footer={<Button
|
||||
buttonRef={okButtonRef}
|
||||
text={t("info.okButton")}
|
||||
onClick={() => setShown(false)}
|
||||
/>}
|
||||
modalRef={modalRef}
|
||||
footer={<>
|
||||
{opts?.copyToClipboardButton && (
|
||||
<Button
|
||||
text={t("info.copy_to_clipboard")}
|
||||
icon="bx bx-copy"
|
||||
onClick={async () => {
|
||||
const htmlContent = modalRef.current?.querySelector<HTMLDivElement>(".modal-body")?.innerHTML;
|
||||
if (!htmlContent) return;
|
||||
|
||||
const { markdownContent } = await server.post<ToMarkdownResponse>("other/to-markdown", { htmlContent });
|
||||
copyTextWithToast(markdownContent);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
buttonRef={okButtonRef}
|
||||
text={t("info.okButton")}
|
||||
onClick={() => setShown(false)}
|
||||
/>
|
||||
</>}
|
||||
show={shown}
|
||||
stackable
|
||||
scrollable
|
||||
>
|
||||
<RawHtmlBlock className="info-dialog-content" html={opts?.message ?? ""} />
|
||||
{isValidElement(opts?.message)
|
||||
? opts?.message
|
||||
: <RawHtmlBlock className="info-dialog-content" html={opts?.message} />
|
||||
}
|
||||
</Modal>);
|
||||
}
|
||||
|
||||
@ -7,15 +7,12 @@ import Modal from "../react/Modal";
|
||||
import Button from "../react/Button";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import { CKEditorApi } from "../type_widgets/text/CKEditorWithWatchdog";
|
||||
import { RenderMarkdownResponse } from "@triliumnext/commons";
|
||||
|
||||
export interface MarkdownImportOpts {
|
||||
editorApi: CKEditorApi;
|
||||
}
|
||||
|
||||
interface RenderMarkdownResponse {
|
||||
htmlContent: string;
|
||||
}
|
||||
|
||||
export default function MarkdownImportDialog() {
|
||||
const markdownImportTextArea = useRef<HTMLTextAreaElement>(null);
|
||||
const editorApiRef = useRef<CKEditorApi>(null);
|
||||
|
||||
@ -14,7 +14,7 @@ interface CustomTitleBarButton {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
interface ModalProps {
|
||||
export interface ModalProps {
|
||||
className: string;
|
||||
title: string | ComponentChildren;
|
||||
customTitleBarButtons?: (CustomTitleBarButton | null)[];
|
||||
|
||||
@ -78,12 +78,23 @@ export function useSpacedUpdate(callback: () => void | Promise<void>, interval =
|
||||
return spacedUpdateRef.current;
|
||||
}
|
||||
|
||||
export interface SavedData {
|
||||
content: string;
|
||||
attachments?: {
|
||||
role: string;
|
||||
title: string;
|
||||
mime: string;
|
||||
content: string;
|
||||
position: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export function useEditorSpacedUpdate({ note, noteContext, getData, onContentChange, dataSaved, updateInterval }: {
|
||||
note: FNote,
|
||||
noteContext: NoteContext | null | undefined,
|
||||
getData: () => Promise<object | undefined> | object | undefined,
|
||||
getData: () => Promise<SavedData | undefined> | SavedData | undefined,
|
||||
onContentChange: (newContent: string) => void,
|
||||
dataSaved?: () => void,
|
||||
dataSaved?: (savedData: SavedData) => void,
|
||||
updateInterval?: number;
|
||||
}) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
@ -99,7 +110,7 @@ export function useEditorSpacedUpdate({ note, noteContext, getData, onContentCha
|
||||
protected_session_holder.touchProtectedSessionIfNecessary(note);
|
||||
await server.put(`notes/${note.noteId}/data`, data, parentComponent?.componentId);
|
||||
|
||||
dataSaved?.();
|
||||
dataSaved?.(data);
|
||||
}
|
||||
}, [ note, getData, dataSaved ])
|
||||
const spacedUpdate = useSpacedUpdate(callback);
|
||||
|
||||
@ -11,6 +11,7 @@ import { useEditorSpacedUpdate, useNoteLabelBoolean, useSyncedRef, useTriliumEve
|
||||
import { refToJQuerySelector } from "../react/react_utils";
|
||||
import utils from "../../services/utils";
|
||||
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
|
||||
import { snapdom, SnapdomOptions } from "@zumer/snapdom";
|
||||
|
||||
const NEW_TOPIC_NAME = "";
|
||||
|
||||
@ -45,11 +46,24 @@ export default function MindMap({ note, ntxId, noteContext }: TypeWidgetProps) {
|
||||
const apiRef = useRef<MindElixirInstance>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [ isReadOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
|
||||
|
||||
const spacedUpdate = useEditorSpacedUpdate({
|
||||
note,
|
||||
noteContext,
|
||||
getData: async () => {
|
||||
if (!apiRef.current) return;
|
||||
|
||||
const result = await snapdom(apiRef.current.nodes, {
|
||||
backgroundColor: "transparent",
|
||||
scale: 2
|
||||
});
|
||||
|
||||
// a data URL in the format: "data:image/svg+xml;charset=utf-8,<url-encoded-svg>"
|
||||
// We need to extract the content after the comma and decode the URL encoding (%3C to <, %20 to space, etc.)
|
||||
// to get raw SVG content that Trilium's backend can store as an attachment
|
||||
const svgContent = decodeURIComponent(result.url.split(',')[1]);
|
||||
|
||||
return {
|
||||
content: apiRef.current.getDataString(),
|
||||
attachments: [
|
||||
@ -57,7 +71,7 @@ export default function MindMap({ note, ntxId, noteContext }: TypeWidgetProps) {
|
||||
role: "image",
|
||||
title: "mindmap-export.svg",
|
||||
mime: "image/svg+xml",
|
||||
content: await apiRef.current.exportSvg().text(),
|
||||
content: svgContent,
|
||||
position: 0
|
||||
}
|
||||
]
|
||||
@ -88,13 +102,13 @@ export default function MindMap({ note, ntxId, noteContext }: TypeWidgetProps) {
|
||||
// Export as PNG or SVG.
|
||||
useTriliumEvents([ "exportSvg", "exportPng" ], async ({ ntxId: eventNtxId }, eventName) => {
|
||||
if (eventNtxId !== ntxId || !apiRef.current) return;
|
||||
const title = note.title;
|
||||
const svg = await apiRef.current.exportSvg().text();
|
||||
const nodes = apiRef.current.nodes;
|
||||
if (eventName === "exportSvg") {
|
||||
utils.downloadSvg(title, svg);
|
||||
await utils.downloadAsSvg(note.title, nodes);
|
||||
} else {
|
||||
utils.downloadSvgAsPng(title, svg);
|
||||
await utils.downloadAsPng(note.title, nodes);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
const onKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
|
||||
@ -3,7 +3,7 @@ import NoteContext from "../../../components/note_context";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import { AppState, BinaryFileData, ExcalidrawImperativeAPI, ExcalidrawProps, LibraryItem } from "@excalidraw/excalidraw/types";
|
||||
import { useRef } from "preact/hooks";
|
||||
import { useEditorSpacedUpdate } from "../../react/hooks";
|
||||
import { SavedData, useEditorSpacedUpdate } from "../../react/hooks";
|
||||
import { ExcalidrawElement, NonDeletedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
|
||||
import { exportToSvg, getSceneVersion } from "@excalidraw/excalidraw";
|
||||
import server from "../../../services/server";
|
||||
@ -77,7 +77,7 @@ export default function useCanvasPersistence(note: FNote, noteContext: NoteConte
|
||||
const api = apiRef.current;
|
||||
if (!api) return;
|
||||
const { content, svg } = await getData(api);
|
||||
const attachments = [{ role: "image", title: "canvas-export.svg", mime: "image/svg+xml", content: svg, position: 0 }];
|
||||
const attachments: SavedData["attachments"] = [{ role: "image", title: "canvas-export.svg", mime: "image/svg+xml", content: svg, position: 0 }];
|
||||
|
||||
// libraryChanged is unset in dataSaved()
|
||||
if (libraryChanged.current) {
|
||||
@ -124,7 +124,7 @@ export default function useCanvasPersistence(note: FNote, noteContext: NoteConte
|
||||
title: libraryItem.id + libraryItem.name,
|
||||
mime: "application/json",
|
||||
content: JSON.stringify(libraryItem),
|
||||
position: position
|
||||
position
|
||||
});
|
||||
|
||||
position += 10;
|
||||
|
||||
@ -77,7 +77,7 @@ export function EditableCode({ note, ntxId, noteContext, debounceUpdate, parentC
|
||||
const spacedUpdate = useEditorSpacedUpdate({
|
||||
note,
|
||||
noteContext,
|
||||
getData: () => ({ content: editorRef.current?.getText() }),
|
||||
getData: () => ({ content: editorRef.current?.getText() ?? "" }),
|
||||
onContentChange: (content) => {
|
||||
const codeEditor = editorRef.current;
|
||||
if (!codeEditor) return;
|
||||
|
||||
@ -77,14 +77,25 @@ export default function SvgSplitEditor({ ntxId, note, attachmentName, renderSvg,
|
||||
}, [ note ]);
|
||||
|
||||
// Import/export
|
||||
useTriliumEvent("exportSvg", ({ ntxId: eventNtxId }) => {
|
||||
useTriliumEvent("exportSvg", async({ ntxId: eventNtxId }) => {
|
||||
if (eventNtxId !== ntxId || !svg) return;
|
||||
utils.downloadSvg(note.title, svg);
|
||||
|
||||
try {
|
||||
const svgEl = containerRef.current?.querySelector("svg");
|
||||
if (!svgEl) throw new Error("SVG element not found");
|
||||
await utils.downloadAsSvg(note.title, svgEl);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
toast.showError(t("svg.export_to_svg"));
|
||||
}
|
||||
});
|
||||
|
||||
useTriliumEvent("exportPng", async ({ ntxId: eventNtxId }) => {
|
||||
if (eventNtxId !== ntxId || !svg) return;
|
||||
try {
|
||||
await utils.downloadSvgAsPng(note.title, svg);
|
||||
const svgEl = containerRef.current?.querySelector("svg");
|
||||
if (!svgEl) throw new Error("SVG element not found");
|
||||
await utils.downloadAsPng(note.title, svgEl);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
toast.showError(t("svg.export_to_png"));
|
||||
|
||||
@ -25,7 +25,7 @@ interface CKEditorWithWatchdogProps extends Pick<HTMLProps<HTMLDivElement>, "cla
|
||||
watchdogRef: RefObject<EditorWatchdog>;
|
||||
watchdogConfig?: WatchdogConfig;
|
||||
onNotificationWarning?: (evt: any, data: any) => void;
|
||||
onWatchdogStateChange?: (watchdog: EditorWatchdog<any>) => void;
|
||||
onWatchdogStateChange?: (watchdog: EditorWatchdog) => void;
|
||||
onChange: () => void;
|
||||
/** Called upon whenever a new CKEditor instance is initialized, whether it's the first initialization, after a crash or after a config change that requires it (e.g. content language). */
|
||||
onEditorInitialized?: (editor: CKTextEditor) => void;
|
||||
@ -182,7 +182,7 @@ export default function CKEditorWithWatchdog({ containerRef: externalContainerRe
|
||||
watchdog.create(container);
|
||||
|
||||
return () => watchdog.destroy();
|
||||
}, [ contentLanguage, templates, uiLanguage ]);
|
||||
}, [ contentLanguage, templates, uiLanguage ]); // TODO: adding all dependencies here will cause errors during CK init.
|
||||
|
||||
// React to notification warning callback.
|
||||
useEffect(() => {
|
||||
@ -204,7 +204,7 @@ export default function CKEditorWithWatchdog({ containerRef: externalContainerRe
|
||||
);
|
||||
}
|
||||
|
||||
function buildWatchdog(isClassicEditor: boolean, watchdogConfig?: WatchdogConfig): EditorWatchdog<CKTextEditor> {
|
||||
function buildWatchdog(isClassicEditor: boolean, watchdogConfig?: WatchdogConfig): EditorWatchdog {
|
||||
if (isClassicEditor) {
|
||||
return new EditorWatchdog(ClassicEditor, watchdogConfig);
|
||||
} else {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
import dialog from "../../../services/dialog";
|
||||
import toast from "../../../services/toast";
|
||||
import utils, { hasTouchBar, isMobile } from "../../../services/utils";
|
||||
@ -57,6 +57,10 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
|
||||
onContentChange(newContent) {
|
||||
contentRef.current = newContent;
|
||||
watchdogRef.current?.editor?.setData(newContent);
|
||||
},
|
||||
dataSaved(savedData) {
|
||||
// Store back the saved data in order to retrieve it in case the CKEditor crashes.
|
||||
contentRef.current = savedData.content;
|
||||
}
|
||||
});
|
||||
const templates = useTemplates();
|
||||
@ -121,7 +125,7 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
|
||||
|
||||
const resp = await note_create.createNoteWithTypePrompt(notePath, {
|
||||
activate: false,
|
||||
title: title
|
||||
title
|
||||
});
|
||||
|
||||
if (!resp || !resp.note) return;
|
||||
@ -210,6 +214,8 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
|
||||
addTextToEditor(text);
|
||||
});
|
||||
|
||||
const onWatchdogStateChange = useWatchdogCrashHandling();
|
||||
|
||||
return (
|
||||
<>
|
||||
{note && !!templates && <CKEditorWithWatchdog
|
||||
@ -226,7 +232,7 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
|
||||
// A threshold specifying the number of errors (defaults to 3). After this limit is reached and the time between last errors is shorter than minimumNonErrorTimePeriod, the watchdog changes its state to crashedPermanently, and it stops restarting the editor. This prevents an infinite restart loop.
|
||||
crashNumberLimit: 10,
|
||||
// A minimum number of milliseconds between saving the editor data internally (defaults to 5000). Note that for large documents, this might impact the editor performance.
|
||||
saveInterval: 5000
|
||||
saveInterval: Number.MAX_SAFE_INTEGER
|
||||
}}
|
||||
templates={templates}
|
||||
onNotificationWarning={onNotificationWarning}
|
||||
@ -245,7 +251,9 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
|
||||
}
|
||||
|
||||
initialized.current.resolve();
|
||||
editor.setData(contentRef.current ?? "");
|
||||
// Restore the data, either on the first render or if the editor crashes.
|
||||
// We are not using CKEditor's built-in watch dog content, instead we are using the data we store regularly in the spaced update (see `dataSaved`).
|
||||
editor.setData(contentRef.current);
|
||||
parentComponent?.triggerEvent("textEditorRefreshed", { ntxId, editor });
|
||||
}}
|
||||
/>}
|
||||
@ -269,20 +277,57 @@ function useTemplates() {
|
||||
return templates;
|
||||
}
|
||||
|
||||
function onWatchdogStateChange(watchdog: EditorWatchdog) {
|
||||
const currentState = watchdog.state;
|
||||
logInfo(`CKEditor state changed to ${currentState}`);
|
||||
function useWatchdogCrashHandling() {
|
||||
const hasCrashed = useRef(false);
|
||||
const onWatchdogStateChange = useCallback((watchdog: EditorWatchdog) => {
|
||||
const currentState = watchdog.state;
|
||||
logInfo(`CKEditor state changed to ${currentState}`);
|
||||
|
||||
if (!["crashed", "crashedPermanently"].includes(currentState)) {
|
||||
return;
|
||||
}
|
||||
if (currentState === "ready") {
|
||||
hasCrashed.current = false;
|
||||
watchdog.editor?.focus();
|
||||
}
|
||||
|
||||
logError(`CKEditor crash logs: ${JSON.stringify(watchdog.crashes, null, 4)}`);
|
||||
if (!["crashed", "crashedPermanently"].includes(currentState)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentState === "crashedPermanently") {
|
||||
dialog.info(t("editable-text.keeps-crashing"));
|
||||
watchdog.editor?.enableReadOnlyMode("crashed-editor");
|
||||
}
|
||||
hasCrashed.current = true;
|
||||
const formattedCrash = JSON.stringify(watchdog.crashes, null, 4);
|
||||
logError(`CKEditor crash logs: ${formattedCrash}`);
|
||||
|
||||
if (currentState === "crashed") {
|
||||
toast.showPersistent({
|
||||
id: "editor-crashed",
|
||||
icon: "bx bx-bug",
|
||||
title: t("editable_text.editor_crashed_title"),
|
||||
message: t("editable_text.editor_crashed_content"),
|
||||
buttons: [
|
||||
{
|
||||
text: t("editable_text.editor_crashed_details_button"),
|
||||
onClick: ({ dismissToast }) => {
|
||||
dismissToast();
|
||||
dialog.info(<>
|
||||
<p>{t("editable_text.editor_crashed_details_intro")}</p>
|
||||
<h3>{t("editable_text.editor_crashed_details_title")}</h3>
|
||||
<pre><code class="language-application-json">{formattedCrash}</code></pre>
|
||||
</>, {
|
||||
title: t("editable_text.editor_crashed_title"),
|
||||
size: "lg",
|
||||
copyToClipboardButton: true
|
||||
});
|
||||
}
|
||||
}
|
||||
],
|
||||
timeout: 20_000
|
||||
});
|
||||
} else if (currentState === "crashedPermanently") {
|
||||
dialog.info(t("editable_text.keeps-crashing"));
|
||||
watchdog.editor?.enableReadOnlyMode("crashed-editor");
|
||||
}
|
||||
}, []);
|
||||
|
||||
return onWatchdogStateChange;
|
||||
}
|
||||
|
||||
function onNotificationWarning(data, evt) {
|
||||
@ -302,7 +347,7 @@ function EditableTextTouchBar({ watchdogRef, refreshTouchBarRef }: { watchdogRef
|
||||
const [ headingSelectedIndex, setHeadingSelectedIndex ] = useState<number>();
|
||||
|
||||
function refresh() {
|
||||
let headingSelectedIndex: number | undefined = undefined;
|
||||
let headingSelectedIndex: number | undefined;
|
||||
const editor = watchdogRef.current?.editor;
|
||||
const headingCommand = editor?.commands.get("heading");
|
||||
const paragraphCommand = editor?.commands.get("paragraph");
|
||||
@ -316,7 +361,7 @@ function EditableTextTouchBar({ watchdogRef, refreshTouchBarRef }: { watchdogRef
|
||||
setHeadingSelectedIndex(headingSelectedIndex);
|
||||
}
|
||||
|
||||
useEffect(refresh, []);
|
||||
useEffect(refresh, [ watchdogRef ]);
|
||||
refreshTouchBarRef.current = refresh;
|
||||
|
||||
return (
|
||||
|
||||
@ -220,7 +220,7 @@ function buildListOfLanguages() {
|
||||
return [
|
||||
{
|
||||
language: mimeTypesService.MIME_TYPE_AUTO,
|
||||
label: t("editable-text.auto-detect-language")
|
||||
label: t("editable_text.auto-detect-language")
|
||||
},
|
||||
...userLanguages
|
||||
];
|
||||
|
||||
@ -4328,6 +4328,28 @@ paths:
|
||||
text/html:
|
||||
schema:
|
||||
type: string
|
||||
/api/other/to-markdown:
|
||||
post:
|
||||
tags: [Utilities]
|
||||
summary: Renders given HTML to Markdown
|
||||
operationId: toMarkdown
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [htmlContent]
|
||||
properties:
|
||||
htmlContent:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: The input text rendered as Markdown
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<h2>Breaking change in Traefik 3.6.4</h2>
|
||||
<p>Traefik 3.6.4 introduced a <a href="https://doc.traefik.io/traefik/migrate/v3/#encoded-characters-in-request-path">breaking change</a> regarding
|
||||
how percent-encoded characters are handled in URLs. More specifically some
|
||||
URLs used by Trilium (such as <code spellcheck="false">search/%23workspace%20%23!template</code>)
|
||||
URLs used by Trilium (such as <code>search/%23workspace%20%23!template</code>)
|
||||
are automatically rejected by Traefik, resulting in HTTP 400 errors.</p>
|
||||
<p>To solve this, the Traefik <a href="https://doc.traefik.io/traefik/getting-started/configuration-overview/#the-install-configuration"><strong>static</strong> configuration</a> must
|
||||
be modified in order to allow those characters:</p><pre><code class="language-text-x-yaml">entryPoints:
|
||||
@ -36,7 +36,7 @@
|
||||
<h3>Setup needed environment variables</h3>
|
||||
<p>After setting up a reverse proxy, make sure to configure the <a class="reference-link"
|
||||
href="#root/_help_LLzSMXACKhUs">Trusted proxy</a>.</p>
|
||||
<h3>Example <code spellcheck="false">docker-compose.yaml</code></h3><pre><code class="language-text-x-yaml">services:
|
||||
<h3>Example <code>docker-compose.yaml</code></h3><pre><code class="language-text-x-yaml">services:
|
||||
trilium:
|
||||
image: triliumnext/trilium
|
||||
container_name: trilium
|
||||
|
||||
@ -5,122 +5,169 @@
|
||||
<p>Global events are attached to the script note via label. Simply create
|
||||
e.g. "run" label with some of these values and script note will be executed
|
||||
once the event occurs.</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Label</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>run</code>
|
||||
</td>
|
||||
<td>
|
||||
<p>Defines on which events script should run. Possible values are:</p>
|
||||
<ul>
|
||||
<li><code>frontendStartup</code> - when Trilium frontend starts up (or is refreshed),
|
||||
but not on mobile.</li>
|
||||
<li><code>mobileStartup</code> - when Trilium frontend starts up (or is refreshed),
|
||||
on mobile.</li>
|
||||
<li><code>backendStartup</code> - when Trilium backend starts up</li>
|
||||
<li><code>hourly</code> - run once an hour. You can use additional label <code>runAtHour</code> to
|
||||
specify at which hour, on the back-end.</li>
|
||||
<li><code>daily</code> - run once a day, on the back-end</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>runOnInstance</code>
|
||||
</td>
|
||||
<td>Specifies that the script should only run on a particular <a class="reference-link"
|
||||
href="#root/_help_c5xB8m4g2IY6">Trilium instance</a>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>runAtHour</code>
|
||||
</td>
|
||||
<td>On which hour should this run. Should be used together with <code>#run=hourly</code>.
|
||||
Can be defined multiple times for more runs during the day.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<figure class="table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Label</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code spellcheck="false">run</code>
|
||||
</td>
|
||||
<td>
|
||||
<p>Defines on which events script should run. Possible values are:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e244b14e102cf1b0d4954e8fd455ea77b"><code spellcheck="false">frontendStartup</code> - when Trilium frontend
|
||||
starts up (or is refreshed), but not on mobile.</li>
|
||||
<li data-list-item-id="ea8f8ca86e7b351dd86108848ccb9103a"><code spellcheck="false">mobileStartup</code> - when Trilium frontend starts
|
||||
up (or is refreshed), on mobile.</li>
|
||||
<li data-list-item-id="e658488cf1a0862603088ef384e41b8b6"><code spellcheck="false">backendStartup</code> - when Trilium backend starts
|
||||
up</li>
|
||||
<li data-list-item-id="ef40ba992fc450d33a18ca4cb031eca66"><code spellcheck="false">hourly</code> - run once an hour. You can use
|
||||
additional label <code spellcheck="false">runAtHour</code> to specify at
|
||||
which hour, on the back-end.</li>
|
||||
<li data-list-item-id="e07458d4f55b6eb42468a5535b8425c5f"><code spellcheck="false">daily</code> - run once a day, on the back-end</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code spellcheck="false">runOnInstance</code>
|
||||
</td>
|
||||
<td>Specifies that the script should only run on a particular <a class="reference-link"
|
||||
href="#root/_help_c5xB8m4g2IY6">Trilium instance</a>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code spellcheck="false">runAtHour</code>
|
||||
</td>
|
||||
<td>On which hour should this run. Should be used together with <code spellcheck="false">#run=hourly</code>.
|
||||
Can be defined multiple times for more runs during the day.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
<h2>Entity events</h2>
|
||||
<p>Other events are bound to some entity, these are defined as <a href="#root/_help_zEY4DaJG4YT5">relations</a> -
|
||||
meaning that script is triggered only if note has this script attached
|
||||
to it through relations (or it can inherit it).</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Relation</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>runOnNoteCreation</code>
|
||||
</td>
|
||||
<td>executes when note is created on backend. Use this relation if you want
|
||||
to run the script for all notes created under a specific subtree. In that
|
||||
case, create it on the subtree root note and make it inheritable. A new
|
||||
note created within the subtree (any depth) will trigger the script.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>runOnChildNoteCreation</code>
|
||||
</td>
|
||||
<td>executes when new note is created under the note where this relation is
|
||||
defined</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>runOnNoteTitleChange</code>
|
||||
</td>
|
||||
<td>executes when note title is changed (includes note creation as well)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>runOnNoteContentChange</code>
|
||||
</td>
|
||||
<td>executes when note content is changed (includes note creation as well).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>runOnNoteChange</code>
|
||||
</td>
|
||||
<td>executes when note is changed (includes note creation as well). Does not
|
||||
include content changes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>runOnNoteDeletion</code>
|
||||
</td>
|
||||
<td>executes when note is being deleted</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>runOnBranchCreation</code>
|
||||
</td>
|
||||
<td>executes when a branch is created. Branch is a link between parent note
|
||||
and child note and is created e.g. when cloning or moving note.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>runOnBranchChange</code>
|
||||
</td>
|
||||
<td>executes when a branch is updated. (since v0.62)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>runOnBranchDeletion</code>
|
||||
</td>
|
||||
<td>executes when a branch is deleted. Branch is a link between parent note
|
||||
and child note and is deleted e.g. when moving note (old branch/link is
|
||||
deleted).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>runOnAttributeCreation</code>
|
||||
</td>
|
||||
<td>executes when new attribute is created for the note which defines this
|
||||
relation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>runOnAttributeChange</code>
|
||||
</td>
|
||||
<td>executes when the attribute is changed of a note which defines this relation.
|
||||
This is triggered also when the attribute is deleted</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<figure class="table"
|
||||
style="width:100%;">
|
||||
<table class="ck-table-resized">
|
||||
<colgroup>
|
||||
<col style="width:20.65%;">
|
||||
<col style="width:47.48%;">
|
||||
<col style="width:31.87%;">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Relation</th>
|
||||
<th>Trigger condition</th>
|
||||
<th>Origin entity (see below)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code spellcheck="false">runOnNoteCreation</code>
|
||||
</td>
|
||||
<td>executes when note is created on backend. Use this relation if you want
|
||||
to run the script for all notes created under a specific subtree. In that
|
||||
case, create it on the subtree root note and make it inheritable. A new
|
||||
note created within the subtree (any depth) will trigger the script.</td>
|
||||
<td>The <code spellcheck="false">BNote</code> that got created.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code spellcheck="false">runOnChildNoteCreation</code>
|
||||
</td>
|
||||
<td>executes when new note is created under the note where this relation is
|
||||
defined</td>
|
||||
<td>The <code spellcheck="false">BNote</code> of the child that got created.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code spellcheck="false">runOnNoteTitleChange</code>
|
||||
</td>
|
||||
<td>executes when note title is changed (includes note creation as well)</td>
|
||||
<td>The <code spellcheck="false">BNote</code> of the note whose title got changed.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code spellcheck="false">runOnNoteContentChange</code>
|
||||
</td>
|
||||
<td>executes when note content is changed (includes note creation as well).</td>
|
||||
<td>The <code spellcheck="false">BNote</code> of the note whose content got
|
||||
changed.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code spellcheck="false">runOnNoteChange</code>
|
||||
</td>
|
||||
<td>executes when note is changed (includes note creation as well). Does not
|
||||
include content changes</td>
|
||||
<td>The <code spellcheck="false">BNote</code> of the note that got changed.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code spellcheck="false">runOnNoteDeletion</code>
|
||||
</td>
|
||||
<td>executes when note is being deleted</td>
|
||||
<td>The <code spellcheck="false">BNote</code> of the note that got (soft) deleted.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code spellcheck="false">runOnBranchCreation</code>
|
||||
</td>
|
||||
<td>executes when a branch is created. Branch is a link between parent note
|
||||
and child note and is created e.g. when cloning or moving note.</td>
|
||||
<td>The <code spellcheck="false">BBranch</code> that got created.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code spellcheck="false">runOnBranchChange</code>
|
||||
</td>
|
||||
<td>executes when a branch is updated. (since v0.62)</td>
|
||||
<td>The <code spellcheck="false">BBranch</code> that got changed.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code spellcheck="false">runOnBranchDeletion</code>
|
||||
</td>
|
||||
<td>executes when a branch is deleted. Branch is a link between parent note
|
||||
and child note and is deleted e.g. when moving note (old branch/link is
|
||||
deleted).</td>
|
||||
<td>The <code spellcheck="false">BBranch</code> that got (soft) deleted.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code spellcheck="false">runOnAttributeCreation</code>
|
||||
</td>
|
||||
<td>executes when new attribute is created for the note which defines this
|
||||
relation</td>
|
||||
<td>The <code spellcheck="false">BAttribute</code> that got created.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code spellcheck="false">runOnAttributeChange</code>
|
||||
</td>
|
||||
<td>executes when the attribute is changed of a note which defines this relation.
|
||||
This is triggered also when the attribute is deleted</td>
|
||||
<td>The <code spellcheck="false">BAttribute</code> that got changed.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
<h2>Origin entity</h2>
|
||||
<p>When a script is run by an event such as the ones described above,
|
||||
<code
|
||||
spellcheck="false">api.originEntity</code>will get populated with the note, branch or attribute
|
||||
that triggered the change.</p>
|
||||
<p>For example, here's a script with <code spellcheck="false">~runOnAttributeChange</code> which
|
||||
automatically changes the color of a note based on the value of the
|
||||
<code
|
||||
spellcheck="false">mycategory</code>label:</p><pre><code class="language-application-javascript-env-frontend">const attr = api.originEntity;
|
||||
|
||||
if (attr.name !== "mycategory") return;
|
||||
|
||||
const note = api.getNote(attr.noteId);
|
||||
|
||||
if (attr.value === "Health") {
|
||||
|
||||
note.setLabel("color", "green");
|
||||
|
||||
} else {
|
||||
|
||||
note.removeLabel("color");
|
||||
|
||||
}</code></pre>
|
||||
@ -2,6 +2,8 @@ import type { Request } from "express";
|
||||
|
||||
import becca from "../../becca/becca.js";
|
||||
import markdownService from "../../services/import/markdown.js";
|
||||
import markdown from "../../services/export/markdown.js";
|
||||
import { RenderMarkdownResponse, ToMarkdownResponse } from "@triliumnext/commons";
|
||||
|
||||
function getIconUsage() {
|
||||
const iconClassToCountMap: Record<string, number> = {};
|
||||
@ -29,13 +31,26 @@ function getIconUsage() {
|
||||
|
||||
function renderMarkdown(req: Request) {
|
||||
const { markdownContent } = req.body;
|
||||
|
||||
if (!markdownContent || typeof markdownContent !== 'string') {
|
||||
throw new Error('markdownContent parameter is required and must be a string');
|
||||
}
|
||||
return {
|
||||
htmlContent: markdownService.renderToHtml(markdownContent, "")
|
||||
};
|
||||
} satisfies RenderMarkdownResponse;
|
||||
}
|
||||
|
||||
function toMarkdown(req: Request) {
|
||||
const { htmlContent } = req.body;
|
||||
if (!htmlContent || typeof htmlContent !== 'string') {
|
||||
throw new Error('htmlContent parameter is required and must be a string');
|
||||
}
|
||||
return {
|
||||
markdownContent: markdown.toMarkdown(htmlContent)
|
||||
} satisfies ToMarkdownResponse;
|
||||
}
|
||||
|
||||
export default {
|
||||
getIconUsage,
|
||||
renderMarkdown
|
||||
renderMarkdown,
|
||||
toMarkdown
|
||||
};
|
||||
|
||||
@ -348,6 +348,7 @@ function register(app: express.Application) {
|
||||
route(GET, "/api/fonts", [auth.checkApiAuthOrElectron], fontsRoute.getFontCss);
|
||||
apiRoute(GET, "/api/other/icon-usage", otherRoute.getIconUsage);
|
||||
apiRoute(PST, "/api/other/render-markdown", otherRoute.renderMarkdown);
|
||||
apiRoute(PST, "/api/other/to-markdown", otherRoute.toMarkdown);
|
||||
apiRoute(GET, "/api/recent-changes/:ancestorNoteId", recentChangesApiRoute.getRecentChanges);
|
||||
apiRoute(GET, "/api/edited-notes/:date", revisionsApiRoute.getEditedNotesOnDate);
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# Documentation
|
||||
There are multiple types of documentation for Trilium:<img class="image-style-align-right" src="api/images/tZh9T30Ojdsq/Documentation_image.png" width="205" height="162">
|
||||
There are multiple types of documentation for Trilium:<img class="image-style-align-right" src="api/images/iivQU4mW8qJE/Documentation_image.png" width="205" height="162">
|
||||
|
||||
* The _User Guide_ represents the user-facing documentation. This documentation can be browsed by users directly from within Trilium, by pressing <kbd>F1</kbd>.
|
||||
* The _Developer's Guide_ represents a set of Markdown documents that present the internals of Trilium, for developers.
|
||||
|
||||
@ -5,22 +5,39 @@
|
||||
|
||||
Global events are attached to the script note via label. Simply create e.g. "run" label with some of these values and script note will be executed once the event occurs.
|
||||
|
||||
<table><thead><tr><th>Label</th><th>Description</th></tr></thead><tbody><tr><td><code>run</code></td><td><p>Defines on which events script should run. Possible values are:</p><ul><li><code>frontendStartup</code> - when Trilium frontend starts up (or is refreshed), but not on mobile.</li><li><code>mobileStartup</code> - when Trilium frontend starts up (or is refreshed), on mobile.</li><li><code>backendStartup</code> - when Trilium backend starts up</li><li><code>hourly</code> - run once an hour. You can use additional label <code>runAtHour</code> to specify at which hour, on the back-end.</li><li><code>daily</code> - run once a day, on the back-end</li></ul></td></tr><tr><td><code>runOnInstance</code></td><td>Specifies that the script should only run on a particular <a class="reference-link" href="../../Advanced%20Usage/Configuration%20(config.ini%20or%20environment%20variables)/Trilium%20instance.md">Trilium instance</a>.</td></tr><tr><td><code>runAtHour</code></td><td>On which hour should this run. Should be used together with <code>#run=hourly</code>. Can be defined multiple times for more runs during the day.</td></tr></tbody></table>
|
||||
<table><thead><tr><th>Label</th><th>Description</th></tr></thead><tbody><tr><td><code spellcheck="false">run</code></td><td><p>Defines on which events script should run. Possible values are:</p><ul><li data-list-item-id="e244b14e102cf1b0d4954e8fd455ea77b"><code spellcheck="false">frontendStartup</code> - when Trilium frontend starts up (or is refreshed), but not on mobile.</li><li data-list-item-id="ea8f8ca86e7b351dd86108848ccb9103a"><code spellcheck="false">mobileStartup</code> - when Trilium frontend starts up (or is refreshed), on mobile.</li><li data-list-item-id="e658488cf1a0862603088ef384e41b8b6"><code spellcheck="false">backendStartup</code> - when Trilium backend starts up</li><li data-list-item-id="ef40ba992fc450d33a18ca4cb031eca66"><code spellcheck="false">hourly</code> - run once an hour. You can use additional label <code spellcheck="false">runAtHour</code> to specify at which hour, on the back-end.</li><li data-list-item-id="e07458d4f55b6eb42468a5535b8425c5f"><code spellcheck="false">daily</code> - run once a day, on the back-end</li></ul></td></tr><tr><td><code spellcheck="false">runOnInstance</code></td><td>Specifies that the script should only run on a particular <a class="reference-link" href="../../Advanced%20Usage/Configuration%20(config.ini%20or%20environment%20variables)/Trilium%20instance.md">Trilium instance</a>.</td></tr><tr><td><code spellcheck="false">runAtHour</code></td><td>On which hour should this run. Should be used together with <code spellcheck="false">#run=hourly</code>. Can be defined multiple times for more runs during the day.</td></tr></tbody></table>
|
||||
|
||||
## Entity events
|
||||
|
||||
Other events are bound to some entity, these are defined as [relations](../../Advanced%20Usage/Attributes.md) - meaning that script is triggered only if note has this script attached to it through relations (or it can inherit it).
|
||||
|
||||
| Relation | Description |
|
||||
| --- | --- |
|
||||
| `runOnNoteCreation` | executes when note is created on backend. Use this relation if you want to run the script for all notes created under a specific subtree. In that case, create it on the subtree root note and make it inheritable. A new note created within the subtree (any depth) will trigger the script. |
|
||||
| `runOnChildNoteCreation` | executes when new note is created under the note where this relation is defined |
|
||||
| `runOnNoteTitleChange` | executes when note title is changed (includes note creation as well) |
|
||||
| `runOnNoteContentChange` | executes when note content is changed (includes note creation as well). |
|
||||
| `runOnNoteChange` | executes when note is changed (includes note creation as well). Does not include content changes |
|
||||
| `runOnNoteDeletion` | executes when note is being deleted |
|
||||
| `runOnBranchCreation` | executes when a branch is created. Branch is a link between parent note and child note and is created e.g. when cloning or moving note. |
|
||||
| `runOnBranchChange` | executes when a branch is updated. (since v0.62) |
|
||||
| `runOnBranchDeletion` | executes when a branch is deleted. Branch is a link between parent note and child note and is deleted e.g. when moving note (old branch/link is deleted). |
|
||||
| `runOnAttributeCreation` | executes when new attribute is created for the note which defines this relation |
|
||||
| `runOnAttributeChange` | executes when the attribute is changed of a note which defines this relation. This is triggered also when the attribute is deleted |
|
||||
| Relation | Trigger condition | Origin entity (see below) |
|
||||
| --- | --- | --- |
|
||||
| `runOnNoteCreation` | executes when note is created on backend. Use this relation if you want to run the script for all notes created under a specific subtree. In that case, create it on the subtree root note and make it inheritable. A new note created within the subtree (any depth) will trigger the script. | The `BNote` that got created. |
|
||||
| `runOnChildNoteCreation` | executes when new note is created under the note where this relation is defined | The `BNote` of the child that got created. |
|
||||
| `runOnNoteTitleChange` | executes when note title is changed (includes note creation as well) | The `BNote` of the note whose title got changed. |
|
||||
| `runOnNoteContentChange` | executes when note content is changed (includes note creation as well). | The `BNote` of the note whose content got changed. |
|
||||
| `runOnNoteChange` | executes when note is changed (includes note creation as well). Does not include content changes | The `BNote` of the note that got changed. |
|
||||
| `runOnNoteDeletion` | executes when note is being deleted | The `BNote` of the note that got (soft) deleted. |
|
||||
| `runOnBranchCreation` | executes when a branch is created. Branch is a link between parent note and child note and is created e.g. when cloning or moving note. | The `BBranch` that got created. |
|
||||
| `runOnBranchChange` | executes when a branch is updated. (since v0.62) | The `BBranch` that got changed. |
|
||||
| `runOnBranchDeletion` | executes when a branch is deleted. Branch is a link between parent note and child note and is deleted e.g. when moving note (old branch/link is deleted). | The `BBranch` that got (soft) deleted. |
|
||||
| `runOnAttributeCreation` | executes when new attribute is created for the note which defines this relation | The `BAttribute` that got created. |
|
||||
| `runOnAttributeChange` | executes when the attribute is changed of a note which defines this relation. This is triggered also when the attribute is deleted | The `BAttribute` that got changed. |
|
||||
|
||||
## Origin entity
|
||||
|
||||
When a script is run by an event such as the ones described above, `api.originEntity` will get populated with the note, branch or attribute that triggered the change.
|
||||
|
||||
For example, here's a script with `~runOnAttributeChange` which automatically changes the color of a note based on the value of the `mycategory` label:
|
||||
|
||||
```javascript
|
||||
const attr = api.originEntity;
|
||||
if (attr.name !== "mycategory") return;
|
||||
const note = api.getNote(attr.noteId);
|
||||
if (attr.value === "Health") {
|
||||
note.setLabel("color", "green");
|
||||
} else {
|
||||
note.removeLabel("color");
|
||||
}
|
||||
```
|
||||
23
packages/ckeditor5/src/custom_watchdog.ts
Normal file
23
packages/ckeditor5/src/custom_watchdog.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { CKEditorError, EditorWatchdog } from "ckeditor5";
|
||||
|
||||
const IGNORED_ERRORS = [
|
||||
// See: https://github.com/TriliumNext/Trilium/issues/5776
|
||||
"TypeError: Cannot read properties of null (reading 'parent')",
|
||||
|
||||
// See: https://github.com/TriliumNext/Trilium/issues/7739
|
||||
"model-nodelist-offset-out-of-bounds"
|
||||
]
|
||||
|
||||
export default class CustomWatchdog extends EditorWatchdog {
|
||||
|
||||
_isErrorComingFromThisItem(error: CKEditorError): boolean {
|
||||
for (const ignoredError of IGNORED_ERRORS) {
|
||||
if (error.message.includes(ignoredError)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return super._isErrorComingFromThisItem(error);
|
||||
}
|
||||
|
||||
}
|
||||
@ -4,7 +4,7 @@ import "./theme/code_block_toolbar.css";
|
||||
import { COMMON_PLUGINS, CORE_PLUGINS, POPUP_EDITOR_PLUGINS } from "./plugins.js";
|
||||
import { BalloonEditor, DecoupledEditor, FindAndReplaceEditing, FindCommand } from "ckeditor5";
|
||||
import "./translation_overrides.js";
|
||||
export { EditorWatchdog } from "ckeditor5";
|
||||
export { default as EditorWatchdog } from "./custom_watchdog";
|
||||
export { PREMIUM_PLUGINS } from "./plugins.js";
|
||||
export type { EditorConfig, MentionFeed, MentionFeedObjectItem, ModelNode, ModelPosition, ModelElement, WatchdogConfig, WatchdogState } from "ckeditor5";
|
||||
export type { TemplateDefinition } from "ckeditor5-premium-features";
|
||||
|
||||
@ -277,3 +277,11 @@ export interface NoteMapPostResponse {
|
||||
export interface UpdateAttributeResponse {
|
||||
attributeId: string;
|
||||
}
|
||||
|
||||
export interface RenderMarkdownResponse {
|
||||
htmlContent: string;
|
||||
}
|
||||
|
||||
export interface ToMarkdownResponse {
|
||||
markdownContent: string;
|
||||
}
|
||||
|
||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@ -211,6 +211,9 @@ importers:
|
||||
'@triliumnext/split.js':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/splitjs
|
||||
'@zumer/snapdom':
|
||||
specifier: 2.0.1
|
||||
version: 2.0.1
|
||||
autocomplete.js:
|
||||
specifier: 0.38.1
|
||||
version: 0.38.1
|
||||
@ -5882,6 +5885,9 @@ packages:
|
||||
resolution: {integrity: sha512-PI6UdgpSeVoGvzguKHmy2bwOqI3UYkntLZOCpyJSKIi7234c5aJmQYkJB/P4P2YUJkqhbqvu7iM2/0eJZ178nA==}
|
||||
engines: {bun: '>=0.7.0', deno: '>=1.0.0', node: '>=16.5.0'}
|
||||
|
||||
'@zumer/snapdom@2.0.1':
|
||||
resolution: {integrity: sha512-78/qbYl2FTv4H6qaXcNfAujfIOSzdvs83NW63VbyC9QA3sqNPfPvhn4xYMO6Gy11hXwJUEhd0z65yKiNzDwy9w==}
|
||||
|
||||
abab@2.0.6:
|
||||
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
|
||||
deprecated: Use your platform's native atob() and btoa() methods instead
|
||||
@ -20833,6 +20839,8 @@ snapshots:
|
||||
|
||||
'@zip.js/zip.js@2.8.2': {}
|
||||
|
||||
'@zumer/snapdom@2.0.1': {}
|
||||
|
||||
abab@2.0.6: {}
|
||||
|
||||
abbrev@1.1.1: {}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user