Active content badges (#8716)

This commit is contained in:
Elian Doran
2026-02-15 14:34:04 +02:00
committed by GitHub
36 changed files with 1549 additions and 694 deletions

View File

@@ -700,6 +700,15 @@ export default class FNote {
return this.hasAttribute(LABEL, name);
}
/**
* Returns `true` if the note has a label with the given name (same as {@link hasOwnedLabel}), or it has a label with the `disabled:` prefix (for example due to a safe import).
* @param name the name of the label to look for.
* @returns `true` if the label exists, or its version with the `disabled:` prefix.
*/
hasLabelOrDisabled(name: string) {
return this.hasLabel(name) || this.hasLabel(`disabled:${name}`);
}
/**
* @param name - label name
* @returns true if label exists (including inherited) and does not have "false" value.

View File

@@ -210,6 +210,7 @@
--badge-share-background-color: #4d4d4d;
--badge-clipped-note-background-color: #295773;
--badge-execute-background-color: #604180;
--badge-active-content-background-color: rgb(12, 68, 70);
--note-icon-background-color: #444444;
--note-icon-color: #d4d4d4;
@@ -238,9 +239,9 @@
--bottom-panel-background-color: #11111180;
--bottom-panel-title-bar-background-color: #3F3F3F80;
--status-bar-border-color: var(--main-border-color);
--scrollbar-thumb-color: #fdfdfd5c;
--scrollbar-thumb-hover-color: #ffffff7d;
--scrollbar-background-color: transparent;
@@ -351,4 +352,4 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
.note-split.with-hue *::selection,
.quick-edit-dialog-wrapper.with-hue *::selection {
--selection-background-color: hsl(var(--custom-color-hue), 49.2%, 35%);
}
}

View File

@@ -202,6 +202,7 @@
--badge-share-background-color: #6b6b6b;
--badge-clipped-note-background-color: #2284c0;
--badge-execute-background-color: #7b47af;
--badge-active-content-background-color: rgb(27, 164, 168);
--note-icon-background-color: #4f4f4f;
--note-icon-color: white;
@@ -322,4 +323,4 @@
.note-split.with-hue *::selection,
.quick-edit-dialog-wrapper.with-hue *::selection {
--selection-background-color: hsl(var(--custom-color-hue), 60%, 90%);
}
}

View File

@@ -2288,5 +2288,29 @@
},
"bookmark_buttons": {
"bookmarks": "Bookmarks"
},
"active_content_badges": {
"type_icon_pack": "Icon pack",
"type_backend_script": "Backend script",
"type_frontend_script": "Frontend script",
"type_widget": "Widget",
"type_app_css": "Custom CSS",
"type_render_note": "Render note",
"type_web_view": "Web view",
"type_app_theme": "Custom theme",
"toggle_tooltip_enable_tooltip": "Click to enable this {{type}}.",
"toggle_tooltip_disable_tooltip": "Click to disable this {{type}}.",
"menu_docs": "Open documentation",
"menu_execute_now": "Execute script now",
"menu_run": "Run automatically",
"menu_run_disabled": "Manually",
"menu_run_backend_startup": "When the backend starts up",
"menu_run_hourly": "Hourly",
"menu_run_daily": "Daily",
"menu_run_frontend_startup": "When the desktop frontend starts up",
"menu_run_mobile_startup": "When the mobile frontend starts up",
"menu_change_to_widget": "Change to widget",
"menu_change_to_frontend_script": "Change to frontend script",
"menu_theme_base": "Theme base"
}
}

View File

@@ -11,6 +11,10 @@
}
}
body.mobile .geo-view > .collection-properties {
z-index: 2500;
}
.geo-map-container {
height: 100%;
overflow: hidden;

View File

@@ -0,0 +1,301 @@
import { BUILTIN_ATTRIBUTES } from "@triliumnext/commons";
import clsx from "clsx";
import { useEffect, useState } from "preact/hooks";
import FNote from "../../entities/fnote";
import attributes from "../../services/attributes";
import { t } from "../../services/i18n";
import { openInAppHelpFromUrl } from "../../services/utils";
import { BadgeWithDropdown } from "../react/Badge";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
import FormToggle from "../react/FormToggle";
import { useNoteContext, useTriliumEvent } from "../react/hooks";
import { BookProperty, ViewProperty } from "../react/NotePropertyMenu";
const NON_DANGEROUS_ACTIVE_CONTENT = [ "appCss", "appTheme" ];
const DANGEROUS_ATTRIBUTES = BUILTIN_ATTRIBUTES.filter(a => a.isDangerous || NON_DANGEROUS_ACTIVE_CONTENT.includes(a.name));
const activeContentLabels = [ "iconPack", "widget", "appCss", "appTheme" ] as const;
interface ActiveContentInfo {
type: "iconPack" | "backendScript" | "frontendScript" | "widget" | "appCss" | "renderNote" | "webView" | "appTheme";
isEnabled: boolean;
canToggleEnabled: boolean;
}
const executeOption: BookProperty = {
type: "button",
icon: "bx bx-play",
label: t("active_content_badges.menu_execute_now"),
onClick: context => context.triggerCommand("runActiveNote")
};
const typeMappings: Record<ActiveContentInfo["type"], {
title: string;
icon: string;
helpPage: string;
apiDocsPage?: string;
isExecutable?: boolean;
additionalOptions?: BookProperty[];
}> = {
iconPack: {
title: t("active_content_badges.type_icon_pack"),
icon: "bx bx-package",
helpPage: "g1mlRoU8CsqC",
},
backendScript: {
title: t("active_content_badges.type_backend_script"),
icon: "bx bx-server",
helpPage: "SPirpZypehBG",
apiDocsPage: "MEtfsqa5VwNi",
isExecutable: true,
additionalOptions: [
executeOption,
{
type: "combobox",
bindToLabel: "run",
label: t("active_content_badges.menu_run"),
icon: "bx bx-rss",
dropStart: true,
options: [
{ value: null, label: t("active_content_badges.menu_run_disabled") },
{ value: "backendStartup", label: t("active_content_badges.menu_run_backend_startup") },
{ value: "daily", label: t("active_content_badges.menu_run_daily") },
{ value: "hourly", label: t("active_content_badges.menu_run_hourly") }
]
}
]
},
frontendScript: {
title: t("active_content_badges.type_frontend_script"),
icon: "bx bx-window",
helpPage: "yIhgI5H7A2Sm",
apiDocsPage: "Q2z6av6JZVWm",
isExecutable: true,
additionalOptions: [
executeOption,
{
type: "combobox",
bindToLabel: "run",
label: t("active_content_badges.menu_run"),
icon: "bx bx-rss",
dropStart: true,
options: [
{ value: null, label: t("active_content_badges.menu_run_disabled") },
{ value: "frontendStartup", label: t("active_content_badges.menu_run_frontend_startup") },
{ value: "mobileStartup", label: t("active_content_badges.menu_run_mobile_startup") },
]
},
{ type: "separator" },
{
type: "button",
label: t("active_content_badges.menu_change_to_widget"),
icon: "bx bxs-widget",
onClick: ({ note }) => attributes.setLabel(note.noteId, "widget")
}
]
},
widget: {
title: t("active_content_badges.type_widget"),
icon: "bx bxs-widget",
helpPage: "MgibgPcfeuGz",
additionalOptions: [
{
type: "button",
label: t("active_content_badges.menu_change_to_frontend_script"),
icon: "bx bx-window",
onClick: ({ note }) => {
attributes.removeOwnedLabelByName(note, "widget");
attributes.removeOwnedLabelByName(note, "disabled:widget");
}
}
]
},
appCss: {
title: t("active_content_badges.type_app_css"),
icon: "bx bxs-file-css",
helpPage: "AlhDUqhENtH7"
},
renderNote: {
title: t("active_content_badges.type_render_note"),
icon: "bx bx-extension",
helpPage: "HcABDtFCkbFN"
},
webView: {
title: t("active_content_badges.type_web_view"),
icon: "bx bx-globe",
helpPage: "1vHRoWCEjj0L"
},
appTheme: {
title :t("active_content_badges.type_app_theme"),
icon: "bx bx-palette",
helpPage: "7NfNr5pZpVKV",
additionalOptions: [
{
type: "combobox",
bindToLabel: "appThemeBase",
label: t("active_content_badges.menu_theme_base"),
icon: "bx bx-layer",
dropStart: true,
options: [
{ label: t("theme.auto_theme"), value: null },
{ type: "separator" },
{ label: t("theme.triliumnext"), value: "next" },
{ label: t("theme.triliumnext-light"), value: "next-light" },
{ label: t("theme.triliumnext-dark"), value: "next-dark" }
]
}
]
}
};
export function ActiveContentBadges() {
const { note } = useNoteContext();
const info = useActiveContentInfo(note);
return (note && info &&
<>
{info.canToggleEnabled && <ActiveContentToggle info={info} note={note} />}
<ActiveContentBadge info={info} note={note} />
</>
);
}
function ActiveContentBadge({ info, note }: { note: FNote, info: ActiveContentInfo }) {
const { title, icon, helpPage, apiDocsPage, additionalOptions } = typeMappings[info.type];
return (
<BadgeWithDropdown
className={clsx("active-content-badge", info.canToggleEnabled && !info.isEnabled && "disabled")}
icon={icon}
text={title}
dropdownOptions={{
dropdownContainerClassName: "mobile-bottom-menu",
mobileBackdrop: true
}}
>
{additionalOptions?.length && (
<>
{additionalOptions?.map((property, i) => (
<ViewProperty key={i} note={note} property={property} />
))}
<FormDropdownDivider />
</>
)}
<FormListItem
icon="bx bx-help-circle"
onClick={() => openInAppHelpFromUrl(helpPage)}
>{t("active_content_badges.menu_docs")}</FormListItem>
{apiDocsPage && <FormListItem
icon="bx bx-book-content"
onClick={() => openInAppHelpFromUrl(apiDocsPage)}
>{t("code_buttons.trilium_api_docs_button_title")}</FormListItem>}
</BadgeWithDropdown>
);
}
function ActiveContentToggle({ note, info }: { note: FNote, info: ActiveContentInfo }) {
const { title } = typeMappings[info.type];
return info && <FormToggle
switchOnName="" switchOffName=""
currentValue={info.isEnabled}
switchOffTooltip={t("active_content_badges.toggle_tooltip_disable_tooltip", { type: title })}
switchOnTooltip={t("active_content_badges.toggle_tooltip_enable_tooltip", { type: title })}
onChange={async (willEnable) => {
const attrs = note.getOwnedAttributes()
.filter(attr => {
if (attr.isInheritable) return false;
const baseName = getNameWithoutPrefix(attr.name);
return DANGEROUS_ATTRIBUTES.some(item => item.name === baseName && item.type === attr.type);
});
for (const attr of attrs) {
const baseName = getNameWithoutPrefix(attr.name);
const newName = willEnable ? baseName : `disabled:${baseName}`;
if (newName === attr.name) continue;
// We are adding and removing afterwards to avoid a flicker (because for a moment there would be no active content attribute anymore) because the operations are done in sequence and not atomically.
if (attr.type === "label") {
await attributes.setLabel(note.noteId, newName, attr.value);
} else {
await attributes.setRelation(note.noteId, newName, attr.value);
}
await attributes.removeAttributeById(note.noteId, attr.attributeId);
}
}}
/>;
}
function getNameWithoutPrefix(name: string) {
return name.startsWith("disabled:") ? name.substring(9) : name;
}
function useActiveContentInfo(note: FNote | null | undefined) {
const [ info, setInfo ] = useState<ActiveContentInfo | null>(null);
function refresh() {
let type: ActiveContentInfo["type"] | null = null;
let isEnabled = false;
let canToggleEnabled = false;
if (!note) {
setInfo(null);
return;
}
if (note.type === "render") {
type = "renderNote";
isEnabled = note.hasRelation("renderNote");
canToggleEnabled = note.hasRelation("renderNote") || note.hasRelation("disabled:renderNote");
} else if (note.type === "webView") {
type = "webView";
isEnabled = note.hasLabel("webViewSrc");
canToggleEnabled = note.hasLabelOrDisabled("webViewSrc");
} else if (note.type === "code" && note.mime === "application/javascript;env=backend") {
type = "backendScript";
for (const backendLabel of [ "run", "customRequestHandler", "customResourceProvider" ]) {
isEnabled ||= note.hasLabel(backendLabel);
if (!canToggleEnabled && note.hasLabelOrDisabled(backendLabel)) {
canToggleEnabled = true;
}
}
} else if (note.type === "code" && note.mime === "application/javascript;env=frontend") {
type = "frontendScript";
isEnabled = note.hasLabel("widget") || note.hasLabel("run");
canToggleEnabled = note.hasLabelOrDisabled("widget") || note.hasLabelOrDisabled("run");
} else if (note.type === "code" && note.hasLabelOrDisabled("appTheme")) {
isEnabled = note.hasLabel("appTheme");
canToggleEnabled = true;
}
for (const labelToCheck of activeContentLabels) {
if (note.hasLabel(labelToCheck)) {
type = labelToCheck;
break;
} else if (note.hasLabel(`disabled:${labelToCheck}`)) {
type = labelToCheck;
isEnabled = false;
break;
}
}
if (type) {
setInfo({ type, isEnabled, canToggleEnabled });
} else {
setInfo(null);
}
}
// Refresh on note change.
useEffect(refresh, [ note ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
refresh();
}
});
return info;
}

View File

@@ -37,6 +37,10 @@
pointer-events: none;
}
}
&.active-content-badge { --color: var(--badge-active-content-background-color); }
&.active-content-badge.disabled {
opacity: 0.5;
}
min-width: 0;
@@ -45,6 +49,11 @@
text-overflow: ellipsis;
min-width: 0;
}
.switch-button {
--switch-track-height: 8px;
--switch-track-width: 30px;
}
}
.dropdown-badge {

View File

@@ -10,6 +10,7 @@ import { FormDropdownDivider, FormListItem } from "../react/FormList";
import { useGetContextData, useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean } from "../react/hooks";
import { useShareState } from "../ribbon/BasicPropertiesTab";
import { useShareInfo } from "../shared_info";
import { ActiveContentBadges } from "./ActiveContentBadges";
export default function NoteBadges() {
return (
@@ -19,6 +20,7 @@ export default function NoteBadges() {
<ShareBadge />
<ClippedNoteBadge />
<ExecuteBadge />
<ActiveContentBadges />
</div>
);
}

View File

@@ -2,18 +2,16 @@ import "./CollectionProperties.css";
import { t } from "i18next";
import { ComponentChildren } from "preact";
import { useContext, useRef } from "preact/hooks";
import { Fragment } from "preact/jsx-runtime";
import { useRef } from "preact/hooks";
import FNote from "../../entities/fnote";
import { ViewTypeOptions } from "../collections/interface";
import Dropdown from "../react/Dropdown";
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList";
import FormTextBox from "../react/FormTextBox";
import { useNoteLabel, useNoteLabelBoolean, useNoteLabelWithDefault, useNoteProperty, useTriliumEvent } from "../react/hooks";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
import { useNoteProperty, useTriliumEvent } from "../react/hooks";
import Icon from "../react/Icon";
import { ParentComponent } from "../react/react_utils";
import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../ribbon/collection-properties-config";
import { CheckBoxProperty, ViewProperty } from "../react/NotePropertyMenu";
import { bookPropertiesConfig } from "../ribbon/collection-properties-config";
import { useViewType, VIEW_TYPE_MAPPINGS } from "../ribbon/CollectionPropertiesTab";
export const ICON_MAPPINGS: Record<ViewTypeOptions, string> = {
@@ -85,9 +83,11 @@ function ViewOptions({ note, viewType }: { note: FNote, viewType: ViewTypeOption
<Dropdown
buttonClassName="bx bx-cog icon-action"
hideToggleArrow
dropdownContainerClassName="mobile-bottom-menu"
mobileBackdrop
>
{properties.map(property => (
<ViewProperty key={property.label} note={note} property={property} />
{properties.map((property, index) => (
<ViewProperty key={index} note={note} property={property} />
))}
{properties.length > 0 && <FormDropdownDivider />}
@@ -107,127 +107,3 @@ function ViewOptions({ note, viewType }: { note: FNote, viewType: ViewTypeOption
</Dropdown>
);
}
function ViewProperty({ note, property }: { note: FNote, property: BookProperty }) {
switch (property.type) {
case "button":
return <ButtonPropertyView note={note} property={property} />;
case "split-button":
return <SplitButtonPropertyView note={note} property={property} />;
case "checkbox":
return <CheckBoxPropertyView note={note} property={property} />;
case "number":
return <NumberPropertyView note={note} property={property} />;
case "combobox":
return <ComboBoxPropertyView note={note} property={property} />;
}
}
function ButtonPropertyView({ note, property }: { note: FNote, property: ButtonProperty }) {
const parentComponent = useContext(ParentComponent);
return (
<FormListItem
icon={property.icon}
title={property.title}
onClick={() => {
if (!parentComponent) return;
property.onClick({
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
});
}}
>{property.label}</FormListItem>
);
}
function SplitButtonPropertyView({ note, property }: { note: FNote, property: SplitButtonProperty }) {
const parentComponent = useContext(ParentComponent);
const ItemsComponent = property.items;
const clickContext = parentComponent && {
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
};
return (parentComponent &&
<FormDropdownSubmenu
icon={property.icon ?? "bx bx-empty"}
title={property.label}
onDropdownToggleClicked={() => clickContext && property.onClick(clickContext)}
>
<ItemsComponent note={note} parentComponent={parentComponent} />
</FormDropdownSubmenu>
);
}
function NumberPropertyView({ note, property }: { note: FNote, property: NumberProperty }) {
//@ts-expect-error Interop with text box which takes in string values even for numbers.
const [ value, setValue ] = useNoteLabel(note, property.bindToLabel);
const disabled = property.disabled?.(note);
return (
<FormListItem
icon={property.icon}
disabled={disabled}
onClick={(e) => e.stopPropagation()}
>
{property.label}
<FormTextBox
type="number"
currentValue={value ?? ""} onChange={setValue}
style={{ width: (property.width ?? 100) }}
min={property.min ?? 0}
disabled={disabled}
/>
</FormListItem>
);
}
function ComboBoxPropertyView({ note, property }: { note: FNote, property: ComboBoxProperty }) {
const [ value, setValue ] = useNoteLabelWithDefault(note, property.bindToLabel, property.defaultValue ?? "");
function renderItem(option: ComboBoxItem) {
return (
<FormListItem
key={option.value}
checked={value === option.value}
onClick={() => setValue(option.value)}
>
{option.label}
</FormListItem>
);
}
return (
<FormDropdownSubmenu
title={property.label}
icon={property.icon ?? "bx bx-empty"}
>
{(property.options).map((option, index) => {
if ("items" in option) {
return (
<Fragment key={option.title}>
<FormListItem key={option.title} disabled>{option.title}</FormListItem>
{option.items.map(renderItem)}
{index < property.options.length - 1 && <FormDropdownDivider />}
</Fragment>
);
}
return renderItem(option);
})}
</FormDropdownSubmenu>
);
}
function CheckBoxPropertyView({ note, property }: { note: FNote, property: CheckBoxProperty }) {
const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel);
return (
<FormListToggleableItem
icon={property.icon}
title={property.label}
currentValue={value}
onChange={setValue}
/>
);
}

View File

@@ -0,0 +1,210 @@
import { FilterLabelsByType } from "@triliumnext/commons";
import { Fragment, VNode } from "preact";
import { useContext } from "preact/hooks";
import Component from "../../components/component";
import FNote from "../../entities/fnote";
import NoteContextAwareWidget from "../note_context_aware_widget";
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "./FormList";
import FormTextBox from "./FormTextBox";
import { useNoteLabel, useNoteLabelBoolean, useNoteLabelWithDefault } from "./hooks";
import { ParentComponent } from "./react_utils";
export interface ClickContext {
note: FNote;
triggerCommand: NoteContextAwareWidget["triggerCommand"];
}
export interface CheckBoxProperty {
type: "checkbox",
label: string;
bindToLabel: FilterLabelsByType<boolean>;
icon?: string;
}
export interface ButtonProperty {
type: "button",
label: string;
title?: string;
icon?: string;
onClick(context: ClickContext): void;
}
export interface SplitButtonProperty extends Omit<ButtonProperty, "type"> {
type: "split-button";
items({ note, parentComponent }: { note: FNote, parentComponent: Component }): VNode;
}
export interface NumberProperty {
type: "number",
label: string;
bindToLabel: FilterLabelsByType<number>;
width?: number;
min?: number;
icon?: string;
disabled?: (note: FNote) => boolean;
}
export interface ComboBoxItem {
/**
* The value to set to the bound label, `null` has a special meaning which removes the label entirely.
*/
value: string | null;
label: string;
}
export interface ComboBoxGroup {
title: string;
items: ComboBoxItem[];
}
interface Separator {
type: "separator"
}
export interface ComboBoxProperty {
type: "combobox",
label: string;
icon?: string;
bindToLabel: FilterLabelsByType<string>;
/**
* The default value is used when the label is not set.
*/
defaultValue?: string;
options: (ComboBoxItem | Separator | ComboBoxGroup)[];
dropStart?: boolean;
}
export type BookProperty = CheckBoxProperty | ButtonProperty | NumberProperty | ComboBoxProperty | SplitButtonProperty | Separator;
export function ViewProperty({ note, property }: { note: FNote, property: BookProperty }) {
switch (property.type) {
case "button":
return <ButtonPropertyView note={note} property={property} />;
case "split-button":
return <SplitButtonPropertyView note={note} property={property} />;
case "checkbox":
return <CheckBoxPropertyView note={note} property={property} />;
case "number":
return <NumberPropertyView note={note} property={property} />;
case "combobox":
return <ComboBoxPropertyView note={note} property={property} />;
case "separator":
return <FormDropdownDivider />;
}
}
function ButtonPropertyView({ note, property }: { note: FNote, property: ButtonProperty }) {
const parentComponent = useContext(ParentComponent);
return (
<FormListItem
icon={property.icon}
title={property.title}
onClick={() => {
if (!parentComponent) return;
property.onClick({
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
});
}}
>{property.label}</FormListItem>
);
}
function SplitButtonPropertyView({ note, property }: { note: FNote, property: SplitButtonProperty }) {
const parentComponent = useContext(ParentComponent);
const ItemsComponent = property.items;
const clickContext = parentComponent && {
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
};
return (parentComponent &&
<FormDropdownSubmenu
icon={property.icon ?? "bx bx-empty"}
title={property.label}
onDropdownToggleClicked={() => clickContext && property.onClick(clickContext)}
>
<ItemsComponent note={note} parentComponent={parentComponent} />
</FormDropdownSubmenu>
);
}
function NumberPropertyView({ note, property }: { note: FNote, property: NumberProperty }) {
//@ts-expect-error Interop with text box which takes in string values even for numbers.
const [ value, setValue ] = useNoteLabel(note, property.bindToLabel);
const disabled = property.disabled?.(note);
return (
<FormListItem
icon={property.icon}
disabled={disabled}
onClick={(e) => e.stopPropagation()}
>
{property.label}
<FormTextBox
type="number"
currentValue={value ?? ""} onChange={setValue}
style={{ width: (property.width ?? 100) }}
min={property.min ?? 0}
disabled={disabled}
/>
</FormListItem>
);
}
function ComboBoxPropertyView({ note, property }: { note: FNote, property: ComboBoxProperty }) {
const [ value, setValue ] = useNoteLabel(note, property.bindToLabel);
const valueWithDefault = value ?? property.defaultValue ?? null;
function renderItem(option: ComboBoxItem) {
return (
<FormListItem
key={option.value}
checked={valueWithDefault === option.value}
onClick={() => setValue(option.value)}
>
{option.label}
</FormListItem>
);
}
return (
<FormDropdownSubmenu
title={property.label}
icon={property.icon ?? "bx bx-empty"}
dropStart={property.dropStart}
>
{(property.options).map((option, index) => {
if ("items" in option) {
return (
<Fragment key={option.title}>
<FormListItem key={option.title} disabled>{option.title}</FormListItem>
{option.items.map(renderItem)}
{index < property.options.length - 1 && <FormDropdownDivider />}
</Fragment>
);
}
if ("type" in option) {
return <FormDropdownDivider key={index} />;
}
return renderItem(option);
})}
</FormDropdownSubmenu>
);
}
function CheckBoxPropertyView({ note, property }: { note: FNote, property: CheckBoxProperty }) {
const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel);
return (
<FormListToggleableItem
icon={property.icon}
title={property.label}
currentValue={value}
onChange={setValue}
/>
);
}

View File

@@ -1,18 +1,20 @@
import { useContext, useMemo } from "preact/hooks";
import { t } from "../../services/i18n";
import FormSelect, { FormSelectWithGroups } from "../react/FormSelect";
import { TabContext } from "./ribbon-interface";
import { mapToKeyValueArray } from "../../services/utils";
import { useNoteLabel, useNoteLabelBoolean } from "../react/hooks";
import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "./collection-properties-config";
import Button, { SplitButton } from "../react/Button";
import { ParentComponent } from "../react/react_utils";
import FNote from "../../entities/fnote";
import FormCheckbox from "../react/FormCheckbox";
import FormTextBox from "../react/FormTextBox";
import { ComponentChildren } from "preact";
import { ViewTypeOptions } from "../collections/interface";
import { useContext, useMemo } from "preact/hooks";
import FNote from "../../entities/fnote";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
import { t } from "../../services/i18n";
import { mapToKeyValueArray } from "../../services/utils";
import { ViewTypeOptions } from "../collections/interface";
import Button, { SplitButton } from "../react/Button";
import FormCheckbox from "../react/FormCheckbox";
import FormSelect, { FormSelectWithGroups } from "../react/FormSelect";
import FormTextBox from "../react/FormTextBox";
import { useNoteLabel, useNoteLabelBoolean } from "../react/hooks";
import { BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxGroup, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../react/NotePropertyMenu";
import { ParentComponent } from "../react/react_utils";
import { bookPropertiesConfig } from "./collection-properties-config";
import { TabContext } from "./ribbon-interface";
export const VIEW_TYPE_MAPPINGS: Record<ViewTypeOptions, string> = {
grid: t("book_properties.grid"),
@@ -50,70 +52,70 @@ export function useViewType(note: FNote | null | undefined) {
}
function CollectionTypeSwitcher({ viewType, setViewType }: { viewType: string, setViewType: (newValue: string) => void }) {
const collectionTypes = useMemo(() => mapToKeyValueArray(VIEW_TYPE_MAPPINGS), []);
const collectionTypes = useMemo(() => mapToKeyValueArray(VIEW_TYPE_MAPPINGS), []);
return (
<div style={{ display: "flex", alignItems: "baseline" }}>
<span style={{ whiteSpace: "nowrap" }}>{t("book_properties.view_type")}:&nbsp; &nbsp;</span>
<FormSelect
currentValue={viewType ?? "grid"} onChange={setViewType}
values={collectionTypes}
keyProperty="key" titleProperty="value"
/>
</div>
)
return (
<div style={{ display: "flex", alignItems: "baseline" }}>
<span style={{ whiteSpace: "nowrap" }}>{t("book_properties.view_type")}:&nbsp; &nbsp;</span>
<FormSelect
currentValue={viewType ?? "grid"} onChange={setViewType}
values={collectionTypes}
keyProperty="key" titleProperty="value"
/>
</div>
);
}
function BookProperties({ viewType, note, properties }: { viewType: ViewTypeOptions, note: FNote, properties: BookProperty[] }) {
return (
<>
{properties.map(property => (
<div className={`type-${property}`}>
{mapPropertyView({ note, property })}
</div>
))}
function BookProperties({ note, properties }: { viewType: ViewTypeOptions, note: FNote, properties: BookProperty[] }) {
return (
<>
{properties.map((property, index) => (
<div key={index} className={`type-${property}`}>
{mapPropertyView({ note, property })}
</div>
))}
<CheckboxPropertyView
note={note} property={{
bindToLabel: "includeArchived",
label: t("book_properties.include_archived_notes"),
type: "checkbox"
}}
/>
</>
)
<CheckboxPropertyView
note={note} property={{
bindToLabel: "includeArchived",
label: t("book_properties.include_archived_notes"),
type: "checkbox"
}}
/>
</>
);
}
function mapPropertyView({ note, property }: { note: FNote, property: BookProperty }) {
switch (property.type) {
case "button":
return <ButtonPropertyView note={note} property={property} />
case "split-button":
return <SplitButtonPropertyView note={note} property={property} />
case "checkbox":
return <CheckboxPropertyView note={note} property={property} />
case "number":
return <NumberPropertyView note={note} property={property} />
case "combobox":
return <ComboBoxPropertyView note={note} property={property} />
}
switch (property.type) {
case "button":
return <ButtonPropertyView note={note} property={property} />;
case "split-button":
return <SplitButtonPropertyView note={note} property={property} />;
case "checkbox":
return <CheckboxPropertyView note={note} property={property} />;
case "number":
return <NumberPropertyView note={note} property={property} />;
case "combobox":
return <ComboBoxPropertyView note={note} property={property} />;
}
}
function ButtonPropertyView({ note, property }: { note: FNote, property: ButtonProperty }) {
const parentComponent = useContext(ParentComponent);
const parentComponent = useContext(ParentComponent);
return <Button
text={property.label}
title={property.title}
icon={property.icon}
onClick={() => {
if (!parentComponent) return;
property.onClick({
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
});
}}
/>
return <Button
text={property.label}
title={property.title}
icon={property.icon}
onClick={() => {
if (!parentComponent) return;
property.onClick({
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
});
}}
/>;
}
function SplitButtonPropertyView({ note, property }: { note: FNote, property: SplitButtonProperty }) {
@@ -131,18 +133,18 @@ function SplitButtonPropertyView({ note, property }: { note: FNote, property: Sp
onClick={() => clickContext && property.onClick(clickContext)}
>
{parentComponent && <ItemsComponent note={note} parentComponent={parentComponent} />}
</SplitButton>
</SplitButton>;
}
function CheckboxPropertyView({ note, property }: { note: FNote, property: CheckBoxProperty }) {
const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel);
const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel);
return (
<FormCheckbox
label={property.label}
currentValue={value} onChange={setValue}
/>
)
return (
<FormCheckbox
label={property.label}
currentValue={value} onChange={setValue}
/>
);
}
function NumberPropertyView({ note, property }: { note: FNote, property: NumberProperty }) {
@@ -160,7 +162,7 @@ function NumberPropertyView({ note, property }: { note: FNote, property: NumberP
disabled={disabled}
/>
</LabelledEntry>
)
);
}
function ComboBoxPropertyView({ note, property }: { note: FNote, property: ComboBoxProperty }) {
@@ -169,12 +171,12 @@ function ComboBoxPropertyView({ note, property }: { note: FNote, property: Combo
return (
<LabelledEntry label={property.label}>
<FormSelectWithGroups
values={property.options}
values={property.options.filter(i => !("type" in i)) as (ComboBoxItem | ComboBoxGroup)[]}
keyProperty="value" titleProperty="label"
currentValue={value ?? property.defaultValue} onChange={setValue}
/>
</LabelledEntry>
)
);
}
function LabelledEntry({ label, children }: { label: string, children: ComponentChildren }) {
@@ -186,5 +188,5 @@ function LabelledEntry({ label, children }: { label: string, children: Component
{children}
</label>
</>
)
);
}

View File

@@ -70,7 +70,6 @@ export default function NoteActionsCustom(props: NoteActionsCustomProps) {
>
<AddChildButton {...innerProps} />
<RunActiveNoteButton {...innerProps } />
<OpenTriliumApiDocsButton {...innerProps} />
<SwitchSplitOrientationButton {...innerProps} />
<ToggleReadOnlyButton {...innerProps} />
<SaveToNoteButton {...innerProps} />
@@ -230,15 +229,6 @@ function SaveToNoteButton({ note, noteMime }: NoteActionsCustomInnerProps) {
/>;
}
function OpenTriliumApiDocsButton({ noteMime }: NoteActionsCustomInnerProps) {
const isEnabled = noteMime.startsWith("application/javascript;env=");
return isEnabled && <NoteAction
icon="bx bx-help-circle"
text={t("code_buttons.trilium_api_docs_button_title")}
onClick={() => openInAppHelpFromUrl(noteMime.endsWith("frontend") ? "Q2z6av6JZVWm" : "MEtfsqa5VwNi")}
/>;
}
function InAppHelpButton({ note }: NoteActionsCustomInnerProps) {
const helpUrl = getHelpUrlForNote(note);
const isEnabled = !!helpUrl;

View File

@@ -1,79 +1,19 @@
import { t } from "i18next";
import Component from "../../components/component";
import FNote from "../../entities/fnote";
import attributes from "../../services/attributes";
import NoteContextAwareWidget from "../note_context_aware_widget";
import { DEFAULT_MAP_LAYER_NAME, MAP_LAYERS, type MapLayer } from "../collections/geomap/map_layer";
import { ViewTypeOptions } from "../collections/interface";
import { FilterLabelsByType } from "@triliumnext/commons";
import { DEFAULT_THEME, getPresentationThemes } from "../collections/presentation/themes";
import { VNode } from "preact";
import { useNoteLabel } from "../react/hooks";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
import Component from "../../components/component";
import { useNoteLabel } from "../react/hooks";
import { BookProperty, ClickContext, ComboBoxItem } from "../react/NotePropertyMenu";
interface BookConfig {
properties: BookProperty[];
}
export interface CheckBoxProperty {
type: "checkbox",
label: string;
bindToLabel: FilterLabelsByType<boolean>;
icon?: string;
}
export interface ButtonProperty {
type: "button",
label: string;
title?: string;
icon?: string;
onClick(context: BookContext): void;
}
export interface SplitButtonProperty extends Omit<ButtonProperty, "type"> {
type: "split-button";
items({ note, parentComponent }: { note: FNote, parentComponent: Component }): VNode;
}
export interface NumberProperty {
type: "number",
label: string;
bindToLabel: FilterLabelsByType<number>;
width?: number;
min?: number;
icon?: string;
disabled?: (note: FNote) => boolean;
}
export interface ComboBoxItem {
value: string;
label: string;
}
interface ComboBoxGroup {
title: string;
items: ComboBoxItem[];
}
export interface ComboBoxProperty {
type: "combobox",
label: string;
icon?: string;
bindToLabel: FilterLabelsByType<string>;
/**
* The default value is used when the label is not set.
*/
defaultValue?: string;
options: (ComboBoxItem | ComboBoxGroup)[];
}
export type BookProperty = CheckBoxProperty | ButtonProperty | NumberProperty | ComboBoxProperty | SplitButtonProperty;
interface BookContext {
note: FNote;
triggerCommand: NoteContextAwareWidget["triggerCommand"];
}
export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
grid: {
properties: []
@@ -211,7 +151,7 @@ function ListExpandDepth(context: { note: FNote, parentComponent: Component }) {
<FormDropdownDivider />
<ListExpandDepthButton label={t("book_properties.expand_all_levels")} depth="all" checked={currentDepth === "all"} {...context} />
</>
)
);
}
function ListExpandDepthButton({ label, depth, note, parentComponent, checked }: { label: string, depth: number | "all", note: FNote, parentComponent: Component, checked?: boolean }) {
@@ -226,7 +166,7 @@ function ListExpandDepthButton({ label, depth, note, parentComponent, checked }:
}
function buildExpandListHandler(depth: number | "all") {
return async ({ note, triggerCommand }: BookContext) => {
return async ({ note, triggerCommand }: ClickContext) => {
const { noteId } = note;
const existingValue = note.getLabelValue("expanded");
@@ -236,5 +176,5 @@ function buildExpandListHandler(depth: number | "all") {
await attributes.setLabel(noteId, "expanded", newValue);
triggerCommand("refreshNoteList", { noteId });
}
};
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,126 @@
<p><em>Active content</em> is a generic name for powerful features in Trilium,
these range from customizing the UI to advanced scripting that can alter
your notes or even your PC.</p>
<h2>Safe import</h2>
<p>Active content problem of safety, especially when this active content
comes from a third-party such as if it is downloaded from a website and
then imported into Trilium.</p>
<p>When <a href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/_help_mHbBMPDPkVV5">importing</a> .zip
archives into Trilium, <em>safe mode</em> is active by default which will
try to prevent untrusted code from executing. For example, a <a href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/yIhgI5H7A2Sm/_help_MgibgPcfeuGz">custom widget</a> needs
the <code spellcheck="false">#widget</code> <a href="#root/pOsGYCXsbNQG/tC7s2alapj8V/zEY4DaJG4YT5/_help_HI6GBBIduIgv">label</a> in
order to function; safe import works by renaming that label to <code spellcheck="false">#disabled:widget</code>.</p>
<h2>Safe mode</h2>
<p>Sometimes active content can cause issues with the UI or the server, preventing
it from functioning properly.&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/tC7s2alapj8V/_help_64ZTlUPgEPtW">Safe mode</a>&nbsp;allows
starting Trilium in such a way that active content is not loaded by default
at start-up, allowing the user to fix the problematic scripts or widgets.</p>
<h2>Types of active content</h2>
<p>These are the types of active content in Trilium, along with a few examples
of what untrusted content of that type could cause:</p>
<figure class="table"
style="width:100%;">
<table class="ck-table-resized">
<colgroup>
<col style="width:15.79%;">
<col style="width:10.35%;">
<col style="width:40.71%;">
<col style="width:33.15%;">
</colgroup>
<thead>
<tr>
<th>Name</th>
<th>Disabled on a safe <a href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/_help_mHbBMPDPkVV5">import</a>
</th>
<th>Description</th>
<th>Potential risks of untrusted code</th>
</tr>
</thead>
<tbody>
<tr>
<th><a href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/_help_yIhgI5H7A2Sm">Front-end scripts</a>
</th>
<td>Yes</td>
<td>Allow running arbitrary code on the client (UI) of Trilium, which can
alter the user interface.</td>
<td>A malicious script can execute server-side code, access un-encrypted notes
or change their contents.</td>
</tr>
<tr>
<th><a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/yIhgI5H7A2Sm/_help_MgibgPcfeuGz">Custom Widgets</a>
</th>
<td>Yes</td>
<td>Can add new UI features to Trilium, for example by adding a new section
in the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_RnaPdbciOfeq">Right Sidebar</a>.</td>
<td>The UI can be altered in such a way that it can be used to extract sensitive
information or it can simply cause the application to crash.</td>
</tr>
<tr>
<th><a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/_help_SPirpZypehBG">Backend scripts</a>
</th>
<td>Yes</td>
<td>Can run custom code on the server of Trilium (Node.js environment), with
full access to the notes and the database.</td>
<td>Has access to all the unencrypted notes, but with full access to the database
it can completely destroy the data. It also has access to execute other
applications or alter the files and folders on the server).</td>
</tr>
<tr>
<th><a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_1vHRoWCEjj0L">Web View</a>
</th>
<td>Yes</td>
<td>Displays a website inside a note.</td>
<td>Can point to a phishing website which can collect the data (for example
on a log in page).</td>
</tr>
<tr>
<th><a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_HcABDtFCkbFN">Render Note</a>
</th>
<td>Yes</td>
<td>Renders custom content inside a note, such as a dashboard or a new editor
that is not officially supported by Trilium.</td>
<td>Can affect the UI similar to front-end scripts or custom widgets since
the scripts are not completely encapsulated, or they can act similar to
a web view where they can collect data entered by the user.</td>
</tr>
<tr>
<th><a class="reference-link" href="#root/pOsGYCXsbNQG/pKK96zzmvBGf/_help_AlhDUqhENtH7">Custom app-wide CSS</a>
</th>
<td>No</td>
<td>Can alter the layout and style of the UI using CSS, applied regardless
of theme.</td>
<td>Generally less problematic than the rest of active content, but a badly
written CSS can affect the layout of the application, requiring the use
of&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/tC7s2alapj8V/_help_64ZTlUPgEPtW">Safe mode</a>&nbsp;to
be able to use the application.</td>
</tr>
<tr>
<th><a href="#root/pOsGYCXsbNQG/_help_pKK96zzmvBGf">Custom themes</a>
</th>
<td>No</td>
<td>Can change the style of the entire UI.</td>
<td>Similar to custom app-wide CSS.</td>
</tr>
<tr>
<th><a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Wy267RK4M69c/_help_gOKqSJgXLcIj">Icon Packs</a>
</th>
<td>No</td>
<td>Introduces new icons that can be used for notes.</td>
<td>Generally are more contained and less prone to cause issues, but they
can cause performance issues (for example if the icon pack has millions
of icons in it).</td>
</tr>
</tbody>
</table>
</figure>
<h2>Active content badge</h2>
<p>Starting with v0.102.0, on the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_IjZS7iK5EXtb">New Layout</a>&nbsp;a
badge will be displayed near the note title, indicating that an active
content is detected. Clicking the badge will reveal a menu with various
options related to that content type, for example to open the documentation
or to configure the execution of scripts.</p>
<p>For some active content types, such as backend scripts with custom triggering
conditions a toggle button will appear. This makes it possible to easily
disable scripts or widgets, but also to re-enable them if an import was
made with safe mode active.</p>
<p>&nbsp;</p>

View File

@@ -29,10 +29,9 @@
<li>Ideally, create a dedicated spot in your note tree where to place the
icon packs.</li>
<li>Right click the note where to put it and select <em>Import into note</em>.</li>
<li
>Uncheck <em>Safe import</em>.</li>
<li>Select <em>Import</em>.</li>
<li><a href="#root/_help_s8alTXmpFR61">Refresh the application</a>.</li>
<li>Uncheck <em>Safe import</em>.</li>
<li>Select <em>Import</em>.</li>
<li><a href="#root/_help_s8alTXmpFR61">Refresh the application</a>.</li>
</ol>
<aside class="admonition warning">
<p>Since <em>Safe import</em> is disabled, make sure you trust the source as

View File

@@ -2,17 +2,24 @@
<img style="aspect-ratio:601/216;" src="Render Note_image.png"
width="601" height="216">
</figure>
<p>Render Note is used in&nbsp;<a class="reference-link" href="#root/_help_CdNpE2pqjmI6">Scripting</a>.
It works by displaying the HTML of a&nbsp;<a class="reference-link" href="#root/_help_6f9hih2hXXZk">Code</a>&nbsp;note,
via an attribute.</p>
<p>Render Note is a special case of <a href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/_help_yIhgI5H7A2Sm">front-end scripting</a> which
allows rendering custom content inside a note. This makes it possible to
create custom dashboards, or to use a custom note editor.</p>
<p>The content can either be a vanilla HTML, or Preact JSX.</p>
<h2>Creating a render note</h2>
<ol>
<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
<code
spellcheck="false">&lt;p&gt;Hello world.&lt;/p&gt;</code>).</li>
with the:
<ol>
<li>HTML language for the legacy/vanilla method, with what needs to be displayed
(for example <code spellcheck="false">&lt;p&gt;Hello world.&lt;/p&gt;</code>).</li>
<li
>JSX for the Preact-based approach (see below).</li>
</ol>
</li>
<li>Create a&nbsp;<a class="reference-link" href="#root/_help_HcABDtFCkbFN">Render Note</a>.</li>
<li>Assign the <code spellcheck="false">renderNote</code> <a href="#root/_help_zEY4DaJG4YT5">relation</a> to
<li
>Assign the <code spellcheck="false">renderNote</code> <a href="#root/_help_zEY4DaJG4YT5">relation</a> to
point at the previously created code note.</li>
</ol>
<h2>Legacy scripting using jQuery</h2>
@@ -41,33 +48,29 @@ $dateEl.text(new Date());</code></pre>
need to provide a HTML anymore.</p>
<p>Here are the steps to creating a simple render note:</p>
<ol>
<li>
<p>Create a note of type&nbsp;<a class="reference-link" href="#root/_help_HcABDtFCkbFN">Render Note</a>.</p>
</li>
<li>
<li>Create a note of type&nbsp;<a class="reference-link" href="#root/_help_HcABDtFCkbFN">Render Note</a>.</li>
<li
>
<p>Create a child&nbsp;<a class="reference-link" href="#root/_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() {
<br>As an example, use the following content:</p><pre><code class="language-text-x-trilium-auto">export default function() {
return (
&lt;&gt;
&lt;p&gt;Hello world.&lt;/p&gt;
&lt;/&gt;
);
}</code></pre>
</li>
<li>
<p>In the parent render note, define a <code spellcheck="false">~renderNote</code> relation
pointing to the newly created child.</p>
</li>
<li>
<p>Refresh the render note and it should display a “Hello world” message.</p>
</li>
</li>
<li>In the parent render note, define a <code spellcheck="false">~renderNote</code> relation
pointing to the newly created child.</li>
<li>Refresh the render note and it should display a “Hello world” message.</li>
</ol>
<h2>Refreshing the note</h2>
<p>It's possible to refresh the note via:</p>
<ul>
<li>The corresponding button in&nbsp;<a class="reference-link" href="#root/_help_XpOYSgsLkTJy">Floating buttons</a>.</li>
<li>The “Render active note” <a href="#root/_help_A9Oc6YKKc65v">keyboard shortcut</a> (not
<li
>The “Render active note” <a href="#root/_help_A9Oc6YKKc65v">keyboard shortcut</a> (not
assigned by default).</li>
</ul>
<h2>Examples</h2>

View File

@@ -0,0 +1,29 @@
<p>Unlike <a href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/_help_yIhgI5H7A2Sm">front-end scripts</a> which
run on the client / browser-side, back-end scripts run directly on the
Node.js environment of the Trilium server.</p>
<p>Back-end scripts can be used both on a&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/Otzi9La2YAUX/_help_WOcw2SLH6tbX">Server Installation</a>&nbsp;(where
it will run on the device the server is running on), or on the&nbsp;
<a
class="reference-link" href="#root/pOsGYCXsbNQG/Otzi9La2YAUX/_help_poXkQfguuA0U">Desktop Installation</a>&nbsp;(where it will run on the PC).</p>
<h2>Advantages of backend scripts</h2>
<p>The benefit of backend scripts is that they can be pretty powerful, for
example to have access to the underlying system, for example it can read
files or execute processes.</p>
<p>However, the main benefit of backend scripts is that they have easier
access to the notes since the information about them is already loaded
in memory. Whereas on the client, notes have to be manually loaded first.</p>
<h2>Creating a backend script</h2>
<p>Create a new&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_6f9hih2hXXZk">Code</a>&nbsp;note
and select the language <em>JS backend</em>.</p>
<h2>Running backend scripts</h2>
<p>Backend scripts can be either run manually (via the Execute button on
the script page), or they can be triggered on certain events.</p>
<p>In addition, scripts can be run automatically when the server starts up,
on a fixed time interval or when a certain event occurs (such as an attribute
being modified). For more information, see the dedicated&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/SPirpZypehBG/_help_GPERMystNGTB">Events</a>&nbsp;page.</p>
<h2>Script API</h2>
<p>Trilium exposes a set of APIs that can be directly consumed by scripts,
under the <code spellcheck="false">api</code> object. For a reference of
this API, see&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/GLks18SNjxmC/_help_MEtfsqa5VwNi">Backend API</a>.</p>

View File

@@ -5,139 +5,139 @@
<p>Global events are attached to the script note via label. Simply create
e.g. "run" label with some of these values and script note will be executed
once the event occurs.</p>
<table>
<thead>
<tr>
<th>Label</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>run</code>
</td>
<td>
<p>Defines on which events script should run. Possible values are:</p>
<ul>
<li><code>frontendStartup</code> - when Trilium frontend starts up (or is refreshed),
but not on mobile.</li>
<li><code>mobileStartup</code> - when Trilium frontend starts up (or is refreshed),
on mobile.</li>
<li><code>backendStartup</code> - when Trilium backend starts up</li>
<li><code>hourly</code> - run once an hour. You can use additional label <code>runAtHour</code> to
specify at which hour, on the back-end.</li>
<li><code>daily</code> - run once a day, on the back-end</li>
</ul>
</td>
</tr>
<tr>
<td><code>runOnInstance</code>
</td>
<td>Specifies that the script should only run on a particular&nbsp;<a class="reference-link"
href="#root/_help_c5xB8m4g2IY6">Trilium instance</a>.</td>
</tr>
<tr>
<td><code>runAtHour</code>
</td>
<td>On which hour should this run. Should be used together with <code>#run=hourly</code>.
Can be defined multiple times for more runs during the day.</td>
</tr>
</tbody>
</table>
<figure class="table">
<table>
<thead>
<tr>
<th>Label</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code spellcheck="false">run</code>
</td>
<td>
<p>Defines on which events script should run. Possible values are:</p>
<ul>
<li><code spellcheck="false">backendStartup</code> - when Trilium backend starts
up</li>
<li><code spellcheck="false">hourly</code> - run once an hour. You can use
additional label <code spellcheck="false">runAtHour</code> to specify at
which hour, on the back-end.</li>
<li><code spellcheck="false">daily</code> - run once a day, on the back-end</li>
</ul>
</td>
</tr>
<tr>
<td><code spellcheck="false">runOnInstance</code>
</td>
<td>Specifies that the script should only run on a particular&nbsp;<a class="reference-link"
href="#root/_help_c5xB8m4g2IY6">Trilium instance</a>.</td>
</tr>
<tr>
<td><code spellcheck="false">runAtHour</code>
</td>
<td>On which hour should this run. Should be used together with <code spellcheck="false">#run=hourly</code>.
Can be defined multiple times for more runs during the day.</td>
</tr>
</tbody>
</table>
</figure>
<h2>Entity events</h2>
<p>Other events are bound to some entity, these are defined as <a href="#root/_help_zEY4DaJG4YT5">relations</a> -
meaning that script is triggered only if note has this script attached
to it through relations (or it can inherit it).</p>
<table>
<thead>
<tr>
<th>Relation</th>
<th>Trigger condition</th>
<th>Origin entity (see below)</th>
</tr>
</thead>
<tbody>
<tr>
<td><code spellcheck="false">runOnNoteCreation</code>
</td>
<td>executes when note is created on backend. Use this relation if you want
to run the script for all notes created under a specific subtree. In that
case, create it on the subtree root note and make it inheritable. A new
note created within the subtree (any depth) will trigger the script.</td>
<td>The <code spellcheck="false">BNote</code> that got created.</td>
</tr>
<tr>
<td><code spellcheck="false">runOnChildNoteCreation</code>
</td>
<td>executes when new note is created under the note where this relation is
defined</td>
<td>The <code spellcheck="false">BNote</code> of the child that got created.</td>
</tr>
<tr>
<td><code spellcheck="false">runOnNoteTitleChange</code>
</td>
<td>executes when note title is changed (includes note creation as well)</td>
<td>The <code spellcheck="false">BNote</code> of the note whose title got changed.</td>
</tr>
<tr>
<td><code spellcheck="false">runOnNoteContentChange</code>
</td>
<td>executes when note content is changed (includes note creation as well).</td>
<td>The <code spellcheck="false">BNote</code> of the note whose content got
changed.</td>
</tr>
<tr>
<td><code spellcheck="false">runOnNoteChange</code>
</td>
<td>executes when note is changed (includes note creation as well). Does not
include content changes</td>
<td>The <code spellcheck="false">BNote</code> of the note that got changed.</td>
</tr>
<tr>
<td><code spellcheck="false">runOnNoteDeletion</code>
</td>
<td>executes when note is being deleted</td>
<td>The <code spellcheck="false">BNote</code> of the note that got (soft) deleted.</td>
</tr>
<tr>
<td><code spellcheck="false">runOnBranchCreation</code>
</td>
<td>executes when a branch is created. Branch is a link between parent note
and child note and is created e.g. when cloning or moving note.</td>
<td>The <code spellcheck="false">BBranch</code> that got created.</td>
</tr>
<tr>
<td><code spellcheck="false">runOnBranchChange</code>
</td>
<td>executes when a branch is updated. (since v0.62)</td>
<td>The <code spellcheck="false">BBranch</code> that got changed.</td>
</tr>
<tr>
<td><code spellcheck="false">runOnBranchDeletion</code>
</td>
<td>executes when a branch is deleted. Branch is a link between parent note
and child note and is deleted e.g. when moving note (old branch/link is
deleted).</td>
<td>The <code spellcheck="false">BBranch</code> that got (soft) deleted.</td>
</tr>
<tr>
<td><code spellcheck="false">runOnAttributeCreation</code>
</td>
<td>executes when new attribute is created for the note which defines this
relation</td>
<td>The <code spellcheck="false">BAttribute</code> that got created.</td>
</tr>
<tr>
<td><code spellcheck="false">runOnAttributeChange</code>
</td>
<td>executes when the attribute is changed of a note which defines this relation.
This is triggered also when the attribute is deleted</td>
<td>The <code spellcheck="false">BAttribute</code> that got changed.</td>
</tr>
</tbody>
</table>
<figure class="table">
<table>
<thead>
<tr>
<th>Relation</th>
<th>Trigger condition</th>
<th>Origin entity (see below)</th>
</tr>
</thead>
<tbody>
<tr>
<td><code spellcheck="false">runOnNoteCreation</code>
</td>
<td>executes when note is created on backend. Use this relation if you want
to run the script for all notes created under a specific subtree. In that
case, create it on the subtree root note and make it inheritable. A new
note created within the subtree (any depth) will trigger the script.</td>
<td>The <code spellcheck="false">BNote</code> that got created.</td>
</tr>
<tr>
<td><code spellcheck="false">runOnChildNoteCreation</code>
</td>
<td>executes when new note is created under the note where this relation is
defined</td>
<td>The <code spellcheck="false">BNote</code> of the child that got created.</td>
</tr>
<tr>
<td><code spellcheck="false">runOnNoteTitleChange</code>
</td>
<td>executes when note title is changed (includes note creation as well)</td>
<td>The <code spellcheck="false">BNote</code> of the note whose title got changed.</td>
</tr>
<tr>
<td><code spellcheck="false">runOnNoteContentChange</code>
</td>
<td>executes when note content is changed (includes note creation as well).</td>
<td>The <code spellcheck="false">BNote</code> of the note whose content got
changed.</td>
</tr>
<tr>
<td><code spellcheck="false">runOnNoteChange</code>
</td>
<td>executes when note is changed (includes note creation as well). Does not
include content changes</td>
<td>The <code spellcheck="false">BNote</code> of the note that got changed.</td>
</tr>
<tr>
<td><code spellcheck="false">runOnNoteDeletion</code>
</td>
<td>executes when note is being deleted</td>
<td>The <code spellcheck="false">BNote</code> of the note that got (soft) deleted.</td>
</tr>
<tr>
<td><code spellcheck="false">runOnBranchCreation</code>
</td>
<td>executes when a branch is created. Branch is a link between parent note
and child note and is created e.g. when cloning or moving note.</td>
<td>The <code spellcheck="false">BBranch</code> that got created.</td>
</tr>
<tr>
<td><code spellcheck="false">runOnBranchChange</code>
</td>
<td>executes when a branch is updated. (since v0.62)</td>
<td>The <code spellcheck="false">BBranch</code> that got changed.</td>
</tr>
<tr>
<td><code spellcheck="false">runOnBranchDeletion</code>
</td>
<td>executes when a branch is deleted. Branch is a link between parent note
and child note and is deleted e.g. when moving note (old branch/link is
deleted).</td>
<td>The <code spellcheck="false">BBranch</code> that got (soft) deleted.</td>
</tr>
<tr>
<td><code spellcheck="false">runOnAttributeCreation</code>
</td>
<td>executes when new attribute is created for the note which defines this
relation</td>
<td>The <code spellcheck="false">BAttribute</code> that got created.</td>
</tr>
<tr>
<td><code spellcheck="false">runOnAttributeChange</code>
</td>
<td>executes when the attribute is changed of a note which defines this relation.
This is triggered also when the attribute is deleted</td>
<td>The <code spellcheck="false">BAttribute</code> that got changed.</td>
</tr>
</tbody>
</table>
</figure>
<h2>Origin entity</h2>
<p>When a script is run by an event such as the ones described above,
<code

View File

@@ -1,92 +1,77 @@
<h2>Frontend API</h2>
<p>The frontend api supports two styles, regular scripts that are run with
the current app and note context, and widgets that export an object to
Trilium to be used in the UI. In both cases, the frontend api of Trilium
is available to scripts running in the frontend context as global variable
<code
spellcheck="false">api</code>. The members and methods of the api can be seen on the <a href="#root/_help_GLks18SNjxmC">Script API</a> page.</p>
<p>Front-end scripts are custom JavaScript notes that are run on the client
(browser environment)</p>
<p>There are four flavors of front-end scripts:</p>
<figure class="table"
style="width:100%;">
<table class="ck-table-resized">
<colgroup>
<col style="width:21.82%;">
<col style="width:78.18%;">
</colgroup>
<tbody>
<tr>
<th>Regular scripts</th>
<td>These are run with the current app and note context. These can be run
either manually or automatically on start-up.</td>
</tr>
<tr>
<th><a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/yIhgI5H7A2Sm/_help_MgibgPcfeuGz">Custom Widgets</a>
</th>
<td>These can introduce new UI elements in various positions, such as near
the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a>,
content area or even the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_RnaPdbciOfeq">Right Sidebar</a>.</td>
</tr>
<tr>
<th><a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/yIhgI5H7A2Sm/_help_4Gn3psZKsfSm">Launch Bar Widgets</a>
</th>
<td>Similar to&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/yIhgI5H7A2Sm/_help_MgibgPcfeuGz">Custom Widgets</a>,
but dedicated to the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_xYmIYSP6wE3F">Launch Bar</a>.
These can simply introduce new buttons or graphical elements to the bar.</td>
</tr>
<tr>
<th><a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/_help_HcABDtFCkbFN">Render Note</a>
</th>
<td>This allows rendering custom content inside a note, using either HTML
or Preact JSX.</td>
</tr>
</tbody>
</table>
</figure>
<p>For more advanced behaviors that do not require a user interface (e.g.
batch modifying notes), see&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/_help_SPirpZypehBG">Backend scripts</a>.</p>
<h2>Scripts</h2>
<p>Scripts don't have any special requirements. They can be run at will using
the execute button in the UI or they can be configured to run at certain
times using <a href="#root/_help_zEY4DaJG4YT5">Attributes</a> on the note containing
the script.</p>
<h3>Global Events</h3>
<p>This attribute is called <code spellcheck="false">#run</code> and it can
have any of the following values:</p>
<p>Scripts don't have any special requirements. They can be run manually
using the <em>Execute</em> button on the code note or they can be run automatically;
to do so, set the <code spellcheck="false">run</code> <a href="#root/pOsGYCXsbNQG/tC7s2alapj8V/zEY4DaJG4YT5/_help_HI6GBBIduIgv">label</a> to
either:</p>
<ul>
<li><code spellcheck="false">frontendStartup</code> - executes on frontend
upon startup.</li>
<li><code spellcheck="false">mobileStartup</code> - executes on mobile frontend
upon startup.</li>
<li><code spellcheck="false">backendStartup</code> - executes on backend upon
startup.</li>
<li><code spellcheck="false">hourly</code> - executes once an hour on backend.</li>
<li><code spellcheck="false">daily</code> - executes once a day on backend.</li>
</ul>
<h3>Entity Events</h3>
<p>These events are triggered by certain <a href="#root/_help_zEY4DaJG4YT5">relations</a> to
other notes. Meaning that the script is triggered only if the note has
this script attached to it through relations (or it can inherit it).</p>
<ul>
<li><code spellcheck="false">runOnNoteCreation</code> - executes when note
is created on backend.</li>
<li><code spellcheck="false">runOnNoteTitleChange</code> - executes when note
title is changed (includes note creation as well).</li>
<li><code spellcheck="false">runOnNoteContentChange</code> - executes when
note content is changed (includes note creation as well).</li>
<li><code spellcheck="false">runOnNoteChange</code> - executes when note is
changed (includes note creation as well).</li>
<li><code spellcheck="false">runOnNoteDeletion</code> - executes when note
is being deleted.</li>
<li><code spellcheck="false">runOnBranchCreation</code> - executes when a branch
is created. Branch is a link between parent note and child note and is
created e.g. when cloning or moving note.</li>
<li><code spellcheck="false">runOnBranchDeletion</code> - executes when a branch
is delete. Branch is a link between parent note and child note and is deleted
e.g. when moving note (old branch/link is deleted).</li>
<li><code spellcheck="false">runOnChildNoteCreation</code> - executes when
new note is created under this note.</li>
<li><code spellcheck="false">runOnAttributeCreation</code> - executes when
new attribute is created under this note.</li>
<li><code spellcheck="false">runOnAttributeChange</code> - executes when attribute
is changed under this note.</li>
<li><code spellcheck="false">frontendStartup</code> - when Trilium frontend
starts up (or is refreshed), but not on mobile.</li>
<li><code spellcheck="false">mobileStartup</code> - when Trilium frontend starts
up (or is refreshed), on mobile.</li>
</ul>
<aside class="admonition note">
<p>Backend scripts have more powerful triggering conditions, for example
they can run automatically on a hourly or daily basis, but also on events
such as when a note is created or an attribute is modified. See the server-side&nbsp;
<a
class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/SPirpZypehBG/_help_GPERMystNGTB">Events</a>&nbsp;for more information.</p>
</aside>
<h2>Widgets</h2>
<p>Conversely to scripts, widgets do have some specific requirements in order
to work. A widget must:</p>
<p>Widgets require a certain format in order for Trilium to be able to integrate
them into the UI.</p>
<ul>
<li>Extend <a href="https://triliumnext.github.io/Notes/frontend_api/BasicWidget.html">BasicWidget</a> or
one of it's subclasses.</li>
<li>Create a new instance and assign it to <code spellcheck="false">module.exports</code>.</li>
<li>Define a <code spellcheck="false">parentWidget</code> member to determine
where it should be displayed.</li>
<li>Define a <code spellcheck="false">position</code> (integer) that determines
the location via sort order.</li>
<li>Have a <code spellcheck="false">#widget</code> attribute on the containing
note.</li>
<li>Create, render, and return your element in the render function.
<ul>
<li>For <a href="https://triliumnext.github.io/Notes/frontend_api/BasicWidget.html">BasicWidget</a> and
<a
href="https://triliumnext.github.io/Notes/frontend_api/NoteContextAwareWidget.html">NoteContextAwareWidget</a>you should create <code spellcheck="false">this.$widget</code> and
render it in <code spellcheck="false">doRender()</code>.</li>
<li>For <a href="https://triliumnext.github.io/Notes/frontend_api/RightPanelWidget.html">RightPanelWidget</a> the
<code
spellcheck="false">this.$widget</code>and <code spellcheck="false">doRender()</code> are already
handled and you should instead return the value in <code spellcheck="false">doRenderBody()</code>.</li>
</ul>
</li>
</ul>
<h3>parentWidget</h3>
<ul>
<li><code spellcheck="false">left-pane</code> - This renders the widget on
the left side of the screen where the note tree lives.</li>
<li><code spellcheck="false">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 spellcheck="false">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 spellcheck="false">right-pane</code> - This renders the widget to
the right of any opened notes.</li>
<li>For legacy widgets, the script note must export a <code spellcheck="false">BasicWidget</code> or
a derived one (see&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/yIhgI5H7A2Sm/MgibgPcfeuGz/_help_GhurYZjh8e1V">Note context aware widget</a>&nbsp;or&nbsp;
<a
class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/yIhgI5H7A2Sm/MgibgPcfeuGz/_help_M8IppdwVHSjG">Right pane widget</a>).</li>
<li>For Preact widgets, a built-in helper called <code spellcheck="false">defineWidget</code> needs
to be used.</li>
</ul>
<p>For more information, see&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/yIhgI5H7A2Sm/_help_MgibgPcfeuGz">Custom Widgets</a>.</p>
<h2>Script API</h2>
<p>The front-end API of Trilium is available to all scripts running in the
front-end context as global variable <code spellcheck="false">api</code>.
For a reference of the API, see&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/GLks18SNjxmC/_help_Q2z6av6JZVWm">Frontend API</a>.</p>
<h3>Tutorial</h3>
<p>For more information on building widgets, take a look at <a href="#root/_help_SynTBQiBsdYJ">Widget Basics</a>.</p>

View File

@@ -20,14 +20,15 @@
<h2>Creating a custom widget</h2>
<ol>
<li>Create a&nbsp;<a class="reference-link" href="#root/_help_6f9hih2hXXZk">Code</a>&nbsp;note.</li>
<li>Set the language to:
<li
>Set the language to:
<ol>
<li>JavaScript (frontend) for legacy widgets using jQuery.</li>
<li>JSX for Preact widgets. You might need to go to Options → Code to enable
the language first.</li>
</ol>
</li>
<li>Apply the <code spellcheck="false">#widget</code> <a href="#root/_help_HI6GBBIduIgv">label</a>.</li>
</li>
<li>Apply the <code spellcheck="false">#widget</code> <a href="#root/_help_HI6GBBIduIgv">label</a>.</li>
</ol>
<h2>Getting started with a simple example</h2>
<p>Let's start by creating a widget that shows a message near the content
@@ -61,76 +62,81 @@ export default defineWidget({
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>
<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>parentWidget</code>
</th>
<th>Description</th>
<th>Sample widget</th>
<th>Special requirements</th>
</tr>
</thead>
<tbody>
<tr>
<th><code>left-pane</code>
</th>
<td>Appears within the same pane that holds the&nbsp;<a class="reference-link"
href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>.</td>
<td>Same as above, with only a different <code>parentWidget</code>.</td>
<td>None.</td>
</tr>
<tr>
<th><code>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>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/_help_GhurYZjh8e1V">Note context aware widget</a>
</td>
<td>
<ul>
<li>The widget must export a <code>class</code> and not an instance of the class
(e.g. <code>no new</code>) because it needs to be multiplied for each note,
so that splits work correctly.</li>
<li>Since the <code>class</code> is exported instead of an instance, the <code>parentWidget</code> getter
must be <code>static</code>, otherwise the widget is ignored.</li>
</ul>
</td>
</tr>
<tr>
<th><code>right-pane</code>
</th>
<td>In the&nbsp;<a class="reference-link" href="#root/_help_RnaPdbciOfeq">Right Sidebar</a>,
as a dedicated section.</td>
<td><a class="reference-link" href="#root/_help_M8IppdwVHSjG">Right pane widget</a>
</td>
<td>
<ul>
<li>Although not mandatory, it's best to use a <code>RightPanelWidget</code> instead
of a <code>BasicWidget</code> or a <code>NoteContextAwareWidget</code>.</li>
</ul>
</td>
</tr>
</tbody>
<figure
class="table">
<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/_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/_help_GhurYZjh8e1V">Note context aware widget</a>
</td>
<td>
<ul>
<li>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
>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/_help_RnaPdbciOfeq">Right Sidebar</a>,
as a dedicated section.</td>
<td><a class="reference-link" href="#root/_help_M8IppdwVHSjG">Right pane widget</a>
</td>
<td>
<ul>
<li>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
@@ -143,4 +149,13 @@ class="ck-table-resized">
to the&nbsp;<a class="reference-link" href="#root/_help_xYmIYSP6wE3F">Launch Bar</a>.
See&nbsp;<a class="reference-link" href="#root/_help_4Gn3psZKsfSm">Launch Bar Widgets</a>&nbsp;for
more information.</p>
<h2>Custom position</h2>
<h2>Custom position</h2>
<p>The position of a custom widget is defined via a <code spellcheck="false">position</code> integer.</p>
<p>In legacy widgets:</p><pre><code class="language-text-x-trilium-auto">class MyWidget extends api.BasicWidget {
// [..
get position() { return 10; }
}</code></pre>
<p>In Preact widgets:</p><pre><code class="language-text-x-trilium-auto">export default defineWidget({
// [...]
position: 10
});</code></pre>

View File

@@ -5,25 +5,31 @@
<p>Unlike&nbsp;<a class="reference-link" href="#root/_help_MgibgPcfeuGz">Custom Widgets</a>,
the process of setting up a launch bar widget is slightly different:</p>
<ol>
<li>Create a Code note of type <em>JavaScript (front-end)</em>.
<li>Create a Code note of type <em>JavaScript (front-end)</em> or JSX (for Preact-based
widgets).
<ul>
<li>The script itself uses the same concepts as&nbsp;<a class="reference-link"
href="#root/_help_MgibgPcfeuGz">Custom Widgets</a>, including the use of a
<code
spellcheck="false">NoteContextAwareWidget</code>or a <code spellcheck="false">BasicWidget</code> (according
to needs).</li>
<li>As examples, see&nbsp;<a class="reference-link" href="#root/_help_IPArqVfDQ4We">Note Title Widget</a>&nbsp;and&nbsp;
<a
class="reference-link" href="#root/_help_gcI7RPbaNSh3">Analog Watch</a>.</li>
<li>As examples in both legacy and Preact format, see&nbsp;<a class="reference-link"
href="#root/_help_IPArqVfDQ4We">Note Title Widget</a>&nbsp;and&nbsp;<a class="reference-link"
href="#root/_help_gcI7RPbaNSh3">Analog Watch</a>.</li>
</ul>
</li>
<li>Don't set <code spellcheck="false">#widget</code>, as that attribute is
reserved for&nbsp;<a class="reference-link" href="#root/_help_MgibgPcfeuGz">Custom Widgets</a>.</li>
<li>In the&nbsp;<a class="reference-link" href="#root/_help_x3i7MxGccDuM">Global menu</a>,
<li
>In the&nbsp;<a class="reference-link" href="#root/_help_x3i7MxGccDuM">Global menu</a>,
select <em>Configure launchbar</em>.</li>
<li>In the <em>Visible Launchers</em> section, select <em>Add a custom widget</em>.</li>
<li>Give the newly created launcher a name (and optionally a name).</li>
<li>In the&nbsp;<a class="reference-link" href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>&nbsp;section,
modify the <em>widget</em> field to point to the newly created note.</li>
<li>Refresh the UI.</li>
<li>In the <em>Visible Launchers</em> section, select <em>Add a custom widget</em>.</li>
<li
>Give the newly created launcher a name (and optionally a name).</li>
<li
>In the&nbsp;<a class="reference-link" href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>&nbsp;section,
modify the <em>widget</em> field to point to the newly created note.</li>
<li
><a href="#root/pOsGYCXsbNQG/BgmBlOIl72jZ/_help_s8alTXmpFR61">Refresh</a> the
UI.</li>
</ol>

View File

@@ -1,11 +1,11 @@
import BUILTIN_ATTRIBUTES from "./builtin_attributes.js";
import { AnonymizedDbResponse, BUILTIN_ATTRIBUTES, DatabaseAnonymizeResponse } from "@triliumnext/commons";
import Database from "better-sqlite3";
import fs from "fs";
import path from "path";
import dataDir from "./data_dir.js";
import dateUtils from "./date_utils.js";
import Database from "better-sqlite3";
import sql from "./sql.js";
import path from "path";
import { AnonymizedDbResponse, DatabaseAnonymizeResponse } from "@triliumnext/commons";
function getFullAnonymizationScript() {
// we want to delete all non-builtin attributes because they can contain sensitive names and values
@@ -86,7 +86,7 @@ function getExistingAnonymizedDatabases() {
.readdirSync(dataDir.ANONYMIZED_DB_DIR)
.filter((fileName) => fileName.includes("anonymized"))
.map((fileName) => ({
fileName: fileName,
fileName,
filePath: path.resolve(dataDir.ANONYMIZED_DB_DIR, fileName)
})) satisfies AnonymizedDbResponse[];
}

View File

@@ -1,13 +1,11 @@
"use strict";
import { type AttributeRow, BUILTIN_ATTRIBUTES } from "@triliumnext/commons";
import searchService from "./search/services/search.js";
import sql from "./sql.js";
import becca from "../becca/becca.js";
import BAttribute from "../becca/entities/battribute.js";
import attributeFormatter from "./attribute_formatter.js";
import BUILTIN_ATTRIBUTES from "./builtin_attributes.js";
import type BNote from "../becca/entities/bnote.js";
import type { AttributeRow } from "@triliumnext/commons";
import attributeFormatter from "./attribute_formatter.js";
import searchService from "./search/services/search.js";
import sql from "./sql.js";
const ATTRIBUTE_TYPES = new Set(["label", "relation"]);
@@ -41,18 +39,18 @@ function getNoteWithLabel(name: string, value?: string): BNote | null {
function createLabel(noteId: string, name: string, value: string = "") {
return createAttribute({
noteId: noteId,
noteId,
type: "label",
name: name,
value: value
name,
value
});
}
function createRelation(noteId: string, name: string, targetNoteId: string) {
return createAttribute({
noteId: noteId,
noteId,
type: "relation",
name: name,
name,
value: targetNoteId
});
}

View File

@@ -1,5 +1,5 @@
# Documentation
There are multiple types of documentation for Trilium:<img class="image-style-align-right" src="api/images/vAGCdo7kgoVD/Documentation_image.png" width="205" height="162">
There are multiple types of documentation for Trilium:<img class="image-style-align-right" src="api/images/1gHwZjPsyRj8/Documentation_image.png" width="205" height="162">
* The _User Guide_ represents the user-facing documentation. This documentation can be browsed by users directly from within Trilium, by pressing <kbd>F1</kbd>.
* The _Developer's Guide_ represents a set of Markdown documents that present the internals of Trilium, for developers.

View File

@@ -6443,6 +6443,131 @@
"dataFileName": "3_Zen mode_image.png"
}
]
},
{
"isClone": false,
"noteId": "YzMcWlCVeW09",
"notePath": [
"pOsGYCXsbNQG",
"gh7bpGYxajRS",
"YzMcWlCVeW09"
],
"title": "Active content",
"notePosition": 110,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "label",
"name": "iconClass",
"value": "bx bxs-widget",
"isInheritable": false,
"position": 30
},
{
"type": "label",
"name": "shareAlias",
"value": "active-content",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "MgibgPcfeuGz",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
"value": "1vHRoWCEjj0L",
"isInheritable": false,
"position": 70
},
{
"type": "relation",
"name": "internalLink",
"value": "yIhgI5H7A2Sm",
"isInheritable": false,
"position": 100
},
{
"type": "relation",
"name": "internalLink",
"value": "HcABDtFCkbFN",
"isInheritable": false,
"position": 110
},
{
"type": "relation",
"name": "internalLink",
"value": "RnaPdbciOfeq",
"isInheritable": false,
"position": 120
},
{
"type": "relation",
"name": "internalLink",
"value": "SPirpZypehBG",
"isInheritable": false,
"position": 130
},
{
"type": "relation",
"name": "internalLink",
"value": "mHbBMPDPkVV5",
"isInheritable": false,
"position": 150
},
{
"type": "relation",
"name": "internalLink",
"value": "AlhDUqhENtH7",
"isInheritable": false,
"position": 160
},
{
"type": "relation",
"name": "internalLink",
"value": "pKK96zzmvBGf",
"isInheritable": false,
"position": 170
},
{
"type": "relation",
"name": "internalLink",
"value": "gOKqSJgXLcIj",
"isInheritable": false,
"position": 180
},
{
"type": "relation",
"name": "internalLink",
"value": "64ZTlUPgEPtW",
"isInheritable": false,
"position": 190
},
{
"type": "relation",
"name": "internalLink",
"value": "HI6GBBIduIgv",
"isInheritable": false,
"position": 200
},
{
"type": "relation",
"name": "internalLink",
"value": "IjZS7iK5EXtb",
"isInheritable": false,
"position": 210
}
],
"format": "markdown",
"dataFileName": "Active content.md",
"attachments": []
}
]
},
@@ -9884,6 +10009,13 @@
"value": "render-note",
"isInheritable": false,
"position": 70
},
{
"type": "relation",
"name": "internalLink",
"value": "yIhgI5H7A2Sm",
"isInheritable": false,
"position": 90
}
],
"format": "markdown",
@@ -16113,20 +16245,6 @@
"type": "text",
"mime": "text/markdown",
"attributes": [
{
"type": "relation",
"name": "internalLink",
"value": "GLks18SNjxmC",
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "zEY4DaJG4YT5",
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
@@ -16147,6 +16265,90 @@
"value": "bx bx-window",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "Q2z6av6JZVWm",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
"value": "GhurYZjh8e1V",
"isInheritable": false,
"position": 60
},
{
"type": "relation",
"name": "internalLink",
"value": "M8IppdwVHSjG",
"isInheritable": false,
"position": 70
},
{
"type": "relation",
"name": "internalLink",
"value": "MgibgPcfeuGz",
"isInheritable": false,
"position": 80
},
{
"type": "relation",
"name": "internalLink",
"value": "SPirpZypehBG",
"isInheritable": false,
"position": 90
},
{
"type": "relation",
"name": "internalLink",
"value": "RnaPdbciOfeq",
"isInheritable": false,
"position": 100
},
{
"type": "relation",
"name": "internalLink",
"value": "oPVyFC7WL2Lp",
"isInheritable": false,
"position": 110
},
{
"type": "relation",
"name": "internalLink",
"value": "4Gn3psZKsfSm",
"isInheritable": false,
"position": 120
},
{
"type": "relation",
"name": "internalLink",
"value": "xYmIYSP6wE3F",
"isInheritable": false,
"position": 130
},
{
"type": "relation",
"name": "internalLink",
"value": "HcABDtFCkbFN",
"isInheritable": false,
"position": 140
},
{
"type": "relation",
"name": "internalLink",
"value": "HI6GBBIduIgv",
"isInheritable": false,
"position": 150
},
{
"type": "relation",
"name": "internalLink",
"value": "GPERMystNGTB",
"isInheritable": false,
"position": 160
}
],
"format": "markdown",
@@ -16791,6 +16993,13 @@
"value": "launch-bar-widgets",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "s8alTXmpFR61",
"isInheritable": false,
"position": 70
}
],
"format": "markdown",
@@ -17248,9 +17457,52 @@
"value": "bx bx-server",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "yIhgI5H7A2Sm",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
"value": "WOcw2SLH6tbX",
"isInheritable": false,
"position": 60
},
{
"type": "relation",
"name": "internalLink",
"value": "poXkQfguuA0U",
"isInheritable": false,
"position": 70
},
{
"type": "relation",
"name": "internalLink",
"value": "MEtfsqa5VwNi",
"isInheritable": false,
"position": 80
},
{
"type": "relation",
"name": "internalLink",
"value": "6f9hih2hXXZk",
"isInheritable": false,
"position": 90
},
{
"type": "relation",
"name": "internalLink",
"value": "GPERMystNGTB",
"isInheritable": false,
"position": 100
}
],
"format": "markdown",
"dataFileName": "Backend scripts.md",
"attachments": [],
"dirFileName": "Backend scripts",
"children": [

View File

@@ -0,0 +1,33 @@
# Active content
_Active content_ is a generic name for powerful features in Trilium, these range from customizing the UI to advanced scripting that can alter your notes or even your PC.
## Safe import
Active content problem of safety, especially when this active content comes from a third-party such as if it is downloaded from a website and then imported into Trilium.
When [importing](Import%20%26%20Export.md) .zip archives into Trilium, _safe mode_ is active by default which will try to prevent untrusted code from executing. For example, a [custom widget](../Scripting/Frontend%20Basics/Custom%20Widgets.md) needs the `#widget` [label](../Advanced%20Usage/Attributes/Labels.md) in order to function; safe import works by renaming that label to `#disabled:widget`.
## Safe mode
Sometimes active content can cause issues with the UI or the server, preventing it from functioning properly. <a class="reference-link" href="../Advanced%20Usage/Safe%20mode.md">Safe mode</a> allows starting Trilium in such a way that active content is not loaded by default at start-up, allowing the user to fix the problematic scripts or widgets.
## Types of active content
These are the types of active content in Trilium, along with a few examples of what untrusted content of that type could cause:
| Name | Disabled on a safe [import](Import%20%26%20Export.md) | Description | Potential risks of untrusted code |
| --- | --- | --- | --- |
| [Front-end scripts](../Scripting/Frontend%20Basics.md) | Yes | Allow running arbitrary code on the client (UI) of Trilium, which can alter the user interface. | A malicious script can execute server-side code, access un-encrypted notes or change their contents. |
| <a class="reference-link" href="../Scripting/Frontend%20Basics/Custom%20Widgets.md">Custom Widgets</a> | Yes | Can add new UI features to Trilium, for example by adding a new section in the <a class="reference-link" href="UI%20Elements/Right%20Sidebar.md">Right Sidebar</a>. | The UI can be altered in such a way that it can be used to extract sensitive information or it can simply cause the application to crash. |
| <a class="reference-link" href="../Scripting/Backend%20scripts.md">Backend scripts</a> | Yes | Can run custom code on the server of Trilium (Node.js environment), with full access to the notes and the database. | Has access to all the unencrypted notes, but with full access to the database it can completely destroy the data. It also has access to execute other applications or alter the files and folders on the server). |
| <a class="reference-link" href="../Note%20Types/Web%20View.md">Web View</a> | Yes | Displays a website inside a note. | Can point to a phishing website which can collect the data (for example on a log in page). |
| <a class="reference-link" href="../Note%20Types/Render%20Note.md">Render Note</a> | Yes | Renders custom content inside a note, such as a dashboard or a new editor that is not officially supported by Trilium. | Can affect the UI similar to front-end scripts or custom widgets since the scripts are not completely encapsulated, or they can act similar to a web view where they can collect data entered by the user. |
| <a class="reference-link" href="../Theme%20development/Custom%20app-wide%20CSS.md">Custom app-wide CSS</a> | No | Can alter the layout and style of the UI using CSS, applied regardless of theme. | Generally less problematic than the rest of active content, but a badly written CSS can affect the layout of the application, requiring the use of <a class="reference-link" href="../Advanced%20Usage/Safe%20mode.md">Safe mode</a> to be able to use the application. |
| [Custom themes](../Theme%20development) | No | Can change the style of the entire UI. | Similar to custom app-wide CSS. |
| <a class="reference-link" href="Themes/Icon%20Packs.md">Icon Packs</a> | No | Introduces new icons that can be used for notes. | Generally are more contained and less prone to cause issues, but they can cause performance issues (for example if the icon pack has millions of icons in it). |
## Active content badge
Starting with v0.102.0, on the <a class="reference-link" href="UI%20Elements/New%20Layout.md">New Layout</a> a badge will be displayed near the note title, indicating that an active content is detected. Clicking the badge will reveal a menu with various options related to that content type, for example to open the documentation or to configure the execution of scripts.
For some active content types, such as backend scripts with custom triggering conditions a toggle button will appear. This makes it possible to easily disable scripts or widgets, but also to re-enable them if an import was made with safe mode active.

View File

@@ -1,11 +1,15 @@
# Render Note
<figure class="image"><img style="aspect-ratio:601/216;" src="Render Note_image.png" width="601" height="216"></figure>
Render Note is used in <a class="reference-link" href="../Scripting.md">Scripting</a>. It works by displaying the HTML of a <a class="reference-link" href="Code.md">Code</a> note, via an attribute.
Render Note is a special case of [front-end scripting](../Scripting/Frontend%20Basics.md) which allows rendering custom content inside a note. This makes it possible to create custom dashboards, or to use a custom note editor.
The content can either be a vanilla HTML, or Preact JSX.
## Creating a render note
1. Create a <a class="reference-link" href="Code.md">Code</a> note with the HTML language, with what needs to be displayed (for example `<p>Hello world.</p>`).
1. Create a <a class="reference-link" href="Code.md">Code</a> note with the:
1. HTML language for the legacy/vanilla method, with what needs to be displayed (for example `<p>Hello world.</p>`).
2. JSX for the Preact-based approach (see below).
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.
@@ -44,7 +48,7 @@ Here are the steps to creating a simple render note:
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 (
<>

View File

@@ -0,0 +1,24 @@
# Backend scripts
Unlike [front-end scripts](Frontend%20Basics.md) which run on the client / browser-side, back-end scripts run directly on the Node.js environment of the Trilium server.
Back-end scripts can be used both on a <a class="reference-link" href="../Installation%20%26%20Setup/Server%20Installation.md">Server Installation</a> (where it will run on the device the server is running on), or on the <a class="reference-link" href="../Installation%20%26%20Setup/Desktop%20Installation.md">Desktop Installation</a> (where it will run on the PC).
## Advantages of backend scripts
The benefit of backend scripts is that they can be pretty powerful, for example to have access to the underlying system, for example it can read files or execute processes.
However, the main benefit of backend scripts is that they have easier access to the notes since the information about them is already loaded in memory. Whereas on the client, notes have to be manually loaded first.
## Creating a backend script
Create a new <a class="reference-link" href="../Note%20Types/Code.md">Code</a> note and select the language _JS backend_.
## Running backend scripts
Backend scripts can be either run manually (via the Execute button on the script page), or they can be triggered on certain events.
In addition, scripts can be run automatically when the server starts up, on a fixed time interval or when a certain event occurs (such as an attribute being modified). For more information, see the dedicated <a class="reference-link" href="Backend%20scripts/Events.md">Events</a> page.
## Script API
Trilium exposes a set of APIs that can be directly consumed by scripts, under the `api` object. For a reference of this API, see <a class="reference-link" href="Script%20API/Backend%20API.dat">Backend API</a>.

View File

@@ -5,7 +5,7 @@
Global events are attached to the script note via label. Simply create e.g. "run" label with some of these values and script note will be executed once the event occurs.
<table><thead><tr><th>Label</th><th>Description</th></tr></thead><tbody><tr><td><code>run</code></td><td><p>Defines on which events script should run. Possible values are:</p><ul><li data-list-item-id="e244b14e102cf1b0d4954e8fd455ea77b"><code>frontendStartup</code> - when Trilium frontend starts up (or is refreshed), but not on mobile.</li><li data-list-item-id="ea8f8ca86e7b351dd86108848ccb9103a"><code>mobileStartup</code> - when Trilium frontend starts up (or is refreshed), on mobile.</li><li data-list-item-id="e658488cf1a0862603088ef384e41b8b6"><code>backendStartup</code> - when Trilium backend starts up</li><li data-list-item-id="ef40ba992fc450d33a18ca4cb031eca66"><code>hourly</code> - run once an hour. You can use additional label <code>runAtHour</code> to specify at which hour, on the back-end.</li><li data-list-item-id="e07458d4f55b6eb42468a5535b8425c5f"><code>daily</code> - run once a day, on the back-end</li></ul></td></tr><tr><td><code>runOnInstance</code></td><td>Specifies that the script should only run on a particular&nbsp;<a class="reference-link" href="../../Advanced%20Usage/Configuration%20(config.ini%20or%20environment%20variables)/Trilium%20instance.md">Trilium instance</a>.</td></tr><tr><td><code>runAtHour</code></td><td>On which hour should this run. Should be used together with <code>#run=hourly</code>. Can be defined multiple times for more runs during the day.</td></tr></tbody></table>
<table><thead><tr><th>Label</th><th>Description</th></tr></thead><tbody><tr><td><code spellcheck="false">run</code></td><td><p>Defines on which events script should run. Possible values are:</p><ul><li data-list-item-id="e658488cf1a0862603088ef384e41b8b6"><code spellcheck="false">backendStartup</code> - when Trilium backend starts up</li><li data-list-item-id="ef40ba992fc450d33a18ca4cb031eca66"><code spellcheck="false">hourly</code> - run once an hour. You can use additional label <code spellcheck="false">runAtHour</code> to specify at which hour, on the back-end.</li><li data-list-item-id="e07458d4f55b6eb42468a5535b8425c5f"><code spellcheck="false">daily</code> - run once a day, on the back-end</li></ul></td></tr><tr><td><code spellcheck="false">runOnInstance</code></td><td>Specifies that the script should only run on a particular&nbsp;<a class="reference-link" href="../../Advanced%20Usage/Configuration%20(config.ini%20or%20environment%20variables)/Trilium%20instance.md">Trilium instance</a>.</td></tr><tr><td><code spellcheck="false">runAtHour</code></td><td>On which hour should this run. Should be used together with <code spellcheck="false">#run=hourly</code>. Can be defined multiple times for more runs during the day.</td></tr></tbody></table>
## Entity events

View File

@@ -1,56 +1,39 @@
# Frontend Basics
## Frontend API
Front-end scripts are custom JavaScript notes that are run on the client (browser environment)
The frontend api supports two styles, regular scripts that are run with the current app and note context, and widgets that export an object to Trilium to be used in the UI. In both cases, the frontend api of Trilium is available to scripts running in the frontend context as global variable `api`. The members and methods of the api can be seen on the [Script API](Script%20API.md) page.
There are four flavors of front-end scripts:
| | |
| --- | --- |
| Regular scripts | These are run with the current app and note context. These can be run either manually or automatically on start-up. |
| <a class="reference-link" href="Frontend%20Basics/Custom%20Widgets.md">Custom Widgets</a> | These can introduce new UI elements in various positions, such as near the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Note%20Tree.md">Note Tree</a>, content area or even the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Right%20Sidebar.md">Right Sidebar</a>. |
| <a class="reference-link" href="Frontend%20Basics/Launch%20Bar%20Widgets.md">Launch Bar Widgets</a> | Similar to <a class="reference-link" href="Frontend%20Basics/Custom%20Widgets.md">Custom Widgets</a>, but dedicated to the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Launch%20Bar.md">Launch Bar</a>. These can simply introduce new buttons or graphical elements to the bar. |
| <a class="reference-link" href="../Note%20Types/Render%20Note.md">Render Note</a> | This allows rendering custom content inside a note, using either HTML or Preact JSX. |
For more advanced behaviors that do not require a user interface (e.g. batch modifying notes), see <a class="reference-link" href="Backend%20scripts.md">Backend scripts</a>.
## Scripts
Scripts don't have any special requirements. They can be run at will using the execute button in the UI or they can be configured to run at certain times using [Attributes](../Advanced%20Usage/Attributes.md) on the note containing the script.
Scripts don't have any special requirements. They can be run manually using the _Execute_ button on the code note or they can be run automatically; to do so, set the `run` [label](../Advanced%20Usage/Attributes/Labels.md) to either:
### Global Events
* `frontendStartup` - when Trilium frontend starts up (or is refreshed), but not on mobile.
* `mobileStartup` - when Trilium frontend starts up (or is refreshed), on mobile.
This attribute is called `#run` and it can have any of the following values:
* `frontendStartup` - executes on frontend upon startup.
* `mobileStartup` - executes on mobile frontend upon startup.
* `backendStartup` - executes on backend upon startup.
* `hourly` - executes once an hour on backend.
* `daily` - executes once a day on backend.
### Entity Events
These events are triggered by certain [relations](../Advanced%20Usage/Attributes.md) to other notes. Meaning that the script is triggered only if the note has this script attached to it through relations (or it can inherit it).
* `runOnNoteCreation` - executes when note is created on backend.
* `runOnNoteTitleChange` - executes when note title is changed (includes note creation as well).
* `runOnNoteContentChange` - executes when note content is changed (includes note creation as well).
* `runOnNoteChange` - executes when note is changed (includes note creation as well).
* `runOnNoteDeletion` - executes when note is being deleted.
* `runOnBranchCreation` - executes when a branch is created. Branch is a link between parent note and child note and is created e.g. when cloning or moving note.
* `runOnBranchDeletion` - executes when a branch is delete. Branch is a link between parent note and child note and is deleted e.g. when moving note (old branch/link is deleted).
* `runOnChildNoteCreation` - executes when new note is created under this note.
* `runOnAttributeCreation` - executes when new attribute is created under this note.
* `runOnAttributeChange` - executes when attribute is changed under this note.
> [!NOTE]
> Backend scripts have more powerful triggering conditions, for example they can run automatically on a hourly or daily basis, but also on events such as when a note is created or an attribute is modified. See the server-side <a class="reference-link" href="Backend%20scripts/Events.md">Events</a> for more information.
## Widgets
Conversely to scripts, widgets do have some specific requirements in order to work. A widget must:
Widgets require a certain format in order for Trilium to be able to integrate them into the UI.
* Extend [BasicWidget](https://triliumnext.github.io/Notes/frontend_api/BasicWidget.html) or one of it's subclasses.
* Create a new instance and assign it to `module.exports`.
* Define a `parentWidget` member to determine where it should be displayed.
* Define a `position` (integer) that determines the location via sort order.
* Have a `#widget` attribute on the containing note.
* Create, render, and return your element in the render function.
* For [BasicWidget](https://triliumnext.github.io/Notes/frontend_api/BasicWidget.html) and [NoteContextAwareWidget](https://triliumnext.github.io/Notes/frontend_api/NoteContextAwareWidget.html)you should create `this.$widget` and render it in `doRender()`.
* For [RightPanelWidget](https://triliumnext.github.io/Notes/frontend_api/RightPanelWidget.html) the `this.$widget` and `doRender()` are already handled and you should instead return the value in `doRenderBody()`.
* For legacy widgets, the script note must export a `BasicWidget` or a derived one (see <a class="reference-link" href="Frontend%20Basics/Custom%20Widgets/Note%20context%20aware%20widget.md">Note context aware widget</a> or <a class="reference-link" href="Frontend%20Basics/Custom%20Widgets/Right%20pane%20widget.md">Right pane widget</a>).
* For Preact widgets, a built-in helper called `defineWidget` needs to be used.
### parentWidget
For more information, see <a class="reference-link" href="Frontend%20Basics/Custom%20Widgets.md">Custom Widgets</a>.
* `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.
## Script API
The front-end API of Trilium is available to all scripts running in the front-end context as global variable `api`. For a reference of the API, see <a class="reference-link" href="Script%20API/Frontend%20API">Frontend API</a>.
### Tutorial

View File

@@ -63,7 +63,7 @@ export default defineWidget({
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>parentWidget</code></th><th>Description</th><th>Sample widget</th><th>Special requirements</th></tr></thead><tbody><tr><th><code>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>parentWidget</code>.</td><td>None.</td></tr><tr><th><code>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>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>class</code> and not an instance of the class (e.g. <code>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>class</code> is exported instead of an instance, the <code>parentWidget</code> getter must be <code>static</code>, otherwise the widget is ignored.</li></ul></td></tr><tr><th><code>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>RightPanelWidget</code> instead of a <code>BasicWidget</code> or a <code>NoteContextAwareWidget</code>.</li></ul></td></tr></tbody></table>
<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).
@@ -71,4 +71,24 @@ To position the widget somewhere else, just change the value passed to `get pare
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
## Custom position
The position of a custom widget is defined via a `position` integer.
In legacy widgets:
```
class MyWidget extends api.BasicWidget {
// [..
get position() { return 10; }
}
```
In Preact widgets:
```
export default defineWidget({
// [...]
position: 10
});
```

View File

@@ -5,12 +5,12 @@ Launch bar widgets are a subset of <a class="reference-link" href="Custom%20Wid
Unlike <a class="reference-link" href="Custom%20Widgets.md">Custom Widgets</a>, the process of setting up a launch bar widget is slightly different:
1. Create a Code note of type _JavaScript (front-end)_.
1. Create a Code note of type _JavaScript (front-end)_ or JSX (for Preact-based widgets).
* The script itself uses the same concepts as <a class="reference-link" href="Custom%20Widgets.md">Custom Widgets</a>, including the use of a `NoteContextAwareWidget` or a `BasicWidget` (according to needs).
* As examples, see <a class="reference-link" href="Launch%20Bar%20Widgets/Note%20Title%20Widget.md">Note Title Widget</a> and <a class="reference-link" href="Launch%20Bar%20Widgets/Analog%20Watch.md">Analog Watch</a>.
* As examples in both legacy and Preact format, see <a class="reference-link" href="Launch%20Bar%20Widgets/Note%20Title%20Widget.md">Note Title Widget</a> and <a class="reference-link" href="Launch%20Bar%20Widgets/Analog%20Watch.md">Analog Watch</a>.
2. Don't set `#widget`, as that attribute is reserved for <a class="reference-link" href="Custom%20Widgets.md">Custom Widgets</a>.
3. In the <a class="reference-link" href="../../Basic%20Concepts%20and%20Features/UI%20Elements/Global%20menu.md">Global menu</a>, select _Configure launchbar_.
4. In the _Visible Launchers_ section, select _Add a custom widget_.
5. Give the newly created launcher a name (and optionally a name).
6. In the <a class="reference-link" href="../../Advanced%20Usage/Attributes/Promoted%20Attributes.md">Promoted Attributes</a> section, modify the _widget_ field to point to the newly created note.
7. Refresh the UI.
7. [Refresh](../../Troubleshooting/Refreshing%20the%20application.md) the UI.

View File

@@ -13,3 +13,4 @@ export * from "./lib/attribute_names.js";
export * from "./lib/utils.js";
export * from "./lib/dayjs.js";
export * from "./lib/notes.js";
export { default as BUILTIN_ATTRIBUTES } from "./lib/builtin_attributes.js";

View File

@@ -22,6 +22,11 @@ type Labels = {
pageUrl: string;
dateNote: string;
// Scripting
run: string;
widget: boolean;
"disabled:widget": boolean;
// Tree specific
subtreeHidden: boolean;
@@ -59,6 +64,9 @@ type Labels = {
readOnly: boolean;
mapType: string;
mapRootNoteId: string;
appTheme: string;
appThemeBase: string;
}
/**