Merge branch 'TriliumNext:main' into main

This commit is contained in:
Lucas 2025-11-22 15:53:36 -08:00 committed by GitHub
commit 0bcc02dfab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 946 additions and 757 deletions

View File

@ -40,7 +40,7 @@
"@types/express": "5.0.5",
"@types/node": "24.10.1",
"@types/yargs": "17.0.35",
"@vitest/coverage-v8": "4.0.12",
"@vitest/coverage-v8": "4.0.13",
"eslint": "9.39.1",
"eslint-plugin-simple-import-sort": "12.1.1",
"esm": "3.2.25",

View File

@ -445,6 +445,7 @@ type EventMappings = {
error: string;
};
searchRefreshed: { ntxId?: string | null };
textEditorRefreshed: { ntxId?: string | null, editor: CKTextEditor };
hoistedNoteChanged: {
noteId: string;
ntxId: string | null;

View File

@ -44,6 +44,7 @@ import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
import utils from "../services/utils.js";
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
import NoteDetail from "../widgets/NoteDetail.jsx";
import RightPanelWidget from "../widgets/sidebar/RightPanelWidget.jsx";
export default class DesktopLayout {

View File

@ -22,16 +22,8 @@ import RevisionsDialog from "../widgets/dialogs/revisions.js";
import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js";
import InfoDialog from "../widgets/dialogs/info.js";
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";
import PopupEditorDialog from "../widgets/dialogs/popup_editor.js";
import FlexContainer from "../widgets/containers/flex_container.js";
import NoteIconWidget from "../widgets/note_icon";
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
import CallToActionDialog from "../widgets/dialogs/call_to_action.jsx";
import NoteTitleWidget from "../widgets/note_title.jsx";
import FormattingToolbar from "../widgets/ribbon/FormattingToolbar.js";
import NoteList from "../widgets/collections/NoteList.jsx";
import NoteDetail from "../widgets/NoteDetail.jsx";
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
import PopupEditorDialog from "../widgets/dialogs/PopupEditor.jsx";
export function applyModals(rootContainer: RootContainer) {
rootContainer
@ -57,16 +49,6 @@ export function applyModals(rootContainer: RootContainer) {
.child(<ConfirmDialog />)
.child(<PromptDialog />)
.child(<IncorrectCpuArchDialog />)
.child(new PopupEditorDialog()
.child(new FlexContainer("row")
.class("title-row")
.css("align-items", "center")
.cssBlock(".title-row > * { margin: 5px; }")
.child(<NoteIconWidget />)
.child(<NoteTitleWidget />))
.child(<StandaloneRibbonAdapter component={FormattingToolbar} />)
.child(new PromotedAttributesWidget())
.child(<NoteDetail />)
.child(<NoteList media="screen" displayOnlyCollections />))
.child(<PopupEditorDialog />)
.child(<CallToActionDialog />);
}

View File

@ -165,16 +165,19 @@ class ContextMenu {
let $group = $parent; // The current group or parent element to which items are being appended
let shouldStartNewGroup = false; // If true, the next item will start a new group
let shouldResetGroup = false; // If true, the next item will be the last one from the group
let prevItemKind: string = "";
for (let index = 0; index < items.length; index++) {
const item = items[index];
const itemKind = ("kind" in item) ? item.kind : "";
if (!item) {
continue;
}
// If the current item is a header, start a new group. This group will contain the
// header and the next item that follows the header.
if ("kind" in item && item.kind === "header") {
if (itemKind === "header") {
if (multicolumn && !shouldResetGroup) {
shouldStartNewGroup = true;
}
@ -200,19 +203,23 @@ class ContextMenu {
shouldStartNewGroup = false;
}
if ("kind" in item && item.kind === "separator") {
if (itemKind === "separator") {
if (prevItemKind === "separator") {
// Skip consecutive separators
continue;
}
$group.append($("<div>").addClass("dropdown-divider"));
shouldResetGroup = true; // End the group after the next item
} else if ("kind" in item && item.kind === "header") {
$group.append($("<h6>").addClass("dropdown-header").text(item.title));
} else if (itemKind === "header") {
$group.append($("<h6>").addClass("dropdown-header").text((item as MenuHeader).title));
shouldResetGroup = true;
} else {
if ("kind" in item && item.kind === "custom") {
if (itemKind === "custom") {
// Custom menu item
$group.append(this.createCustomMenuItem(item));
$group.append(this.createCustomMenuItem(item as CustomMenuItem));
} else {
// Standard menu item
$group.append(this.createMenuItem(item));
$group.append(this.createMenuItem(item as MenuCommandItem<any>));
}
// After adding a menu item, if the previous item was a separator or header,
@ -222,6 +229,9 @@ class ContextMenu {
shouldResetGroup = false;
};
}
prevItemKind = itemKind;
}
}

View File

@ -244,16 +244,12 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
{ kind: "separator"},
{
(notOptionsOrHelp && selectedNotes.length === 1) ? {
kind: "custom",
componentFn: () => {
if (notOptionsOrHelp && selectedNotes.length === 1) {
return NoteColorPicker({note});
} else {
return null;
}
}
},
} : null,
{ kind: "separator" },

View File

@ -28,7 +28,7 @@ async function getActionsForScope(scope: string) {
return actions.filter((action) => action.scope === scope);
}
async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, component: Component) {
async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, component: Component, ntxId: string | null | undefined) {
if (!$el[0]) return [];
const actions = await getActionsForScope(scope);
@ -36,7 +36,9 @@ async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, c
for (const action of actions) {
for (const shortcut of action.effectiveShortcuts ?? []) {
const binding = shortcutService.bindElShortcut($el, shortcut, () => component.triggerCommand(action.actionName, { ntxId: appContext.tabManager.activeNtxId }));
const binding = shortcutService.bindElShortcut($el, shortcut, () => {
component.triggerCommand(action.actionName, { ntxId });
});
if (binding) {
bindings.push(binding);
}

View File

@ -2591,7 +2591,7 @@ iframe.print-iframe {
flex-direction: column;
}
.scrolling-container > .note-detail.full-height,
.note-detail.full-height,
.scrolling-container > .note-list-widget.full-height {
position: relative;
flex-grow: 1;

View File

@ -2091,5 +2091,10 @@
"auto-read-only-note": "这条笔记以只读模式显示便于快速加载。",
"auto-read-only-learn-more": "了解更多",
"edit-note": "编辑笔记"
},
"note-color": {
"clear-color": "清除笔记颜色",
"set-color": "设置笔记颜色",
"set-custom-color": "设置自定义笔记颜色"
}
}

View File

@ -39,7 +39,10 @@
"help_on_tree_prefix": "Aide sur le préfixe de l'arbre",
"prefix": "Préfixe : ",
"save": "Sauvegarder",
"branch_prefix_saved": "Le préfixe de la branche a été enregistré."
"branch_prefix_saved": "Le préfixe de la branche a été enregistré.",
"edit_branch_prefix_multiple": "Modifier le préfixe de branche pour {{count}} branches",
"branch_prefix_saved_multiple": "Le préfixe de la branche a été sauvegardé pour {{count}} branches.",
"affected_branches": "Branches impactées ({{count}}):"
},
"bulk_actions": {
"bulk_actions": "Actions groupées",

View File

@ -1930,7 +1930,7 @@
"search-for": "「{{term}}」を検索",
"create-note": "子ノート「{{term}}」を作成してリンクする",
"insert-external-link": "「{{term}}」への外部リンクを挿入",
"clear-text-field": "テキストフィールドを消去",
"clear-text-field": "テキストフィールドをクリア",
"show-recent-notes": "最近のノートを表示",
"full-text-search": "全文検索"
},
@ -2091,5 +2091,10 @@
"auto-read-only-note": "このノートは読み込みを高速化するために読み取り専用モードで表示されています。",
"auto-read-only-learn-more": "さらに詳しく",
"edit-note": "ノートを編集"
},
"note-color": {
"clear-color": "ノートの色をクリア",
"set-color": "ノートの色を設定",
"set-custom-color": "ノートの色をカスタム設定"
}
}

View File

@ -2091,5 +2091,10 @@
"auto-read-only-note": "此筆記以唯讀模式顯示以加快載入速度。",
"auto-read-only-learn-more": "了解更多",
"edit-note": "編輯筆記"
},
"note-color": {
"clear-color": "清除筆記顏色",
"set-color": "設定筆記顏色",
"set-custom-color": "設定自訂筆記顏色"
}
}

View File

@ -2,19 +2,13 @@ import { allViewTypes, ViewModeMedia, ViewModeProps, ViewTypeOptions } from "./i
import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useTriliumEvent } from "../react/hooks";
import FNote from "../../entities/fnote";
import "./NoteList.css";
import { ListView, GridView } from "./legacy/ListOrGridView";
import { useEffect, useRef, useState } from "preact/hooks";
import GeoView from "./geomap";
import ViewModeStorage from "./view_mode_storage";
import CalendarView from "./calendar";
import TableView from "./table";
import BoardView from "./board";
import { subscribeToMessages, unsubscribeToMessage as unsubscribeFromMessage } from "../../services/ws";
import { WebSocketMessage } from "@triliumnext/commons";
import froca from "../../services/froca";
import PresentationView from "./presentation";
import { ListPrintView } from "./legacy/ListPrintView";
import { lazy, Suspense } from "preact/compat";
import { VNode } from "preact";
interface NoteListProps {
note: FNote | null | undefined;
notePath: string | null | undefined;
@ -29,6 +23,33 @@ interface NoteListProps {
onProgressChanged?(progress: number): void;
}
type LazyLoadedComponent = ((props: ViewModeProps<any>) => VNode<any> | undefined);
const ViewComponents: Record<ViewTypeOptions, { normal: LazyLoadedComponent, print?: LazyLoadedComponent }> = {
list: {
normal: lazy(() => import("./legacy/ListOrGridView.js").then(i => i.ListView)),
print: lazy(() => import("./legacy/ListPrintView.js").then(i => i.ListPrintView))
},
grid: {
normal: lazy(() => import("./legacy/ListOrGridView.js").then(i => i.GridView)),
},
geoMap: {
normal: lazy(() => import("./geomap/index.js")),
},
calendar: {
normal: lazy(() => import("./calendar/index.js"))
},
table: {
normal: lazy(() => import("./table/index.js")),
print: lazy(() => import("./table/TablePrintView.js"))
},
board: {
normal: lazy(() => import("./board/index.js"))
},
presentation: {
normal: lazy(() => import("./presentation/index.js"))
}
}
export default function NoteList(props: Pick<NoteListProps, "displayOnlyCollections" | "media" | "onReady" | "onProgressChanged">) {
const { note, noteContext, notePath, ntxId } = useNoteContext();
const viewType = useNoteViewType(note);
@ -93,40 +114,23 @@ export function CustomNoteList({ note, viewType, isEnabled: shouldEnable, notePa
}
}
const ComponentToRender = viewType && props && isEnabled && (
props.media === "print" ? ViewComponents[viewType].print : ViewComponents[viewType].normal
);
return (
<div ref={widgetRef} className={`note-list-widget component ${isFullHeight && isEnabled ? "full-height" : ""}`}>
{props && isEnabled && (
{ComponentToRender && props && (
<div className="note-list-widget-content">
{getComponentByViewType(viewType, props)}
<Suspense fallback="">
<ComponentToRender {...props} />
</Suspense>
</div>
)}
</div>
);
}
function getComponentByViewType(viewType: ViewTypeOptions, props: ViewModeProps<any>) {
switch (viewType) {
case "list":
if (props.media !== "print") {
return <ListView {...props} />;
} else {
return <ListPrintView {...props} />;
}
case "grid":
return <GridView {...props} />;
case "geoMap":
return <GeoView {...props} />;
case "calendar":
return <CalendarView {...props} />
case "table":
return <TableView {...props} />
case "board":
return <BoardView {...props} />
case "presentation":
return <PresentationView {...props} />
}
}
export function useNoteViewType(note?: FNote | null): ViewTypeOptions | undefined {
const [ viewType ] = useNoteLabel(note, "viewType");

View File

@ -2,6 +2,7 @@
position: relative;
height: 100%;
user-select: none;
overflow-x: auto;
--card-font-size: 0.9em;
--card-line-height: 1.2;

View File

@ -0,0 +1,20 @@
.table-print-view .tabulator-print-table table,
.table-print-view .tabulator-print-table th,
.table-print-view .tabulator-print-table tr,
.table-print-view .tabulator-print-table td {
border: 1px solid black;
border-collapse: collapse;
}
.table-print-view .tabulator-print-table th {
background-color: #f0f0f0;
}
.table-print-view .tabulator-print-table th,
.table-print-view .tabulator-print-table td {
padding: 0.25rem 0.5rem;
}
.table-print-view .tabulator-print-table td[aria-checked] svg path {
fill: currentColor;
}

View File

@ -0,0 +1,49 @@
import { useEffect, useRef, useState } from "preact/hooks";
import { ViewModeProps } from "../interface";
import useData, { TableConfig } from "./data";
import { ExportModule, FormatModule, Tabulator as VanillaTabulator} from 'tabulator-tables';
import Tabulator from "./tabulator";
import { RawHtmlBlock } from "../../react/RawHtml";
import "./TablePrintView.css";
export default function TablePrintView({ note, noteIds, viewConfig, onReady }: ViewModeProps<TableConfig>) {
const tabulatorRef = useRef<VanillaTabulator>(null);
const { columnDefs, rowData, hasChildren } = useData(note, noteIds, viewConfig, undefined, () => {});
const [ html, setHtml ] = useState<string>();
useEffect(() => {
if (!html) return;
onReady?.();
}, [ html ]);
return rowData && (
<>
<h1>{note.title}</h1>
<div className="table-print-view">
{!html ? (
<Tabulator
tabulatorRef={tabulatorRef}
className="table-print-view-container"
modules={[ ExportModule, FormatModule ]}
columns={columnDefs ?? []}
data={rowData}
index="branchId"
dataTree={hasChildren}
printAsHtml={true}
printStyled={false}
onReady={() => {
const tabulator = tabulatorRef.current;
if (!tabulator) return;
setHtml(tabulator.getHtml());
}}
/>
) : (
<RawHtmlBlock html={html} />
)}
</div>
</>
)
}

View File

@ -0,0 +1,77 @@
import type { ColumnDefinition } from "tabulator-tables";
import FNote from "../../../entities/fnote";
import { useNoteLabelBoolean, useNoteLabelInt, useTriliumEvent } from "../../react/hooks";
import { useEffect, useState } from "preact/hooks";
import getAttributeDefinitionInformation, { buildRowDefinitions, TableData } from "./rows";
import froca from "../../../services/froca";
import { buildColumnDefinitions } from "./columns";
import attributes from "../../../services/attributes";
import { RefObject } from "preact";
export interface TableConfig {
tableData: {
columns?: ColumnDefinition[];
};
}
export default function useData(note: FNote, noteIds: string[], viewConfig: TableConfig | undefined, newAttributePosition: RefObject<number | undefined> | undefined, resetNewAttributePosition: () => void) {
const [ maxDepth ] = useNoteLabelInt(note, "maxNestingDepth");
const [ includeArchived ] = useNoteLabelBoolean(note, "includeArchived");
const [ columnDefs, setColumnDefs ] = useState<ColumnDefinition[]>();
const [ rowData, setRowData ] = useState<TableData[]>();
const [ hasChildren, setHasChildren ] = useState<boolean>();
const [ isSorted ] = useNoteLabelBoolean(note, "sorted");
const [ movableRows, setMovableRows ] = useState(false);
async function refresh() {
const info = getAttributeDefinitionInformation(note);
// Ensure all note IDs are loaded.
await froca.getNotes(noteIds);
const { definitions: rowData, hasSubtree: hasChildren, rowNumber } = await buildRowDefinitions(note, info, includeArchived, maxDepth);
const columnDefs = buildColumnDefinitions({
info,
movableRows,
existingColumnData: viewConfig?.tableData?.columns,
rowNumberHint: rowNumber,
position: newAttributePosition?.current ?? undefined
});
setColumnDefs(columnDefs);
setRowData(rowData);
setHasChildren(hasChildren);
resetNewAttributePosition();
}
useEffect(() => { refresh() }, [ note, noteIds, maxDepth, movableRows ]);
useTriliumEvent("entitiesReloaded", ({ loadResults}) => {
if (glob.device === "print") return;
// React to column changes.
if (loadResults.getAttributeRows().find(attr =>
attr.type === "label" &&
(attr.name?.startsWith("label:") || attr.name?.startsWith("relation:")) &&
attributes.isAffecting(attr, note))) {
refresh();
return;
}
// React to external row updates.
if (loadResults.getBranchRows().some(branch => branch.parentNoteId === note.noteId || noteIds.includes(branch.parentNoteId ?? ""))
|| loadResults.getNoteIds().some(noteId => noteIds.includes(noteId))
|| loadResults.getAttributeRows().some(attr => noteIds.includes(attr.noteId!))
|| loadResults.getAttributeRows().some(attr => attr.name === "archived" && attr.noteId && noteIds.includes(attr.noteId))) {
refresh();
return;
}
});
// Identify if movable rows.
useEffect(() => {
setMovableRows(!isSorted && note.type !== "search" && !hasChildren);
}, [ isSorted, note, hasChildren ]);
return { columnDefs, rowData, movableRows, hasChildren };
}

View File

@ -1,10 +1,9 @@
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { ViewModeProps } from "../interface";
import { buildColumnDefinitions } from "./columns";
import getAttributeDefinitionInformation, { buildRowDefinitions, TableData } from "./rows";
import { useLegacyWidget, useNoteLabelBoolean, useNoteLabelInt, useTriliumEvent } from "../../react/hooks";
import { TableData } from "./rows";
import { useLegacyWidget } from "../../react/hooks";
import Tabulator from "./tabulator";
import { Tabulator as VanillaTabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, ColumnDefinition, DataTreeModule, Options, RowComponent} from 'tabulator-tables';
import { Tabulator as VanillaTabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, DataTreeModule, Options, RowComponent} from 'tabulator-tables';
import { useContextMenu } from "./context_menu";
import { ParentComponent } from "../../react/react_utils";
import FNote from "../../../entities/fnote";
@ -14,16 +13,8 @@ import "./index.css";
import useRowTableEditing from "./row_editing";
import useColTableEditing from "./col_editing";
import AttributeDetailWidget from "../../attribute_widgets/attribute_detail";
import attributes from "../../../services/attributes";
import { RefObject } from "preact";
import SpacedUpdate from "../../../services/spaced_update";
import froca from "../../../services/froca";
interface TableConfig {
tableData: {
columns?: ColumnDefinition[];
};
}
import useData, { TableConfig } from "./data";
export default function TableView({ note, noteIds, notePath, viewConfig, saveConfig }: ViewModeProps<TableConfig>) {
const tabulatorRef = useRef<VanillaTabulator>(null);
@ -118,67 +109,7 @@ function usePersistence(viewConfig: TableConfig | null | undefined, saveConfig:
return () => {
spacedUpdate.updateNowIfNecessary();
};
}, [ viewConfig, saveConfig ])
}, [ viewConfig, saveConfig ]);
return persistenceProps;
}
function useData(note: FNote, noteIds: string[], viewConfig: TableConfig | undefined, newAttributePosition: RefObject<number | undefined>, resetNewAttributePosition: () => void) {
const [ maxDepth ] = useNoteLabelInt(note, "maxNestingDepth") ?? -1;
const [ includeArchived ] = useNoteLabelBoolean(note, "includeArchived");
const [ columnDefs, setColumnDefs ] = useState<ColumnDefinition[]>();
const [ rowData, setRowData ] = useState<TableData[]>();
const [ hasChildren, setHasChildren ] = useState<boolean>();
const [ isSorted ] = useNoteLabelBoolean(note, "sorted");
const [ movableRows, setMovableRows ] = useState(false);
async function refresh() {
const info = getAttributeDefinitionInformation(note);
// Ensure all note IDs are loaded.
await froca.getNotes(noteIds);
const { definitions: rowData, hasSubtree: hasChildren, rowNumber } = await buildRowDefinitions(note, info, includeArchived, maxDepth);
const columnDefs = buildColumnDefinitions({
info,
movableRows,
existingColumnData: viewConfig?.tableData?.columns,
rowNumberHint: rowNumber,
position: newAttributePosition.current ?? undefined
});
setColumnDefs(columnDefs);
setRowData(rowData);
setHasChildren(hasChildren);
resetNewAttributePosition();
}
useEffect(() => { refresh() }, [ note, noteIds, maxDepth, movableRows ]);
useTriliumEvent("entitiesReloaded", ({ loadResults}) => {
// React to column changes.
if (loadResults.getAttributeRows().find(attr =>
attr.type === "label" &&
(attr.name?.startsWith("label:") || attr.name?.startsWith("relation:")) &&
attributes.isAffecting(attr, note))) {
refresh();
return;
}
// React to external row updates.
if (loadResults.getBranchRows().some(branch => branch.parentNoteId === note.noteId || noteIds.includes(branch.parentNoteId ?? ""))
|| loadResults.getNoteIds().some(noteId => noteIds.includes(noteId))
|| loadResults.getAttributeRows().some(attr => noteIds.includes(attr.noteId!))
|| loadResults.getAttributeRows().some(attr => attr.name === "archived" && attr.noteId && noteIds.includes(attr.noteId))) {
refresh();
return;
}
});
// Identify if movable rows.
useEffect(() => {
setMovableRows(!isSorted && note.type !== "search" && !hasChildren);
}, [ isSorted, note, hasChildren ]);
return { columnDefs, rowData, movableRows, hasChildren };
}

View File

@ -14,9 +14,10 @@ interface TableProps<T> extends Omit<Options, "data" | "footerElement" | "index"
events?: Partial<EventCallBackMethods>;
index: keyof T;
footerElement?: string | HTMLElement | JSX.Element;
onReady?: () => void;
}
export default function Tabulator<T>({ className, columns, data, modules, tabulatorRef: externalTabulatorRef, footerElement, events, index, dataTree, ...restProps }: TableProps<T>) {
export default function Tabulator<T>({ className, columns, data, modules, tabulatorRef: externalTabulatorRef, footerElement, events, index, dataTree, onReady, ...restProps }: TableProps<T>) {
const parentComponent = useContext(ParentComponent);
const containerRef = useRef<HTMLDivElement>(null);
const tabulatorRef = useRef<VanillaTabulator>(null);
@ -43,6 +44,7 @@ export default function Tabulator<T>({ className, columns, data, modules, tabula
tabulator.on("tableBuilt", () => {
tabulatorRef.current = tabulator;
externalTabulatorRef.current = tabulator;
onReady?.();
});
return () => tabulator.destroy();

View File

@ -5,6 +5,7 @@ import type { EventData, EventNames } from "../../components/app_context.js";
export default class RightPaneContainer extends FlexContainer<RightPanelWidget> {
private rightPaneHidden: boolean;
private firstRender: boolean;
constructor() {
super("column");
@ -14,6 +15,7 @@ export default class RightPaneContainer extends FlexContainer<RightPanelWidget>
this.collapsible();
this.rightPaneHidden = false;
this.firstRender = true;
}
isEnabled() {
@ -41,10 +43,11 @@ export default class RightPaneContainer extends FlexContainer<RightPanelWidget>
const oldToggle = !this.isHiddenInt();
const newToggle = this.isEnabled();
if (oldToggle !== newToggle) {
if (oldToggle !== newToggle || this.firstRender) {
this.toggleInt(newToggle);
splitService.setupRightPaneResizer();
this.firstRender = false;
}
}

View File

@ -0,0 +1,64 @@
/** Reduce the z-index of modals so that ckeditor popups are properly shown on top of it. */
body.popup-editor-open > .modal-backdrop { z-index: 998; }
body.popup-editor-open .popup-editor-dialog { z-index: 999; }
body.popup-editor-open .ck-clipboard-drop-target-line { z-index: 1000; }
body.desktop .modal.popup-editor-dialog .modal-dialog {
max-width: 75vw;
}
.modal.popup-editor-dialog .modal-header .modal-title {
font-size: 1.1em;
}
.modal.popup-editor-dialog .modal-header .title-row {
flex-grow: 1;
display: flex;
align-items: center;
}
.modal.popup-editor-dialog .modal-header .title-row > * {
margin: 5px;
}
.modal.popup-editor-dialog .modal-body {
padding: 0;
height: 75vh;
overflow: auto;
display: flex;
flex-direction: column;
}
.modal.popup-editor-dialog .note-detail-editable-text {
padding: 0 1em;
}
.modal.popup-editor-dialog .title-row,
.modal.popup-editor-dialog .modal-title,
.modal.popup-editor-dialog .note-icon-widget {
height: 32px;
}
.modal.popup-editor-dialog .note-icon-widget {
width: 32px;
margin: 0;
padding: 0;
}
.modal.popup-editor-dialog .note-icon-widget button.note-icon,
.modal.popup-editor-dialog .note-title-widget input.note-title {
font-size: 1em;
}
.modal.popup-editor-dialog .classic-toolbar-widget {
position: sticky;
top: 0;
inset-inline-start: 0;
inset-inline-end: 0;
background: var(--modal-background-color);
z-index: 998;
}
.modal.popup-editor-dialog .note-detail-file {
padding: 0;
}

View File

@ -0,0 +1,85 @@
import { useContext, useEffect, useRef, useState } from "preact/hooks";
import Modal from "../react/Modal";
import "./PopupEditor.css";
import { useNoteContext, useTriliumEvent } from "../react/hooks";
import NoteTitleWidget from "../note_title";
import NoteIcon from "../note_icon";
import NoteContext from "../../components/note_context";
import { NoteContextContext, ParentComponent } from "../react/react_utils";
import NoteDetail from "../NoteDetail";
import { ComponentChildren } from "preact";
import NoteList from "../collections/NoteList";
import StandaloneRibbonAdapter from "../ribbon/components/StandaloneRibbonAdapter";
import FormattingToolbar from "../ribbon/FormattingToolbar";
export default function PopupEditor() {
const [ shown, setShown ] = useState(false);
const parentComponent = useContext(ParentComponent);
const [ noteContext, setNoteContext ] = useState(new NoteContext("_popup-editor"));
useTriliumEvent("openInPopup", async ({ noteIdOrPath }) => {
const noteContext = new NoteContext("_popup-editor");
await noteContext.setNote(noteIdOrPath, {
viewScope: {
readOnlyTemporarilyDisabled: true
}
});
setNoteContext(noteContext);
setShown(true);
});
// Add a global class to be able to handle issues with z-index due to rendering in a popup.
useEffect(() => {
document.body.classList.toggle("popup-editor-open", shown);
}, [shown]);
return (
<NoteContextContext.Provider value={noteContext}>
<DialogWrapper>
<Modal
title={<TitleRow />}
className="popup-editor-dialog"
size="lg"
show={shown}
onShown={() => {
parentComponent?.handleEvent("focusOnDetail", { ntxId: noteContext.ntxId });
}}
onHidden={() => setShown(false)}
>
<StandaloneRibbonAdapter component={FormattingToolbar} />
<NoteDetail />
<NoteList media="screen" displayOnlyCollections />
</Modal>
</DialogWrapper>
</NoteContextContext.Provider>
)
}
export function DialogWrapper({ children }: { children: ComponentChildren }) {
const { note } = useNoteContext();
const wrapperRef = useRef<HTMLDivElement>(null);
const [ hasTint, setHasTint ] = useState(false);
// Apply the tinted-dialog class only if the custom color CSS class specifies a hue
useEffect(() => {
if (!wrapperRef.current) return;
const customHue = getComputedStyle(wrapperRef.current).getPropertyValue("--custom-color-hue");
setHasTint(!!customHue);
}, [ note ]);
return (
<div ref={wrapperRef} class={`quick-edit-dialog-wrapper ${note?.getColorClass() ?? ""} ${hasTint ? "tinted-quick-edit-dialog" : ""}`}>
{children}
</div>
)
}
export function TitleRow() {
return (
<div className="title-row">
<NoteIcon />
<NoteTitleWidget />
</div>
)
}

View File

@ -1,187 +0,0 @@
import type { EventNames, EventData } from "../../components/app_context.js";
import NoteContext from "../../components/note_context.js";
import { openDialog } from "../../services/dialog.js";
import BasicWidget, { ReactWrappedWidget } from "../basic_widget.js";
import Container from "../containers/container.js";
const TPL = /*html*/`\
<div class="popup-editor-dialog modal fade mx-auto" tabindex="-1" role="dialog">
<style>
/** Reduce the z-index of modals so that ckeditor popups are properly shown on top of it. */
body.popup-editor-open > .modal-backdrop { z-index: 998; }
body.popup-editor-open .popup-editor-dialog { z-index: 999; }
body.popup-editor-open .ck-clipboard-drop-target-line { z-index: 1000; }
body.desktop .modal.popup-editor-dialog .modal-dialog {
max-width: 75vw;
}
.modal.popup-editor-dialog .modal-header .modal-title {
font-size: 1.1em;
}
.modal.popup-editor-dialog .modal-body {
padding: 0;
height: 75vh;
overflow: auto;
}
.modal.popup-editor-dialog .note-detail-editable-text {
padding: 0 1em;
}
.modal.popup-editor-dialog .title-row,
.modal.popup-editor-dialog .modal-title,
.modal.popup-editor-dialog .note-icon-widget {
height: 32px;
}
.modal.popup-editor-dialog .note-icon-widget {
width: 32px;
margin: 0;
padding: 0;
}
.modal.popup-editor-dialog .note-icon-widget button.note-icon,
.modal.popup-editor-dialog .note-title-widget input.note-title {
font-size: 1em;
}
.modal.popup-editor-dialog .classic-toolbar-widget {
position: sticky;
top: 0;
inset-inline-start: 0;
inset-inline-end: 0;
background: var(--modal-background-color);
z-index: 998;
}
.modal.popup-editor-dialog .note-detail-file {
padding: 0;
}
</style>
<div class="quick-edit-dialog-wrapper">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">
<!-- This is where the first child will be injected -->
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- This is where all but the first child will be injected. -->
</div>
</div>
</div>
</div>
</div>
`;
export default class PopupEditorDialog extends Container<BasicWidget> {
private noteContext: NoteContext;
private $modalHeader!: JQuery<HTMLElement>;
private $modalBody!: JQuery<HTMLElement>;
private $wrapper!: JQuery<HTMLDivElement>;
constructor() {
super();
this.noteContext = new NoteContext("_popup-editor");
}
doRender() {
// This will populate this.$widget with the content of the children.
super.doRender();
// Now we wrap it in the modal.
const $newWidget = $(TPL);
this.$modalHeader = $newWidget.find(".modal-title");
this.$modalBody = $newWidget.find(".modal-body");
this.$wrapper = $newWidget.find(".quick-edit-dialog-wrapper");
const children = this.$widget.children();
this.$modalHeader.append(children[0]);
this.$modalBody.append(children.slice(1));
this.$widget = $newWidget;
this.setVisibility(false);
}
async openInPopupEvent({ noteIdOrPath }: EventData<"openInPopup">) {
const $dialog = await openDialog(this.$widget, false, {
focus: false
});
await this.noteContext.setNote(noteIdOrPath, {
viewScope: {
readOnlyTemporarilyDisabled: true
}
});
const colorClass = this.noteContext.note?.getColorClass();
const wrapperElement = this.$wrapper.get(0)!;
if (colorClass) {
wrapperElement.className = "quick-edit-dialog-wrapper " + colorClass;
} else {
wrapperElement.className = "quick-edit-dialog-wrapper";
}
const customHue = getComputedStyle(wrapperElement).getPropertyValue("--custom-color-hue");
if (customHue) {
/* Apply the tinted-dialog class only if the custom color CSS class specifies a hue */
wrapperElement.classList.add("tinted-quick-edit-dialog");
}
const activeEl = document.activeElement;
if (activeEl && "blur" in activeEl) {
(activeEl as HTMLElement).blur();
}
$dialog.on("shown.bs.modal", async () => {
await this.handleEventInChildren("activeContextChanged", { noteContext: this.noteContext });
this.setVisibility(true);
await this.handleEventInChildren("focusOnDetail", { ntxId: this.noteContext.ntxId });
});
$dialog.on("hidden.bs.modal", () => {
const $typeWidgetEl = $dialog.find(".note-detail-printable");
if ($typeWidgetEl.length) {
const typeWidget = glob.getComponentByEl($typeWidgetEl[0]) as ReactWrappedWidget;
typeWidget.cleanup();
}
this.setVisibility(false);
});
}
setVisibility(visible: boolean) {
const $bodyItems = this.$modalBody.find("> div");
if (visible) {
$bodyItems.fadeIn();
this.$modalHeader.children().show();
document.body.classList.add("popup-editor-open");
} else {
$bodyItems.hide();
this.$modalHeader.children().hide();
document.body.classList.remove("popup-editor-open");
}
}
handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null {
// Avoid events related to the current tab interfere with our popup.
if (["noteSwitched", "noteSwitchedAndActivated", "exportAsPdf", "printActiveNote"].includes(name)) {
return Promise.resolve();
}
// Avoid not showing recent notes when creating a new empty tab.
if ("noteContext" in data && data.noteContext.ntxId !== "_popup-editor") {
return Promise.resolve();
}
return super.handleEventInChildren(name, data);
}
}

View File

@ -2,7 +2,7 @@ import { CSSProperties } from "preact/compat";
import { DragData } from "../note_tree";
import { FilterLabelsByType, KeyboardActionNames, OptionNames, RelationNames } from "@triliumnext/commons";
import { MutableRef, useCallback, useContext, useDebugValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
import { ParentComponent, refToJQuerySelector } from "./react_utils";
import { NoteContextContext, ParentComponent, refToJQuerySelector } from "./react_utils";
import { RefObject, VNode } from "preact";
import { Tooltip } from "bootstrap";
import { ViewMode, ViewScope } from "../../services/link";
@ -257,18 +257,29 @@ export function useUniqueName(prefix?: string) {
}
export function useNoteContext() {
const [ noteContext, setNoteContext ] = useState<NoteContext>();
const noteContextContext = useContext(NoteContextContext);
const [ noteContext, setNoteContext ] = useState<NoteContext | undefined>(noteContextContext ?? undefined);
const [ notePath, setNotePath ] = useState<string | null | undefined>();
const [ note, setNote ] = useState<FNote | null | undefined>();
const [ , setViewScope ] = useState<ViewScope>();
const [ isReadOnlyTemporarilyDisabled, setIsReadOnlyTemporarilyDisabled ] = useState<boolean | null | undefined>(noteContext?.viewScope?.isReadOnly);
const [ refreshCounter, setRefreshCounter ] = useState(0);
useEffect(() => {
if (!noteContextContext) return;
setNoteContext(noteContextContext);
setNote(noteContextContext.note);
setNotePath(noteContextContext.notePath);
setViewScope(noteContextContext.viewScope);
setIsReadOnlyTemporarilyDisabled(noteContextContext?.viewScope?.readOnlyTemporarilyDisabled);
}, [ noteContextContext ]);
useEffect(() => {
setNote(noteContext?.note);
}, [ notePath ]);
useTriliumEvents([ "setNoteContext", "activeContextChanged", "noteSwitchedAndActivated", "noteSwitched" ], ({ noteContext }) => {
if (noteContextContext) return;
setNoteContext(noteContext);
setNotePath(noteContext.notePath);
setViewScope(noteContext.viewScope);
@ -282,6 +293,7 @@ export function useNoteContext() {
}
});
useTriliumEvent("readOnlyTemporarilyDisabled", ({ noteContext: eventNoteContext }) => {
if (noteContextContext) return;
if (eventNoteContext.ntxId === noteContext?.ntxId) {
setIsReadOnlyTemporarilyDisabled(eventNoteContext?.viewScope?.readOnlyTemporarilyDisabled);
}
@ -760,18 +772,18 @@ export function useResizeObserver(ref: RefObject<HTMLElement>, callback: () => v
}, [ callback, ref ]);
}
export function useKeyboardShortcuts(scope: "code-detail" | "text-detail", containerRef: RefObject<HTMLElement>, parentComponent: Component | undefined) {
export function useKeyboardShortcuts(scope: "code-detail" | "text-detail", containerRef: RefObject<HTMLElement>, parentComponent: Component | undefined, ntxId: string | null | undefined) {
useEffect(() => {
if (!parentComponent) return;
const $container = refToJQuerySelector(containerRef);
const bindingPromise = keyboard_actions.setupActionsForElement(scope, $container, parentComponent);
const bindingPromise = keyboard_actions.setupActionsForElement(scope, $container, parentComponent, ntxId);
return async () => {
const bindings = await bindingPromise;
for (const binding of bindings) {
removeIndividualBinding(binding);
}
}
}, []);
}, [ scope, containerRef, parentComponent, ntxId ]);
}
/**

View File

@ -1,8 +1,11 @@
import { ComponentChild, createContext, render, type JSX, type RefObject } from "preact";
import Component from "../../components/component";
import NoteContext from "../../components/note_context";
export const ParentComponent = createContext<Component | null>(null);
export const NoteContextContext = createContext<NoteContext | null>(null);
/**
* Takes in a React ref and returns a corresponding JQuery selector.
*

View File

@ -1,4 +1,5 @@
import { useTriliumOption } from "../react/hooks";
import { useRef } from "preact/hooks";
import { useTriliumEvent, useTriliumOption } from "../react/hooks";
import { TabContext } from "./ribbon-interface";
/**
@ -9,11 +10,28 @@ import { TabContext } from "./ribbon-interface";
* The ribbon item is active by default for text notes, as long as they are not in read-only mode.
*
* ! The toolbar is not only used in the ribbon, but also in the quick edit feature.
* * The mobile toolbar is handled separately (see `MobileEditorToolbar`).
*/
export default function FormattingToolbar({ hidden }: TabContext) {
export default function FormattingToolbar({ hidden, ntxId }: TabContext) {
const containerRef = useRef<HTMLDivElement>(null);
const [ textNoteEditorType ] = useTriliumOption("textNoteEditorType");
// Attach the toolbar from the CKEditor.
useTriliumEvent("textEditorRefreshed", ({ ntxId: eventNtxId, editor }) => {
if (eventNtxId !== ntxId || !containerRef.current) return;
const toolbar = editor.ui.view.toolbar?.element;
if (toolbar) {
containerRef.current.replaceChildren(toolbar);
} else {
containerRef.current.replaceChildren();
}
});
return (textNoteEditorType === "ckeditor-classic" &&
<div className={`classic-toolbar-widget ${hidden ? "hidden-ext" : ""}`} />
<div
ref={containerRef}
className={`classic-toolbar-widget ${hidden ? "hidden-ext" : ""}`}
/>
)
};

View File

@ -49,7 +49,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment();
const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(note.type);
const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help");
const isPrintable = ["text", "code"].includes(note.type) || (note.type === "book" && ["presentation", "list"].includes(note.getLabelValue("viewType") ?? ""));
const isPrintable = ["text", "code"].includes(note.type) || (note.type === "book" && ["presentation", "list", "table"].includes(note.getLabelValue("viewType") ?? ""));
const isElectron = getIsElectron();
const isMac = getIsMac();
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "aiChat"].includes(note.type);

View File

@ -0,0 +1,33 @@
import { useContext, useRef } from "preact/hooks";
import { ParentComponent } from "../react/react_utils";
import { ComponentChildren } from "preact";
interface RightPanelWidgetProps {
title: string;
children: ComponentChildren;
buttons?: ComponentChildren;
}
export default function RightPanelWidget({ title, buttons, children }: RightPanelWidgetProps) {
const containerRef = useRef<HTMLDivElement>(null);
const parentComponent = useContext(ParentComponent);
if (parentComponent) {
parentComponent.initialized = Promise.resolve();
}
return (
<div ref={containerRef} class="card widget" style={{contain: "none"}}>
<div class="card-header">
<div class="card-header-title">{title}</div>
<div class="card-header-buttons">{buttons}</div>
</div>
<div id={parentComponent?.componentId} class="body-wrapper">
<div class="card-body">
{children}
</div>
</div>
</div>
);
}

View File

@ -199,6 +199,7 @@ export default class TocWidget extends RightPanelWidget {
* For document note types, we obtain the content directly from the DOM since it allows us to obtain processed data without
* requesting data twice. However, when immediately navigating to a new note the new document is not yet attached to the hierarchy,
* resulting in an empty TOC. The fix is to simply wait for it to pop up.
* TODO: Use a better method that is not prone to unnecessary delays and race conditions.
*/
setTimeout(async () => {
const $contentEl = await this.noteContext?.getContentElement();
@ -209,7 +210,7 @@ export default class TocWidget extends RightPanelWidget {
} else {
console.warn("Unable to get content element for doctype");
}
}, 10);
}, 250);
}
}

View File

@ -100,7 +100,7 @@ export function EditableCode({ note, ntxId, noteContext, debounceUpdate, parentC
}
});
useKeyboardShortcuts("code-detail", containerRef, parentComponent);
useKeyboardShortcuts("code-detail", containerRef, parentComponent, ntxId);
return (
<>

View File

@ -163,7 +163,12 @@ function useResizer(containerRef: RefObject<HTMLDivElement>, noteId: string, svg
pan: zoomInstance.getPan(),
zoom: zoomInstance.getZoom()
}
try {
zoomInstance.destroy();
} catch (e) {
// Sometimes crashes with "Matrix is not invertible" which can cause havoc such as breaking the popup editor from ever showing up again.
console.warn(e);
}
};
}, [ svg ]);

View File

@ -20,7 +20,6 @@ export interface CKEditorApi {
}
interface CKEditorWithWatchdogProps extends Pick<HTMLProps<HTMLDivElement>, "className" | "tabIndex"> {
content: string | undefined;
contentLanguage: string | null | undefined;
isClassicEditor?: boolean;
watchdogRef: RefObject<EditorWatchdog>;
@ -35,14 +34,14 @@ interface CKEditorWithWatchdogProps extends Pick<HTMLProps<HTMLDivElement>, "cla
containerRef?: RefObject<HTMLDivElement>;
}
export default function CKEditorWithWatchdog({ containerRef: externalContainerRef, content, contentLanguage, className, tabIndex, isClassicEditor, watchdogRef: externalWatchdogRef, watchdogConfig, onNotificationWarning, onWatchdogStateChange, onChange, onEditorInitialized, editorApi, templates }: CKEditorWithWatchdogProps) {
export default function CKEditorWithWatchdog({ containerRef: externalContainerRef, contentLanguage, className, tabIndex, isClassicEditor, watchdogRef: externalWatchdogRef, watchdogConfig, onNotificationWarning, onWatchdogStateChange, onChange, onEditorInitialized, editorApi, templates }: CKEditorWithWatchdogProps) {
const containerRef = useSyncedRef<HTMLDivElement>(externalContainerRef, null);
const watchdogRef = useRef<EditorWatchdog>(null);
const [ uiLanguage ] = useTriliumOption("locale");
const [ editor, setEditor ] = useState<CKTextEditor>();
const { parentComponent } = useNoteContext();
const { parentComponent, ntxId } = useNoteContext();
useKeyboardShortcuts("text-detail", containerRef, parentComponent);
useKeyboardShortcuts("text-detail", containerRef, parentComponent, ntxId);
useImperativeHandle(editorApi, () => ({
hasSelection() {
@ -185,9 +184,6 @@ export default function CKEditorWithWatchdog({ containerRef: externalContainerRe
return () => watchdog.destroy();
}, [ contentLanguage, templates, uiLanguage ]);
// React to content changes.
useEffect(() => editor?.setData(content ?? ""), [ editor, content ]);
// React to notification warning callback.
useEffect(() => {
if (!onNotificationWarning || !editor) return;

View File

@ -2,12 +2,11 @@ import { useEffect, useRef, useState } from "preact/hooks";
import dialog from "../../../services/dialog";
import toast from "../../../services/toast";
import utils, { hasTouchBar, isMobile } from "../../../services/utils";
import { useEditorSpacedUpdate, useKeyboardShortcuts, useLegacyImperativeHandlers, useNoteLabel, useTriliumEvent, useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
import { useEditorSpacedUpdate, useLegacyImperativeHandlers, useNoteLabel, useTriliumEvent, useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
import { TypeWidgetProps } from "../type_widget";
import CKEditorWithWatchdog, { CKEditorApi } from "./CKEditorWithWatchdog";
import "./EditableText.css";
import { CKTextEditor, ClassicEditor, EditorWatchdog, TemplateDefinition } from "@triliumnext/ckeditor5";
import Component from "../../../components/component";
import { CKTextEditor, EditorWatchdog, TemplateDefinition } from "@triliumnext/ckeditor5";
import options from "../../../services/options";
import { loadIncludedNote, refreshIncludedNote, setupImageOpening } from "./utils";
import getTemplates, { updateTemplateCache } from "./snippets.js";
@ -27,7 +26,7 @@ import { deferred } from "@triliumnext/commons";
*/
export default function EditableText({ note, parentComponent, ntxId, noteContext }: TypeWidgetProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [ content, setContent ] = useState<string>();
const contentRef = useRef<string>("");
const watchdogRef = useRef<EditorWatchdog>(null);
const editorApiRef = useRef<CKEditorApi>(null);
const refreshTouchBarRef = useRef<() => void>(null);
@ -55,7 +54,8 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
};
},
onContentChange(newContent) {
setContent(newContent);
contentRef.current = newContent;
watchdogRef.current?.editor?.setData(newContent);
}
});
const templates = useTemplates();
@ -215,7 +215,6 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
containerRef={containerRef}
className={`note-detail-editable-text-editor use-tn-links ${codeBlockWordWrap ? "word-wrap" : ""}`}
tabIndex={300}
content={content}
contentLanguage={language}
isClassicEditor={isClassicEditor}
editorApi={editorApiRef}
@ -233,12 +232,6 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
onWatchdogStateChange={onWatchdogStateChange}
onChange={() => spacedUpdate.scheduleUpdate()}
onEditorInitialized={(editor) => {
console.log("Editor has been initialized!", parentComponent, editor);
if (isClassicEditor) {
setupClassicEditor(editor, parentComponent);
}
if (hasTouchBar) {
const handler = () => refreshTouchBarRef.current?.();
for (const event of [ "bold", "italic", "underline", "paragraph", "heading" ]) {
@ -251,6 +244,8 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
}
initialized.current.resolve();
editor.setData(contentRef.current ?? "");
parentComponent?.triggerEvent("textEditorRefreshed", { ntxId, editor });
}}
/>}
@ -302,55 +297,6 @@ function onNotificationWarning(data, evt) {
evt.stop();
}
function setupClassicEditor(editor: CKTextEditor, parentComponent: Component | undefined) {
if (!parentComponent) return;
const $classicToolbarWidget = findClassicToolbar(parentComponent);
$classicToolbarWidget.empty();
if ($classicToolbarWidget.length) {
const toolbarView = (editor as ClassicEditor).ui.view.toolbar;
if (toolbarView.element) {
$classicToolbarWidget[0].appendChild(toolbarView.element);
}
}
if (utils.isMobile()) {
$classicToolbarWidget.addClass("visible");
// Reposition all dropdowns to point upwards instead of downwards.
// See https://ckeditor.com/docs/ckeditor5/latest/examples/framework/bottom-toolbar-editor.html for more info.
const toolbarView = (editor as ClassicEditor).ui.view.toolbar;
for (const item of toolbarView.items) {
if (!("panelView" in item)) continue;
item.on("change:isOpen", () => {
if (!("isOpen" in item) || !item.isOpen) return;
// @ts-ignore
item.panelView.position = item.panelView.position.replace("s", "n");
});
}
}
}
function findClassicToolbar(parentComponent: Component): JQuery<HTMLElement> {
const $widget = $(parentComponent.$widget);
if (!utils.isMobile()) {
const $parentSplit = $widget.parents(".note-split.type-text");
if ($parentSplit.length) {
// The editor is in a normal tab.
return $parentSplit.find("> .ribbon-container .classic-toolbar-widget");
} else {
// The editor is in a popup.
return $widget.closest(".modal-body").find(".classic-toolbar-widget");
}
} else {
return $("body").find(".classic-toolbar-widget");
}
}
function EditableTextTouchBar({ watchdogRef, refreshTouchBarRef }: { watchdogRef: RefObject<EditorWatchdog | null>, refreshTouchBarRef: RefObject<() => void> }) {
const [ headingSelectedIndex, setHeadingSelectedIndex ] = useState<number>();

View File

@ -1,7 +1,8 @@
import { MutableRef, useCallback, useEffect, useRef, useState } from "preact/hooks";
import { useNoteContext } from "../../react/hooks";
import { useNoteContext, useTriliumEvent } from "../../react/hooks";
import "./mobile_editor_toolbar.css";
import { isIOS } from "../../../services/utils";
import { CKTextEditor, ClassicEditor } from "@triliumnext/ckeditor5";
/**
* Handles the editing toolbar for CKEditor in mobile mode. The toolbar acts as a floating bar, with two different mechanism:
@ -10,12 +11,12 @@ import { isIOS } from "../../../services/utils";
* - On Android, the viewport change makes the keyboard resize the content area, all we have to do is to hide the tab bar and global menu (handled in the global style).
*/
export default function MobileEditorToolbar() {
const wrapperRef = useRef<HTMLDivElement>(null);
const { note, noteContext } = useNoteContext();
const containerRef = useRef<HTMLDivElement>(null);
const { note, noteContext, ntxId } = useNoteContext();
const [ shouldDisplay, setShouldDisplay ] = useState(false);
const [ dropdownActive, setDropdownActive ] = useState(false);
usePositioningOniOS(wrapperRef);
usePositioningOniOS(containerRef);
useEffect(() => {
noteContext?.isReadOnly().then(isReadOnly => {
@ -23,15 +24,28 @@ export default function MobileEditorToolbar() {
});
}, [ note ]);
// Attach the toolbar from the CKEditor.
useTriliumEvent("textEditorRefreshed", ({ ntxId: eventNtxId, editor }) => {
if (eventNtxId !== ntxId || !containerRef.current) return;
const toolbar = editor.ui.view.toolbar?.element;
repositionDropdowns(editor);
if (toolbar) {
containerRef.current.replaceChildren(toolbar);
} else {
containerRef.current.replaceChildren();
}
});
// Observe when a dropdown is expanded to apply a style that allows the dropdown to be visible, since we can't have the element both visible and the toolbar scrollable.
useEffect(() => {
if (!wrapperRef.current) return;
if (!containerRef.current) return;
const observer = new MutationObserver(e => {
setDropdownActive(e.map((e) => (e.target as any).ariaExpanded === "true").reduce((acc, e) => acc && e));
});
observer.observe(wrapperRef.current, {
observer.observe(containerRef.current, {
attributeFilter: ["aria-expanded"],
subtree: true
});
@ -41,7 +55,7 @@ export default function MobileEditorToolbar() {
return (
<div className={`classic-toolbar-outer-container ${!shouldDisplay ? "hidden-ext" : "visible"} ${isIOS() ? "ios" : ""}`}>
<div ref={wrapperRef} className={`classic-toolbar-widget ${dropdownActive ? "dropdown-active" : ""}`}></div>
<div ref={containerRef} className={`classic-toolbar-widget ${dropdownActive ? "dropdown-active" : ""}`}></div>
</div>
)
}
@ -65,3 +79,22 @@ function usePositioningOniOS(wrapperRef: MutableRef<HTMLDivElement | null>) {
};
}, []);
}
/**
* Reposition all dropdowns to point upwards instead of downwards.
* See https://ckeditor.com/docs/ckeditor5/latest/examples/framework/bottom-toolbar-editor.html for more info.
* @param editor
*/
function repositionDropdowns(editor: CKTextEditor) {
const toolbarView = (editor as ClassicEditor).ui.view.toolbar;
for (const item of toolbarView.items) {
if (!("panelView" in item)) continue;
item.on("change:isOpen", () => {
if (!("isOpen" in item) || !item.isOpen) return;
// @ts-ignore
item.panelView.position = item.panelView.position.replace("s", "n");
});
}
}

View File

@ -23,7 +23,7 @@
},
"dependencies": {
"@electron/remote": "2.1.3",
"better-sqlite3": "12.4.1",
"better-sqlite3": "12.4.6",
"electron-debug": "4.1.0",
"electron-dl": "4.0.0",
"electron-squirrel-startup": "1.0.1",

View File

@ -4,8 +4,8 @@
"description": "Standalone tool to dump contents of Trilium document.db file into a directory tree of notes",
"private": true,
"dependencies": {
"better-sqlite3": "12.4.1",
"mime-types": "3.0.1",
"better-sqlite3": "12.4.6",
"mime-types": "3.0.2",
"sanitize-filename": "1.6.3",
"tsx": "4.20.6",
"yargs": "18.0.0"

View File

@ -5,7 +5,7 @@
"description": "Desktop version of Trilium which imports the demo database (presented to new users at start-up) or the user guide and other documentation and saves the modifications for committing.",
"dependencies": {
"archiver": "7.0.1",
"better-sqlite3": "12.4.1"
"better-sqlite3": "12.4.6"
},
"devDependencies": {
"@triliumnext/client": "workspace:*",

View File

@ -1,5 +1,5 @@
{
"dependencies": {
"better-sqlite3": "12.4.1"
"better-sqlite3": "12.4.6"
}
}

View File

@ -25,12 +25,12 @@
"docker-start-rootless-alpine": "pnpm docker-build-rootless-alpine && docker run -p 8081:8080 triliumnext-rootless-alpine"
},
"dependencies": {
"better-sqlite3": "12.4.1",
"better-sqlite3": "12.4.6",
"html-to-text": "9.0.5",
"node-html-parser": "7.0.1"
},
"devDependencies": {
"@anthropic-ai/sdk": "0.70.0",
"@anthropic-ai/sdk": "0.70.1",
"@braintree/sanitize-url": "7.1.1",
"@electron/remote": "2.1.3",
"@preact/preset-vite": "2.10.2",
@ -86,7 +86,7 @@
"escape-html": "1.0.3",
"express": "5.1.0",
"express-http-proxy": "2.1.2",
"express-openid-connect": "2.19.2",
"express-openid-connect": "2.19.3",
"express-rate-limit": "8.2.1",
"express-session": "1.18.2",
"file-uri-to-path": "2.0.0",
@ -104,7 +104,7 @@
"is-svg": "6.1.0",
"jimp": "1.6.0",
"marked": "17.0.1",
"mime-types": "3.0.1",
"mime-types": "3.0.2",
"multer": "2.0.2",
"normalize-strings": "1.1.1",
"ollama": "0.6.3",

View File

@ -4,6 +4,7 @@
<figcaption>Screenshot of the note contextual menu indicating the “Export as PDF”
option.</figcaption>
</figure>
<h2>Printing</h2>
<p>This feature allows printing of notes. It works on both the desktop client,
but also on the web.</p>
@ -59,9 +60,9 @@ class="admonition note">
orientation, size. However, there are a few&nbsp;<a class="reference-link"
href="#root/_help_zEY4DaJG4YT5">Attributes</a>&nbsp;to adjust some of the settings:</p>
<ul>
<li data-list-item-id="e05b1bc3a57c550c493c8b1030c301673">To print in landscape mode instead of portrait (useful for big diagrams
<li>To print in landscape mode instead of portrait (useful for big diagrams
or slides), add <code>#printLandscape</code>.</li>
<li data-list-item-id="e6d7f6bb720e1f94994aa178881885dbd">By default, the resulting PDF will be in Letter format. It is possible
<li>By default, the resulting PDF will be in Letter format. It is possible
to adjust it to another page size via the <code>#printPageSize</code> attribute,
with one of the following values: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>.</li>
</ul>
@ -71,12 +72,11 @@ class="admonition note">
</aside>
<h2>Printing multiple notes</h2>
<p>Since v0.100.0, it is possible to print more than one note at the time
by using&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/_help_GTwFsgaA0lCt">Collections</a>:</p>
by using&nbsp;<a class="reference-link" href="#root/_help_GTwFsgaA0lCt">Collections</a>:</p>
<ol>
<li data-list-item-id="e1caaf943b13fd4764f93c58ea5f4f0c4">First create a collection.</li>
<li data-list-item-id="e3593024c9c69c3d26295d1e0152c813d">Configure it to use&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/GTwFsgaA0lCt/_help_mULW0Q3VojwY">List View</a>.</li>
<li
data-list-item-id="ebeea878f04af6f1da53fc0e8a80caf2d">Print the collection note normally.</li>
<li>First create a collection.</li>
<li>Configure it to use&nbsp;<a class="reference-link" href="#root/_help_mULW0Q3VojwY">List View</a>.</li>
<li>Print the collection note normally.</li>
</ol>
<p>The resulting collection will contain all the children of the collection,
while maintaining the hierarchy.</p>
@ -86,9 +86,9 @@ class="admonition note">
href="#root/_help_4TIF1oA4VQRO">Options</a>&nbsp;and assigning a key combination
for:</p>
<ul>
<li class="ck-list-marker-italic" data-list-item-id="e9595278e625ee8de30a6e88fb00d48e3"><em>Print Active Note</em>
<li><em>Print Active Note</em>
</li>
<li class="ck-list-marker-italic" data-list-item-id="e981d4cf371e1ff69416a796d88e88709"><em>Export Active Note as PDF</em>
<li><em>Export Active Note as PDF</em>
</li>
</ul>
<h2>Constraints &amp; limitations</h2>
@ -96,28 +96,39 @@ class="admonition note">
supported when printing, in which case the <em>Print</em> and <em>Export as PDF</em> options
will be disabled.</p>
<ul>
<li data-list-item-id="e82f01875cc03dcdab5328121654d815c">For&nbsp;<a class="reference-link" href="#root/_help_6f9hih2hXXZk">Code</a>&nbsp;notes:
<li>For&nbsp;<a class="reference-link" href="#root/_help_6f9hih2hXXZk">Code</a>&nbsp;notes:
<ul>
<li data-list-item-id="eea76e6bf545a3b54270ff86a74ca0d8d">Line numbers are not printed.</li>
<li data-list-item-id="edea65d8d3dedd354431e1e3a5dcd2e08">Syntax highlighting is enabled, however a default theme (Visual Studio)
<li>Line numbers are not printed.</li>
<li>Syntax highlighting is enabled, however a default theme (Visual Studio)
is enforced.</li>
</ul>
</li>
<li data-list-item-id="ec32cca86e4b0e2f75a2f1a06d2219e0b">For&nbsp;<a class="reference-link" href="#root/_help_GTwFsgaA0lCt">Collections</a>:
<li>For&nbsp;<a class="reference-link" href="#root/_help_GTwFsgaA0lCt">Collections</a>,
the following are supported:
<ul>
<li data-list-item-id="e0e1fc82e1141d3f4a609699e228ccc73"><a class="reference-link" href="#root/pOsGYCXsbNQG/GTwFsgaA0lCt/_help_mULW0Q3VojwY">List View</a>&nbsp;is
supported, allowing to print multiple notes at once while preserving hierarchy
(similar to a book).</li>
<li data-list-item-id="ee114b8468eaf24bce451f5ec4bda3da4"><a class="reference-link" href="#root/_help_zP3PMqaG71Ct">Presentation</a>&nbsp;is
also supported, where each slide/subnote is displayed.</li>
<li data-list-item-id="e4efe886c3ca1d19a49196340d9e1f6c8">The rest of the collections are not supported, but we plan to add support
<li><a class="reference-link" href="#root/_help_mULW0Q3VojwY">List View</a>, allowing
to print multiple notes at once while preserving hierarchy (similar to
a book).</li>
<li><a class="reference-link" href="#root/_help_zP3PMqaG71Ct">Presentation</a>,
where each slide/sub-note is displayed.</li>
<li><a class="reference-link" href="#root/_help_2FvYrpmOXm29">Table</a>, where the
table is rendered in a print-friendly way.
<ul>
<li>Tables that are too complex (especially if they have multiple columns)
might not fit properly, however tables with a large number of rows are
supported thanks to pagination.</li>
<li>Consider printing in landscape mode, or using <code>#printLandscape</code> if
exporting to PDF.</li>
</ul>
</li>
<li>The rest of the collections are not supported, but we plan to add support
for all the collection types at some point.</li>
</ul>
</li>
<li data-list-item-id="ee721d0145486818bd914a26594699cbd">Using&nbsp;<a class="reference-link" href="#root/_help_AlhDUqhENtH7">Custom app-wide CSS</a>&nbsp;for
<li>Using&nbsp;<a class="reference-link" href="#root/_help_AlhDUqhENtH7">Custom app-wide CSS</a>&nbsp;for
printing is not longer supported, due to a more stable but isolated mechanism.
<ul>
<li data-list-item-id="e2e1228d8d62cbdc8e96a7cbc9655c2ca">We plan to introduce a new mechanism specifically for a print CSS.</li>
<li>We plan to introduce a new mechanism specifically for a print CSS.</li>
</ul>
</li>
</ul>
@ -128,10 +139,10 @@ class="admonition note">
printing.</p>
<p>To do so:</p>
<ul>
<li data-list-item-id="ea90c233190428f0aacfcca4abe2f6b18">Create a CSS <a href="#root/_help_6f9hih2hXXZk">code note</a>.</li>
<li data-list-item-id="ec0756dfa1ce83087dd2c9bcc289d234b">On the note being printed, apply the <code>~printCss</code> relation to
<li>Create a CSS <a href="#root/_help_6f9hih2hXXZk">code note</a>.</li>
<li>On the note being printed, apply the <code>~printCss</code> relation to
point to the newly created CSS code note.</li>
<li data-list-item-id="ed05b40a29e6b442327bf439286096ac6">To apply the CSS to multiple notes, consider using <a href="#root/_help_bwZpz2ajCEwO">inheritable attributes</a> or&nbsp;
<li>To apply the CSS to multiple notes, consider using <a href="#root/_help_bwZpz2ajCEwO">inheritable attributes</a> or&nbsp;
<a
class="reference-link" href="#root/_help_KC1HB96bqqHX">Templates</a>.</li>
</ul>
@ -142,11 +153,10 @@ class="admonition note">
}</code></pre>
<p>To remark:</p>
<ul>
<li data-list-item-id="ec7fa7fb43c85ba65185b42a9ed590da7">Multiple CSS notes can be add by using multiple <code>~printCss</code> relations.</li>
<li
data-list-item-id="e1db64e345bbaf53151b84a69ff91376f">If the note pointing to the <code>printCss</code> doesn't have the right
<li>Multiple CSS notes can be add by using multiple <code>~printCss</code> relations.</li>
<li>If the note pointing to the <code>printCss</code> doesn't have the right
note type or mime type, it will be ignored.</li>
<li data-list-item-id="e8b2d24c4a6781c5516d0551f51b3947b">If migrating from a previous version where&nbsp;<a class="reference-link"
<li>If migrating from a previous version where&nbsp;<a class="reference-link"
href="#root/_help_AlhDUqhENtH7">Custom app-wide CSS</a>, there's no need for <code>@media print {</code> since
the style-sheet is used only for printing.</li>
</ul>

View File

@ -12,22 +12,21 @@
as a single continuous document.</p>
<h2>Interaction</h2>
<ul>
<li data-list-item-id="ee85c9dce1f91b700d8f13bdc9500bc62">Each note can be expanded or collapsed by clicking on the arrow to the
<li>Each note can be expanded or collapsed by clicking on the arrow to the
left of the title.</li>
<li data-list-item-id="e84faa71c2b0bf22a09490b35134f2687">In the&nbsp;<a class="reference-link" href="#root/_help_BlN9DFI679QC">Ribbon</a>,
<li>In the&nbsp;<a class="reference-link" href="#root/_help_BlN9DFI679QC">Ribbon</a>,
in the <em>Collection</em> tab there are options to expand and to collapse
all notes easily.</li>
</ul>
<h2>Printing and exporting to PDF</h2>
<p>Since v0.100.0, list collections can be <a href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/BFs8mudNFgCS/_help_NRnIZmSMc5sj">printed or exported to PDF</a>.</p>
<p>Since v0.100.0, list collections can be <a href="#root/_help_NRnIZmSMc5sj">printed or exported to PDF</a>.</p>
<p>A printed list collection will print all the notes in the collection,
in the right order and preserving the full hierarchy.</p>
<p>If exported to PDF within the desktop application, there is additional
functionality:</p>
<ul>
<li data-list-item-id="ec4b9a29dd7f601d1415b3ca9fa414fde">The table of contents of the PDF will reflect the structure of the notes.</li>
<li
data-list-item-id="ef5fa5e9c68e7cbdf9a5e468b406e298a">Reference and inline links to other notes within the same hierarchy will
<li>The table of contents of the PDF will reflect the structure of the notes.</li>
<li>Reference and inline links to other notes within the same hierarchy will
be functional (will jump to the corresponding page). If a link refers to
a note that is not in the printed hierarchy, it will be unlinked.</li>
</ul>

View File

@ -1,12 +1,13 @@
<ul>
<li><code>doRender</code> must not be overridden, instead <code>doRenderBody()</code> has
<li data-list-item-id="ea7daf7caa74a0f97f6f17625eacc6125"><code>doRender</code> must not be overridden, instead <code>doRenderBody()</code> has
to be overridden.
<ul>
<li><code>doRenderBody</code> can optionally be <code>async</code>.</li>
<li data-list-item-id="ef0f078ef265c54da5ff012f12cf7adee"><code>doRenderBody</code> can optionally be <code>async</code>.</li>
</ul>
</li>
<li><code>parentWidget()</code> must be set to <code>“rightPane”</code>.</li>
<li><code>widgetTitle()</code> getter can optionally be overriden, otherwise
<li data-list-item-id="e8b97337d5d8d708f5555b6f78fc1a993"><code>parentWidget()</code> must be set to <code>“rightPane”</code>.</li>
<li
data-list-item-id="eccb7323be12f3facd371512a19582705"><code>widgetTitle()</code> getter can optionally be overriden, otherwise
the widget will be displayed as “Untitled widget”.</li>
</ul><pre><code class="language-text-x-trilium-auto">const template = `&lt;div&gt;Hi&lt;/div&gt;`;
@ -23,12 +24,13 @@ class ToDoListWidget extends api.RightPanelWidget {
}
async refreshWithNote(note) {
this.toggleInt(false);
this.triggerCommand("reEvaluateRightPaneVisibility");
this.toggleInt(true);
this.triggerCommand("reEvaluateRightPaneVisibility");
// Do something when the note changes.
}
}
module.exports = new ToDoListWidget();</code></pre>
<p>The implementation is in <code>src/public/app/widgets/right_panel_widget.js</code>.</p>
<h2>Conditionally changing visibility</h2>
<p>In <code>refreshWithNote</code>:</p><pre><code class="language-text-x-trilium-auto">const visible = true; // replace with your own visibility logic
this.toggleInt(visible);
this.triggerCommand("reEvaluateRightPaneVisibility");</code></pre>

View File

@ -73,8 +73,7 @@ const EXTENSION_TO_MIME = new Map<string, string>([
[".ts", "text/x-typescript"],
[".excalidraw", "application/json"],
[".mermaid", "text/vnd.mermaid"],
[".mmd", "text/vnd.mermaid"],
[".mp4", "video/mp4"] // https://github.com/jshttp/mime-types/issues/138
[".mmd", "text/vnd.mermaid"]
]);
/** @returns false if MIME is not detected */

View File

@ -82,7 +82,7 @@ interface ExportAsPdfOpts {
electron.ipcMain.on("print-note", async (e, { notePath }: PrintOpts) => {
const browserWindow = await getBrowserWindowForPrinting(e, notePath, "printing");
browserWindow.webContents.print({}, (success, failureReason) => {
if (!success) {
if (!success && failureReason !== "Print job canceled") {
electron.dialog.showErrorBox(t("pdf.unable-to-print"), failureReason);
}
e.sender.send("print-done");

View File

@ -23,7 +23,7 @@
"typescript": "5.9.3",
"user-agent-data-types": "0.4.2",
"vite": "7.2.4",
"vitest": "4.0.12"
"vitest": "4.0.13"
},
"eslintConfig": {
"extends": "preact"

View File

@ -51,7 +51,8 @@
"mermaid_description": "Créez des diagrammes tels que des organigrammes, des diagrammes de classes et de séquences, des diagrammes de Gantt et bien d'autres, en utilisant la syntaxe Mermaid.",
"mindmap_title": "Carte mentale",
"mindmap_description": "Organisez vos pensées visuellement ou faites une séance de brainstorming.",
"others_list": "et autres : <0>carte de notes</0>, <1>carte de relations</1>, <2>recherches enregistrées</2>, <3>note de rendu</3> et <4>vues Web</4>."
"others_list": "et autres : <0>carte de notes</0>, <1>carte de relations</1>, <2>recherches enregistrées</2>, <3>note de rendu</3> et <4>vues Web</4>.",
"title": "Plusieurs façons de représenter vos informations"
},
"faq": {
"database_question": "Où sont les données stockées?",
@ -168,7 +169,9 @@
"board_title": "Tableau de bord",
"board_description": "Organisez vos tâches ou l'état de vos projets dans un tableau Kanban avec un moyen simple de créer de nouveaux éléments et colonnes et de modifier simplement leur état en les faisant glisser sur le tableau.",
"geomap_title": "Géocarte",
"geomap_description": "Planifiez vos vacances ou marquez vos points d'intérêt directement sur une carte géographique grâce à des marqueurs personnalisables. Affichez les traces GPX enregistrées pour suivre vos itinéraires."
"geomap_description": "Planifiez vos vacances ou marquez vos points d'intérêt directement sur une carte géographique grâce à des marqueurs personnalisables. Affichez les traces GPX enregistrées pour suivre vos itinéraires.",
"title": "Collections",
"presentation_title": "Présentation"
},
"download_now": {
"text": "Télécharger maintenant. ",

View File

@ -1,5 +1,5 @@
# Documentation
There are multiple types of documentation for Trilium:<img class="image-style-align-right" src="api/images/CJFtZbAX4Otj/Documentation_image.png" width="205" height="162">
There are multiple types of documentation for Trilium:<img class="image-style-align-right" src="api/images/gKWerx46R13O/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

@ -4062,66 +4062,80 @@
{
"type": "relation",
"name": "internalLink",
"value": "4TIF1oA4VQRO",
"value": "GTwFsgaA0lCt",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "KSZ04uQ2D1St",
"value": "mULW0Q3VojwY",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
"value": "6f9hih2hXXZk",
"value": "4TIF1oA4VQRO",
"isInheritable": false,
"position": 60
},
{
"type": "relation",
"name": "internalLink",
"value": "GTwFsgaA0lCt",
"value": "KSZ04uQ2D1St",
"isInheritable": false,
"position": 70
},
{
"type": "relation",
"name": "internalLink",
"value": "zP3PMqaG71Ct",
"value": "6f9hih2hXXZk",
"isInheritable": false,
"position": 80
},
{
"type": "relation",
"name": "internalLink",
"value": "AlhDUqhENtH7",
"value": "zP3PMqaG71Ct",
"isInheritable": false,
"position": 90
},
{
"type": "relation",
"name": "internalLink",
"value": "bwZpz2ajCEwO",
"value": "2FvYrpmOXm29",
"isInheritable": false,
"position": 100
},
{
"type": "relation",
"name": "internalLink",
"value": "KC1HB96bqqHX",
"value": "AlhDUqhENtH7",
"isInheritable": false,
"position": 110
},
{
"type": "relation",
"name": "internalLink",
"value": "0ESUbbAxVnoK",
"value": "bwZpz2ajCEwO",
"isInheritable": false,
"position": 120
},
{
"type": "relation",
"name": "internalLink",
"value": "KC1HB96bqqHX",
"isInheritable": false,
"position": 130
},
{
"type": "relation",
"name": "internalLink",
"value": "0ESUbbAxVnoK",
"isInheritable": false,
"position": 140
},
{
"type": "label",
"name": "iconClass",
@ -4135,13 +4149,6 @@
"value": "printing-and-pdf-export",
"isInheritable": false,
"position": 110
},
{
"type": "relation",
"name": "internalLink",
"value": "mULW0Q3VojwY",
"isInheritable": false,
"position": 130
}
],
"format": "markdown",
@ -10472,6 +10479,13 @@
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
"value": "NRnIZmSMc5sj",
"isInheritable": false,
"position": 30
},
{
"type": "label",
"name": "iconClass",
@ -10485,13 +10499,6 @@
"value": "list",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "NRnIZmSMc5sj",
"isInheritable": false,
"position": 40
}
],
"format": "markdown",

View File

@ -73,9 +73,12 @@ Not all <a class="reference-link" href="../../Note%20Types.md">Note Types</a> 
* For <a class="reference-link" href="../../Note%20Types/Code.md">Code</a> notes:
* Line numbers are not printed.
* Syntax highlighting is enabled, however a default theme (Visual Studio) is enforced.
* For <a class="reference-link" href="../../Collections.md">Collections</a>:
* <a class="reference-link" href="../../Collections/List%20View.md">List View</a> is supported, allowing to print multiple notes at once while preserving hierarchy (similar to a book).
* <a class="reference-link" href="../../Collections/Presentation.md">Presentation</a> is also supported, where each slide/subnote is displayed.
* For <a class="reference-link" href="../../Collections.md">Collections</a>, the following are supported:
* <a class="reference-link" href="../../Collections/List%20View.md">List View</a>, allowing to print multiple notes at once while preserving hierarchy (similar to a book).
* <a class="reference-link" href="../../Collections/Presentation.md">Presentation</a>, where each slide/sub-note is displayed.
* <a class="reference-link" href="../../Collections/Table.md">Table</a>, where the table is rendered in a print-friendly way.
* Tables that are too complex (especially if they have multiple columns) might not fit properly, however tables with a large number of rows are supported thanks to pagination.
* Consider printing in landscape mode, or using `#printLandscape` if exporting to PDF.
* The rest of the collections are not supported, but we plan to add support for all the collection types at some point.
* Using <a class="reference-link" href="../../Theme%20development/Custom%20app-wide%20CSS.md">Custom app-wide CSS</a> for printing is not longer supported, due to a more stable but isolated mechanism.
* We plan to introduce a new mechanism specifically for a print CSS.

View File

@ -20,10 +20,7 @@ class ToDoListWidget extends api.RightPanelWidget {
}
async refreshWithNote(note) {
this.toggleInt(false);
this.triggerCommand("reEvaluateRightPaneVisibility");
this.toggleInt(true);
this.triggerCommand("reEvaluateRightPaneVisibility");
// Do something when the note changes.
}
}
@ -31,3 +28,13 @@ module.exports = new ToDoListWidget();
```
The implementation is in `src/public/app/widgets/right_panel_widget.js`.
## Conditionally changing visibility
In `refreshWithNote`:
```
const visible = true; // replace with your own visibility logic
this.toggleInt(visible);
this.triggerCommand("reEvaluateRightPaneVisibility");
```

View File

@ -44,9 +44,9 @@
"@triliumnext/server": "workspace:*",
"@types/express": "5.0.5",
"@types/node": "24.10.1",
"@vitest/browser-webdriverio": "4.0.12",
"@vitest/coverage-v8": "4.0.12",
"@vitest/ui": "4.0.12",
"@vitest/browser-webdriverio": "4.0.13",
"@vitest/coverage-v8": "4.0.13",
"@vitest/ui": "4.0.13",
"chalk": "5.6.2",
"cross-env": "10.1.0",
"dpdm": "3.14.0",
@ -68,7 +68,7 @@
"upath": "2.0.1",
"vite": "7.2.4",
"vite-plugin-dts": "~4.5.0",
"vitest": "4.0.12"
"vitest": "4.0.13"
},
"license": "AGPL-3.0-only",
"author": {

View File

@ -26,19 +26,19 @@
"@ckeditor/ckeditor5-package-tools": "5.0.1",
"@typescript-eslint/eslint-plugin": "~8.47.0",
"@typescript-eslint/parser": "8.47.0",
"@vitest/browser": "4.0.12",
"@vitest/coverage-istanbul": "4.0.12",
"@vitest/browser": "4.0.13",
"@vitest/coverage-istanbul": "4.0.13",
"ckeditor5": "47.2.0",
"eslint": "9.39.1",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.2.7",
"stylelint": "16.25.0",
"stylelint": "16.26.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",
"typescript": "5.9.3",
"vite-plugin-svgo": "~2.0.0",
"vitest": "4.0.12",
"vitest": "4.0.13",
"webdriverio": "9.20.1"
},
"peerDependencies": {

View File

@ -27,19 +27,19 @@
"@ckeditor/ckeditor5-package-tools": "5.0.1",
"@typescript-eslint/eslint-plugin": "~8.47.0",
"@typescript-eslint/parser": "8.47.0",
"@vitest/browser": "4.0.12",
"@vitest/coverage-istanbul": "4.0.12",
"@vitest/browser": "4.0.13",
"@vitest/coverage-istanbul": "4.0.13",
"ckeditor5": "47.2.0",
"eslint": "9.39.1",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.2.7",
"stylelint": "16.25.0",
"stylelint": "16.26.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",
"typescript": "5.9.3",
"vite-plugin-svgo": "~2.0.0",
"vitest": "4.0.12",
"vitest": "4.0.13",
"webdriverio": "9.20.1"
},
"peerDependencies": {

View File

@ -29,19 +29,19 @@
"@ckeditor/ckeditor5-package-tools": "5.0.1",
"@typescript-eslint/eslint-plugin": "~8.47.0",
"@typescript-eslint/parser": "8.47.0",
"@vitest/browser": "4.0.12",
"@vitest/coverage-istanbul": "4.0.12",
"@vitest/browser": "4.0.13",
"@vitest/coverage-istanbul": "4.0.13",
"ckeditor5": "47.2.0",
"eslint": "9.39.1",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.2.7",
"stylelint": "16.25.0",
"stylelint": "16.26.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",
"typescript": "5.9.3",
"vite-plugin-svgo": "~2.0.0",
"vitest": "4.0.12",
"vitest": "4.0.13",
"webdriverio": "9.20.1"
},
"peerDependencies": {

View File

@ -30,19 +30,19 @@
"@ckeditor/ckeditor5-package-tools": "5.0.1",
"@typescript-eslint/eslint-plugin": "~8.47.0",
"@typescript-eslint/parser": "8.47.0",
"@vitest/browser": "4.0.12",
"@vitest/coverage-istanbul": "4.0.12",
"@vitest/browser": "4.0.13",
"@vitest/coverage-istanbul": "4.0.13",
"ckeditor5": "47.2.0",
"eslint": "9.39.1",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.2.7",
"stylelint": "16.25.0",
"stylelint": "16.26.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",
"typescript": "5.9.3",
"vite-plugin-svgo": "~2.0.0",
"vitest": "4.0.12",
"vitest": "4.0.13",
"webdriverio": "9.20.1"
},
"peerDependencies": {

View File

@ -29,19 +29,19 @@
"@ckeditor/ckeditor5-package-tools": "5.0.1",
"@typescript-eslint/eslint-plugin": "~8.47.0",
"@typescript-eslint/parser": "8.47.0",
"@vitest/browser": "4.0.12",
"@vitest/coverage-istanbul": "4.0.12",
"@vitest/browser": "4.0.13",
"@vitest/coverage-istanbul": "4.0.13",
"ckeditor5": "47.2.0",
"eslint": "9.39.1",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.2.7",
"stylelint": "16.25.0",
"stylelint": "16.26.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",
"typescript": "5.9.3",
"vite-plugin-svgo": "~2.0.0",
"vitest": "4.0.12",
"vitest": "4.0.13",
"webdriverio": "9.20.1"
},
"peerDependencies": {

487
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff