Compare commits

..

3 Commits

65 changed files with 575 additions and 2828 deletions

View File

@@ -989,10 +989,6 @@ export default class FNote {
); );
} }
isJsx() {
return (this.type === "code" && this.mime === "text/jsx");
}
/** @returns true if this note is HTML */ /** @returns true if this note is HTML */
isHtml() { isHtml() {
return (this.type === "code" || this.type === "file" || this.type === "render") && this.mime === "text/html"; return (this.type === "code" || this.type === "file" || this.type === "render") && this.mime === "text/html";
@@ -1000,7 +996,7 @@ export default class FNote {
/** @returns JS script environment - either "frontend" or "backend" */ /** @returns JS script environment - either "frontend" or "backend" */
getScriptEnv() { getScriptEnv() {
if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith("env=frontend")) || this.isJsx()) { if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith("env=frontend"))) {
return "frontend"; return "frontend";
} }
@@ -1022,7 +1018,7 @@ export default class FNote {
* @returns a promise that resolves when the script has been run. Additionally, for front-end notes, the promise will contain the value that is returned by the script. * @returns a promise that resolves when the script has been run. Additionally, for front-end notes, the promise will contain the value that is returned by the script.
*/ */
async executeScript() { async executeScript() {
if (!(this.isJavaScript() || this.isJsx())) { if (!this.isJavaScript()) {
throw new Error(`Note ${this.noteId} is of type ${this.type} and mime ${this.mime} and thus cannot be executed`); throw new Error(`Note ${this.noteId} is of type ${this.type} and mime ${this.mime} and thus cannot be executed`);
} }

View File

@@ -184,7 +184,7 @@ export default class DesktopLayout {
.child(new HighlightsListWidget()) .child(new HighlightsListWidget())
.child(...this.customWidgets.get("right-pane")) .child(...this.customWidgets.get("right-pane"))
) )
.optChild(isNewLayout, <RightPanelContainer widgetsByParent={this.customWidgets} />) .optChild(isNewLayout, <RightPanelContainer customWidgets={this.customWidgets.get("right-pane")} />)
) )
.optChild(!launcherPaneIsHorizontal && isNewLayout, <StatusBar />) .optChild(!launcherPaneIsHorizontal && isNewLayout, <StatusBar />)
) )

View File

@@ -1,16 +1,10 @@
import { h, VNode } from "preact";
import Component from "../components/component.js";
import BasicWidget, { ReactWrappedWidget } from "../widgets/basic_widget.js";
import RightPanelWidget from "../widgets/right_panel_widget.js";
import froca from "./froca.js";
import type { Entity } from "./frontend_script_api.js";
import { WidgetDefinitionWithType } from "./frontend_script_api_preact.js";
import { t } from "./i18n.js";
import ScriptContext from "./script_context.js"; import ScriptContext from "./script_context.js";
import server from "./server.js"; import server from "./server.js";
import toastService, { showErrorForScriptNote } from "./toast.js"; import toastService, { showError } from "./toast.js";
import utils, { getErrorMessage } from "./utils.js"; import froca from "./froca.js";
import utils from "./utils.js";
import { t } from "./i18n.js";
import type { Entity } from "./frontend_script_api.js";
// TODO: Deduplicate with server. // TODO: Deduplicate with server.
export interface Bundle { export interface Bundle {
@@ -20,13 +14,9 @@ export interface Bundle {
allNoteIds: string[]; allNoteIds: string[];
} }
type LegacyWidget = (BasicWidget | RightPanelWidget) & { interface Widget {
parentWidget?: string; parentWidget?: string;
}; }
type WithNoteId<T> = T & {
_noteId: string;
};
export type Widget = WithNoteId<(LegacyWidget | WidgetDefinitionWithType)>;
async function getAndExecuteBundle(noteId: string, originEntity = null, script = null, params = null) { async function getAndExecuteBundle(noteId: string, originEntity = null, script = null, params = null) {
const bundle = await server.post<Bundle>(`script/bundle/${noteId}`, { const bundle = await server.post<Bundle>(`script/bundle/${noteId}`, {
@@ -37,8 +27,6 @@ async function getAndExecuteBundle(noteId: string, originEntity = null, script =
return await executeBundle(bundle, originEntity); return await executeBundle(bundle, originEntity);
} }
export type ParentName = "left-pane" | "center-pane" | "note-detail-pane" | "right-pane";
export async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $container?: JQuery<HTMLElement>) { export async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $container?: JQuery<HTMLElement>) {
const apiContext = await ScriptContext(bundle.noteId, bundle.allNoteIds, originEntity, $container); const apiContext = await ScriptContext(bundle.noteId, bundle.allNoteIds, originEntity, $container);
@@ -64,7 +52,7 @@ export async function executeBundle(bundle: Bundle, originEntity?: Entity | null
async function executeStartupBundles() { async function executeStartupBundles() {
const isMobile = utils.isMobile(); const isMobile = utils.isMobile();
const scriptBundles = await server.get<Bundle[]>(`script/startup${ isMobile ? "?mobile=true" : ""}`); const scriptBundles = await server.get<Bundle[]>("script/startup" + (isMobile ? "?mobile=true" : ""));
for (const bundle of scriptBundles) { for (const bundle of scriptBundles) {
await executeBundle(bundle); await executeBundle(bundle);
@@ -72,75 +60,42 @@ async function executeStartupBundles() {
} }
export class WidgetsByParent { export class WidgetsByParent {
private legacyWidgets: Record<string, WithNoteId<LegacyWidget>[]>; private byParent: Record<string, Widget[]>;
private preactWidgets: Record<string, WithNoteId<WidgetDefinitionWithType>[]>;
constructor() { constructor() {
this.legacyWidgets = {}; this.byParent = {};
this.preactWidgets = {};
} }
add(widget: Widget) { add(widget: Widget) {
let hasParentWidget = false; if (!widget.parentWidget) {
let isPreact = false; console.log(`Custom widget does not have mandatory 'parentWidget' property defined`);
if ("type" in widget && widget.type === "preact-widget") { return;
// React-based script.
const reactWidget = widget as WithNoteId<WidgetDefinitionWithType>;
this.preactWidgets[reactWidget.parent] = this.preactWidgets[reactWidget.parent] || [];
this.preactWidgets[reactWidget.parent].push(reactWidget);
isPreact = true;
hasParentWidget = !!reactWidget.parent;
} else if ("parentWidget" in widget && widget.parentWidget) {
this.legacyWidgets[widget.parentWidget] = this.legacyWidgets[widget.parentWidget] || [];
this.legacyWidgets[widget.parentWidget].push(widget);
hasParentWidget = !!widget.parentWidget;
} }
if (!hasParentWidget) { this.byParent[widget.parentWidget] = this.byParent[widget.parentWidget] || [];
showErrorForScriptNote(widget._noteId, t("toast.widget-missing-parent", { this.byParent[widget.parentWidget].push(widget);
property: isPreact ? "parent" : "parentWidget"
}));
}
} }
get(parentName: ParentName) { get(parentName: string) {
const widgets: (BasicWidget | VNode)[] = this.getLegacyWidgets(parentName); if (!this.byParent[parentName]) {
for (const preactWidget of this.getPreactWidgets(parentName)) { return [];
const el = h(preactWidget.render, {});
const widget = new ReactWrappedWidget(el);
widget.contentSized();
if (preactWidget.position) {
widget.position = preactWidget.position;
} }
widgets.push(widget);
}
return widgets;
}
getLegacyWidgets(parentName: ParentName): (BasicWidget | RightPanelWidget)[] {
if (!this.legacyWidgets[parentName]) return [];
return ( return (
this.legacyWidgets[parentName] this.byParent[parentName]
// previously, custom widgets were provided as a single instance, but that has the disadvantage // previously, custom widgets were provided as a single instance, but that has the disadvantage
// for splits where we actually need multiple instaces and thus having a class to instantiate is better // for splits where we actually need multiple instaces and thus having a class to instantiate is better
// https://github.com/zadam/trilium/issues/4274 // https://github.com/zadam/trilium/issues/4274
.map((w: any) => (w.prototype ? new w() : w)) .map((w: any) => (w.prototype ? new w() : w))
); );
} }
getPreactWidgets(parentName: ParentName) {
return this.preactWidgets[parentName] ?? [];
}
} }
async function getWidgetBundlesByParent() { async function getWidgetBundlesByParent() {
const widgetsByParent = new WidgetsByParent();
try {
const scriptBundles = await server.get<Bundle[]>("script/widgets"); const scriptBundles = await server.get<Bundle[]>("script/widgets");
const widgetsByParent = new WidgetsByParent();
for (const bundle of scriptBundles) { for (const bundle of scriptBundles) {
let widget; let widget;
@@ -168,14 +123,6 @@ async function getWidgetBundlesByParent() {
continue; continue;
} }
} }
} catch (e) {
toastService.showPersistent({
id: `custom-widget-list-failure`,
title: t("toast.widget-list-error.title"),
message: getErrorMessage(e),
icon: "bx bx-error-circle"
});
}
return widgetsByParent; return widgetsByParent;
} }

View File

@@ -1,27 +1,26 @@
import { dayjs, formatLogMessage } from "@triliumnext/commons"; import server from "./server.js";
import utils from "./utils.js";
import appContext from "../components/app_context.js"; import toastService from "./toast.js";
import type Component from "../components/component.js";
import type NoteContext from "../components/note_context.js";
import type FNote from "../entities/fnote.js";
import BasicWidget, { ReactWrappedWidget } from "../widgets/basic_widget.js";
import NoteContextAwareWidget from "../widgets/note_context_aware_widget.js";
import RightPanelWidget from "../widgets/right_panel_widget.js";
import dateNotesService from "./date_notes.js";
import dialogService from "./dialog.js";
import froca from "./froca.js";
import { preactAPI } from "./frontend_script_api_preact.js";
import { t } from "./i18n.js";
import linkService from "./link.js"; import linkService from "./link.js";
import froca from "./froca.js";
import noteTooltipService from "./note_tooltip.js"; import noteTooltipService from "./note_tooltip.js";
import protectedSessionService from "./protected_session.js"; import protectedSessionService from "./protected_session.js";
import dateNotesService from "./date_notes.js";
import searchService from "./search.js"; import searchService from "./search.js";
import server from "./server.js"; import RightPanelWidget from "../widgets/right_panel_widget.js";
import shortcutService from "./shortcuts.js";
import SpacedUpdate from "./spaced_update.js";
import toastService from "./toast.js";
import utils from "./utils.js";
import ws from "./ws.js"; import ws from "./ws.js";
import appContext from "../components/app_context.js";
import NoteContextAwareWidget from "../widgets/note_context_aware_widget.js";
import BasicWidget, { ReactWrappedWidget } from "../widgets/basic_widget.js";
import SpacedUpdate from "./spaced_update.js";
import shortcutService from "./shortcuts.js";
import dialogService from "./dialog.js";
import type FNote from "../entities/fnote.js";
import { t } from "./i18n.js";
import { dayjs } from "@triliumnext/commons";
import type NoteContext from "../components/note_context.js";
import type Component from "../components/component.js";
import { formatLogMessage } from "@triliumnext/commons";
/** /**
* A whole number * A whole number
@@ -465,8 +464,6 @@ export interface Api {
* Log given message to the log pane in UI * Log given message to the log pane in UI
*/ */
log(message: string | object): void; log(message: string | object): void;
preact: typeof preactAPI;
} }
/** /**
@@ -536,9 +533,9 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig
return params.map((p) => { return params.map((p) => {
if (typeof p === "function") { if (typeof p === "function") {
return `!@#Function: ${p.toString()}`; return `!@#Function: ${p.toString()}`;
} } else {
return p; return p;
}
}); });
} }
@@ -565,9 +562,9 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
return ret.executionResult; return ret.executionResult;
} } else {
throw new Error(`server error: ${ret.error}`); throw new Error(`server error: ${ret.error}`);
}
}; };
this.runOnBackend = async (func, params = []) => { this.runOnBackend = async (func, params = []) => {
@@ -724,8 +721,6 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig
this.logMessages[noteId].push(message); this.logMessages[noteId].push(message);
this.logSpacedUpdates[noteId].scheduleUpdate(); this.logSpacedUpdates[noteId].scheduleUpdate();
}; };
this.preact = preactAPI;
} }
export default FrontendScriptApi as any as { export default FrontendScriptApi as any as {

View File

@@ -1,101 +0,0 @@
import { Fragment, h, VNode } from "preact";
import * as hooks from "preact/hooks";
import ActionButton from "../widgets/react/ActionButton";
import Admonition from "../widgets/react/Admonition";
import Button from "../widgets/react/Button";
import CKEditor from "../widgets/react/CKEditor";
import Collapsible from "../widgets/react/Collapsible";
import Dropdown from "../widgets/react/Dropdown";
import FormCheckbox from "../widgets/react/FormCheckbox";
import FormDropdownList from "../widgets/react/FormDropdownList";
import { FormFileUploadActionButton,FormFileUploadButton } from "../widgets/react/FormFileUpload";
import FormGroup from "../widgets/react/FormGroup";
import { FormDropdownDivider, FormDropdownSubmenu,FormListItem } from "../widgets/react/FormList";
import FormRadioGroup from "../widgets/react/FormRadioGroup";
import FormText from "../widgets/react/FormText";
import FormTextArea from "../widgets/react/FormTextArea";
import FormTextBox from "../widgets/react/FormTextBox";
import FormToggle from "../widgets/react/FormToggle";
import * as triliumHooks from "../widgets/react/hooks";
import Icon from "../widgets/react/Icon";
import LinkButton from "../widgets/react/LinkButton";
import LoadingSpinner from "../widgets/react/LoadingSpinner";
import Modal from "../widgets/react/Modal";
import NoteAutocomplete from "../widgets/react/NoteAutocomplete";
import NoteLink from "../widgets/react/NoteLink";
import RawHtml from "../widgets/react/RawHtml";
import Slider from "../widgets/react/Slider";
import RightPanelWidget from "../widgets/sidebar/RightPanelWidget";
export interface WidgetDefinition {
parent: "right-pane",
render: () => VNode,
position?: number,
}
export interface WidgetDefinitionWithType extends WidgetDefinition {
type: "preact-widget"
}
export interface LauncherWidgetDefinitionWithType {
type: "preact-launcher-widget"
render: () => VNode
}
export const preactAPI = Object.freeze({
// Core
h,
Fragment,
/**
* Method that must be run for widget scripts that run on Preact, using JSX. The method just returns the same definition, reserved for future typechecking and perhaps validation purposes.
*
* @param definition the widget definition.
*/
defineWidget(definition: WidgetDefinition) {
return {
type: "preact-widget",
...definition
};
},
defineLauncherWidget(definition: Omit<LauncherWidgetDefinitionWithType, "type">) {
return {
type: "preact-launcher-widget",
...definition
};
},
// Basic widgets
ActionButton,
Admonition,
Button,
CKEditor,
Collapsible,
Dropdown,
FormCheckbox,
FormDropdownList,
FormFileUploadButton, FormFileUploadActionButton,
FormGroup,
FormListItem, FormDropdownDivider, FormDropdownSubmenu,
FormRadioGroup,
FormText,
FormTextArea,
FormTextBox,
FormToggle,
Icon,
LinkButton,
LoadingSpinner,
Modal,
NoteAutocomplete,
NoteLink,
RawHtml,
Slider,
// Specialized widgets
RightPanelWidget,
...hooks,
...triliumHooks
});

View File

@@ -1,10 +1,6 @@
import { h, VNode } from "preact";
import type FNote from "../entities/fnote.js";
import { renderReactWidgetAtElement } from "../widgets/react/react_utils.jsx";
import bundleService, { type Bundle } from "./bundle.js";
import froca from "./froca.js";
import server from "./server.js"; import server from "./server.js";
import bundleService, { type Bundle } from "./bundle.js";
import type FNote from "../entities/fnote.js";
async function render(note: FNote, $el: JQuery<HTMLElement>) { async function render(note: FNote, $el: JQuery<HTMLElement>) {
const relations = note.getRelations("renderNote"); const relations = note.getRelations("renderNote");
@@ -21,34 +17,12 @@ async function render(note: FNote, $el: JQuery<HTMLElement>) {
$scriptContainer.append(bundle.html); $scriptContainer.append(bundle.html);
// async so that scripts cannot block trilium execution // async so that scripts cannot block trilium execution
bundleService.executeBundle(bundle, note, $scriptContainer).then(result => { bundleService.executeBundle(bundle, note, $scriptContainer);
// Render JSX
if (bundle.html === "") {
renderIfJsx(bundle, result, $el);
}
});
} }
return renderNoteIds.length > 0; return renderNoteIds.length > 0;
} }
async function renderIfJsx(bundle: Bundle, result: unknown, $el: JQuery<HTMLElement>) {
// Ensure the root script note is actually a JSX.
const rootScriptNoteId = await froca.getNote(bundle.noteId);
if (rootScriptNoteId?.mime !== "text/jsx") return;
// Ensure the output is a valid el.
if (typeof result !== "function") return;
// Obtain the parent component.
const closestComponent = glob.getComponentByEl($el.closest(".component")[0]);
if (!closestComponent) return;
// Render the element.
const el = h(result as () => VNode, {});
renderReactWidgetAtElement(closestComponent, el, $el[0]);
}
export default { export default {
render render
}; };

View File

@@ -133,11 +133,11 @@ async function call<T>(method: string, url: string, componentId?: string, option
}; };
ipc.send("server-request", { ipc.send("server-request", {
requestId, requestId: requestId,
headers, headers: headers,
method, method: method,
url: `/${window.glob.baseApiUrl}${url}`, url: `/${window.glob.baseApiUrl}${url}`,
data data: data
}); });
})) as any; })) as any;
} else { } else {
@@ -161,7 +161,7 @@ function ajax(url: string, method: string, data: unknown, headers: Headers, sile
const options: JQueryAjaxSettings = { const options: JQueryAjaxSettings = {
url: window.glob.baseApiUrl + url, url: window.glob.baseApiUrl + url,
type: method, type: method,
headers, headers: headers,
timeout: 60000, timeout: 60000,
success: (body, textStatus, jqXhr) => { success: (body, textStatus, jqXhr) => {
const respHeaders: Headers = {}; const respHeaders: Headers = {};
@@ -288,8 +288,8 @@ async function reportError(method: string, url: string, statusCode: number, resp
t("server.unknown_http_error_content", { statusCode, method, url, message: messageStr }), t("server.unknown_http_error_content", { statusCode, method, url, message: messageStr }),
15_000); 15_000);
} }
const { logError } = await import("./ws.js"); const { throwError } = await import("./ws.js");
logError(`${statusCode} ${method} ${url} - ${message}`); throwError(`${statusCode} ${method} ${url} - ${message}`);
} }
} }

View File

@@ -1,8 +1,5 @@
import { signal } from "@preact/signals"; import { signal } from "@preact/signals";
import appContext from "../components/app_context.js";
import froca from "./froca.js";
import { t } from "./i18n.js";
import utils from "./utils.js"; import utils from "./utils.js";
export interface ToastOptions { export interface ToastOptions {
@@ -64,24 +61,6 @@ function showErrorTitleAndMessage(title: string, message: string, timeout = 1000
}); });
} }
export async function showErrorForScriptNote(noteId: string, message: string) {
const note = await froca.getNote(noteId, true);
showPersistent({
id: `custom-widget-failure-${noteId}`,
title: note?.title ?? "",
icon: note?.getIcon() ?? "bx bx-error-circle",
message,
timeout: 15_000,
buttons: [
{
text: t("toast.open-script-note"),
onClick: () => appContext.tabManager.openInNewTab(noteId, null, true)
}
]
});
}
//#region Toast store //#region Toast store
export const toasts = signal<ToastOptionsWithRequiredId[]>([]); export const toasts = signal<ToastOptionsWithRequiredId[]>([]);
@@ -95,7 +74,7 @@ function addToast(opts: ToastOptions) {
function updateToast(id: string, partial: Partial<ToastOptions>) { function updateToast(id: string, partial: Partial<ToastOptions>) {
toasts.value = toasts.value.map(toast => { toasts.value = toasts.value.map(toast => {
if (toast.id === id) { if (toast.id === id) {
return { ...toast, ...partial }; return { ...toast, ...partial }
} }
return toast; return toast;
}); });

View File

@@ -22,15 +22,7 @@
"bundle-error": { "bundle-error": {
"title": "Failed to load a custom script", "title": "Failed to load a custom script",
"message": "Script from note with ID \"{{id}}\", titled \"{{title}}\" could not be executed due to:\n\n{{message}}" "message": "Script from note with ID \"{{id}}\", titled \"{{title}}\" could not be executed due to:\n\n{{message}}"
}, }
"widget-list-error": {
"title": "Failed to obtain the list of widgets from the server"
},
"widget-render-error": {
"title": "Failed to render a custom React widget"
},
"widget-missing-parent": "Custom widget does not have mandatory '{{property}}' property defined.",
"open-script-note": "Open script note"
}, },
"add_link": { "add_link": {
"add_link": "Add link", "add_link": "Add link",

View File

@@ -1,9 +1,8 @@
import { isValidElement, VNode } from "preact"; import { isValidElement, VNode } from "preact";
import Component, { TypedComponent } from "../components/component.js"; import Component, { TypedComponent } from "../components/component.js";
import froca from "../services/froca.js"; import froca from "../services/froca.js";
import { t } from "../services/i18n.js"; import { t } from "../services/i18n.js";
import toastService, { showErrorForScriptNote } from "../services/toast.js"; import toastService from "../services/toast.js";
import { renderReactWidget } from "./react/react_utils.jsx"; import { renderReactWidget } from "./react/react_utils.jsx";
export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedComponent<T> { export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedComponent<T> {
@@ -57,9 +56,9 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
optChild(condition: boolean, ...components: (T | VNode)[]) { optChild(condition: boolean, ...components: (T | VNode)[]) {
if (condition) { if (condition) {
return this.child(...components); return this.child(...components);
} } else {
return this; return this;
}
} }
id(id: string) { id(id: string) {
@@ -173,11 +172,16 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
const noteId = this._noteId; const noteId = this._noteId;
if (this._noteId) { if (this._noteId) {
froca.getNote(noteId, true).then((note) => { froca.getNote(noteId, true).then((note) => {
showErrorForScriptNote(noteId, t("toast.widget-error.message-custom", { toastService.showPersistent({
id: `custom-widget-failure-${noteId}`,
title: t("toast.widget-error.title"),
icon: "bx bx-error-circle",
message: t("toast.widget-error.message-custom", {
id: noteId, id: noteId,
title: note?.title, title: note?.title,
message: e.message || e.toString() message: e.message || e.toString()
})); })
});
}); });
} else { } else {
toastService.showPersistent({ toastService.showPersistent({
@@ -246,9 +250,9 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
getClosestNtxId() { getClosestNtxId() {
if (this.$widget) { if (this.$widget) {
return this.$widget.closest("[data-ntx-id]").attr("data-ntx-id"); return this.$widget.closest("[data-ntx-id]").attr("data-ntx-id");
} } else {
return null; return null;
}
} }
cleanup() {} cleanup() {}

View File

@@ -1,9 +1,9 @@
import type { EventData, EventNames } from "../../components/app_context.js";
import splitService from "../../services/resizer.js";
import type BasicWidget from "../basic_widget.js";
import FlexContainer from "./flex_container.js"; import FlexContainer from "./flex_container.js";
import splitService from "../../services/resizer.js";
import type RightPanelWidget from "../right_panel_widget.js";
import type { EventData, EventNames } from "../../components/app_context.js";
export default class RightPaneContainer extends FlexContainer<BasicWidget> { export default class RightPaneContainer extends FlexContainer<RightPanelWidget> {
private rightPaneHidden: boolean; private rightPaneHidden: boolean;
private firstRender: boolean; private firstRender: boolean;

View File

@@ -1,20 +1,17 @@
import { useCallback, useContext, useEffect, useMemo, useState } from "preact/hooks"; import { useCallback, useContext, useEffect, useMemo, useState } from "preact/hooks";
import appContext, { CommandNames } from "../../components/app_context";
import FNote from "../../entities/fnote";
import date_notes from "../../services/date_notes";
import dialog from "../../services/dialog";
import { LauncherWidgetDefinitionWithType } from "../../services/frontend_script_api_preact";
import { t } from "../../services/i18n";
import toast from "../../services/toast";
import { getErrorMessage, isMobile } from "../../services/utils";
import BasicWidget from "../basic_widget";
import NoteContextAwareWidget from "../note_context_aware_widget";
import QuickSearchWidget from "../quick_search";
import { useGlobalShortcut, useLegacyWidget, useNoteLabel, useNoteRelationTarget, useTriliumOptionBool } from "../react/hooks"; import { useGlobalShortcut, useLegacyWidget, useNoteLabel, useNoteRelationTarget, useTriliumOptionBool } from "../react/hooks";
import { ParentComponent } from "../react/react_utils"; import { ParentComponent } from "../react/react_utils";
import BasicWidget from "../basic_widget";
import FNote from "../../entities/fnote";
import QuickSearchWidget from "../quick_search";
import { getErrorMessage, isMobile } from "../../services/utils";
import date_notes from "../../services/date_notes";
import { CustomNoteLauncher } from "./GenericButtons"; import { CustomNoteLauncher } from "./GenericButtons";
import { LaunchBarActionButton, LaunchBarContext, LauncherNoteProps, useLauncherIconAndTitle } from "./launch_bar_widgets"; import { LaunchBarActionButton, LaunchBarContext, LauncherNoteProps, useLauncherIconAndTitle } from "./launch_bar_widgets";
import dialog from "../../services/dialog";
import { t } from "../../services/i18n";
import appContext, { CommandNames } from "../../components/app_context";
import toast from "../../services/toast";
export function CommandButton({ launcherNote }: LauncherNoteProps) { export function CommandButton({ launcherNote }: LauncherNoteProps) {
const { icon, title } = useLauncherIconAndTitle(launcherNote); const { icon, title } = useLauncherIconAndTitle(launcherNote);
@@ -26,7 +23,7 @@ export function CommandButton({ launcherNote }: LauncherNoteProps) {
text={title} text={title}
triggerCommand={command as CommandNames} triggerCommand={command as CommandNames}
/> />
); )
} }
// we're intentionally displaying the launcher title and icon instead of the target, // we're intentionally displaying the launcher title and icon instead of the target,
@@ -78,7 +75,7 @@ export function ScriptLauncher({ launcherNote }: LauncherNoteProps) {
text={title} text={title}
onClick={launch} onClick={launch}
/> />
); )
} }
export function AiChatButton({ launcherNote }: LauncherNoteProps) { export function AiChatButton({ launcherNote }: LauncherNoteProps) {
@@ -91,7 +88,7 @@ export function AiChatButton({ launcherNote }: LauncherNoteProps) {
text={title} text={title}
triggerCommand="createAiChat" triggerCommand="createAiChat"
/> />
); )
} }
export function TodayLauncher({ launcherNote }: LauncherNoteProps) { export function TodayLauncher({ launcherNote }: LauncherNoteProps) {
@@ -117,13 +114,12 @@ export function QuickSearchLauncherWidget() {
<div> <div>
{isEnabled && <LegacyWidgetRenderer widget={widget} />} {isEnabled && <LegacyWidgetRenderer widget={widget} />}
</div> </div>
); )
} }
export function CustomWidget({ launcherNote }: LauncherNoteProps) { export function CustomWidget({ launcherNote }: LauncherNoteProps) {
const [ widgetNote ] = useNoteRelationTarget(launcherNote, "widget"); const [ widgetNote ] = useNoteRelationTarget(launcherNote, "widget");
const [ widget, setWidget ] = useState<BasicWidget | NoteContextAwareWidget | LauncherWidgetDefinitionWithType>(); const [ widget, setWidget ] = useState<BasicWidget>();
const parentComponent = useContext(ParentComponent) as BasicWidget | null; const parentComponent = useContext(ParentComponent) as BasicWidget | null;
parentComponent?.contentSized(); parentComponent?.contentSized();
@@ -150,13 +146,9 @@ export function CustomWidget({ launcherNote }: LauncherNoteProps) {
return ( return (
<div> <div>
{widget && ( {widget && <LegacyWidgetRenderer widget={widget} />}
("type" in widget && widget.type === "preact-launcher-widget")
? <ReactWidgetRenderer widget={widget as LauncherWidgetDefinitionWithType} />
: <LegacyWidgetRenderer widget={widget as BasicWidget} />
)}
</div> </div>
); )
} }
export function LegacyWidgetRenderer({ widget }: { widget: BasicWidget }) { export function LegacyWidgetRenderer({ widget }: { widget: BasicWidget }) {
@@ -166,8 +158,3 @@ export function LegacyWidgetRenderer({ widget }: { widget: BasicWidget }) {
return widgetEl; return widgetEl;
} }
function ReactWidgetRenderer({ widget }: { widget: LauncherWidgetDefinitionWithType }) {
const El = widget.render;
return <El />;
}

View File

@@ -1,9 +1,8 @@
import { Tooltip } from "bootstrap"; import { Tooltip } from "bootstrap";
import { useEffect, useRef, useMemo, useCallback } from "preact/hooks";
import { escapeQuotes } from "../../services/utils";
import { ComponentChildren } from "preact"; import { ComponentChildren } from "preact";
import { CSSProperties, memo } from "preact/compat"; import { CSSProperties, memo } from "preact/compat";
import { useCallback,useEffect, useMemo, useRef } from "preact/hooks";
import { escapeQuotes } from "../../services/utils";
import { useUniqueName } from "./hooks"; import { useUniqueName } from "./hooks";
interface FormCheckboxProps { interface FormCheckboxProps {
@@ -19,7 +18,7 @@ interface FormCheckboxProps {
containerStyle?: CSSProperties; containerStyle?: CSSProperties;
} }
export default function FormCheckbox({ name, disabled, label, currentValue, onChange, hint, containerStyle }: FormCheckboxProps) { const FormCheckbox = memo(({ name, disabled, label, currentValue, onChange, hint, containerStyle }: FormCheckboxProps) => {
const labelRef = useRef<HTMLLabelElement>(null); const labelRef = useRef<HTMLLabelElement>(null);
const id = useUniqueName(name); const id = useUniqueName(name);
@@ -66,4 +65,6 @@ export default function FormCheckbox({ name, disabled, label, currentValue, onCh
</label> </label>
</div> </div>
); );
} });
export default FormCheckbox;

View File

@@ -2,11 +2,10 @@
import "./RightPanelContainer.css"; import "./RightPanelContainer.css";
import Split from "@triliumnext/split.js"; import Split from "@triliumnext/split.js";
import { isValidElement, VNode } from "preact"; import { VNode } from "preact";
import { useEffect, useRef } from "preact/hooks"; import { useEffect, useRef } from "preact/hooks";
import appContext from "../../components/app_context"; import appContext from "../../components/app_context";
import { WidgetsByParent } from "../../services/bundle";
import { t } from "../../services/i18n"; import { t } from "../../services/i18n";
import options from "../../services/options"; import options from "../../services/options";
import { DEFAULT_GUTTER_SIZE } from "../../services/resizer"; import { DEFAULT_GUTTER_SIZE } from "../../services/resizer";
@@ -24,12 +23,12 @@ const MIN_WIDTH_PERCENT = 5;
interface RightPanelWidgetDefinition { interface RightPanelWidgetDefinition {
el: VNode; el: VNode;
enabled: boolean; enabled: boolean;
position?: number; position: number;
} }
export default function RightPanelContainer({ widgetsByParent }: { widgetsByParent: WidgetsByParent }) { export default function RightPanelContainer({ customWidgets }: { customWidgets: BasicWidget[] }) {
const [ rightPaneVisible, setRightPaneVisible ] = useTriliumOptionBool("rightPaneVisible"); const [ rightPaneVisible, setRightPaneVisible ] = useTriliumOptionBool("rightPaneVisible");
const items = useItems(rightPaneVisible, widgetsByParent); const items = useItems(rightPaneVisible, customWidgets);
useSplit(rightPaneVisible); useSplit(rightPaneVisible);
return ( return (
@@ -52,7 +51,7 @@ export default function RightPanelContainer({ widgetsByParent }: { widgetsByPare
); );
} }
function useItems(rightPaneVisible: boolean, widgetsByParent: WidgetsByParent) { function useItems(rightPaneVisible: boolean, customWidgets: BasicWidget[]) {
const { note } = useActiveNoteContext(); const { note } = useActiveNoteContext();
const noteType = useNoteProperty(note, "type"); const noteType = useNoteProperty(note, "type");
const [ highlightsList ] = useTriliumOptionJson<string[]>("highlightsList"); const [ highlightsList ] = useTriliumOptionJson<string[]>("highlightsList");
@@ -62,38 +61,23 @@ function useItems(rightPaneVisible: boolean, widgetsByParent: WidgetsByParent) {
{ {
el: <TableOfContents />, el: <TableOfContents />,
enabled: (noteType === "text" || noteType === "doc"), enabled: (noteType === "text" || noteType === "doc"),
position: 10,
}, },
{ {
el: <HighlightsList />, el: <HighlightsList />,
enabled: noteType === "text" && highlightsList.length > 0, enabled: noteType === "text" && highlightsList.length > 0,
position: 20,
}, },
...widgetsByParent.getLegacyWidgets("right-pane").map((widget, i) => ({ ...customWidgets.map((w, i) => ({
el: <CustomLegacyWidget key={widget._noteId} originalWidget={widget as LegacyRightPanelWidget} />, el: <CustomWidget key={w._noteId} originalWidget={w as LegacyRightPanelWidget} />,
enabled: true, enabled: true,
position: widget.position position: w.position ?? 30 + i * 10
})), }))
...widgetsByParent.getPreactWidgets("right-pane").map((widget) => {
const El = widget.render;
return {
el: <El />,
enabled: true,
position: widget.position
};
})
]; ];
// Assign a position to items that don't have one yet.
let pos = 10;
for (const definition of definitions) {
if (!definition.position) {
definition.position = pos;
pos += 10;
}
}
return definitions return definitions
.filter(e => e.enabled) .filter(e => e.enabled)
.toSorted((a, b) => (a.position ?? 10) - (b.position ?? 10)) .toSorted((a, b) => a.position - b.position)
.map(e => e.el); .map(e => e.el);
} }
@@ -115,7 +99,7 @@ function useSplit(visible: boolean) {
}, [ visible ]); }, [ visible ]);
} }
function CustomLegacyWidget({ originalWidget }: { originalWidget: LegacyRightPanelWidget }) { function CustomWidget({ originalWidget }: { originalWidget: LegacyRightPanelWidget }) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
return ( return (

View File

@@ -30,8 +30,7 @@
"dependencies": { "dependencies": {
"better-sqlite3": "12.5.0", "better-sqlite3": "12.5.0",
"html-to-text": "9.0.5", "html-to-text": "9.0.5",
"node-html-parser": "7.0.1", "node-html-parser": "7.0.1"
"sucrase": "3.35.1"
}, },
"devDependencies": { "devDependencies": {
"@anthropic-ai/sdk": "0.71.2", "@anthropic-ai/sdk": "0.71.2",

File diff suppressed because one or more lines are too long

View File

@@ -9,8 +9,8 @@
feel free to report them either via a ticket or via the Matrix.</p> feel free to report them either via a ticket or via the Matrix.</p>
<h2>Downloading the nightly release manually</h2> <h2>Downloading the nightly release manually</h2>
<p>Go to <a href="https://github.com/TriliumNext/Trilium/releases/tag/nightly">github.com/TriliumNext/Trilium/releases/tag/nightly</a> and <p>Go to <a href="https://github.com/TriliumNext/Trilium/releases/tag/nightly">github.com/TriliumNext/Trilium/releases/tag/nightly</a> and
look for the artifacts starting with <code>TriliumNotes-main</code>. Choose look for the artifacts starting with <code spellcheck="false">TriliumNotes-main</code>.
the appropriate one for your platform (e.g. <code>windows-x64.zip</code>).</p> Choose the appropriate one for your platform (e.g. <code spellcheck="false">windows-x64.zip</code>).</p>
<p>Depending on your use case, you can either test the portable version or <p>Depending on your use case, you can either test the portable version or
even use the installer.</p> even use the installer.</p>
<aside class="admonition note"> <aside class="admonition note">

View File

@@ -15,8 +15,7 @@ class="image">
<img style="aspect-ratio:1150/27;" src="4_New Layout_image.png" <img style="aspect-ratio:1150/27;" src="4_New Layout_image.png"
width="1150" height="27"> width="1150" height="27">
</figure> </figure>
<h3>Inline title</h3>
<h3>Inline title</h3>
<p>In previous versions of Trilium, the title bar was fixed at all times. <p>In previous versions of Trilium, the title bar was fixed at all times.
In the new layout, there is both a fixed title bar and one that scrolls In the new layout, there is both a fixed title bar and one that scrolls
with the text. The newly introduced title is called the <em>Inline title</em> and with the text. The newly introduced title is called the <em>Inline title</em> and
@@ -42,18 +41,18 @@ class="image">
width="910" height="104"> width="910" height="104">
<figcaption>The fixed title bar. The title only appears after scrolling past the <em>Inline title</em>.</figcaption> <figcaption>The fixed title bar. The title only appears after scrolling past the <em>Inline title</em>.</figcaption>
</figure> </figure>
<h3>New note type switcher</h3>
<h3>New note type switcher</h3>
<p>When a new&nbsp;<a class="reference-link" href="#root/_help_iPIMuisry3hd">Text</a>&nbsp;or&nbsp; <p>When a new&nbsp;<a class="reference-link" href="#root/_help_iPIMuisry3hd">Text</a>&nbsp;or&nbsp;
<a <a
class="reference-link" href="#root/_help_6f9hih2hXXZk">Code</a>&nbsp;note is created, a note type switcher will appear below class="reference-link" href="#root/_help_6f9hih2hXXZk">Code</a>&nbsp;note is created, a note type switcher will appear below
the <em>Inline title</em>. Apart from changing the note type, it's also the <em>Inline title</em>. Apart from changing the note type, it's also
possible to apply a <a href="#root/_help_KC1HB96bqqHX">template</a>.</p> possible to apply a <a href="#root/_help_KC1HB96bqqHX">template</a>.</p>
<p>The switcher will disappear as soon as a text is entered.</p> <p>The switcher will disappear as soon as a text is entered.</p>
<img src="5_New Layout_image.png" <p>
width="735" height="143"> <img src="5_New Layout_image.png" width="735"
height="143">
<h3>Note badges</h3> </p>
<h3>Note badges</h3>
<p>Note badges appear near the fixed note title and indicate important information <p>Note badges appear near the fixed note title and indicate important information
about the note such as whether it is read-only. Some of the badges are about the note such as whether it is read-only. Some of the badges are
also interactive.</p> also interactive.</p>
@@ -63,18 +62,19 @@ class="image">
</figure> </figure>
<p>The following badges are available:</p> <p>The following badges are available:</p>
<ul> <ul>
<li><strong>Read-only badge</strong>, which will be shown if the note is not <li data-list-item-id="e92dd6f75d874ac793f37db886606439d"><strong>Read-only badge</strong>, which will be shown if the note is not
editable due to either automatic read-only or manual read-only. Clicking editable due to either automatic read-only or manual read-only. Clicking
on the badge will temporarily edit the note (similar to the Edit <a href="#root/_help_XpOYSgsLkTJy">floating button</a>).</li> on the badge will temporarily edit the note (similar to the Edit <a href="#root/_help_XpOYSgsLkTJy">floating button</a>).</li>
<li><strong>Share badge</strong>, which will indicate that the current note <li
data-list-item-id="e7bbe2a9a3ddc469e9fe1c31ec7cf1a2f"><strong>Share badge</strong>, which will indicate that the current note
is shared. The badge will also indicate if the share is on the local network is shared. The badge will also indicate if the share is on the local network
(for the desktop application without&nbsp;<a class="reference-link" href="#root/_help_cbkrhQjrkKrh">Synchronization</a>&nbsp;set (for the desktop application without&nbsp;<a class="reference-link" href="#root/_help_cbkrhQjrkKrh">Synchronization</a>&nbsp;set
up) or publicly accessible (for the server).</li> up) or publicly accessible (for the server).</li>
<li><strong>Web clip badge</strong>, which will indicate if the note was clipped <li data-list-item-id="e2f51260db16ace77feb9eddcf987cc52"><strong>Web clip badge</strong>, which will indicate if the note was clipped
using the&nbsp;<a class="reference-link" href="#root/_help_MtPxeAWVAzMg">Web Clipper</a>. using the&nbsp;<a class="reference-link" href="#root/_help_MtPxeAWVAzMg">Web Clipper</a>.
The badge acts as a link, so it can be clicked on to navigate to the page The badge acts as a link, so it can be clicked on to navigate to the page
or right clicked for more options.</li> or right clicked for more options.</li>
<li><strong>Execute badge</strong>, for <a href="#root/_help_CdNpE2pqjmI6">scripts</a> or <li data-list-item-id="ef6bec8ce5a4f3fd249bf57a16e1e235e"><strong>Execute badge</strong>, for <a href="#root/_help_CdNpE2pqjmI6">scripts</a> or
<a <a
href="#root/_help_YKWqdJhzi2VY">saved SQL queries</a>which have an execute button or a description.</li> href="#root/_help_YKWqdJhzi2VY">saved SQL queries</a>which have an execute button or a description.</li>
</ul> </ul>
@@ -86,25 +86,26 @@ class="image">
</figure> </figure>
<p>The following sections have been made collapsible:</p> <p>The following sections have been made collapsible:</p>
<ul> <ul>
<li><em>Promoted Attributes</em> <li class="ck-list-marker-italic" data-list-item-id="e8add36768a67f7f5af68545beb18037f"><em>Promoted Attributes</em>
<ul> <ul>
<li>For full-height notes such as&nbsp;<a class="reference-link" href="#root/_help_grjYqerjn243">Canvas</a>, <li data-list-item-id="e633becb9bcfb1796d6d8203ffd2ac660">For full-height notes such as&nbsp;<a class="reference-link" href="#root/_help_grjYqerjn243">Canvas</a>,
the promoted attributes are collapsed by default to make room.</li> the promoted attributes are collapsed by default to make room.</li>
<li>The keyboard shortcut previously used to trigger the promoted attributes <li
data-list-item-id="ee7cbce0e436fd7a00a176df51f5570a4">The keyboard shortcut previously used to trigger the promoted attributes
ribbon tab (which was no longer working) has been repurposed to toggle ribbon tab (which was no longer working) has been repurposed to toggle
the promoted attributes instead.</li> the promoted attributes instead.</li>
</ul> </ul>
</li> </li>
<li><em>Edited Notes</em>, which appears for&nbsp;<a class="reference-link" <li data-list-item-id="ece11f5c363adc3938abfd8d21b2f655d"><em>Edited Notes</em>, which appears for&nbsp;<a class="reference-link"
href="#root/_help_l0tKav7yLHGF">Day Notes</a>&nbsp;is now shown underneath the href="#root/_help_l0tKav7yLHGF">Day Notes</a>&nbsp;is now shown underneath the
title. title.
<ul> <ul>
<li>Whether the section is collapsed or not depends on the choice in&nbsp; <li data-list-item-id="ef3f8aac2e5f49b4188acd62868b32020">Whether the section is collapsed or not depends on the choice in&nbsp;
<a <a
class="reference-link" href="#root/_help_4TIF1oA4VQRO">Options</a>&nbsp;→ Appearance.</li> class="reference-link" href="#root/_help_4TIF1oA4VQRO">Options</a>&nbsp;→ Appearance.</li>
</ul> </ul>
</li> </li>
<li><em>Search Properties</em>, which appears for the full&nbsp;<a class="reference-link" <li data-list-item-id="e42393439936bb76c397de5ae6e5ecd80"><em>Search Properties</em>, which appears for the full&nbsp;<a class="reference-link"
href="#root/_help_eIg8jdvaoNNd">Search</a>&nbsp;and&nbsp;<a class="reference-link" href="#root/_help_eIg8jdvaoNNd">Search</a>&nbsp;and&nbsp;<a class="reference-link"
href="#root/_help_m523cpzocqaD">Saved Search</a>.</li> href="#root/_help_m523cpzocqaD">Saved Search</a>.</li>
</ul> </ul>
@@ -115,75 +116,78 @@ class="image">
<p>Here's how all the different tabs that were once part of the ribbon are <p>Here's how all the different tabs that were once part of the ribbon are
now available in the new layout:</p> now available in the new layout:</p>
<ul> <ul>
<li>“Formatting toolbar” was relocated to the top of the page. <li data-list-item-id="e758293ceb33f07215fcf5fc4700a4e5f">“Formatting toolbar” was relocated to the top of the page.
<ul> <ul>
<li>Instead of having one per split, now there is a single formatting toolbar <li data-list-item-id="e8dded8673adf680dde8615934b3f857b">Instead of having one per split, now there is a single formatting toolbar
per tab. This allows more space for the toolbar items.</li> per tab. This allows more space for the toolbar items.</li>
</ul> </ul>
</li> </li>
<li>“Owned attributes” and “Inherited attributes” were merged and moved to <li data-list-item-id="e2c510fd7f19155ed0e05946acf7a4cd0">“Owned attributes” and “Inherited attributes” were merged and moved to
the status bar region (displayed one above the other).</li> the status bar region (displayed one above the other).</li>
<li>“Basic Properties” were integrated in the&nbsp;<a class="reference-link" <li data-list-item-id="ec032e05420954455db0884651e2d1c4e">“Basic Properties” were integrated in the&nbsp;<a class="reference-link"
href="#root/_help_8YBEPzcpUgxw">Note buttons</a>&nbsp;menu. href="#root/_help_8YBEPzcpUgxw">Note buttons</a>&nbsp;menu.
<ul> <ul>
<li>The only exception here is the Language combo box which can now be found <li data-list-item-id="ebcff28c9a8d07526187559ebd5faa211">The only exception here is the Language combo box which can now be found
in the status bar (top-right of the screen).</li> in the status bar (top-right of the screen).</li>
</ul> </ul>
</li> </li>
<li>“File” and “Image” tabs <li data-list-item-id="e0b76ace8a0be9d7bb6870398b87338ab">“File” and “Image” tabs
<ul> <ul>
<li>The buttons were moved to the right of the note title, as dedicated entries <li data-list-item-id="e55913c9bba25585fbdcb2098a2977e0e">The buttons were moved to the right of the note title, as dedicated entries
in&nbsp;<a class="reference-link" href="#root/_help_8YBEPzcpUgxw">Note buttons</a>.</li> in&nbsp;<a class="reference-link" href="#root/_help_8YBEPzcpUgxw">Note buttons</a>.</li>
<li>The info section has been merged into the <em>Note info</em> section of <li
data-list-item-id="e749af557c7ec8307b65cebfc0b375508">The info section has been merged into the <em>Note info</em> section of
the status bar.</li> the status bar.</li>
</ul> </ul>
</li> </li>
<li>Edited notes <li data-list-item-id="ee3556b093a5a07fd6d239c33d8cf2611">Edited notes
<ul> <ul>
<li>Moved underneath the title, displayed under a collapsible area and the <li data-list-item-id="e541470be93aa45c6495efb6cd8063e47">Moved underneath the title, displayed under a collapsible area and the
notes are represented as badges/chips.</li> notes are represented as badges/chips.</li>
<li>Whether the section is expanded or collapsed depends on the “Edited Notes <li data-list-item-id="e16f53a81cda714958d00ca53dbb1ffa9">Whether the section is expanded or collapsed depends on the “Edited Notes
ribbon tab will automatically open on day notes” setting from Options → ribbon tab will automatically open on day notes” setting from Options →
Appearance.</li> Appearance.</li>
</ul> </ul>
</li> </li>
<li>Search definition tab <li data-list-item-id="e0fc642280dfcbc6ca7cdc995c57941f8">Search definition tab
<ul> <ul>
<li>Moved underneath the title under a collapsible area.</li> <li data-list-item-id="e616bd30c16829e863accfcab35c41874">Moved underneath the title under a collapsible area.</li>
<li>Expanded by default for new searches, collapsed for saved searches.</li> <li data-list-item-id="e1c2b80776b0ffe26ae69410131c1e0f2">Expanded by default for new searches, collapsed for saved searches.</li>
</ul> </ul>
</li> </li>
<li>The Note map is now available in the Note actions menu. <li data-list-item-id="ec4b6bd664511175e82281caf113b13b8">The Note map is now available in the Note actions menu.
<ul> <ul>
<li>Instead of opening into a panel in the ribbon, the note map now opens <li data-list-item-id="ecd6bd6144cea4bc2148e6baaa940f9b0">Instead of opening into a panel in the ribbon, the note map now opens
in a side split (similar to the in-app help).</li> in a side split (similar to the in-app help).</li>
</ul> </ul>
</li> </li>
<li>“Note info” tab was moved to a small (i) icon in the status bar.</li> <li data-list-item-id="ea0cd465194ced54082c799b53a1839c0">“Note info” tab was moved to a small (i) icon in the status bar.</li>
<li>“Similar notes” tab <li
data-list-item-id="eeb0fd706272546ff31b629917b8fcb0a">“Similar notes” tab
<ul> <ul>
<li>Moved to the status bar, by going to the “Note info” section and pressing <li data-list-item-id="eb01edf7d21f73998b99516cd46e0e321">Moved to the status bar, by going to the “Note info” section and pressing
the button to show similar notes.</li> the button to show similar notes.</li>
<li>Displayed as a fixed panel, similar to the attributes.</li> <li data-list-item-id="e3aa6b54672681d8d90795c57372f2595">Displayed as a fixed panel, similar to the attributes.</li>
</ul> </ul>
</li> </li>
<li>The Collection properties tab were relocated under the note title and <li data-list-item-id="e791313bf732b70bbf4d6f91755db2494">The Collection properties tab were relocated under the note title and
grouped into: grouped into:
<ul> <ul>
<li>A combo box to quickly switch between views.</li> <li data-list-item-id="e712f0f389eab1a329a4df3ed6d2b49aa">A combo box to quickly switch between views.</li>
<li>Individual settings for the current view in a submenu.</li> <li data-list-item-id="edaba6f82803ed727489575b9c46d667c">Individual settings for the current view in a submenu.</li>
</ul> </ul>
</li> </li>
<li>Some smaller ribbon tabs were converted to badges that appear near the <li data-list-item-id="e26e827650e594f717a90c0cd94c1c3a1">Some smaller ribbon tabs were converted to badges that appear near the
note title in the breadcrumb section: note title in the breadcrumb section:
<ul> <ul>
<li>Original URL indicator for clipped web pages (<code>#pageUrl</code>).</li> <li data-list-item-id="eaf6b3ff2c30e7167a29cb58c160c55f8">Original URL indicator for clipped web pages (<code spellcheck="false">#pageUrl</code>).</li>
<li>SQL and script execute buttons.</li> <li
data-list-item-id="ee7fc72c13a93f9e5d0edc0bba6bc0b34">SQL and script execute buttons.</li>
</ul> </ul>
</li> </li>
</ul> </ul>
<aside class="admonition note"> <aside class="admonition note">
<p>The ribbon keyboard shortcuts (e.g. <code>toggleRibbonTabClassicEditor</code>) <p>The ribbon keyboard shortcuts (e.g. <code spellcheck="false">toggleRibbonTabClassicEditor</code>)
have been repurposed to work on the new layout, where they will toggle have been repurposed to work on the new layout, where they will toggle
the appropriate panel.</p> the appropriate panel.</p>
</aside> </aside>
@@ -192,11 +196,13 @@ class="image">
the&nbsp;<a class="reference-link" href="#root/_help_8YBEPzcpUgxw">Note buttons</a>&nbsp;area, the&nbsp;<a class="reference-link" href="#root/_help_8YBEPzcpUgxw">Note buttons</a>&nbsp;area,
with the exception of:</p> with the exception of:</p>
<ul> <ul>
<li>The Edit button is displayed near the note title, as a badge.</li> <li data-list-item-id="e68c4ff8e52782bbef0104bca4b3f972a">The Edit button is displayed near the note title, as a badge.</li>
<li><em>Backlinks</em> is displayed in the status bar. When clicked, the same <li
data-list-item-id="e45b03477a6cb8c98327562abd551967c"><em>Backlinks</em> is displayed in the status bar. When clicked, the same
list of backlinks is displayed.</li> list of backlinks is displayed.</li>
<li>Relation map zoom buttons are now part of the relation map itself.</li> <li data-list-item-id="e7241aa3a6ec8dd92a3debade61439d81">Relation map zoom buttons are now part of the relation map itself.</li>
<li>Export image to PNG/SVG are now in the Note actions menu, in the <em>Export as image</em> option.</li> <li
data-list-item-id="e2d44d21ae8b0d77bef0327f2aaf4e050">Export image to PNG/SVG are now in the Note actions menu, in the <em>Export as image</em> option.</li>
</ul> </ul>
<h3>Changes to the sidebar</h3> <h3>Changes to the sidebar</h3>
<p>The sidebar (also known as the right pane) also received some important <p>The sidebar (also known as the right pane) also received some important
@@ -204,7 +210,7 @@ class="image">
<p>The previous iteration of the sidebar would appear contextually, depending <p>The previous iteration of the sidebar would appear contextually, depending
on whether there are any items to be displayed. This caused occasional on whether there are any items to be displayed. This caused occasional
content shifts when moving between two panes in a split view. In the new content shifts when moving between two panes in a split view. In the new
layout, the sidebar acts more like the&nbsp;<a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;pane, layout, the sidebar acts more like the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;pane,
remaining visible even if there is nothing to display.</p> remaining visible even if there is nothing to display.</p>
<p>In order to toggle the sidebar, there is a new button on the top-right <p>In order to toggle the sidebar, there is a new button on the top-right
side of the screen, near the window buttons (on Windows and Linux).</p> side of the screen, near the window buttons (on Windows and Linux).</p>

View File

@@ -8,48 +8,48 @@
displayed in the bottom-left of the screen.</p> displayed in the bottom-left of the screen.</p>
<h2>Layout and Interaction</h2> <h2>Layout and Interaction</h2>
<ul> <ul>
<li>If a note or workspace is hoisted, a badge will appear on the left-most <li data-list-item-id="e235d35dec66a5a6e191ec5cd8af5038a">If a note or workspace is hoisted, a badge will appear on the left-most
side. side.
<ul> <ul>
<li>Clicking on the badge will un-hoist the note/workspace.</li> <li data-list-item-id="e456ddbf22db13b73ffd4915431ef4edb">Clicking on the badge will un-hoist the note/workspace.</li>
</ul> </ul>
</li> </li>
<li>The left-most icon represents the root note, or the hoisted note or workspace. <li data-list-item-id="e1f0000e6ce3c61b41a8d9ee6ec991686">The left-most icon represents the root note, or the hoisted note or workspace.
<ul> <ul>
<li>Clicking the icon will jump to the root note.</li> <li data-list-item-id="e224bf09301a82f6a58038c55bb4769e4">Clicking the icon will jump to the root note.</li>
<li>Right clicking the icon will display a menu that allows opening the note <li data-list-item-id="e5e1de710e5d166aa47ba54ddd46657cb">Right clicking the icon will display a menu that allows opening the note
in a new tab, split, etc.</li> in a new tab, split, etc.</li>
</ul> </ul>
</li> </li>
<li>Each segment shows the title of a note in the current note hierarchy. <li data-list-item-id="e62deaf618aa52bb7062b1801cdaf5bf5">Each segment shows the title of a note in the current note hierarchy.
<ul> <ul>
<li>Clicking the icon will jump to that note.</li> <li data-list-item-id="e03cfdc5ebec9d3dce4503960e24fc693">Clicking the icon will jump to that note.</li>
<li>Right clicking will open a menu with multiple options such as opening <li data-list-item-id="e2b084f07f11586bbcfb0ec85d1fe52c2">Right clicking will open a menu with multiple options such as opening
the note in a different tab/split/window, hoisting, moving/cloning the the note in a different tab/split/window, hoisting, moving/cloning the
note, duplicating as well as changing the color of the note.</li> note, duplicating as well as changing the color of the note.</li>
</ul> </ul>
</li> </li>
<li>Clicking the arrow next to each segment will reveal the child notes of <li data-list-item-id="ed8b04db71853f4c0453d321de96acfdf">Clicking the arrow next to each segment will reveal the child notes of
the segment on the left. the segment on the left.
<ul> <ul>
<li>Clicking on an icon will navigate to that particular note.</li> <li data-list-item-id="e0c88db9aafa125a06b81d9e4ab40bf8e">Clicking on an icon will navigate to that particular note.</li>
<li>It's also possible to create a new child note from here.</li> <li data-list-item-id="e04d705da35d36f06aa734b35e60e454a">It's also possible to create a new child note from here.</li>
<li>The menu can optionally hide the archived notes.</li> <li data-list-item-id="e07c9f1f7bd7ea872181c7c005513ed68">The menu can optionally hide the archived notes.</li>
</ul> </ul>
</li> </li>
<li>If the current note is deep within a hierarchy, the segments will collapse <li data-list-item-id="ee49ef8ad47fbb227dc69ca8f976872aa">If the current note is deep within a hierarchy, the segments will collapse
into a […] button in order not to occupy too much space. into a […] button in order not to occupy too much space.
<ul> <ul>
<li>Clicking this button will display each collapsed entry as a menu item. <li data-list-item-id="e1869d9d1b5a2f0c5def31172891a9b14">Clicking this button will display each collapsed entry as a menu item.
Clicking on it will navigate to that particular note.</li> Clicking on it will navigate to that particular note.</li>
</ul> </ul>
</li> </li>
<li>Right clicking on an empty space to the right of the breadcrumb (before <li data-list-item-id="e8d793fcdf6ddc0a15f422d5ec12b05c4">Right clicking on an empty space to the right of the breadcrumb (before
the other status bar items) will reveal another menu that allows: the other status bar items) will reveal another menu that allows:
<ul> <ul>
<li>Toggling whether archived notes are displayed in the breadcrumb and in <li data-list-item-id="e4d96585f45337c4bac4182db334343eb">Toggling whether archived notes are displayed in the breadcrumb and in
the note tree.</li> the note tree.</li>
<li>Copying the current note path to clipboard.</li> <li data-list-item-id="e37105f037c1fe10fe48e428db4c7c0e7">Copying the current note path to clipboard.</li>
</ul> </ul>
</li> </li>
</ul> </ul>

View File

@@ -7,31 +7,32 @@
<p>On the right side, specific sections will show depending on the type of <p>On the right side, specific sections will show depending on the type of
the current note.</p> the current note.</p>
<ol> <ol>
<li>For code notes, the language mode of the note is indicated (e.g. JavaScript, <li data-list-item-id="e7409c70ee461800fea112abc0cf9ae25">For code notes, the language mode of the note is indicated (e.g. JavaScript,
plain text), as well as allowing easy switching to another mode.</li> plain text), as well as allowing easy switching to another mode.</li>
<li>For text notes, the content language is displayed and can be changed, <li
data-list-item-id="e02ade3f598cdd3dfda1abbcf9d55060b">For text notes, the content language is displayed and can be changed,
thus configuring the spell-check and the right-to-left support. thus configuring the spell-check and the right-to-left support.
<ol> <ol>
<li>Note that this applies to the entire note and not the selection, unlike <li data-list-item-id="e0e530605f6c359593162f582fc9113a1">Note that this applies to the entire note and not the selection, unlike
some text editors.</li> some text editors.</li>
</ol> </ol>
</li> </li>
<li>If a note is placed in multiple places in the tree (cloned), the number <li data-list-item-id="eb55ddf60b5526101fb30234178ccd4b1">If a note is placed in multiple places in the tree (cloned), the number
of the note paths will be displayed. of the note paths will be displayed.
<ol> <ol>
<li>Clicking it will reveal the full list of note paths and a button to place <li data-list-item-id="e9fa65cb2d50422d4d715a29108268c57">Clicking it will reveal the full list of note paths and a button to place
it somewhere else.</li> it somewhere else.</li>
</ol> </ol>
</li> </li>
<li>If a note has attachments, their number will be displayed. <li data-list-item-id="e353272924ea7c0a8817b75978e74e2f4">If a note has attachments, their number will be displayed.
<ol> <ol>
<li>Clicking on it will reveal the list of attachments in a new tab.</li> <li data-list-item-id="e36057cb3080443d03fc54f373c4811f7">Clicking on it will reveal the list of attachments in a new tab.</li>
</ol> </ol>
</li> </li>
<li>If a note is linked from other text notes (backlinks), the number of backlinks <li data-list-item-id="ebed069f4938bd155c5e436fe92e44cea">If a note is linked from other text notes (backlinks), the number of backlinks
will be displayed. will be displayed.
<ol> <ol>
<li>Clicking on it will show the list of notes that link to this note, as <li data-list-item-id="e1a3f17eeda7672abaf283f7283dd462c">Clicking on it will show the list of notes that link to this note, as
well as an excerpt of where the note is referenced.</li> well as an excerpt of where the note is referenced.</li>
</ol> </ol>
</li> </li>
@@ -39,13 +40,14 @@
<p>Regardless of note type, the following items will always be displayed <p>Regardless of note type, the following items will always be displayed
if there is a note:</p> if there is a note:</p>
<ol> <ol>
<li>Note info, which displays: <li data-list-item-id="ecbd6632bd798107863b39fc23e251ae2">Note info, which displays:
<ol> <ol>
<li>The creation/modification date of the note.</li> <li data-list-item-id="e56c0d5f618b79a64326bc2ab739c2df4">The creation/modification date of the note.</li>
<li>The type and MIME of the note.</li> <li data-list-item-id="e7843741f9e99763fcf5ded821f09d789">The type and MIME of the note.</li>
<li>The note ID.</li> <li data-list-item-id="eaf479da2bbf044be78676f7a68ded903">The note ID.</li>
<li>An estimation of the note size of the note itself and its children.</li> <li data-list-item-id="ebbeb7fde731354c1b92951e4d45be579">An estimation of the note size of the note itself and its children.</li>
<li>A button to show Similar notes.</li> <li
data-list-item-id="e0e10095682b51783b11592065b7e28a7">A button to show Similar notes.</li>
</ol> </ol>
</li> </li>
</ol> </ol>

View File

@@ -7,16 +7,13 @@
via an attribute.</p> via an attribute.</p>
<h2>Creating a render note</h2> <h2>Creating a render note</h2>
<ol> <ol>
<li data-list-item-id="e6c172eb567596820dd7f444924e568b3">Create a&nbsp;<a class="reference-link" href="#root/_help_6f9hih2hXXZk">Code</a>&nbsp;note <li>Create a&nbsp;<a class="reference-link" href="#root/_help_6f9hih2hXXZk">Code</a>&nbsp;note
with the HTML language, with what needs to be displayed (for example with the HTML language, with what needs to be displayed (for example <code>&lt;p&gt;Hello world.&lt;/p&gt;</code>).</li>
<code <li>Create a&nbsp;<a class="reference-link" href="#root/_help_HcABDtFCkbFN">Render Note</a>.</li>
spellcheck="false">&lt;p&gt;Hello world.&lt;/p&gt;</code>).</li> <li>Assign the <code>renderNote</code> <a href="#root/_help_zEY4DaJG4YT5">relation</a> to
<li data-list-item-id="e06560e53dd0ab195c0e89dc8db7aec1f">Create a&nbsp;<a class="reference-link" href="#root/_help_HcABDtFCkbFN">Render Note</a>.</li>
<li
data-list-item-id="e9db87d13293c732ac62d6f92d2997cb3">Assign the <code spellcheck="false">renderNote</code> <a href="#root/_help_zEY4DaJG4YT5">relation</a> to
point at the previously created code note.</li> point at the previously created code note.</li>
</ol> </ol>
<h2>Legacy scripting using jQuery</h2> <h2>Dynamic content</h2>
<p>A static HTML is generally not enough for&nbsp;<a class="reference-link" <p>A static HTML is generally not enough for&nbsp;<a class="reference-link"
href="#root/_help_CdNpE2pqjmI6">Scripting</a>. The next step is to automatically href="#root/_help_CdNpE2pqjmI6">Scripting</a>. The next step is to automatically
change parts of the note using JavaScript.</p> change parts of the note using JavaScript.</p>
@@ -29,46 +26,22 @@ The current date &amp; time is &lt;span class="date"&gt;&lt;/span&gt;</code></pr
language. Make sure the newly created note is a direct child of the HTML language. Make sure the newly created note is a direct child of the HTML
note created previously; with the following content:</p><pre><code class="language-text-x-trilium-auto">const $dateEl = api.$container.find(".date"); note created previously; with the following content:</p><pre><code class="language-text-x-trilium-auto">const $dateEl = api.$container.find(".date");
$dateEl.text(new Date());</code></pre> $dateEl.text(new Date());</code></pre>
<p>Now create a render note at any place and set its <code spellcheck="false">~renderNote</code> relation <p>Now create a render note at any place and set its <code>~renderNote</code> relation
to point to the HTML note. When the render note is accessed it will display:</p> to point to the HTML note. When the render note is accessed it will display:</p>
<blockquote> <blockquote>
<p><strong>Current date &amp; time</strong> <p><strong>Current date &amp; time</strong>
<br>The current date &amp; time is Sun Apr 06 2025 15:26:29 GMT+0300 (Eastern <br>The current date &amp; time is Sun Apr 06 2025 15:26:29 GMT+0300 (Eastern
European Summer Time)</p> European Summer Time)</p>
</blockquote> </blockquote>
<h2>Dynamic content using Preact &amp; JSX</h2>
<p>As a more modern alternative to jQuery, it's possible to use Preact &amp;
JSX to render pages. Since JSX is a superset of JavaScript, there's no
need to provide a HTML anymore.</p>
<p>Here are the steps to creating a simple render note:</p>
<ol>
<li data-list-item-id="ee6c6868efde1b910dac6807580e2a8e2">Create a note of type&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_HcABDtFCkbFN">Render Note</a>.</li>
<li
data-list-item-id="eae9b5f496cba824738734828c11448d5">
<p>Create a child&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_6f9hih2hXXZk">Code</a>&nbsp;note
with JSX as the language.
<br>As an example, use the following content:</p><pre><code class="language-text-jsx">export default function() {
return (
&lt;&gt;
&lt;p&gt;Hello world.&lt;/p&gt;
&lt;/&gt;
);
}</code></pre>
</li>
<li data-list-item-id="e28812b27d5227675943ef98ab9477e67">In the parent render note, define a <code spellcheck="false">~renderNote</code> relation
pointing to the newly created child.</li>
<li data-list-item-id="e24cc71d55c7c8ec61014ac0d800653da">Refresh the render note and it should display a “Hello world” message.</li>
</ol>
<h2>Refreshing the note</h2> <h2>Refreshing the note</h2>
<p>It's possible to refresh the note via:</p> <p>It's possible to refresh the note via:</p>
<ul> <ul>
<li data-list-item-id="e01c4ae9c0999b2879e914c5b641215e3">The corresponding button in&nbsp;<a class="reference-link" href="#root/_help_XpOYSgsLkTJy">Floating buttons</a>.</li> <li>The corresponding button in&nbsp;<a class="reference-link" href="#root/_help_XpOYSgsLkTJy">Floating buttons</a>.</li>
<li <li>The “Render active note” <a href="#root/_help_A9Oc6YKKc65v">keyboard shortcut</a> (not
data-list-item-id="e871da2cff905b2daa180fcae27aee43b">The “Render active note” <a href="#root/_help_A9Oc6YKKc65v">keyboard shortcut</a> (not
assigned by default).</li> assigned by default).</li>
</ul> </ul>
<h2>Examples</h2> <h2>Examples</h2>
<ul> <ul>
<li data-list-item-id="ec01930f20370b46f519bd844b67623ee"><a class="reference-link" href="#root/_help_R7abl2fc6Mxi">Weight Tracker</a>&nbsp;which <li><a class="reference-link" href="#root/_help_R7abl2fc6Mxi">Weight Tracker</a>&nbsp;which
is present in the&nbsp;<a class="reference-link" href="#root/_help_6tZeKvSHEUiB">Demo Notes</a>.</li> is present in the&nbsp;<a class="reference-link" href="#root/_help_6tZeKvSHEUiB">Demo Notes</a>.</li>
</ul> </ul>

View File

@@ -1,46 +0,0 @@
<p>For both&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_HcABDtFCkbFN">Render Note</a>&nbsp;and
more complicated scripts, it's generally useful to split the code into
multiple&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_6f9hih2hXXZk">Code</a>&nbsp;notes.</p>
<p>When a script is run, the sub-children of the script being run (or the&nbsp;
<a
class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_HcABDtFCkbFN">Render Note</a>) are checked for children. If the children are Code notes
of the corresponding type (front-end or backend) as the code being run,
they will be evaluated as well.</p>
<p>The collection of a script and its child notes is called a <em>bundle</em>.
A child note inside a bundle is called a <em>module</em>.</p>
<p>As a basic example of dependencies, consider the following note structure:</p>
<ul>
<li class="ck-list-marker-italic" data-list-item-id="e4c343ba6ed99b3a943049b6f0af99dd6">
<p><em>Script with dependency</em>
</p><pre><code class="language-application-javascript-env-frontend">api.log(MyMath.sum(2, 2));</code></pre>
<ul>
<li class="ck-list-marker-italic" data-list-item-id="ea27318ca64cb71cfa5eceb15f9acba75">
<p><em>MyMath</em>
</p><pre><code class="language-application-javascript-env-frontend">module.exports = {
sum(a, b) {
return a + b;
}
};</code></pre>
</li>
</ul>
</li>
</ul>
<p>When <em>Script with dependency</em> is run, it will detect <em>MyMath</em> as
a submodule and provide the result of its <code spellcheck="false">module.exports</code> object
into a global object with the same name as the note.</p>
<aside class="admonition note">
<p>If the note contains spaces or special characters, they will be stripped.
For example <code spellcheck="false">My Nice Note!</code> becomes <code spellcheck="false">MyNiceNote</code>.</p>
</aside>
<h2>Alternative syntax</h2>
<p>Instead of providing an object to <code spellcheck="false">module.exports</code>,
it's also possible to add fields individually:</p><pre><code class="language-application-javascript-env-frontend">module.exports.sum = (a, b) =&gt; a + b;
module.exports.subtract = (a, b) =&gt; a - b;</code></pre>
<h2>Ignoring a code script from a bundle</h2>
<p>To ignore a script from being included in a bundle (e.g. if it's unrelated
to the parent script note), apply the <code spellcheck="false">#disableInclusion</code> label.</p>
<h2>Sharing a module across multiple bundles</h2>
<p>Modules can be reused across multiple scripts by simply cloning the shared
module between two modules (see&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/BFs8mudNFgCS/_help_IakOLONlIfGI">Cloning Notes</a>).</p>
<p>Optionally, a separate note can be used to contain all the different reusable
modules for an easy way to discover them.</p>

View File

@@ -1,167 +1,13 @@
<p>Custom widgets are a special subset of scripts that render graphical elements <p>It's possible to create custom widget in three possible locations where
in certain parts of the application. These can be used to add new functionality you can display your custom content.</p>
to the Trilium application.</p> <p>Positions are:</p>
<h2>Preact with JSX vs. vanilla jQuery</h2>
<p>In older versions of Trilium, custom widgets were exclusively written
in a combination of jQuery with Trilium's internal widget architecture
(e.g., <code spellcheck="false">BasicWidget</code>, <code spellcheck="false">NoteContextAwareWidget</code>).</p>
<p>Starting with v0.101.0, custom widgets can also be written in JSX using
the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/yIhgI5H7A2Sm/_help_KLsqhjaqh1QW">Preact</a>&nbsp;framework.
Both legacy and Preact widgets have the same capabilities, with a single
difference:</p>
<ul> <ul>
<li data-list-item-id="e2a6a28bcf69987db6b33b4902b913282">Preact widgets are content-sized by default whereas legacy widgets need <li><code>left-pane</code>
<code
spellcheck="false">this.contentSized()</code>applied in the constructor. For more information,
see the corresponding section in&nbsp;<a class="reference-link" href="#root/MgibgPcfeuGz/_help_gMkgcLJ6jBkg">Troubleshooting</a>.</li>
</ul>
<p>Wherever possible, widget examples will be both in the legacy and Preact
format.</p>
<h2>Creating a custom widget</h2>
<ol>
<li data-list-item-id="efdec30e3ba380f9691c2955248e662aa">Create a&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_6f9hih2hXXZk">Code</a>&nbsp;note.</li>
<li
data-list-item-id="e6ac401b11f885fbf6ee2eb486e2fb4db">Set the language to:
<ol>
<li data-list-item-id="ea981b36d647bb2020c63b6eb8888ffcb">JavaScript (frontend) for legacy widgets using jQuery.</li>
<li data-list-item-id="e27097e820e8b18d5539035afc4f91cf6">JSX for Preact widgets. You might need to go to Options → Code to enable
the language first.</li>
</ol>
</li> </li>
<li data-list-item-id="e94ba32049422e310a2322f85bb37ab85">Apply the <code spellcheck="false">#widget</code> <a href="#root/pOsGYCXsbNQG/tC7s2alapj8V/zEY4DaJG4YT5/_help_HI6GBBIduIgv">label</a>.</li> <li><code>center-pane</code>
</ol> </li>
<h2>Getting started with a simple example</h2> <li><code>note-detail-pane</code> - located within <code>center-pane</code>,
<p>Let's start by creating a widget that shows a message near the content but specific to note (split)</li>
area. Follow the previous section to create a code note, and use the following <li><code>right-pane</code>
content.</p> </li>
<figure class="table"> </ul>
<table>
<thead>
<tr>
<th>Legacy</th>
<th style="width:50%;">Preact (v0.101.0+)</th>
</tr>
</thead>
<tbody>
<tr>
<td><pre><code class="language-text-x-trilium-auto">class HelloNoteDetail extends api.BasicWidget {
constructor() {
super();
this.contentSized();
}
get parentWidget() { return "center-pane" }
doRender() {
this.$widget = $("&lt;span&gt;Center pane&lt;/span&gt;");
}
}
module.exports = new HelloNoteDetail();</code></pre>
</td>
<td style="vertical-align:top;"><pre><code class="language-text-x-trilium-auto">import { defineWidget } from "trilium:preact";
export default defineWidget({
parent: "center-pane",
render: () =&gt; &lt;span&gt;Center pane from Preact.&lt;/span&gt;
});</code></pre>
</td>
</tr>
</tbody>
</table>
</figure>
<p><a href="#root/pOsGYCXsbNQG/BgmBlOIl72jZ/_help_s8alTXmpFR61">Refresh the application</a> and
the widget should appear underneath the content area.</p>
<h2>Widget location (parent widget)</h2>
<p>A widget can be placed in one of the following sections of the applications:</p>
<figure
class="table" style="width:100%;">
<table class="ck-table-resized">
<colgroup>
<col style="width:15.59%;">
<col style="width:30.42%;">
<col style="width:16.68%;">
<col style="width:37.31%;">
</colgroup>
<thead>
<tr>
<th>Value for <code spellcheck="false">parentWidget</code>
</th>
<th>Description</th>
<th>Sample widget</th>
<th>Special requirements</th>
</tr>
</thead>
<tbody>
<tr>
<th><code spellcheck="false">left-pane</code>
</th>
<td>Appears within the same pane that holds the&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a>.</td>
<td>Same as above, with only a different <code spellcheck="false">parentWidget</code>.</td>
<td>None.</td>
</tr>
<tr>
<th><code spellcheck="false">center-pane</code>
</th>
<td>In the content area. If a split is open, the widget will span all of the
splits.</td>
<td>See example above.</td>
<td>None.</td>
</tr>
<tr>
<th><code spellcheck="false">note-detail-pane</code>
</th>
<td>
<p>In the content area, inside the note detail area. If a split is open,
the widget will be contained inside the split.</p>
<p>This is ideal if the widget is note-specific.</p>
</td>
<td><a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/yIhgI5H7A2Sm/MgibgPcfeuGz/_help_GhurYZjh8e1V">Note context aware widget</a>
</td>
<td>
<ul>
<li data-list-item-id="ec06332efcc3039721606c052f0d913fa">The widget must export a <code spellcheck="false">class</code> and not an
instance of the class (e.g. <code spellcheck="false">no new</code>) because
it needs to be multiplied for each note, so that splits work correctly.</li>
<li
data-list-item-id="e8da690a2a8df148f6b5fc04ba1611688">Since the <code spellcheck="false">class</code> is exported instead of an
instance, the <code spellcheck="false">parentWidget</code> getter must be
<code
spellcheck="false">static</code>, otherwise the widget is ignored.</li>
</ul>
</td>
</tr>
<tr>
<th><code spellcheck="false">right-pane</code>
</th>
<td>In the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_RnaPdbciOfeq">Right Sidebar</a>,
as a dedicated section.</td>
<td><a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/yIhgI5H7A2Sm/MgibgPcfeuGz/_help_M8IppdwVHSjG">Right pane widget</a>
</td>
<td>
<ul>
<li data-list-item-id="efe008d361e224f422582552648e1afe7">Although not mandatory, it's best to use a <code spellcheck="false">RightPanelWidget</code> instead
of a <code spellcheck="false">BasicWidget</code> or a <code spellcheck="false">NoteContextAwareWidget</code>.</li>
</ul>
</td>
</tr>
</tbody>
</table>
</figure>
<p>To position the widget somewhere else, just change the value passed to
<code
spellcheck="false">get parentWidget()</code>for legacy widgets or the <code spellcheck="false">parent</code> field
for Preact. Do note that some positions such as <code spellcheck="false">note-detail-pane</code> and
<code
spellcheck="false">right-pane</code>have special requirements that need to be accounted for
(see the table above).</p>
<h2>Launch bar widgets</h2>
<p>Launch bar widgets are similar to <em>Custom widgets</em> but are specific
to the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_xYmIYSP6wE3F">Launch Bar</a>.
See&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/yIhgI5H7A2Sm/_help_4Gn3psZKsfSm">Launch Bar Widgets</a>&nbsp;for
more information.</p>
<h2>Custom position</h2>
<p>&nbsp;</p>

View File

@@ -1,22 +1,9 @@
<h2>Classic widgets</h2> <p>In <code>doRender()</code>:</p><pre><code class="language-text-x-trilium-auto">this.cssBlock(`#my-widget {
<p>In <code spellcheck="false">doRender()</code>:<span class="footnote-reference"
data-footnote-reference="" data-footnote-index="1" data-footnote-id="1saoftmefpp"
role="doc-noteref" id="fnref1saoftmefpp"><sup><a href="#fn1saoftmefpp">[1]</a></sup></span>
</p><pre><code class="language-text-x-trilium-auto">this.cssBlock(`#my-widget {
position: absolute; position: absolute;
bottom: 40px; bottom: 40px;
left: 60px; left: 60px;
z-index: 1; z-index: 1;
}`);</code></pre> }`)</code></pre>
<h2>Preact widgets</h2> <hr>
<p>See the dedicated page:&nbsp;<a class="reference-link" href="#root/KLsqhjaqh1QW/_help_Sg9GrCtyftZf">CSS</a>.</p> <p>Reference: <a href="https://trilium.rocks/X7pxYpiu0lgU">https://trilium.rocks/X7pxYpiu0lgU</a>
<ol </p>
class="footnote-section footnotes" data-footnote-section="" role="doc-endnotes">
<li class="footnote-item" data-footnote-item="" data-footnote-index="1"
data-footnote-id="1saoftmefpp" role="doc-endnote" id="fn1saoftmefpp"><span class="footnote-back-link" data-footnote-back-link="" data-footnote-id="1saoftmefpp"><sup><strong><a href="#fnref1saoftmefpp">^</a></strong></sup></span>
<div
class="footnote-content" data-footnote-content="">
<p>Reference: <a href="https://trilium.rocks/X7pxYpiu0lgU">https://trilium.rocks/X7pxYpiu0lgU</a>&nbsp;</p>
</div>
</li>
</ol>

View File

@@ -1,48 +0,0 @@
<p>Note context-aware widgets are a different type of widget which automatically
react to changes in the current note.</p>
<p>Important aspects:</p>
<ul>
<li data-list-item-id="ebe74655683475b6a16e53832e6b2c47a">The widget must export a <code spellcheck="false">class</code> and not an
instance of the class (e.g. <code spellcheck="false">no new</code>) because
it needs to be multiplied for each note, so that splits work correctly.</li>
<li
data-list-item-id="edf648bc7ea3325255223d28f0543e5b9">Since the <code spellcheck="false">class</code> is exported instead of an
instance, the <code spellcheck="false">parentWidget</code> getter must be
<code
spellcheck="false">static</code>, otherwise the widget is ignored.</li>
</ul>
<h2>Example displaying the current note title</h2>
<p>This is a note context-aware widget that simply displays the name the
current note.&nbsp;</p>
<h3>Classic example</h3><pre><code class="language-text-x-trilium-auto">class HelloNoteDetail extends api.NoteContextAwareWidget {
constructor() {
super();
this.contentSized();
}
doRender() {
this.$widget = $("&lt;div&gt;");
}
async refreshWithNote(note) {
this.$widget.text("Current note: " + note.title);
}
static get parentWidget() { return "note-detail-pane" }
get position() { return 10 }
}
module.exports = HelloNoteDetail;</code></pre>
<h3>Preact (v0.101.0+)</h3><pre><code class="language-text-x-trilium-auto">import { defineWidget, useNoteContext, useNoteProperty } from "trilium:preact";
export default defineWidget({
parent: "note-detail-pane",
position: 10,
render: () =&gt; {
const { note } = useNoteContext();
const title = useNoteProperty(note, "title");
return &lt;span&gt;Current note JSX: {title}&lt;/span&gt;;
}
});</code></pre>

View File

@@ -1,23 +1,23 @@
<h2>Key highlights</h2> <h2>Key highlights</h2>
<ul> <ul>
<li data-list-item-id="e05167a939f3b6c8083cf62d4f8108b4b"><code spellcheck="false">doRender</code> must not be overridden, instead <li data-list-item-id="e08ebed8d78d0359c71a72be31cd95074"><code spellcheck="false">doRender</code> must not be overridden, instead
<code <code
spellcheck="false">doRenderBody()</code>has to be overridden. spellcheck="false">doRenderBody()</code>has to be overridden.
<ul> <ul>
<li data-list-item-id="eee44709b64b220f4af124ff1458a61e8"><code spellcheck="false">doRenderBody</code> can optionally be <code spellcheck="false">async</code>.</li> <li data-list-item-id="e34a4e9826ab89e0ce8ded8786a803f3a"><code spellcheck="false">doRenderBody</code> can optionally be <code spellcheck="false">async</code>.</li>
</ul> </ul>
</li> </li>
<li data-list-item-id="e94b7be0f1d366c502703ff3a5fc3bfb1"><code spellcheck="false">parentWidget()</code> must be set to <code spellcheck="false">“rightPane”</code>.</li> <li data-list-item-id="e24bc184054a9c87a6c7695bb65c8d30b"><code spellcheck="false">parentWidget()</code> must be set to <code spellcheck="false">“rightPane”</code>.</li>
<li <li
data-list-item-id="ea56b04dbcf63573acbde90d990137829"><code spellcheck="false">widgetTitle()</code> getter can optionally be data-list-item-id="e581cbed615c9be4f249274c5c51c6a18"><code spellcheck="false">widgetTitle()</code> getter can optionally be
overriden, otherwise the widget will be displayed as “Untitled widget”.</li> overriden, otherwise the widget will be displayed as “Untitled widget”.</li>
</ul> </ul>
<h2>Example for new layout</h2> <h2>Example for new layout</h2>
<aside class="admonition important"> <aside class="admonition important">
<p>This section addresses example that are tailored for the&nbsp;<a class="reference-link" <p>This section addresses example that are tailored for the&nbsp;<a class="reference-link"
href="#root/_help_IjZS7iK5EXtb">New Layout</a>&nbsp;(available starting with href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_IjZS7iK5EXtb">New Layout</a>&nbsp;(available
v0.101.0) where the right pane widget/sidebar is no longer shown or hidden starting with v0.101.0) where the right pane widget/sidebar is no longer
based on the widgets it has.&nbsp;</p> shown or hidden based on the widgets it has.&nbsp;</p>
</aside> </aside>
<h3>Title widget</h3> <h3>Title widget</h3>
<p>This is an example of a context-aware widget which displays the title <p>This is an example of a context-aware widget which displays the title
@@ -41,8 +41,7 @@
module.exports = new NoteTitleWidget();</code></pre> module.exports = new NoteTitleWidget();</code></pre>
<h3>Clock</h3> <h3>Clock</h3>
<p>A simple widget which will show the current time, as an example on how <p>A simple widget which will show the current time, as an example on how
to dynamically change the content of the widget periodically.</p> to dynamically change the content of the widget periodically.</p><pre><code class="language-text-x-trilium-auto">const template = `&lt;div&gt;&lt;/div&gt;`;
<h3>Legacy widget</h3><pre><code class="language-text-x-trilium-auto">const template = `&lt;div&gt;&lt;/div&gt;`;
class ToDoListWidget extends api.RightPanelWidget { class ToDoListWidget extends api.RightPanelWidget {
@@ -61,26 +60,6 @@ class ToDoListWidget extends api.RightPanelWidget {
} }
module.exports = new ToDoListWidget();</code></pre> module.exports = new ToDoListWidget();</code></pre>
<h3>Preact widget</h3><pre><code class="language-text-x-trilium-auto">import { defineWidget, RightPanelWidget, useEffect, useState } from "trilium:preact";
export default defineWidget({
parent: "right-pane",
position: 1,
render() {
const [ time, setTime ] = useState();
useEffect(() =&gt; {
const interval = setInterval(() =&gt; {
setTime(new Date().toLocaleString());
}, 1000);
return () =&gt; clearInterval(interval);
});
return (
&lt;RightPanelWidget id="clock-jsx" title="Clock (JSX)"&gt;
&lt;p&gt;The time is: {time}&lt;/p&gt;
&lt;/RightPanelWidget&gt;
);
}
});</code></pre>
<h2>Example for old layout</h2> <h2>Example for old layout</h2>
<p>Here's a widget that displays a basic message ("Hi"):</p><pre><code class="language-text-x-trilium-auto">const template = `&lt;div&gt;Hi&lt;/div&gt;`; <p>Here's a widget that displays a basic message ("Hi"):</p><pre><code class="language-text-x-trilium-auto">const template = `&lt;div&gt;Hi&lt;/div&gt;`;
@@ -110,7 +89,7 @@ this.triggerCommand("reEvaluateRightPaneVisibility");</code></pre>
<p>By default, the sidebar items are displayed in the order they are found <p>By default, the sidebar items are displayed in the order they are found
by the application when searching for <code spellcheck="false">#widget</code> notes.</p> by the application when searching for <code spellcheck="false">#widget</code> notes.</p>
<p>It is possible to make a widget appear higher or lower up, by adjusting <p>It is possible to make a widget appear higher or lower up, by adjusting
its <code spellcheck="false">position</code> property:</p><pre><code class="language-text-x-trilium-auto">class MyWidget extends api.RightPanelWidget { its <code spellcheck="false">position</code> property:</p><pre><code class="language-text-x-diff">class MyWidget extends api.RightPanelWidget {
+ get position() { return 20 }; + get position() { return 20 };

View File

@@ -1,7 +1,7 @@
<h2>Why is my widget clipped by other UI elements</h2> <h2>Why is my widget clipped by other UI elements</h2>
<p>For performance and layout reasons, the size of widgets in Trilium is <p>For performance and layout reasons, the size of widgets in Trilium is
independent from its children. At CSS level, this means that the widget independent from its children. At CSS level, this means that the widget
container has <code>contain: size</code> applied to it.</p> container has <code spellcheck="false">contain: size</code> applied to it.</p>
<p>This works well if the widget has a fixed size (or based on its parent <p>This works well if the widget has a fixed size (or based on its parent
container), however to make a widget resize to fit its content, apply the container), however to make a widget resize to fit its content, apply the
following change:</p><pre><code class="language-text-x-diff">class MyWidget extends api.RightPanelWidget { following change:</p><pre><code class="language-text-x-diff">class MyWidget extends api.RightPanelWidget {
@@ -12,4 +12,5 @@
+ } + }
}</code></pre> }</code></pre>
<p>Alternatively apply <code>contain: none</code> to its CSS.</p> <p>Alternatively apply <code spellcheck="false">contain: none</code> to its
CSS.</p>

View File

@@ -16,17 +16,17 @@
module.exports = new MyWidget();</code></pre> module.exports = new MyWidget();</code></pre>
<p>To implement this widget:</p> <p>To implement this widget:</p>
<ol> <ol>
<li data-list-item-id="e9927fa1c4b393854487094afd914ece9">Create a new <code spellcheck="false">JS Frontend</code> note in Trilium <li>Create a new <code>JS Frontend</code> note in Trilium and paste in the code
and paste in the code above.</li> above.</li>
<li data-list-item-id="e3741fe167b7dccae0081cc0d1a326a5d">Assign the <code spellcheck="false">#widget</code> <a href="#root/_help_zEY4DaJG4YT5">attribute</a> to <li>Assign the <code>#widget</code> <a href="#root/_help_zEY4DaJG4YT5">attribute</a> to
the <a href="#root/_help_BFs8mudNFgCS">note</a>.</li> the <a href="#root/_help_BFs8mudNFgCS">note</a>.</li>
<li data-list-item-id="e300febf71f84c3be97fc8556182ea124">Restart Trilium or reload the window.</li> <li>Restart Trilium or reload the window.</li>
</ol> </ol>
<p>To verify that the widget is working, open the developer tools (<kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd>) <p>To verify that the widget is working, open the developer tools (<kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd>)
and run <code spellcheck="false">document.querySelector("#my-widget")</code>. and run <code>document.querySelector("#my-widget")</code>. If the element
If the element is found, the widget is functioning correctly. If <code spellcheck="false">undefined</code> is is found, the widget is functioning correctly. If <code>undefined</code> is
returned, double-check that the <a href="#root/_help_BFs8mudNFgCS">note</a> has returned, double-check that the <a href="#root/_help_BFs8mudNFgCS">note</a> has
the <code spellcheck="false">#widget</code> <a href="#root/_help_zEY4DaJG4YT5">attribute</a>.</p> the <code>#widget</code> <a href="#root/_help_zEY4DaJG4YT5">attribute</a>.</p>
<h3>Step 2: Adding an UI Element</h3> <h3>Step 2: Adding an UI Element</h3>
<p>Next, let's improve the widget by adding a button to it.</p><pre><code class="language-text-x-trilium-auto">const template = `&lt;div id="my-widget"&gt;&lt;button&gt;Click Me!&lt;/button&gt;&lt;/div&gt;`; <p>Next, let's improve the widget by adding a button to it.</p><pre><code class="language-text-x-trilium-auto">const template = `&lt;div id="my-widget"&gt;&lt;button&gt;Click Me!&lt;/button&gt;&lt;/div&gt;`;
@@ -46,9 +46,7 @@ module.exports = new MyWidget();</code></pre>
<h3>Step 3: Styling the Widget</h3> <h3>Step 3: Styling the Widget</h3>
<p>To make the button more visually appealing and position it correctly, <p>To make the button more visually appealing and position it correctly,
we'll apply some custom styling. Trilium includes <a href="https://boxicons.com">Box Icons</a>, we'll apply some custom styling. Trilium includes <a href="https://boxicons.com">Box Icons</a>,
which we'll use to replace the button text with an icon. For example the which we'll use to replace the button text with an icon. For example the <code>bx bxs-magic-wand</code> icon.</p>
<code
spellcheck="false">bx bxs-magic-wand</code>icon.</p>
<p>Here's the updated template:</p><pre><code class="language-text-x-trilium-auto">const template = `&lt;div id="my-widget"&gt;&lt;button class="tree-floating-button bx bxs-magic-wand tree-settings-button"&gt;&lt;/button&gt;&lt;/div&gt;`;</code></pre> <p>Here's the updated template:</p><pre><code class="language-text-x-trilium-auto">const template = `&lt;div id="my-widget"&gt;&lt;button class="tree-floating-button bx bxs-magic-wand tree-settings-button"&gt;&lt;/button&gt;&lt;/div&gt;`;</code></pre>
<p>Next, we'll adjust the button's position using CSS:</p><pre><code class="language-text-x-trilium-auto">class MyWidget extends api.BasicWidget { <p>Next, we'll adjust the button's position using CSS:</p><pre><code class="language-text-x-trilium-auto">class MyWidget extends api.BasicWidget {
get position() { return 1; } get position() { return 1; }
@@ -71,8 +69,7 @@ module.exports = new MyWidget();</code></pre>
of the left pane, alongside other action buttons.</p> of the left pane, alongside other action buttons.</p>
<h3>Step 4: Adding User Interaction</h3> <h3>Step 4: Adding User Interaction</h3>
<p>Lets make the button interactive by showing a message when its clicked. <p>Lets make the button interactive by showing a message when its clicked.
We'll use the <code spellcheck="false">api.showMessage</code> method from We'll use the <code>api.showMessage</code> method from the <a href="#root/_help_GLks18SNjxmC">Script API</a>.</p><pre><code class="language-text-x-trilium-auto">class MyWidget extends api.BasicWidget {
the <a href="#root/_help_GLks18SNjxmC">Script API</a>.</p><pre><code class="language-text-x-trilium-auto">class MyWidget extends api.BasicWidget {
get position() { return 1; } get position() { return 1; }
get parentWidget() { return "left-pane"; } get parentWidget() { return "left-pane"; }
@@ -90,8 +87,17 @@ module.exports = new MyWidget();</code></pre>
} }
module.exports = new MyWidget();</code></pre> module.exports = new MyWidget();</code></pre>
<p>For the list of possible values for <code spellcheck="false">parentWidget()</code>, <p><code>parentWidget()</code> can be given the following values:</p>
see&nbsp;<a class="reference-link" href="#root/yIhgI5H7A2Sm/_help_MgibgPcfeuGz">Custom Widgets</a>.&nbsp;</p> <ul>
<li><code>left-pane</code> - This renders the widget on the left side of the
screen where the note tree lives.</li>
<li><code>center-pane</code> - This renders the widget in the center of the
layout in the same location that notes and splits appear.</li>
<li><code>note-detail-pane</code> - This renders the widget <em>with</em> the
note in the center pane. This means it can appear multiple times with splits.</li>
<li><code>right-pane</code> - This renders the widget to the right of any opened
notes.</li>
</ul>
<p><a href="#root/_help_s8alTXmpFR61">Reload</a> the application one last time. <p><a href="#root/_help_s8alTXmpFR61">Reload</a> the application one last time.
When you click the button, a "Hello World!" message should appear, confirming When you click the button, a "Hello World!" message should appear, confirming
that your widget is fully functional.</p> that your widget is fully functional.</p>

View File

@@ -5,7 +5,6 @@
<p>This is an example of a note context-aware widget, which reacts to the <p>This is an example of a note context-aware widget, which reacts to the
currently opened note and refreshes automatically as the user navigates currently opened note and refreshes automatically as the user navigates
through the notes.</p> through the notes.</p>
<h2>Legacy widget</h2>
<p>In this example, the title of the note is displayed. It works best on <p>In this example, the title of the note is displayed. It works best on
the <a href="#root/_help_x0JgW8UqGXvq">horizontal layout</a>.</p><pre><code class="language-application-javascript-env-backend">const TPL = `\ the <a href="#root/_help_x0JgW8UqGXvq">horizontal layout</a>.</p><pre><code class="language-application-javascript-env-backend">const TPL = `\
&lt;div style=" &lt;div style="
@@ -30,18 +29,3 @@ class NoteTitleWidget extends api.NoteContextAwareWidget {
} }
module.exports = new NoteTitleWidget();</code></pre> module.exports = new NoteTitleWidget();</code></pre>
<h2>Preact widget (v0.101.0+)</h2><pre><code class="language-text-jsx">import { defineLauncherWidget, useActiveNoteContext } from "trilium:preact";
export default defineLauncherWidget({
render: () =&gt; {
const { note } = useActiveNoteContext();
return &lt;div style={{
display: "flex",
height: "53px",
width: "fit-content",
fontSize: "0.75em",
alignItems: "center",
flexShrink: 0
}}&gt;{note?.title}&lt;/div&gt;;
}
});</code></pre>

View File

@@ -1,64 +0,0 @@
<p>Since v0.101.0, Trilium integrates Preact for front-end scripting, including
support for JSX.</p>
<p>Preact can be used for:</p>
<ul>
<li data-list-item-id="ecaa7ce1d629d04e22a9c9e5de7bbba9d"><a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_HcABDtFCkbFN">Render Note</a>,
where a JSX code note is used instead of a HTML one.</li>
<li data-list-item-id="ebec1c35ba18bb42ce91497eae106fad6"><a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/yIhgI5H7A2Sm/_help_MgibgPcfeuGz">Custom Widgets</a>,
where JSX can be used to replace the old, jQuery-based mechanism.</li>
</ul>
<p>To get started, the first step is to enable JSX in the list of Code languages.
Go to Options → Code Notes and check the “JSX” language. Afterwards, proceed
with the documentation in either&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_HcABDtFCkbFN">Render Note</a>&nbsp;or&nbsp;
<a
class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/yIhgI5H7A2Sm/_help_MgibgPcfeuGz">Custom Widgets</a>, which will both have a section on how to use the new
Preact integration.</p>
<aside class="admonition important">
<p>The documentation assumes prior knowledge with React or Preact. As a starting
point, consider the <a href="https://www.freecodecamp.org/learn/front-end-development-libraries-v9/">FreeCodeCamp course on Front End Development Libraries</a> or
the <a href="https://preactjs.com/tutorial/">Preact Tutorial</a>.</p>
</aside>
<h2>Import/exports</h2>
<p>When using Preact with JSX, there is a special syntax which provides ES-like
imports. This <code spellcheck="false">import</code> syntax makes way for
a more intuitive that doesn't make use of global objects and paves the
way for better auto-completion support that might be introduced in the
future.&nbsp;</p>
<h3>API imports</h3>
<p>Instead of:</p><pre><code class="language-text-jsx">api.showMessage("Hello");</code></pre>
<p>the JSX version looks like this:</p><pre><code class="language-text-jsx">import { showMessage } from "trilium:api";
showMessage("hello");</code></pre>
<h3>Preact API imports (hooks, components)</h3>
<p>There's a new&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/_help_GLks18SNjxmC">Script API</a>&nbsp;dedicated
to Preact, which provides shared components that are also used by Trilium
internally as well as hooks, for example.</p><pre><code class="language-text-jsx">import { useState } from "trilium:preact";
const [ myState, setMyState ] = useState("Hi");</code></pre>
<h3>Exporting</h3>
<p>JSX notes can export a component for use in&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_HcABDtFCkbFN">Render Note</a>&nbsp;or
for&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/yIhgI5H7A2Sm/KLsqhjaqh1QW/_help_Bqde6BvPo05g">Component libraries</a>:</p><pre><code class="language-text-jsx">export default function() {
return (
&lt;&gt;
&lt;p&gt;Hello world.&lt;/p&gt;
&lt;/&gt;
);
}</code></pre>
<h3>Import/export are not required</h3>
<p>These imports are syntactic sugar meant to replace the usage for the
<code
spellcheck="false">api</code>global object (see&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/_help_GLks18SNjxmC">Script API</a>).&nbsp;</p>
<aside
class="admonition note">
<p>The <code spellcheck="false">import</code> and <code spellcheck="false">export</code> syntax
work only for JSX notes. Standard/jQuery code notes still need to use the
<code
spellcheck="false">api</code>global and <code spellcheck="false">module.exports</code>.</p>
</aside>
<h2>Under the hood</h2>
<p>Unlike JavaScript, JSX requires pre-processing to turn it into JavaScript
(just like TypeScript). To do so, Trilium uses <a href="https://github.com/alangpierce/sucrase">Sucrase</a>,
a JavaScript library which processes the JSX to pure JavaScript. The processing
is done each time a script is run (for widgets this happens at every program
startup). If you notice any performance degradation due to long compilation,
consider <a href="#root/pOsGYCXsbNQG/BgmBlOIl72jZ/_help_wy8So3yZZlH9">reporting the issue</a> to
us.</p>

View File

@@ -1,44 +0,0 @@
<figure class="image image_resized" style="width:54.58%;">
<img style="aspect-ratio:896/712;" src="Built-in components_image.png"
width="896" height="712">
<figcaption>A partial screenshot from the Widget showcase example (see below).</figcaption>
</figure>
<p>Trilium comes with its own set of Preact components, some of which are
also available to&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/yIhgI5H7A2Sm/_help_MgibgPcfeuGz">Custom Widgets</a>&nbsp;and&nbsp;
<a
class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_HcABDtFCkbFN">Render Note</a>.</p>
<p>To use these components, simply import them from <code spellcheck="false">trilium:preact</code>:</p><pre><code class="language-text-jsx">import { ActionButton, Button, LinkButton } from "trilium:preact";</code></pre>
<p>and then use them:</p><pre><code class="language-text-jsx">export default function MyRenderNote() {
const onClick = () =&gt; showMessage("A button was pressed");
return (
&lt;&gt;
&lt;h2&gt;Buttons&lt;/h2&gt;
&lt;div style={{ display: "flex", gap: "1em", alignItems: "center" }}&gt;
&lt;ActionButton icon="bx bx-rocket" text="Action button" onClick={onClick} /&gt;
&lt;Button icon="bx bx-rocket" text="Simple button" onClick={onClick} /&gt;
&lt;LinkButton text="Link button" onClick={onClick} /&gt;
&lt;/div&gt;
&lt;/&gt;
)
}</code></pre>
<h2>Widget showcase</h2>
<aside class="admonition tip">
<p>Starting with v0.101.0, the widget showcase is also available in the&nbsp;
<a
class="reference-link" href="#root/pOsGYCXsbNQG/tC7s2alapj8V/wX4HbRucYSDD/_help_6tZeKvSHEUiB">Demo Notes</a>.</p>
</aside>
<p>This is a&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_HcABDtFCkbFN">Render Note</a>&nbsp;example
with JSX that shows most of the built-in components that are accessible
to custom widgets and JSX render notes.</p>
<p>To use it, simply:</p>
<ol>
<li data-list-item-id="ef2da471f61429e4755fdfee1301299d8">Create a render note.</li>
<li data-list-item-id="e2e1475eb5099d3df20de96997712a9dc">Create a child code note of JSX type with the content of this file:&nbsp;
<a
class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/yIhgI5H7A2Sm/KLsqhjaqh1QW/RSssb9S3xgSr/_help_i9B4IW7b6V6z">Widget showcase</a>
</li>
<li data-list-item-id="ed0b442c37c1db5287d95d8d982f025d9">Set the <code spellcheck="false">~renderNote</code> relation of the parent
note to the child note.</li>
<li data-list-item-id="e62386a552bb441bcb830285f5487868b">Refresh the render note to see the results.</li>
</ol>

View File

@@ -1,189 +0,0 @@
import {
ActionButton, Button, LinkButton,
Admonition, Collapsible,
FormCheckbox, FormDropdownList, FormFileUploadButton, FormGroup, FormRadioGroup, FormTextArea,
FormTextBox, FormToggle, Slider, RawHtml, LoadingSpinner, Icon,
Dropdown, FormListItem, FormDropdownDivider, FormDropdownSubmenu,
NoteAutocomplete, NoteLink, Modal,
CKEditor,
useEffect, useState
} from "trilium:preact";
import { showMessage } from "trilium:api";
export default function() {
const [ time, setTime ] = useState();
const lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam accumsan eu odio non gravida. Pellentesque ornare, arcu condimentum molestie dignissim, nibh turpis ultrices elit, eget elementum nunc erat at erat. Maecenas vehicula consectetur elit, nec fermentum elit venenatis eu.";
useEffect(() => {
const interval = setInterval(() => setTime(new Date().toLocaleString()), 1000);
return () => clearInterval(interval);
}, []);
return (
<div style={{ padding: 20, display: "flex", flexDirection: "column", gap: "1em" }}>
<h1>Widget showcase</h1>
<Buttons />
<Admonition type="note">
<strong>Admonition</strong><br />
{lorem}
</Admonition>
<Collapsible title="Collapsible" initiallyExpanded>
{lorem}
</Collapsible>
<FormElements />
<NoteElements />
<ModalSample />
<DropdownSample />
</div>
);
}
function Buttons() {
const onClick = () => showMessage("A button was pressed");
return (
<>
<h2>Buttons</h2>
<div style={{ display: "flex", gap: "1em", alignItems: "center" }}>
<ActionButton icon="bx bx-rocket" text="Action button" onClick={onClick} />
<Button icon="bx bx-rocket" text="Simple button" onClick={onClick} />
<LinkButton text="Link button" onClick={onClick} />
</div>
</>
)
}
function FormElements() {
const [ checkboxChecked, setCheckboxChecked ] = useState(false);
const [ dropdownValue, setDropdownValue ] = useState("key-1");
const [ radioGroupValue, setRadioGroupValue ] = useState("key-1");
const [ sliderValue, setSliderValue ] = useState(50);
return (
<>
<h2>Form elements</h2>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "1em" }}>
<FormGroup name="checkbox" label="Checkbox">
<FormCheckbox label="Checkbox" currentValue={checkboxChecked} onChange={setCheckboxChecked} />
</FormGroup>
<FormGroup name="toggle" label="Toggle">
<FormToggle switchOnName="Off" switchOffName="On" currentValue={checkboxChecked} onChange={setCheckboxChecked} />
</FormGroup>
<FormGroup name="dropdown" label="Dropdown">
<FormDropdownList
values={[
{ key: "key-1", name: "First item" },
{ key: "key-2", name: "Second item" },
{ key: "key-3", name: "Third item" },
]}
currentValue={dropdownValue} onChange={setDropdownValue}
keyProperty="key" titleProperty="name"
/>
</FormGroup>
<FormGroup name="radio-group" label="Radio group">
<FormRadioGroup
values={[
{ value: "key-1", label: "First item" },
{ value: "key-2", label: "Second item" },
{ value: "key-3", label: "Third item" },
]}
currentValue={radioGroupValue} onChange={setRadioGroupValue}
/>
</FormGroup>
<FormGroup name="text-box" label="Text box">
<FormTextBox
placeholder="Type something..."
currentValue="" onChange={(newValue) => {}}
/>
</FormGroup>
<FormGroup name="text-area" label="Text area">
<FormTextArea
placeholder="Type something bigger..."
currentValue="" onChange={(newValue) => {}}
/>
</FormGroup>
<FormGroup name="slider" label="Slider">
<Slider
min={1} max={100}
value={sliderValue} onChange={setSliderValue}
/>
</FormGroup>
<FormGroup name="file-upload" label="File upload">
<FormFileUploadButton
text="Upload"
onChange={(files) => {
const file = files?.[0];
if (!file) return;
showMessage(`Got file "${file.name}" of size ${file.size} B and type ${file.type}.`);
}}
/>
</FormGroup>
<FormGroup name="icon" label="Icon">
<Icon icon="bx bx-smile" />
</FormGroup>
<FormGroup name="loading-spinner" label="Loading spinner">
<LoadingSpinner />
</FormGroup>
<FormGroup name="raw-html" label="Raw HTML">
<RawHtml html="<strong>Hi</strong> <em>there</em>" />
</FormGroup>
</div>
</>
)
}
function NoteElements() {
const [ noteId, setNoteId ] = useState("");
return (
<div>
<h2>Note elements</h2>
<FormGroup name="note-autocomplete" label="Note autocomplete">
<NoteAutocomplete
placeholder="Select a note"
noteId={noteId} noteIdChanged={setNoteId}
/>
</FormGroup>
<FormGroup name="note-link" label="Note link">
{noteId
? <NoteLink notePath={noteId} showNoteIcon />
: <span>Select a note first</span>}
</FormGroup>
</div>
);
}
function ModalSample() {
const [ shown, setShown ] = useState(false);
return (
<>
<h2>Modal</h2>
<Button text="Open modal" onClick={() => setShown(true)} />
<Modal title="Modal title" size="md" show={shown} onHidden={() => setShown(false)}>
Modal goes here.
</Modal>
</>
)
}
function DropdownSample() {
return (
<>
<h2>Dropdown menu</h2>
<Dropdown text="Dropdown" hideToggleArrow>
<FormListItem icon="bx bx-cut">Cut</FormListItem>
<FormListItem icon="bx bx-copy">Copy</FormListItem>
<FormListItem icon="bx bx-paste">Paste</FormListItem>
<FormDropdownDivider />
<FormDropdownSubmenu title="Submenu">
<FormListItem>More items</FormListItem>
</FormDropdownSubmenu>
</Dropdown>
</>
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

View File

@@ -1,19 +0,0 @@
<h2>Inline styles</h2><pre><code class="language-text-jsx">&lt;div style={{
display: "flex",
height: "53px",
width: "fit-content",
fontSize: "0.75em",
alignItems: "center",
flexShrink: 0
}}&gt;/* [...] */&lt;/div&gt;</code></pre>
<h2>Custom CSS file</h2>
<p>Simply create a&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/pKK96zzmvBGf/_help_AlhDUqhENtH7">Custom app-wide CSS</a>.
Make sure the class names are unique enough to not intersect with other
UI elements, consider adding a prefix (e.g. <code spellcheck="false">x-mywidget-</code>).</p>

View File

@@ -1,55 +0,0 @@
<p>Using the concept of&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/wqXwKJl6VpNk/_help_hA834UaHhSNn">Script bundles</a>,
it's possible to create components that are shared for multiple widgets
or&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_HcABDtFCkbFN">Render Note</a>.</p>
<h2>Exporting a single component</h2>
<p>This is generally useful for big components.</p>
<p>Here's an example child hierarchy using&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_HcABDtFCkbFN">Render Note</a>:</p>
<ul>
<li data-list-item-id="e3f972228e04ea1f4801d76cb1145fe54"><em>My render note</em>
<br>Note type: Render Note
<br>Link <code spellcheck="false">~renderNote</code> to the child note (<em>Render note with subcomponents</em>)
<ul>
<li data-list-item-id="eed4ff5d1824e4f8cf1a05feef1117b91">
<p><em>Render note with subcomponents</em>
<br>Type: JSX</p><pre><code class="language-application-javascript-env-frontend">export default function() {
return (
&lt;MyComponent /&gt;
);
}</code></pre>
<ul>
<li data-list-item-id="e9fdf8ed08c3577977dc48be1bc075f0e">
<p><em>MyComponent</em>
<br>Type: Code / JSX</p><pre><code class="language-application-javascript-env-frontend">export default function MyComponent() {
return &lt;p&gt;Hi&lt;/p&gt;;
}</code></pre>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2>Multiple components per note</h2>
<p>To export multiple components, use the <code spellcheck="false">export</code> keyword
next to each of the function components.</p>
<p>Here's how a sub-note called <code spellcheck="false">MyComponents</code> would
look like:</p><pre><code class="language-application-javascript-env-frontend">export function MyFirstComponent() {
return &lt;p&gt;First&lt;/p&gt;;
}
export function MySecondComponent() {
return &lt;p&gt;Bar&lt;/p&gt;;
}</code></pre>
<p>Then in its parent note:</p><pre><code class="language-application-javascript-env-frontend">const { MyFirstComponent, MySecondComponent } = MyComponents;
export default function() {
return (
&lt;&gt;
&lt;MyFirstComponent /&gt;
&lt;MySecondComponent /&gt;
&lt;/&gt;
);
}</code></pre>
<p>Alternatively, it's also possible to use the components directly without
assigning them to a <code spellcheck="false">const</code> first:</p><pre><code class="language-application-javascript-env-frontend">&lt;MyComponents.MyFirstComponent /&gt;
&lt;MyComponents.MySecondComponent /&gt;</code></pre>

View File

@@ -1,35 +0,0 @@
<h2>Standard Preact hooks</h2>
<p>All standard Preact hooks are available as an import in <code spellcheck="false">trilium:api</code>.</p>
<p>For example:</p><pre><code class="language-text-x-trilium-auto">import { useState } from "trilium:preact";
const [ myState, setMyState ] = useState("Hi");</code></pre>
<h2>Custom hooks</h2>
<p>Trilium comes with a large set of custom hooks for Preact, all of which
are also available to custom widgets and&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_HcABDtFCkbFN">Render Note</a>.</p>
<h3><code spellcheck="false">useNoteContext</code></h3>
<p>As a replacement to&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/yIhgI5H7A2Sm/MgibgPcfeuGz/_help_GhurYZjh8e1V">Note context aware widget</a>,
Preact exposes the current note context in the <code spellcheck="false">useNoteContext</code> hook:</p><pre><code class="language-text-x-trilium-auto">import { defineWidget, useNoteContext, useNoteProperty } from "trilium:preact";
export default defineWidget({
parent: "note-detail-pane",
position: 10,
render: () =&gt; {
const { note } = useNoteContext();
const title = useNoteProperty(note, "title");
return &lt;span&gt;Current note JSX: {title}&lt;/span&gt;;
}
});</code></pre>
<p>Note that the custom widget must be inside the content area (so note detail
widget) for this to work properly, especially when dealing with splits.</p>
<h3><code spellcheck="false">useActiveNoteContext</code></h3>
<p><code spellcheck="false">useActiveNoteContext</code> is an alternative
to <code spellcheck="false">useNoteContext</code> which works even if the
widget is not within the note detail section and it automatically switches
the note context as the user is navigating around between tabs and splits.</p>
<h3><code spellcheck="false">useNoteProperty</code></h3>
<p>This hook allows “listening” for changes to a particular property of a
<code
spellcheck="false">FNote</code>, such as the <code spellcheck="false">title</code> or
<code
spellcheck="false">type</code>of a note. The benefit from using the hook is that it actually
reacts to changes, for example if the note title or type is changed.</p>

View File

@@ -1,25 +1,26 @@
import type { AttachmentRow, AttributeType, CloneResponse, NoteRow, NoteType, RevisionRow } from "@triliumnext/commons"; "use strict";
import { dayjs } from "@triliumnext/commons";
import cloningService from "../../services/cloning.js";
import dateUtils from "../../services/date_utils.js";
import eraseService from "../../services/erase.js";
import eventService from "../../services/events.js";
import handlers from "../../services/handlers.js";
import log from "../../services/log.js";
import noteService from "../../services/notes.js";
import optionService from "../../services/options.js";
import protectedSessionService from "../../services/protected_session.js"; import protectedSessionService from "../../services/protected_session.js";
import searchService from "../../services/search/services/search.js"; import log from "../../services/log.js";
import sql from "../../services/sql.js"; import sql from "../../services/sql.js";
import TaskContext from "../../services/task_context.js"; import optionService from "../../services/options.js";
import eraseService from "../../services/erase.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import type { NotePojo } from "../becca-interface.js"; import dateUtils from "../../services/date_utils.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js"; import AbstractBeccaEntity from "./abstract_becca_entity.js";
import BAttachment from "./battachment.js";
import BAttribute from "./battribute.js";
import type BBranch from "./bbranch.js";
import BRevision from "./brevision.js"; import BRevision from "./brevision.js";
import BAttachment from "./battachment.js";
import TaskContext from "../../services/task_context.js";
import { dayjs } from "@triliumnext/commons";
import eventService from "../../services/events.js";
import type { AttachmentRow, AttributeType, CloneResponse, NoteRow, NoteType, RevisionRow } from "@triliumnext/commons";
import type BBranch from "./bbranch.js";
import BAttribute from "./battribute.js";
import type { NotePojo } from "../becca-interface.js";
import searchService from "../../services/search/services/search.js";
import cloningService from "../../services/cloning.js";
import noteService from "../../services/notes.js";
import handlers from "../../services/handlers.js";
const LABEL = "label"; const LABEL = "label";
const RELATION = "relation"; const RELATION = "relation";
@@ -295,10 +296,6 @@ class BNote extends AbstractBeccaEntity<BNote> {
); );
} }
isJsx() {
return (this.type === "code" && this.mime === "text/jsx");
}
/** @returns true if this note is HTML */ /** @returns true if this note is HTML */
isHtml() { isHtml() {
return ["code", "file", "render"].includes(this.type) && this.mime === "text/html"; return ["code", "file", "render"].includes(this.type) && this.mime === "text/html";
@@ -321,7 +318,7 @@ class BNote extends AbstractBeccaEntity<BNote> {
/** @returns JS script environment - either "frontend" or "backend" */ /** @returns JS script environment - either "frontend" or "backend" */
getScriptEnv() { getScriptEnv() {
if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith("env=frontend")) || this.isJsx()) { if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith("env=frontend"))) {
return "frontend"; return "frontend";
} }
@@ -358,9 +355,9 @@ class BNote extends AbstractBeccaEntity<BNote> {
return this.__attributeCache.filter((attr) => attr.type === type); return this.__attributeCache.filter((attr) => attr.type === type);
} else if (name) { } else if (name) {
return this.__attributeCache.filter((attr) => attr.name === name); return this.__attributeCache.filter((attr) => attr.name === name);
} } else {
return this.__attributeCache; return this.__attributeCache;
}
} }
private __ensureAttributeCacheIsAvailable() { private __ensureAttributeCacheIsAvailable() {
@@ -695,9 +692,9 @@ class BNote extends AbstractBeccaEntity<BNote> {
return this.ownedAttributes.filter((attr) => attr.type === type); return this.ownedAttributes.filter((attr) => attr.type === type);
} else if (name) { } else if (name) {
return this.ownedAttributes.filter((attr) => attr.name === name); return this.ownedAttributes.filter((attr) => attr.name === name);
} } else {
return this.ownedAttributes; return this.ownedAttributes;
}
} }
/** /**
@@ -748,9 +745,9 @@ class BNote extends AbstractBeccaEntity<BNote> {
return 1; return 1;
} else if (a.parentNote?.isHiddenCompletely()) { } else if (a.parentNote?.isHiddenCompletely()) {
return 1; return 1;
} } else {
return 0; return 0;
}
}); });
this.parents = this.parentBranches.map((branch) => branch.parentNote).filter((note) => !!note) as BNote[]; this.parents = this.parentBranches.map((branch) => branch.parentNote).filter((note) => !!note) as BNote[];
@@ -1181,9 +1178,9 @@ class BNote extends AbstractBeccaEntity<BNote> {
return a.isArchived ? 1 : -1; return a.isArchived ? 1 : -1;
} else if (a.isHidden !== b.isHidden) { } else if (a.isHidden !== b.isHidden) {
return a.isHidden ? 1 : -1; return a.isHidden ? 1 : -1;
} } else {
return a.notePath.length - b.notePath.length; return a.notePath.length - b.notePath.length;
}
}); });
return notePaths; return notePaths;
@@ -1260,9 +1257,9 @@ class BNote extends AbstractBeccaEntity<BNote> {
} else { } else {
new BAttribute({ new BAttribute({
noteId: this.noteId, noteId: this.noteId,
type, type: type,
name, name: name,
value value: value
}).save(); }).save();
} }
} }
@@ -1295,11 +1292,11 @@ class BNote extends AbstractBeccaEntity<BNote> {
addAttribute(type: AttributeType, name: string, value: string = "", isInheritable: boolean = false, position: number | null = null): BAttribute { addAttribute(type: AttributeType, name: string, value: string = "", isInheritable: boolean = false, position: number | null = null): BAttribute {
return new BAttribute({ return new BAttribute({
noteId: this.noteId, noteId: this.noteId,
type, type: type,
name, name: name,
value, value: value,
isInheritable, isInheritable: isInheritable,
position position: position
}).save(); }).save();
} }
@@ -1473,10 +1470,10 @@ class BNote extends AbstractBeccaEntity<BNote> {
role: "image", role: "image",
mime: this.mime, mime: this.mime,
title: this.title, title: this.title,
content content: content
}); });
const parentContent = parentNote.getContent(); let parentContent = parentNote.getContent();
const oldNoteUrl = `api/images/${this.noteId}/`; const oldNoteUrl = `api/images/${this.noteId}/`;
const newAttachmentUrl = `api/attachments/${attachment.attachmentId}/image/`; const newAttachmentUrl = `api/attachments/${attachment.attachmentId}/image/`;
@@ -1715,14 +1712,14 @@ class BNote extends AbstractBeccaEntity<BNote> {
} else if (this.type === "text") { } else if (this.type === "text") {
if (this.isFolder()) { if (this.isFolder()) {
return "bx bx-folder"; return "bx bx-folder";
} } else {
return "bx bx-note"; return "bx bx-note";
}
} else if (this.type === "code" && this.mime.startsWith("text/x-sql")) { } else if (this.type === "code" && this.mime.startsWith("text/x-sql")) {
return "bx bx-data"; return "bx bx-data";
} } else {
return NOTE_TYPE_ICONS[this.type]; return NOTE_TYPE_ICONS[this.type];
}
} }
// TODO: Deduplicate with fnote // TODO: Deduplicate with fnote
@@ -1732,7 +1729,7 @@ class BNote extends AbstractBeccaEntity<BNote> {
// TODO: Deduplicate with fnote // TODO: Deduplicate with fnote
getFilteredChildBranches() { getFilteredChildBranches() {
const childBranches = this.getChildBranches(); let childBranches = this.getChildBranches();
if (!childBranches) { if (!childBranches) {
console.error(`No children for '${this.noteId}'. This shouldn't happen.`); console.error(`No children for '${this.noteId}'. This shouldn't happen.`);

View File

@@ -108,7 +108,7 @@ function loginSync(req: Request) {
const givenHash = req.body.hash; const givenHash = req.body.hash;
if (expectedHash !== givenHash) { if (!utils.constantTimeCompare(expectedHash, givenHash)) {
return [400, { message: "Sync login credentials are incorrect. It looks like you're trying to sync two different initialized documents which is not possible." }]; return [400, { message: "Sync login credentials are incorrect. It looks like you're trying to sync two different initialized documents which is not possible." }];
} }

View File

@@ -1,3 +1,4 @@
import crypto from "crypto";
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import optionService from "../services/options.js"; import optionService from "../services/options.js";
import myScryptService from "../services/encryption/my_scrypt.js"; import myScryptService from "../services/encryption/my_scrypt.js";
@@ -160,7 +161,11 @@ function verifyPassword(submittedPassword: string) {
const guess_hashed = myScryptService.getVerificationHash(submittedPassword); const guess_hashed = myScryptService.getVerificationHash(submittedPassword);
return guess_hashed.equals(hashed_password); // Use constant-time comparison to prevent timing attacks
if (hashed_password.length !== guess_hashed.length) {
return false;
}
return crypto.timingSafeEqual(guess_hashed, hashed_password);
} }
function sendLoginError(req: Request, res: Response, errorType: 'password' | 'totp' = 'password') { function sendLoginError(req: Request, res: Response, errorType: 'password' | 'totp' = 'password') {

View File

@@ -1,5 +1,5 @@
import myScryptService from "./my_scrypt.js"; import myScryptService from "./my_scrypt.js";
import utils from "../utils.js"; import utils, { constantTimeCompare } from "../utils.js";
import dataEncryptionService from "./data_encryption.js"; import dataEncryptionService from "./data_encryption.js";
import sql from "../sql.js"; import sql from "../sql.js";
import sqlInit from "../sql_init.js"; import sqlInit from "../sql_init.js";
@@ -87,8 +87,7 @@ function verifyOpenIDSubjectIdentifier(subjectIdentifier: string) {
return undefined; return undefined;
} }
console.log("Matches: " + givenHash === savedHash); return constantTimeCompare(givenHash, savedHash as string);
return givenHash === savedHash;
} }
function setDataKey( function setDataKey(

View File

@@ -1,6 +1,6 @@
import optionService from "../options.js"; import optionService from "../options.js";
import myScryptService from "./my_scrypt.js"; import myScryptService from "./my_scrypt.js";
import { toBase64 } from "../utils.js"; import { toBase64, constantTimeCompare } from "../utils.js";
import dataEncryptionService from "./data_encryption.js"; import dataEncryptionService from "./data_encryption.js";
function verifyPassword(password: string) { function verifyPassword(password: string) {
@@ -12,7 +12,7 @@ function verifyPassword(password: string) {
return false; return false;
} }
return givenPasswordHash === dbPasswordHash; return constantTimeCompare(givenPasswordHash, dbPasswordHash);
} }
function setDataKey(password: string, plainTextDataKey: string | Buffer) { function setDataKey(password: string, plainTextDataKey: string | Buffer) {

View File

@@ -1,6 +1,7 @@
import crypto from 'crypto'; import crypto from 'crypto';
import optionService from '../options.js'; import optionService from '../options.js';
import sql from '../sql.js'; import sql from '../sql.js';
import { constantTimeCompare } from '../utils.js';
function isRecoveryCodeSet() { function isRecoveryCodeSet() {
return optionService.getOptionBool('encryptedRecoveryCodes'); return optionService.getOptionBool('encryptedRecoveryCodes');
@@ -55,13 +56,22 @@ function verifyRecoveryCode(recoveryCodeGuess: string) {
const recoveryCodes = getRecoveryCodes(); const recoveryCodes = getRecoveryCodes();
let loginSuccess = false; let loginSuccess = false;
recoveryCodes.forEach((recoveryCode) => { let matchedCode: string | null = null;
if (recoveryCodeGuess === recoveryCode) {
removeRecoveryCode(recoveryCode); // Check ALL codes to prevent timing attacks - do not short-circuit
for (const recoveryCode of recoveryCodes) {
if (constantTimeCompare(recoveryCodeGuess, recoveryCode)) {
matchedCode = recoveryCode;
loginSuccess = true; loginSuccess = true;
return; // Continue checking all codes to maintain constant time
} }
}); }
// Remove the matched code only after checking all codes
if (matchedCode) {
removeRecoveryCode(matchedCode);
}
return loginSuccess; return loginSuccess;
} }

View File

@@ -1,6 +1,6 @@
import optionService from "../options.js"; import optionService from "../options.js";
import myScryptService from "./my_scrypt.js"; import myScryptService from "./my_scrypt.js";
import { randomSecureToken, toBase64 } from "../utils.js"; import { randomSecureToken, toBase64, constantTimeCompare } from "../utils.js";
import dataEncryptionService from "./data_encryption.js"; import dataEncryptionService from "./data_encryption.js";
import type { OptionNames } from "@triliumnext/commons"; import type { OptionNames } from "@triliumnext/commons";
@@ -18,7 +18,7 @@ function verifyTotpSecret(secret: string): boolean {
return false; return false;
} }
return givenSecretHash === dbSecretHash; return constantTimeCompare(givenSecretHash, dbSecretHash);
} }
function setTotpSecret(secret: string) { function setTotpSecret(secret: string) {

View File

@@ -1,5 +1,5 @@
import becca from "../becca/becca.js"; import becca from "../becca/becca.js";
import { fromBase64, randomSecureToken } from "./utils.js"; import { fromBase64, randomSecureToken, constantTimeCompare } from "./utils.js";
import BEtapiToken from "../becca/entities/betapi_token.js"; import BEtapiToken from "../becca/entities/betapi_token.js";
import crypto from "crypto"; import crypto from "crypto";
@@ -83,15 +83,16 @@ function isValidAuthHeader(auth: string | undefined) {
return false; return false;
} }
return etapiToken.tokenHash === authTokenHash; return constantTimeCompare(etapiToken.tokenHash, authTokenHash);
} else { } else {
// Check ALL tokens to prevent timing attacks - do not short-circuit
let isValid = false;
for (const etapiToken of becca.getEtapiTokens()) { for (const etapiToken of becca.getEtapiTokens()) {
if (etapiToken.tokenHash === authTokenHash) { if (constantTimeCompare(etapiToken.tokenHash, authTokenHash)) {
return true; isValid = true;
} }
} }
return isValid;
return false;
} }
} }

View File

@@ -1,11 +1,9 @@
import { trimIndentation } from "@triliumnext/commons";
import becca from "../becca/becca.js"; import becca from "../becca/becca.js";
import BBranch from "../becca/entities/bbranch.js";
import BNote from "../becca/entities/bnote.js";
import { note, NoteBuilder } from "../test/becca_mocking.js"; import { note, NoteBuilder } from "../test/becca_mocking.js";
import cls from "./cls.js"; import cls from "./cls.js";
import { buildJsx, executeBundle, getScriptBundle } from "./script.js"; import { executeBundle, getScriptBundle } from "./script.js";
import BBranch from "../becca/entities/bbranch.js";
import BNote from "../becca/entities/bnote.js";
describe("Script", () => { describe("Script", () => {
@@ -86,95 +84,3 @@ describe("Script", () => {
}); });
}); });
}); });
describe("JSX building", () => {
it("processes basic JSX", () => {
const script = trimIndentation`\
function MyComponent() {
return <p>Hello world.</p>;
}
`;
const expected = trimIndentation`\
"use strict";function MyComponent() {
return api.preact.h('p', null, "Hello world." );
}
`;
expect(buildJsx(script).code).toStrictEqual(expected);
});
it("processes fragments", () => {
const script = trimIndentation`\
function MyComponent() {
return <>
<p>Hi</p>
<p>there</p>
</>;
}
`;
const expected = trimIndentation`\
"use strict";function MyComponent() {
return api.preact.h(api.preact.Fragment, null
, api.preact.h('p', null, "Hi")
, api.preact.h('p', null, "there")
);
}
`;
expect(buildJsx(script).code).toStrictEqual(expected);
});
it("rewrites export", () => {
const script = trimIndentation`\
const { defineWidget } = api.preact;
export default defineWidget({
parent: "right-pane",
render() {
return <></>;
}
});
`;
const expected = trimIndentation`\
"use strict";Object.defineProperty(exports, "__esModule", {value: true});const { defineWidget } = api.preact;
module.exports = defineWidget({
parent: "right-pane",
render() {
return api.preact.h(api.preact.Fragment, null);
}
});
`;
expect(buildJsx(script).code).toStrictEqual(expected);
});
it("rewrites React API imports", () => {
const script = trimIndentation`\
import { defineWidget, RightPanelWidget} from "trilium:preact";
defineWidget({
render() {
return <RightPanelWidget />;
}
});
`;
const expected = trimIndentation`\
"use strict";const _triliumpreact = api.preact;
_triliumpreact.defineWidget.call(void 0, {
render() {
return api.preact.h(_triliumpreact.RightPanelWidget, null );
}
});
`;
expect(buildJsx(script).code).toStrictEqual(expected);
});
it("rewrites internal API imports", () => {
const script = trimIndentation`\
import { log } from "trilium:api";
log("Hi");
`;
const expected = trimIndentation`\
"use strict";const _triliumapi = api;
_triliumapi.log.call(void 0, "Hi");
`;
expect(buildJsx(script).code).toStrictEqual(expected);
});
});

View File

@@ -1,11 +1,9 @@
import { transform } from "sucrase"; import ScriptContext from "./script_context.js";
import cls from "./cls.js";
import log from "./log.js";
import becca from "../becca/becca.js"; import becca from "../becca/becca.js";
import type BNote from "../becca/entities/bnote.js"; import type BNote from "../becca/entities/bnote.js";
import type { ApiParams } from "./backend_script_api_interface.js"; import type { ApiParams } from "./backend_script_api_interface.js";
import cls from "./cls.js";
import log from "./log.js";
import ScriptContext from "./script_context.js";
export interface Bundle { export interface Bundle {
note?: BNote; note?: BNote;
@@ -112,9 +110,9 @@ function getParams(params?: ScriptParams) {
.map((p) => { .map((p) => {
if (typeof p === "string" && p.startsWith("!@#Function: ")) { if (typeof p === "string" && p.startsWith("!@#Function: ")) {
return p.substr(13); return p.substr(13);
} } else {
return JSON.stringify(p); return JSON.stringify(p);
}
}) })
.join(","); .join(",");
} }
@@ -147,7 +145,7 @@ export function getScriptBundle(note: BNote, root: boolean = true, scriptEnv: st
return; return;
} }
if (!(note.isJavaScript() || note.isHtml() || note.isJsx())) { if (!note.isJavaScript() && !note.isHtml()) {
return; return;
} }
@@ -160,7 +158,7 @@ export function getScriptBundle(note: BNote, root: boolean = true, scriptEnv: st
} }
const bundle: Bundle = { const bundle: Bundle = {
note, note: note,
script: "", script: "",
html: "", html: "",
allNotes: [note] allNotes: [note]
@@ -194,18 +192,12 @@ export function getScriptBundle(note: BNote, root: boolean = true, scriptEnv: st
// only frontend scripts are async. Backend cannot be async because of transaction management. // only frontend scripts are async. Backend cannot be async because of transaction management.
const isFrontend = scriptEnv === "frontend"; const isFrontend = scriptEnv === "frontend";
if (note.isJsx() || note.isJavaScript()) { if (note.isJavaScript()) {
let scriptContent = note.getContent();
if (note.isJsx()) {
scriptContent = buildJsx(scriptContent).code;
}
bundle.script += ` bundle.script += `
apiContext.modules['${note.noteId}'] = { exports: {} }; apiContext.modules['${note.noteId}'] = { exports: {} };
${root ? "return " : ""}${isFrontend ? "await" : ""} ((${isFrontend ? "async" : ""} function(exports, module, require, api${modules.length > 0 ? ", " : ""}${modules.map((child) => sanitizeVariableName(child.title)).join(", ")}) { ${root ? "return " : ""}${isFrontend ? "await" : ""} ((${isFrontend ? "async" : ""} function(exports, module, require, api${modules.length > 0 ? ", " : ""}${modules.map((child) => sanitizeVariableName(child.title)).join(", ")}) {
try { try {
${overrideContent || scriptContent}; ${overrideContent || note.getContent()};
} catch (e) { throw new Error("Load of script note \\"${note.title}\\" (${note.noteId}) failed with: " + e.message); } } catch (e) { throw new Error("Load of script note \\"${note.title}\\" (${note.noteId}) failed with: " + e.message); }
for (const exportKey in exports) module.exports[exportKey] = exports[exportKey]; for (const exportKey in exports) module.exports[exportKey] = exports[exportKey];
return module.exports; return module.exports;
@@ -218,39 +210,6 @@ return module.exports;
return bundle; return bundle;
} }
export function buildJsx(contentRaw: string | Buffer) {
const content = Buffer.isBuffer(contentRaw) ? contentRaw.toString("utf-8") : contentRaw;
const output = transform(content, {
transforms: ["jsx", "imports"],
jsxPragma: "api.preact.h",
jsxFragmentPragma: "api.preact.Fragment",
production: true
});
let code = output.code;
// Rewrite ESM-like exports to `module.exports =`.
code = output.code.replaceAll(
/\bexports\s*\.\s*default\s*=\s*/g,
'module.exports = '
);
// Rewrite ESM-like imports to Preact, to `const { foo } = api.preact`
code = code.replaceAll(
/(?:var|let|const)\s+(\w+)\s*=\s*require\(['"]trilium:preact['"]\);?/g,
'const $1 = api.preact;'
);
// Rewrite ESM-like imports to internal API, to `const { foo } = api.preact`
code = code.replaceAll(
/(?:var|let|const)\s+(\w+)\s*=\s*require\(['"]trilium:api['"]\);?/g,
'const $1 = api;'
);
output.code = code;
return output;
}
function sanitizeVariableName(str: string) { function sanitizeVariableName(str: string) {
return str.replace(/[^a-z0-9_]/gim, ""); return str.replace(/[^a-z0-9_]/gim, "");
} }

View File

@@ -74,6 +74,36 @@ export function hmac(secret: any, value: any) {
return hmac.digest("base64"); return hmac.digest("base64");
} }
/**
* Constant-time string comparison to prevent timing attacks.
* Uses crypto.timingSafeEqual to ensure comparison time is independent
* of how many characters match.
*
* @param a First string to compare
* @param b Second string to compare
* @returns true if strings are equal, false otherwise
* @note Returns false for null/undefined/non-string inputs. Empty strings are considered equal.
*/
export function constantTimeCompare(a: string | null | undefined, b: string | null | undefined): boolean {
// Handle null/undefined/non-string cases safely
if (typeof a !== "string" || typeof b !== "string") {
return false;
}
const bufA = Buffer.from(a, "utf-8");
const bufB = Buffer.from(b, "utf-8");
// If lengths differ, we still do a constant-time comparison
// to avoid leaking length information through timing
if (bufA.length !== bufB.length) {
// Compare bufA against itself to maintain constant time behavior
crypto.timingSafeEqual(bufA, bufA);
return false;
}
return crypto.timingSafeEqual(bufA, bufB);
}
export function hash(text: string) { export function hash(text: string) {
text = text.normalize(); text = text.normalize();
@@ -486,6 +516,7 @@ function slugify(text: string) {
export default { export default {
compareVersions, compareVersions,
constantTimeCompare,
crash, crash,
envToBoolean, envToBoolean,
escapeHtml, escapeHtml,

View File

@@ -1,5 +1,5 @@
# Documentation # Documentation
There are multiple types of documentation for Trilium:<img class="image-style-align-right" src="api/images/nvEEQ1vvJMmS/Documentation_image.png" width="205" height="162"> There are multiple types of documentation for Trilium:<img class="image-style-align-right" src="api/images/8sStlTtSCgN7/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 _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. * The _Developer's Guide_ represents a set of Markdown documents that present the internals of Trilium, for developers.

View File

@@ -3600,13 +3600,6 @@
"isInheritable": false, "isInheritable": false,
"position": 170 "position": 170
}, },
{
"type": "relation",
"name": "internalLink",
"value": "oPVyFC7WL2Lp",
"isInheritable": false,
"position": 180
},
{ {
"type": "label", "type": "label",
"name": "iconClass", "name": "iconClass",
@@ -3614,6 +3607,13 @@
"isInheritable": false, "isInheritable": false,
"position": 30 "position": 30
}, },
{
"type": "relation",
"name": "internalLink",
"value": "oPVyFC7WL2Lp",
"isInheritable": false,
"position": 180
},
{ {
"type": "label", "type": "label",
"name": "shareAlias", "name": "shareAlias",
@@ -15631,83 +15631,6 @@
"value": "bx bxs-widget", "value": "bx bxs-widget",
"isInheritable": false, "isInheritable": false,
"position": 30 "position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "oPVyFC7WL2Lp",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "RnaPdbciOfeq",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
"value": "xYmIYSP6wE3F",
"isInheritable": false,
"position": 60
},
{
"type": "relation",
"name": "internalLink",
"value": "4Gn3psZKsfSm",
"isInheritable": false,
"position": 70
},
{
"type": "relation",
"name": "internalLink",
"value": "KLsqhjaqh1QW",
"isInheritable": false,
"position": 80
},
{
"type": "relation",
"name": "internalLink",
"value": "6f9hih2hXXZk",
"isInheritable": false,
"position": 90
},
{
"type": "relation",
"name": "internalLink",
"value": "HI6GBBIduIgv",
"isInheritable": false,
"position": 100
},
{
"type": "relation",
"name": "internalLink",
"value": "s8alTXmpFR61",
"isInheritable": false,
"position": 110
},
{
"type": "relation",
"name": "internalLink",
"value": "GhurYZjh8e1V",
"isInheritable": false,
"position": 120
},
{
"type": "relation",
"name": "internalLink",
"value": "M8IppdwVHSjG",
"isInheritable": false,
"position": 130
},
{
"type": "relation",
"name": "internalLink",
"value": "gMkgcLJ6jBkg",
"isInheritable": false,
"position": 140
} }
], ],
"format": "markdown", "format": "markdown",
@@ -15715,127 +15638,6 @@
"attachments": [], "attachments": [],
"dirFileName": "Custom Widgets", "dirFileName": "Custom Widgets",
"children": [ "children": [
{
"isClone": false,
"noteId": "SynTBQiBsdYJ",
"notePath": [
"pOsGYCXsbNQG",
"CdNpE2pqjmI6",
"yIhgI5H7A2Sm",
"MgibgPcfeuGz",
"SynTBQiBsdYJ"
],
"title": "Widget Basics",
"notePosition": 10,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/markdown",
"attributes": [
{
"type": "relation",
"name": "internalLink",
"value": "zEY4DaJG4YT5",
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "BFs8mudNFgCS",
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
"value": "GLks18SNjxmC",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "s8alTXmpFR61",
"isInheritable": false,
"position": 40
},
{
"type": "label",
"name": "shareAlias",
"value": "widget-basics",
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
"value": "MgibgPcfeuGz",
"isInheritable": false,
"position": 50
}
],
"format": "markdown",
"dataFileName": "Widget Basics.md",
"attachments": []
},
{
"isClone": false,
"noteId": "GhurYZjh8e1V",
"notePath": [
"pOsGYCXsbNQG",
"CdNpE2pqjmI6",
"yIhgI5H7A2Sm",
"MgibgPcfeuGz",
"GhurYZjh8e1V"
],
"title": "Note context aware widget",
"notePosition": 20,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [],
"format": "markdown",
"dataFileName": "Note context aware widget.md",
"attachments": []
},
{
"isClone": false,
"noteId": "M8IppdwVHSjG",
"notePath": [
"pOsGYCXsbNQG",
"CdNpE2pqjmI6",
"yIhgI5H7A2Sm",
"MgibgPcfeuGz",
"M8IppdwVHSjG"
],
"title": "Right pane widget",
"notePosition": 30,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "relation",
"name": "internalLink",
"value": "IjZS7iK5EXtb",
"isInheritable": false,
"position": 10
},
{
"type": "label",
"name": "shareAlias",
"value": "right-pane-widget",
"isInheritable": false,
"position": 20
}
],
"format": "markdown",
"dataFileName": "Right pane widget.md",
"attachments": []
},
{ {
"isClone": false, "isClone": false,
"noteId": "YNxAqkI5Kg1M", "noteId": "YNxAqkI5Kg1M",
@@ -15847,7 +15649,7 @@
"YNxAqkI5Kg1M" "YNxAqkI5Kg1M"
], ],
"title": "Word count widget", "title": "Word count widget",
"notePosition": 40, "notePosition": 10,
"prefix": null, "prefix": null,
"isExpanded": false, "isExpanded": false,
"type": "text", "type": "text",
@@ -15895,6 +15697,99 @@
} }
] ]
}, },
{
"isClone": false,
"noteId": "SynTBQiBsdYJ",
"notePath": [
"pOsGYCXsbNQG",
"CdNpE2pqjmI6",
"yIhgI5H7A2Sm",
"MgibgPcfeuGz",
"SynTBQiBsdYJ"
],
"title": "Widget Basics",
"notePosition": 20,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/markdown",
"attributes": [
{
"type": "relation",
"name": "internalLink",
"value": "zEY4DaJG4YT5",
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "BFs8mudNFgCS",
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
"value": "GLks18SNjxmC",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "s8alTXmpFR61",
"isInheritable": false,
"position": 40
},
{
"type": "label",
"name": "shareAlias",
"value": "widget-basics",
"isInheritable": false,
"position": 20
}
],
"format": "markdown",
"dataFileName": "Widget Basics.md",
"attachments": []
},
{
"isClone": false,
"noteId": "M8IppdwVHSjG",
"notePath": [
"pOsGYCXsbNQG",
"CdNpE2pqjmI6",
"yIhgI5H7A2Sm",
"MgibgPcfeuGz",
"M8IppdwVHSjG"
],
"title": "Right pane widget",
"notePosition": 30,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "label",
"name": "shareAlias",
"value": "right-pane-widget",
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
"value": "IjZS7iK5EXtb",
"isInheritable": false,
"position": 30
}
],
"format": "markdown",
"dataFileName": "Right pane widget.md",
"attachments": []
},
{ {
"isClone": false, "isClone": false,
"noteId": "VqGQnnPGnqAU", "noteId": "VqGQnnPGnqAU",
@@ -15906,7 +15801,7 @@
"VqGQnnPGnqAU" "VqGQnnPGnqAU"
], ],
"title": "CSS", "title": "CSS",
"notePosition": 70, "notePosition": 40,
"prefix": null, "prefix": null,
"isExpanded": false, "isExpanded": false,
"type": "text", "type": "text",
@@ -15918,13 +15813,6 @@
"value": "css", "value": "css",
"isInheritable": false, "isInheritable": false,
"position": 20 "position": 20
},
{
"type": "relation",
"name": "internalLink",
"value": "Sg9GrCtyftZf",
"isInheritable": false,
"position": 30
} }
], ],
"format": "markdown", "format": "markdown",
@@ -15942,7 +15830,7 @@
"gMkgcLJ6jBkg" "gMkgcLJ6jBkg"
], ],
"title": "Troubleshooting", "title": "Troubleshooting",
"notePosition": 80, "notePosition": 50,
"prefix": null, "prefix": null,
"isExpanded": false, "isExpanded": false,
"type": "text", "type": "text",
@@ -16329,292 +16217,6 @@
] ]
} }
] ]
},
{
"isClone": false,
"noteId": "KLsqhjaqh1QW",
"notePath": [
"pOsGYCXsbNQG",
"CdNpE2pqjmI6",
"yIhgI5H7A2Sm",
"KLsqhjaqh1QW"
],
"title": "Preact",
"notePosition": 40,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "label",
"name": "iconClass",
"value": "bx bxl-react",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "MgibgPcfeuGz",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "HcABDtFCkbFN",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
"value": "wy8So3yZZlH9",
"isInheritable": false,
"position": 60
},
{
"type": "relation",
"name": "internalLink",
"value": "GLks18SNjxmC",
"isInheritable": false,
"position": 70
},
{
"type": "relation",
"name": "internalLink",
"value": "Bqde6BvPo05g",
"isInheritable": false,
"position": 80
}
],
"format": "markdown",
"dataFileName": "Preact.md",
"attachments": [],
"dirFileName": "Preact",
"children": [
{
"isClone": false,
"noteId": "Bqde6BvPo05g",
"notePath": [
"pOsGYCXsbNQG",
"CdNpE2pqjmI6",
"yIhgI5H7A2Sm",
"KLsqhjaqh1QW",
"Bqde6BvPo05g"
],
"title": "Component libraries",
"notePosition": 10,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "relation",
"name": "internalLink",
"value": "hA834UaHhSNn",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "HcABDtFCkbFN",
"isInheritable": false,
"position": 40
},
{
"type": "label",
"name": "iconClass",
"value": "bx bxs-component",
"isInheritable": false,
"position": 50
}
],
"format": "markdown",
"dataFileName": "Component libraries.md",
"attachments": []
},
{
"isClone": false,
"noteId": "ykYtbM9k3a7B",
"notePath": [
"pOsGYCXsbNQG",
"CdNpE2pqjmI6",
"yIhgI5H7A2Sm",
"KLsqhjaqh1QW",
"ykYtbM9k3a7B"
],
"title": "Hooks",
"notePosition": 20,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "relation",
"name": "internalLink",
"value": "HcABDtFCkbFN",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "GhurYZjh8e1V",
"isInheritable": false,
"position": 50
},
{
"type": "label",
"name": "iconClass",
"value": "bx bx-question-mark",
"isInheritable": false,
"position": 60
}
],
"format": "markdown",
"dataFileName": "Hooks.md",
"attachments": []
},
{
"isClone": false,
"noteId": "Sg9GrCtyftZf",
"notePath": [
"pOsGYCXsbNQG",
"CdNpE2pqjmI6",
"yIhgI5H7A2Sm",
"KLsqhjaqh1QW",
"Sg9GrCtyftZf"
],
"title": "CSS",
"notePosition": 30,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "relation",
"name": "internalLink",
"value": "AlhDUqhENtH7",
"isInheritable": false,
"position": 30
},
{
"type": "label",
"name": "iconClass",
"value": "bx bxs-file-css",
"isInheritable": false,
"position": 40
}
],
"format": "markdown",
"dataFileName": "CSS.md",
"attachments": []
},
{
"isClone": false,
"noteId": "RSssb9S3xgSr",
"notePath": [
"pOsGYCXsbNQG",
"CdNpE2pqjmI6",
"yIhgI5H7A2Sm",
"KLsqhjaqh1QW",
"RSssb9S3xgSr"
],
"title": "Built-in components",
"notePosition": 40,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "label",
"name": "iconClass",
"value": "bx bxs-component",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "MgibgPcfeuGz",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "HcABDtFCkbFN",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
"value": "i9B4IW7b6V6z",
"isInheritable": false,
"position": 60
},
{
"type": "relation",
"name": "internalLink",
"value": "6tZeKvSHEUiB",
"isInheritable": false,
"position": 70
}
],
"format": "markdown",
"dataFileName": "Built-in components.md",
"attachments": [
{
"attachmentId": "KtDChJYITDxC",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Built-in components_image.png"
}
],
"dirFileName": "Built-in components",
"children": [
{
"isClone": false,
"noteId": "i9B4IW7b6V6z",
"notePath": [
"pOsGYCXsbNQG",
"CdNpE2pqjmI6",
"yIhgI5H7A2Sm",
"KLsqhjaqh1QW",
"RSssb9S3xgSr",
"i9B4IW7b6V6z"
],
"title": "Widget showcase",
"notePosition": 10,
"prefix": null,
"isExpanded": false,
"type": "code",
"mime": "text/jsx",
"attributes": [
{
"type": "label",
"name": "shareAlias",
"value": "widget-showcase",
"isInheritable": false,
"position": 30
}
],
"dataFileName": "Widget showcase.jsx",
"attachments": []
}
]
}
]
} }
] ]
}, },
@@ -16738,98 +16340,6 @@
} }
] ]
}, },
{
"isClone": false,
"noteId": "wqXwKJl6VpNk",
"notePath": [
"pOsGYCXsbNQG",
"CdNpE2pqjmI6",
"wqXwKJl6VpNk"
],
"title": "Common concepts",
"notePosition": 40,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "label",
"name": "iconClass",
"value": "bx bxl-nodejs",
"isInheritable": false,
"position": 30
},
{
"type": "label",
"name": "shareAlias",
"value": "common-concepts",
"isInheritable": false,
"position": 40
}
],
"format": "markdown",
"attachments": [],
"dirFileName": "Common concepts",
"children": [
{
"isClone": false,
"noteId": "hA834UaHhSNn",
"notePath": [
"pOsGYCXsbNQG",
"CdNpE2pqjmI6",
"wqXwKJl6VpNk",
"hA834UaHhSNn"
],
"title": "Script bundles",
"notePosition": 10,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "label",
"name": "iconClass",
"value": "bx bx-package",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "HcABDtFCkbFN",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "6f9hih2hXXZk",
"isInheritable": false,
"position": 50
},
{
"type": "label",
"name": "shareAlias",
"value": "bundles",
"isInheritable": false,
"position": 60
},
{
"type": "relation",
"name": "internalLink",
"value": "IakOLONlIfGI",
"isInheritable": false,
"position": 70
}
],
"format": "markdown",
"dataFileName": "Script bundles.md",
"attachments": []
}
]
},
{ {
"isClone": false, "isClone": false,
"noteId": "GLks18SNjxmC", "noteId": "GLks18SNjxmC",
@@ -16839,7 +16349,7 @@
"GLks18SNjxmC" "GLks18SNjxmC"
], ],
"title": "Script API", "title": "Script API",
"notePosition": 110, "notePosition": 100,
"prefix": null, "prefix": null,
"isExpanded": false, "isExpanded": false,
"type": "text", "type": "text",
@@ -17037,7 +16547,7 @@
"vElnKeDNPSVl" "vElnKeDNPSVl"
], ],
"title": "Logging", "title": "Logging",
"notePosition": 120, "notePosition": 110,
"prefix": null, "prefix": null,
"isExpanded": false, "isExpanded": false,
"type": "text", "type": "text",

View File

@@ -9,7 +9,7 @@ Render Note is used in <a class="reference-link" href="../Scripting.md">Scripti
2. Create a <a class="reference-link" href="Render%20Note.md">Render Note</a>. 2. Create a <a class="reference-link" href="Render%20Note.md">Render Note</a>.
3. Assign the `renderNote` [relation](../Advanced%20Usage/Attributes.md) to point at the previously created code note. 3. Assign the `renderNote` [relation](../Advanced%20Usage/Attributes.md) to point at the previously created code note.
## Legacy scripting using jQuery ## Dynamic content
A static HTML is generally not enough for <a class="reference-link" href="../Scripting.md">Scripting</a>. The next step is to automatically change parts of the note using JavaScript. A static HTML is generally not enough for <a class="reference-link" href="../Scripting.md">Scripting</a>. The next step is to automatically change parts of the note using JavaScript.
@@ -34,28 +34,6 @@ Now create a render note at any place and set its `~renderNote` relation to poin
> **Current date & time** > **Current date & time**
> The current date & time is Sun Apr 06 2025 15:26:29 GMT+0300 (Eastern European Summer Time) > The current date & time is Sun Apr 06 2025 15:26:29 GMT+0300 (Eastern European Summer Time)
## Dynamic content using Preact & JSX
As a more modern alternative to jQuery, it's possible to use Preact & JSX to render pages. Since JSX is a superset of JavaScript, there's no need to provide a HTML anymore.
Here are the steps to creating a simple render note:
1. Create a note of type <a class="reference-link" href="Render%20Note.md">Render Note</a>.
2. Create a child <a class="reference-link" href="Code.md">Code</a> note with JSX as the language.
As an example, use the following content:
```jsx
export default function() {
return (
<>
<p>Hello world.</p>
</>
);
}
```
3. In the parent render note, define a `~renderNote` relation pointing to the newly created child.
4. Refresh the render note and it should display a “Hello world” message.
## Refreshing the note ## Refreshing the note
It's possible to refresh the note via: It's possible to refresh the note via:

View File

@@ -1,48 +0,0 @@
# Script bundles
For both <a class="reference-link" href="../../Note%20Types/Render%20Note.md">Render Note</a> and more complicated scripts, it's generally useful to split the code into multiple <a class="reference-link" href="../../Note%20Types/Code.md">Code</a> notes.
When a script is run, the sub-children of the script being run (or the <a class="reference-link" href="../../Note%20Types/Render%20Note.md">Render Note</a>) are checked for children. If the children are Code notes of the corresponding type (front-end or backend) as the code being run, they will be evaluated as well.
The collection of a script and its child notes is called a _bundle_. A child note inside a bundle is called a _module_.
As a basic example of dependencies, consider the following note structure:
* _Script with dependency_
```javascript
api.log(MyMath.sum(2, 2));
```
* _MyMath_
```javascript
module.exports = {
sum(a, b) {
return a + b;
}
};
```
When _Script with dependency_ is run, it will detect _MyMath_ as a submodule and provide the result of its `module.exports` object into a global object with the same name as the note.
> [!NOTE]
> If the note contains spaces or special characters, they will be stripped. For example `My Nice Note!` becomes `MyNiceNote`.
## Alternative syntax
Instead of providing an object to `module.exports`, it's also possible to add fields individually:
```javascript
module.exports.sum = (a, b) => a + b;
module.exports.subtract = (a, b) => a - b;
```
## Ignoring a code script from a bundle
To ignore a script from being included in a bundle (e.g. if it's unrelated to the parent script note), apply the `#disableInclusion` label.
## Sharing a module across multiple bundles
Modules can be reused across multiple scripts by simply cloning the shared module between two modules (see <a class="reference-link" href="../../Basic%20Concepts%20and%20Features/Notes/Cloning%20Notes.md">Cloning Notes</a>).
Optionally, a separate note can be used to contain all the different reusable modules for an easy way to discover them.

View File

@@ -1,62 +1,9 @@
# Custom Widgets # Custom Widgets
Custom widgets are a special subset of scripts that render graphical elements in certain parts of the application. These can be used to add new functionality to the Trilium application. It's possible to create custom widget in three possible locations where you can display your custom content.
## Preact with JSX vs. vanilla jQuery Positions are:
In older versions of Trilium, custom widgets were exclusively written in a combination of jQuery with Trilium's internal widget architecture (e.g., `BasicWidget`, `NoteContextAwareWidget`). * `left-pane`
* `center-pane`
Starting with v0.101.0, custom widgets can also be written in JSX using the <a class="reference-link" href="Preact.md">Preact</a> framework. Both legacy and Preact widgets have the same capabilities, with a single difference: * `note-detail-pane` - located within `center-pane`, but specific to note (split)
* `right-pane`
* Preact widgets are content-sized by default whereas legacy widgets need `this.contentSized()` applied in the constructor. For more information, see the corresponding section in <a class="reference-link" href="Custom%20Widgets/Troubleshooting.md">Troubleshooting</a>.
Wherever possible, widget examples will be both in the legacy and Preact format.
## Creating a custom widget
1. Create a <a class="reference-link" href="../../Note%20Types/Code.md">Code</a> note.
2. Set the language to:
1. JavaScript (frontend) for legacy widgets using jQuery.
2. JSX for Preact widgets. You might need to go to Options → Code to enable the language first.
3. Apply the `#widget` [label](../../Advanced%20Usage/Attributes/Labels.md).
## Getting started with a simple example
Let's start by creating a widget that shows a message near the content area. Follow the previous section to create a code note, and use the following content.
<table><thead><tr><th>Legacy</th><th style="width:50%;">Preact (v0.101.0+)</th></tr></thead><tbody><tr><td><pre><code class="language-text-x-trilium-auto">class HelloNoteDetail extends api.BasicWidget {
constructor() {
super();
this.contentSized();
}
get parentWidget() { return "center-pane" }
doRender() {
this.$widget = $("&lt;span&gt;Center pane&lt;/span&gt;");
}
}
module.exports = new HelloNoteDetail();</code></pre></td><td style="vertical-align:top;"><pre><code class="language-text-x-trilium-auto">import { defineWidget } from "trilium:preact";
export default defineWidget({
parent: "center-pane",
render: () =&gt; &lt;span&gt;Center pane from Preact.&lt;/span&gt;
});</code></pre></td></tr></tbody></table>
[Refresh the application](../../Troubleshooting/Refreshing%20the%20application.md) and the widget should appear underneath the content area.
## Widget location (parent widget)
A widget can be placed in one of the following sections of the applications:
<table class="ck-table-resized"><colgroup><col style="width:15.59%;"><col style="width:30.42%;"><col style="width:16.68%;"><col style="width:37.31%;"></colgroup><thead><tr><th>Value for <code spellcheck="false">parentWidget</code></th><th>Description</th><th>Sample widget</th><th>Special requirements</th></tr></thead><tbody><tr><th><code spellcheck="false">left-pane</code></th><td>Appears within the same pane that holds the&nbsp;<a class="reference-link" href="../../Basic%20Concepts%20and%20Features/UI%20Elements/Note%20Tree.md">Note Tree</a>.</td><td>Same as above, with only a different <code spellcheck="false">parentWidget</code>.</td><td>None.</td></tr><tr><th><code spellcheck="false">center-pane</code></th><td>In the content area. If a split is open, the widget will span all of the splits.</td><td>See example above.</td><td>None.</td></tr><tr><th><code spellcheck="false">note-detail-pane</code></th><td><p>In the content area, inside the note detail area. If a split is open, the widget will be contained inside the split.</p><p>This is ideal if the widget is note-specific.</p></td><td><a class="reference-link" href="Custom%20Widgets/Note%20context%20aware%20widget.md">Note context aware widget</a></td><td><ul><li data-list-item-id="ec06332efcc3039721606c052f0d913fa">The widget must export a <code spellcheck="false">class</code> and not an instance of the class (e.g. <code spellcheck="false">no new</code>) because it needs to be multiplied for each note, so that splits work correctly.</li><li data-list-item-id="e8da690a2a8df148f6b5fc04ba1611688">Since the <code spellcheck="false">class</code> is exported instead of an instance, the <code spellcheck="false">parentWidget</code> getter must be <code spellcheck="false">static</code>, otherwise the widget is ignored.</li></ul></td></tr><tr><th><code spellcheck="false">right-pane</code></th><td>In the&nbsp;<a class="reference-link" href="../../Basic%20Concepts%20and%20Features/UI%20Elements/Right%20Sidebar.md">Right Sidebar</a>, as a dedicated section.</td><td><a class="reference-link" href="Custom%20Widgets/Right%20pane%20widget.md">Right pane widget</a></td><td><ul><li data-list-item-id="efe008d361e224f422582552648e1afe7">Although not mandatory, it's best to use a <code spellcheck="false">RightPanelWidget</code> instead of a <code spellcheck="false">BasicWidget</code> or a <code spellcheck="false">NoteContextAwareWidget</code>.</li></ul></td></tr></tbody></table>
To position the widget somewhere else, just change the value passed to `get parentWidget()` for legacy widgets or the `parent` field for Preact. Do note that some positions such as `note-detail-pane` and `right-pane` have special requirements that need to be accounted for (see the table above).
## Launch bar widgets
Launch bar widgets are similar to _Custom widgets_ but are specific to the <a class="reference-link" href="../../Basic%20Concepts%20and%20Features/UI%20Elements/Launch%20Bar.md">Launch Bar</a>. See <a class="reference-link" href="Launch%20Bar%20Widgets.md">Launch Bar Widgets</a> for more information.
## Custom position

View File

@@ -1,7 +1,5 @@
# CSS # CSS
## Classic widgets In `doRender()`:
In `doRender()`:<sup><a href="#fn1saoftmefpp">[1]</a></sup>
``` ```
this.cssBlock(`#my-widget { this.cssBlock(`#my-widget {
@@ -9,13 +7,9 @@ this.cssBlock(`#my-widget {
bottom: 40px; bottom: 40px;
left: 60px; left: 60px;
z-index: 1; z-index: 1;
}`); }`)
``` ```
## Preact widgets * * *
See the dedicated page: <a class="reference-link" href="../Preact/CSS.md">CSS</a>. Reference: [https://trilium.rocks/X7pxYpiu0lgU](https://trilium.rocks/X7pxYpiu0lgU)
1. <sup><strong><a href="#fnref1saoftmefpp">^</a></strong></sup>
Reference: [https://trilium.rocks/X7pxYpiu0lgU](https://trilium.rocks/X7pxYpiu0lgU)

View File

@@ -1,53 +0,0 @@
# Note context aware widget
Note context-aware widgets are a different type of widget which automatically react to changes in the current note.
Important aspects:
* The widget must export a `class` and not an instance of the class (e.g. `no new`) because it needs to be multiplied for each note, so that splits work correctly.
* Since the `class` is exported instead of an instance, the `parentWidget` getter must be `static`, otherwise the widget is ignored.
## Example displaying the current note title
This is a note context-aware widget that simply displays the name the current note. 
### Classic example
```
class HelloNoteDetail extends api.NoteContextAwareWidget {
constructor() {
super();
this.contentSized();
}
doRender() {
this.$widget = $("<div>");
}
async refreshWithNote(note) {
this.$widget.text("Current note: " + note.title);
}
static get parentWidget() { return "note-detail-pane" }
get position() { return 10 }
}
module.exports = HelloNoteDetail;
```
### Preact (v0.101.0+)
```
import { defineWidget, useNoteContext, useNoteProperty } from "trilium:preact";
export default defineWidget({
parent: "note-detail-pane",
position: 10,
render: () => {
const { note } = useNoteContext();
const title = useNoteProperty(note, "title");
return <span>Current note JSX: {title}</span>;
}
});
```

View File

@@ -40,8 +40,6 @@ module.exports = new NoteTitleWidget();
A simple widget which will show the current time, as an example on how to dynamically change the content of the widget periodically. A simple widget which will show the current time, as an example on how to dynamically change the content of the widget periodically.
### Legacy widget
``` ```
const template = `<div></div>`; const template = `<div></div>`;
@@ -64,31 +62,6 @@ class ToDoListWidget extends api.RightPanelWidget {
module.exports = new ToDoListWidget(); module.exports = new ToDoListWidget();
``` ```
### Preact widget
```
import { defineWidget, RightPanelWidget, useEffect, useState } from "trilium:preact";
export default defineWidget({
parent: "right-pane",
position: 1,
render() {
const [ time, setTime ] = useState();
useEffect(() => {
const interval = setInterval(() => {
setTime(new Date().toLocaleString());
}, 1000);
return () => clearInterval(interval);
});
return (
<RightPanelWidget id="clock-jsx" title="Clock (JSX)">
<p>The time is: {time}</p>
</RightPanelWidget>
);
}
});
```
## Example for old layout ## Example for old layout
Here's a widget that displays a basic message ("Hi"): Here's a widget that displays a basic message ("Hi"):
@@ -132,7 +105,7 @@ By default, the sidebar items are displayed in the order they are found by the a
It is possible to make a widget appear higher or lower up, by adjusting its `position` property: It is possible to make a widget appear higher or lower up, by adjusting its `position` property:
``` ```diff
class MyWidget extends api.RightPanelWidget { class MyWidget extends api.RightPanelWidget {
+ get position() { return 20 }; + get position() { return 20 };

View File

@@ -108,6 +108,11 @@ class MyWidget extends api.BasicWidget {
module.exports = new MyWidget(); module.exports = new MyWidget();
``` ```
For the list of possible values for `parentWidget()`, see <a class="reference-link" href="../Custom%20Widgets.md">Custom Widgets</a>.  `parentWidget()` can be given the following values:
* `left-pane` - This renders the widget on the left side of the screen where the note tree lives.
* `center-pane` - This renders the widget in the center of the layout in the same location that notes and splits appear.
* `note-detail-pane` - This renders the widget _with_ the note in the center pane. This means it can appear multiple times with splits.
* `right-pane` - This renders the widget to the right of any opened notes.
[Reload](../../../Troubleshooting/Refreshing%20the%20application.md) the application one last time. When you click the button, a "Hello World!" message should appear, confirming that your widget is fully functional. [Reload](../../../Troubleshooting/Refreshing%20the%20application.md) the application one last time. When you click the button, a "Hello World!" message should appear, confirming that your widget is fully functional.

View File

@@ -3,8 +3,6 @@
This is an example of a note context-aware widget, which reacts to the currently opened note and refreshes automatically as the user navigates through the notes. This is an example of a note context-aware widget, which reacts to the currently opened note and refreshes automatically as the user navigates through the notes.
## Legacy widget
In this example, the title of the note is displayed. It works best on the [horizontal layout](../../../Basic%20Concepts%20and%20Features/UI%20Elements/Vertical%20and%20horizontal%20layout.md). In this example, the title of the note is displayed. It works best on the [horizontal layout](../../../Basic%20Concepts%20and%20Features/UI%20Elements/Vertical%20and%20horizontal%20layout.md).
```javascript ```javascript
@@ -32,23 +30,3 @@ class NoteTitleWidget extends api.NoteContextAwareWidget {
module.exports = new NoteTitleWidget(); module.exports = new NoteTitleWidget();
``` ```
## Preact widget (v0.101.0+)
```jsx
import { defineLauncherWidget, useActiveNoteContext } from "trilium:preact";
export default defineLauncherWidget({
render: () => {
const { note } = useActiveNoteContext();
return <div style={{
display: "flex",
height: "53px",
width: "fit-content",
fontSize: "0.75em",
alignItems: "center",
flexShrink: 0
}}>{note?.title}</div>;
}
});
```

View File

@@ -1,65 +0,0 @@
# Preact
Since v0.101.0, Trilium integrates Preact for front-end scripting, including support for JSX.
Preact can be used for:
* <a class="reference-link" href="../../Note%20Types/Render%20Note.md">Render Note</a>, where a JSX code note is used instead of a HTML one.
* <a class="reference-link" href="Custom%20Widgets.md">Custom Widgets</a>, where JSX can be used to replace the old, jQuery-based mechanism.
To get started, the first step is to enable JSX in the list of Code languages. Go to Options → Code Notes and check the “JSX” language. Afterwards, proceed with the documentation in either <a class="reference-link" href="../../Note%20Types/Render%20Note.md">Render Note</a> or <a class="reference-link" href="Custom%20Widgets.md">Custom Widgets</a>, which will both have a section on how to use the new Preact integration.
> [!IMPORTANT]
> The documentation assumes prior knowledge with React or Preact. As a starting point, consider the [FreeCodeCamp course on Front End Development Libraries](https://www.freecodecamp.org/learn/front-end-development-libraries-v9/) or the [Preact Tutorial](https://preactjs.com/tutorial/).
## Import/exports
When using Preact with JSX, there is a special syntax which provides ES-like imports. This `import` syntax makes way for a more intuitive that doesn't make use of global objects and paves the way for better auto-completion support that might be introduced in the future. 
### API imports
Instead of:
```jsx
api.showMessage("Hello");
```
the JSX version looks like this:
```jsx
import { showMessage } from "trilium:api";
showMessage("hello");
```
### Preact API imports (hooks, components)
There's a new <a class="reference-link" href="../Script%20API.md">Script API</a> dedicated to Preact, which provides shared components that are also used by Trilium internally as well as hooks, for example.
```jsx
import { useState } from "trilium:preact";
const [ myState, setMyState ] = useState("Hi");
```
### Exporting
JSX notes can export a component for use in <a class="reference-link" href="../../Note%20Types/Render%20Note.md">Render Note</a> or for <a class="reference-link" href="Preact/Component%20libraries.md">Component libraries</a>:
```jsx
export default function() {
return (
<>
<p>Hello world.</p>
</>
);
}
```
### Import/export are not required
These imports are syntactic sugar meant to replace the usage for the `api` global object (see <a class="reference-link" href="../Script%20API.md">Script API</a>). 
> [!NOTE]
> The `import` and `export` syntax work only for JSX notes. Standard/jQuery code notes still need to use the `api` global and `module.exports`.
## Under the hood
Unlike JavaScript, JSX requires pre-processing to turn it into JavaScript (just like TypeScript). To do so, Trilium uses [Sucrase](https://github.com/alangpierce/sucrase), a JavaScript library which processes the JSX to pure JavaScript. The processing is done each time a script is run (for widgets this happens at every program startup). If you notice any performance degradation due to long compilation, consider [reporting the issue](../../Troubleshooting/Reporting%20issues.md) to us.

View File

@@ -1,43 +0,0 @@
# Built-in components
<figure class="image image_resized" style="width:54.58%;"><img style="aspect-ratio:896/712;" src="Built-in components_image.png" width="896" height="712"><figcaption>A partial screenshot from the Widget showcase example (see below).</figcaption></figure>
Trilium comes with its own set of Preact components, some of which are also available to <a class="reference-link" href="../Custom%20Widgets.md">Custom Widgets</a> and <a class="reference-link" href="../../../Note%20Types/Render%20Note.md">Render Note</a>.
To use these components, simply import them from `trilium:preact`:
```jsx
import { ActionButton, Button, LinkButton } from "trilium:preact";
```
and then use them:
```jsx
export default function MyRenderNote() {
const onClick = () => showMessage("A button was pressed");
return (
<>
<h2>Buttons</h2>
<div style={{ display: "flex", gap: "1em", alignItems: "center" }}>
<ActionButton icon="bx bx-rocket" text="Action button" onClick={onClick} />
<Button icon="bx bx-rocket" text="Simple button" onClick={onClick} />
<LinkButton text="Link button" onClick={onClick} />
</div>
</>
)
}
```
## Widget showcase
> [!TIP]
> Starting with v0.101.0, the widget showcase is also available in the <a class="reference-link" href="../../../Advanced%20Usage/Database/Demo%20Notes.md">Demo Notes</a>.
This is a <a class="reference-link" href="../../../Note%20Types/Render%20Note.md">Render Note</a> example with JSX that shows most of the built-in components that are accessible to custom widgets and JSX render notes.
To use it, simply:
1. Create a render note.
2. Create a child code note of JSX type with the content of this file: <a class="reference-link" href="Built-in%20components/Widget%20showcase.jsx">Widget showcase</a>
3. Set the `~renderNote` relation of the parent note to the child note.
4. Refresh the render note to see the results.

View File

@@ -1,189 +0,0 @@
import {
ActionButton, Button, LinkButton,
Admonition, Collapsible,
FormCheckbox, FormDropdownList, FormFileUploadButton, FormGroup, FormRadioGroup, FormTextArea,
FormTextBox, FormToggle, Slider, RawHtml, LoadingSpinner, Icon,
Dropdown, FormListItem, FormDropdownDivider, FormDropdownSubmenu,
NoteAutocomplete, NoteLink, Modal,
CKEditor,
useEffect, useState
} from "trilium:preact";
import { showMessage } from "trilium:api";
export default function() {
const [ time, setTime ] = useState();
const lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam accumsan eu odio non gravida. Pellentesque ornare, arcu condimentum molestie dignissim, nibh turpis ultrices elit, eget elementum nunc erat at erat. Maecenas vehicula consectetur elit, nec fermentum elit venenatis eu.";
useEffect(() => {
const interval = setInterval(() => setTime(new Date().toLocaleString()), 1000);
return () => clearInterval(interval);
}, []);
return (
<div style={{ padding: 20, display: "flex", flexDirection: "column", gap: "1em" }}>
<h1>Widget showcase</h1>
<Buttons />
<Admonition type="note">
<strong>Admonition</strong><br />
{lorem}
</Admonition>
<Collapsible title="Collapsible" initiallyExpanded>
{lorem}
</Collapsible>
<FormElements />
<NoteElements />
<ModalSample />
<DropdownSample />
</div>
);
}
function Buttons() {
const onClick = () => showMessage("A button was pressed");
return (
<>
<h2>Buttons</h2>
<div style={{ display: "flex", gap: "1em", alignItems: "center" }}>
<ActionButton icon="bx bx-rocket" text="Action button" onClick={onClick} />
<Button icon="bx bx-rocket" text="Simple button" onClick={onClick} />
<LinkButton text="Link button" onClick={onClick} />
</div>
</>
)
}
function FormElements() {
const [ checkboxChecked, setCheckboxChecked ] = useState(false);
const [ dropdownValue, setDropdownValue ] = useState("key-1");
const [ radioGroupValue, setRadioGroupValue ] = useState("key-1");
const [ sliderValue, setSliderValue ] = useState(50);
return (
<>
<h2>Form elements</h2>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "1em" }}>
<FormGroup name="checkbox" label="Checkbox">
<FormCheckbox label="Checkbox" currentValue={checkboxChecked} onChange={setCheckboxChecked} />
</FormGroup>
<FormGroup name="toggle" label="Toggle">
<FormToggle switchOnName="Off" switchOffName="On" currentValue={checkboxChecked} onChange={setCheckboxChecked} />
</FormGroup>
<FormGroup name="dropdown" label="Dropdown">
<FormDropdownList
values={[
{ key: "key-1", name: "First item" },
{ key: "key-2", name: "Second item" },
{ key: "key-3", name: "Third item" },
]}
currentValue={dropdownValue} onChange={setDropdownValue}
keyProperty="key" titleProperty="name"
/>
</FormGroup>
<FormGroup name="radio-group" label="Radio group">
<FormRadioGroup
values={[
{ value: "key-1", label: "First item" },
{ value: "key-2", label: "Second item" },
{ value: "key-3", label: "Third item" },
]}
currentValue={radioGroupValue} onChange={setRadioGroupValue}
/>
</FormGroup>
<FormGroup name="text-box" label="Text box">
<FormTextBox
placeholder="Type something..."
currentValue="" onChange={(newValue) => {}}
/>
</FormGroup>
<FormGroup name="text-area" label="Text area">
<FormTextArea
placeholder="Type something bigger..."
currentValue="" onChange={(newValue) => {}}
/>
</FormGroup>
<FormGroup name="slider" label="Slider">
<Slider
min={1} max={100}
value={sliderValue} onChange={setSliderValue}
/>
</FormGroup>
<FormGroup name="file-upload" label="File upload">
<FormFileUploadButton
text="Upload"
onChange={(files) => {
const file = files?.[0];
if (!file) return;
showMessage(`Got file "${file.name}" of size ${file.size} B and type ${file.type}.`);
}}
/>
</FormGroup>
<FormGroup name="icon" label="Icon">
<Icon icon="bx bx-smile" />
</FormGroup>
<FormGroup name="loading-spinner" label="Loading spinner">
<LoadingSpinner />
</FormGroup>
<FormGroup name="raw-html" label="Raw HTML">
<RawHtml html="<strong>Hi</strong> <em>there</em>" />
</FormGroup>
</div>
</>
)
}
function NoteElements() {
const [ noteId, setNoteId ] = useState("");
return (
<div>
<h2>Note elements</h2>
<FormGroup name="note-autocomplete" label="Note autocomplete">
<NoteAutocomplete
placeholder="Select a note"
noteId={noteId} noteIdChanged={setNoteId}
/>
</FormGroup>
<FormGroup name="note-link" label="Note link">
{noteId
? <NoteLink notePath={noteId} showNoteIcon />
: <span>Select a note first</span>}
</FormGroup>
</div>
);
}
function ModalSample() {
const [ shown, setShown ] = useState(false);
return (
<>
<h2>Modal</h2>
<Button text="Open modal" onClick={() => setShown(true)} />
<Modal title="Modal title" size="md" show={shown} onHidden={() => setShown(false)}>
Modal goes here.
</Modal>
</>
)
}
function DropdownSample() {
return (
<>
<h2>Dropdown menu</h2>
<Dropdown text="Dropdown" hideToggleArrow>
<FormListItem icon="bx bx-cut">Cut</FormListItem>
<FormListItem icon="bx bx-copy">Copy</FormListItem>
<FormListItem icon="bx bx-paste">Paste</FormListItem>
<FormDropdownDivider />
<FormDropdownSubmenu title="Submenu">
<FormListItem>More items</FormListItem>
</FormDropdownSubmenu>
</Dropdown>
</>
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

View File

@@ -1,17 +0,0 @@
# CSS
## Inline styles
```jsx
<div style={{
display: "flex",
height: "53px",
width: "fit-content",
fontSize: "0.75em",
alignItems: "center",
flexShrink: 0
}}>/* [...] */</div>
```
## Custom CSS file
Simply create a <a class="reference-link" href="../../../Theme%20development/Custom%20app-wide%20CSS.md">Custom app-wide CSS</a>. Make sure the class names are unique enough to not intersect with other UI elements, consider adding a prefix (e.g. `x-mywidget-`).

View File

@@ -1,69 +0,0 @@
# Component libraries
Using the concept of <a class="reference-link" href="../../Common%20concepts/Script%20bundles.md">Script bundles</a>, it's possible to create components that are shared for multiple widgets or <a class="reference-link" href="../../../Note%20Types/Render%20Note.md">Render Note</a>.
## Exporting a single component
This is generally useful for big components.
Here's an example child hierarchy using <a class="reference-link" href="../../../Note%20Types/Render%20Note.md">Render Note</a>:
* _My render note_
Note type: Render Note
Link `~renderNote` to the child note (_Render note with subcomponents_)
* _Render note with subcomponents_
Type: JSX
```javascript
export default function() {
return (
<MyComponent />
);
}
```
* _MyComponent_
Type: Code / JSX
```javascript
export default function MyComponent() {
return <p>Hi</p>;
}
```
## Multiple components per note
To export multiple components, use the `export` keyword next to each of the function components.
Here's how a sub-note called `MyComponents` would look like:
```javascript
export function MyFirstComponent() {
return <p>First</p>;
}
export function MySecondComponent() {
return <p>Bar</p>;
}
```
Then in its parent note:
```javascript
const { MyFirstComponent, MySecondComponent } = MyComponents;
export default function() {
return (
<>
<MyFirstComponent />
<MySecondComponent />
</>
);
}
```
Alternatively, it's also possible to use the components directly without assigning them to a `const` first:
```javascript
<MyComponents.MyFirstComponent />
<MyComponents.MySecondComponent />
```

View File

@@ -1,43 +0,0 @@
# Hooks
## Standard Preact hooks
All standard Preact hooks are available as an import in `trilium:api`.
For example:
```
import { useState } from "trilium:preact";
const [ myState, setMyState ] = useState("Hi");
```
## Custom hooks
Trilium comes with a large set of custom hooks for Preact, all of which are also available to custom widgets and <a class="reference-link" href="../../../Note%20Types/Render%20Note.md">Render Note</a>.
### `useNoteContext`
As a replacement to <a class="reference-link" href="../Custom%20Widgets/Note%20context%20aware%20widget.md">Note context aware widget</a>, Preact exposes the current note context in the `useNoteContext` hook:
```
import { defineWidget, useNoteContext, useNoteProperty } from "trilium:preact";
export default defineWidget({
parent: "note-detail-pane",
position: 10,
render: () => {
const { note } = useNoteContext();
const title = useNoteProperty(note, "title");
return <span>Current note JSX: {title}</span>;
}
});
```
Note that the custom widget must be inside the content area (so note detail widget) for this to work properly, especially when dealing with splits.
### `useActiveNoteContext`
`useActiveNoteContext` is an alternative to `useNoteContext` which works even if the widget is not within the note detail section and it automatically switches the note context as the user is navigating around between tabs and splits.
### `useNoteProperty`
This hook allows “listening” for changes to a particular property of a `FNote`, such as the `title` or `type` of a note. The benefit from using the hook is that it actually reacts to changes, for example if the note title or type is changed.

68
pnpm-lock.yaml generated
View File

@@ -494,9 +494,6 @@ importers:
node-html-parser: node-html-parser:
specifier: 7.0.1 specifier: 7.0.1
version: 7.0.1 version: 7.0.1
sucrase:
specifier: 3.35.1
version: 3.35.1
devDependencies: devDependencies:
'@anthropic-ai/sdk': '@anthropic-ai/sdk':
specifier: 0.71.2 specifier: 0.71.2
@@ -6258,9 +6255,6 @@ packages:
any-base@1.1.0: any-base@1.1.0:
resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==} resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==}
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
anymatch@3.1.3: anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -6957,10 +6951,6 @@ packages:
commander@2.20.3: commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
commander@5.1.0: commander@5.1.0:
resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@@ -10628,9 +10618,6 @@ packages:
resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==}
engines: {node: ^18.17.0 || >=20.5.0} engines: {node: ^18.17.0 || >=20.5.0}
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
nan@2.22.2: nan@2.22.2:
resolution: {integrity: sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==} resolution: {integrity: sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==}
@@ -11251,10 +11238,6 @@ packages:
resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
engines: {node: '>=6'} engines: {node: '>=6'}
pirates@4.0.7:
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
engines: {node: '>= 6'}
pixelmatch@5.3.0: pixelmatch@5.3.0:
resolution: {integrity: sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==} resolution: {integrity: sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==}
hasBin: true hasBin: true
@@ -13153,11 +13136,6 @@ packages:
stylis@4.3.6: stylis@4.3.6:
resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==}
sucrase@3.35.1:
resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==}
engines: {node: '>=16 || 14 >=14.17'}
hasBin: true
sugarss@4.0.1: sugarss@4.0.1:
resolution: {integrity: sha512-WCjS5NfuVJjkQzK10s8WOBY+hhDxxNt/N6ZaGwxFZ+wN3/lKKFSaaKUNecULcTTvE4urLcKaZFQD8vO0mOZujw==} resolution: {integrity: sha512-WCjS5NfuVJjkQzK10s8WOBY+hhDxxNt/N6ZaGwxFZ+wN3/lKKFSaaKUNecULcTTvE4urLcKaZFQD8vO0mOZujw==}
engines: {node: '>=12.0'} engines: {node: '>=12.0'}
@@ -13301,13 +13279,6 @@ packages:
text-decoder@1.2.3: text-decoder@1.2.3:
resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==}
thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
thingies@2.5.0: thingies@2.5.0:
resolution: {integrity: sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==} resolution: {integrity: sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==}
engines: {node: '>=10.18'} engines: {node: '>=10.18'}
@@ -13459,9 +13430,6 @@ packages:
resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
engines: {node: '>=6.10'} engines: {node: '>=6.10'}
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
ts-loader@9.5.4: ts-loader@9.5.4:
resolution: {integrity: sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==} resolution: {integrity: sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@@ -15322,6 +15290,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.3.0 '@ckeditor/ckeditor5-utils': 47.3.0
'@ckeditor/ckeditor5-watchdog': 47.3.0 '@ckeditor/ckeditor5-watchdog': 47.3.0
es-toolkit: 1.39.5 es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-dev-build-tools@54.2.2(@swc/helpers@0.5.17)(tslib@2.8.1)(typescript@5.9.3)': '@ckeditor/ckeditor5-dev-build-tools@54.2.2(@swc/helpers@0.5.17)(tslib@2.8.1)(typescript@5.9.3)':
dependencies: dependencies:
@@ -15827,6 +15797,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.3.0 '@ckeditor/ckeditor5-utils': 47.3.0
'@ckeditor/ckeditor5-widget': 47.3.0 '@ckeditor/ckeditor5-widget': 47.3.0
ckeditor5: 47.3.0 ckeditor5: 47.3.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-mention@47.3.0(patch_hash=5981fb59ba35829e4dff1d39cf771000f8a8fdfa7a34b51d8af9549541f2d62d)': '@ckeditor/ckeditor5-mention@47.3.0(patch_hash=5981fb59ba35829e4dff1d39cf771000f8a8fdfa7a34b51d8af9549541f2d62d)':
dependencies: dependencies:
@@ -21177,8 +21149,6 @@ snapshots:
any-base@1.1.0: {} any-base@1.1.0: {}
any-promise@1.3.0: {}
anymatch@3.1.3: anymatch@3.1.3:
dependencies: dependencies:
normalize-path: 3.0.0 normalize-path: 3.0.0
@@ -22127,8 +22097,6 @@ snapshots:
commander@2.20.3: {} commander@2.20.3: {}
commander@4.1.1: {}
commander@5.1.0: {} commander@5.1.0: {}
commander@6.2.0: {} commander@6.2.0: {}
@@ -26848,12 +26816,6 @@ snapshots:
mute-stream@2.0.0: mute-stream@2.0.0:
optional: true optional: true
mz@2.7.0:
dependencies:
any-promise: 1.3.0
object-assign: 4.1.1
thenify-all: 1.6.0
nan@2.22.2: nan@2.22.2:
optional: true optional: true
@@ -27511,8 +27473,6 @@ snapshots:
pify@4.0.1: pify@4.0.1:
optional: true optional: true
pirates@4.0.7: {}
pixelmatch@5.3.0: pixelmatch@5.3.0:
dependencies: dependencies:
pngjs: 6.0.0 pngjs: 6.0.0
@@ -29754,16 +29714,6 @@ snapshots:
stylis@4.3.6: {} stylis@4.3.6: {}
sucrase@3.35.1:
dependencies:
'@jridgewell/gen-mapping': 0.3.13
commander: 4.1.1
lines-and-columns: 1.2.4
mz: 2.7.0
pirates: 4.0.7
tinyglobby: 0.2.15
ts-interface-checker: 0.1.13
sugarss@4.0.1(postcss@8.5.6): sugarss@4.0.1(postcss@8.5.6):
dependencies: dependencies:
postcss: 8.5.6 postcss: 8.5.6
@@ -30000,14 +29950,6 @@ snapshots:
dependencies: dependencies:
b4a: 1.6.7 b4a: 1.6.7
thenify-all@1.6.0:
dependencies:
thenify: 3.3.1
thenify@3.3.1:
dependencies:
any-promise: 1.3.0
thingies@2.5.0(tslib@2.8.1): thingies@2.5.0(tslib@2.8.1):
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@@ -30143,8 +30085,6 @@ snapshots:
ts-dedent@2.2.0: {} ts-dedent@2.2.0: {}
ts-interface-checker@0.1.13: {}
ts-loader@9.5.4(typescript@5.0.4)(webpack@5.101.3(@swc/core@1.11.29(@swc/helpers@0.5.17))(esbuild@0.27.2)): ts-loader@9.5.4(typescript@5.0.4)(webpack@5.101.3(@swc/core@1.11.29(@swc/helpers@0.5.17))(esbuild@0.27.2)):
dependencies: dependencies:
chalk: 4.1.2 chalk: 4.1.2