Merge branch 'develop' into tray

This commit is contained in:
SiriusXT
2025-03-21 11:12:03 +08:00
107 changed files with 12990 additions and 5086 deletions

View File

@@ -22,7 +22,6 @@ import type LoadResults from "../services/load_results.js";
import type { Attribute } from "../services/attribute_parser.js";
import type NoteTreeWidget from "../widgets/note_tree.js";
import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js";
import type { ContextMenuEvent } from "../menus/context_menu.js";
import type TypeWidget from "../widgets/type_widgets/type_widget.js";
import type EditableTextTypeWidget from "../widgets/type_widgets/editable_text.js";
import type FAttribute from "../entities/fattribute.js";
@@ -58,8 +57,8 @@ export interface ContextMenuCommandData extends CommandData {
}
export interface NoteCommandData extends CommandData {
notePath?: string;
hoistedNoteId?: string;
notePath?: string | null;
hoistedNoteId?: string | null;
viewScope?: ViewScope;
}
@@ -174,9 +173,9 @@ export type CommandMappings = {
callback: (value: NoteDetailWidget | PromiseLike<NoteDetailWidget>) => void;
};
executeWithTextEditor: CommandData &
ExecuteCommandData<TextEditor> & {
callback?: GetTextEditorCallback;
};
ExecuteCommandData<TextEditor> & {
callback?: GetTextEditorCallback;
};
executeWithCodeEditor: CommandData & ExecuteCommandData<CodeMirrorInstance>;
/**
* Called upon when attempting to retrieve the content element of a {@link NoteContext}.
@@ -297,16 +296,13 @@ type EventMappings = {
noteContext: NoteContext;
notePath?: string | null;
};
noteSwitchedAndActivatedEvent: {
noteSwitchedAndActivated: {
noteContext: NoteContext;
notePath: string;
};
setNoteContext: {
noteContext: NoteContext;
};
noteTypeMimeChangedEvent: {
noteId: string;
};
reEvaluateHighlightsListWidgetVisibility: {
noteId: string | undefined;
};
@@ -327,14 +323,14 @@ type EventMappings = {
noteId: string;
ntxId: string | null;
};
contextsReopenedEvent: {
mainNtxId: string;
contextsReopened: {
mainNtxId: string | null;
tabPosition: number;
};
noteDetailRefreshed: {
ntxId?: string | null;
};
noteContextReorderEvent: {
noteContextReorder: {
oldMainNtxId: string;
newMainNtxId: string;
ntxIdsInOrder: string[];
@@ -342,7 +338,7 @@ type EventMappings = {
newNoteContextCreated: {
noteContext: NoteContext;
};
noteContextRemovedEvent: {
noteContextRemoved: {
ntxIds: string[];
};
exportSvg: {
@@ -363,12 +359,17 @@ type EventMappings = {
relationMapResetPanZoom: { ntxId: string | null | undefined };
relationMapResetZoomIn: { ntxId: string | null | undefined };
relationMapResetZoomOut: { ntxId: string | null | undefined };
activeNoteChangedEvent: {};
activeNoteChanged: {};
showAddLinkDialog: {
textTypeWidget: EditableTextTypeWidget;
text: string;
};
openBulkActionsDialog: {
selectedOrActiveNoteIds: string[];
};
cloneNoteIdsTo: {
noteIds: string[];
};
};
export type EventListener<T extends EventNames> = {

View File

@@ -66,12 +66,13 @@ export default class Entrypoints extends Component {
}
async toggleNoteHoistingCommand({ noteId = appContext.tabManager.getActiveContextNoteId() }) {
if (!noteId) {
const activeNoteContext = appContext.tabManager.getActiveContext();
if (!activeNoteContext || !noteId) {
return;
}
const noteToHoist = await froca.getNote(noteId);
const activeNoteContext = appContext.tabManager.getActiveContext();
if (noteToHoist?.noteId === activeNoteContext.hoistedNoteId) {
await activeNoteContext.unhoist();
@@ -83,6 +84,11 @@ export default class Entrypoints extends Component {
async hoistNoteCommand({ noteId }: { noteId: string }) {
const noteContext = appContext.tabManager.getActiveContext();
if (!noteContext) {
logError("hoistNoteCommand: noteContext is null");
return;
}
if (noteContext.hoistedNoteId !== noteId) {
await noteContext.setHoistedNoteId(noteId);
}
@@ -174,7 +180,11 @@ export default class Entrypoints extends Component {
}
async runActiveNoteCommand() {
const { ntxId, note } = appContext.tabManager.getActiveContext();
const noteContext = appContext.tabManager.getActiveContext();
if (!noteContext) {
return;
}
const { ntxId, note } = noteContext;
// ctrl+enter is also used elsewhere, so make sure we're running only when appropriate
if (!note || note.type !== "code") {

View File

@@ -1,4 +1,4 @@
import appContext from "./app_context.js";
import appContext, { type EventData } from "./app_context.js";
import noteCreateService from "../services/note_create.js";
import treeService from "../services/tree.js";
import hoistedNoteService from "../services/hoisted_note.js";
@@ -14,23 +14,19 @@ export default class MainTreeExecutors extends Component {
return appContext.noteTreeWidget;
}
async cloneNotesToCommand() {
async cloneNotesToCommand({ selectedOrActiveNoteIds }: EventData<"cloneNotesTo">) {
if (!this.tree) {
return;
}
const selectedOrActiveNoteIds = this.tree.getSelectedOrActiveNodes().map((node) => node.data.noteId);
this.triggerCommand("cloneNoteIdsTo", { noteIds: selectedOrActiveNoteIds });
}
async moveNotesToCommand() {
async moveNotesToCommand({ selectedOrActiveBranchIds }: EventData<"moveNotesTo">) {
if (!this.tree) {
return;
}
const selectedOrActiveBranchIds = this.tree.getSelectedOrActiveNodes().map((node) => node.data.branchId);
this.triggerCommand("moveBranchIdsTo", { branchIds: selectedOrActiveBranchIds });
}

View File

@@ -4,23 +4,40 @@ import server from "../services/server.js";
import options from "../services/options.js";
import froca from "../services/froca.js";
import treeService from "../services/tree.js";
import utils from "../services/utils.js";
import NoteContext from "./note_context.js";
import appContext from "./app_context.js";
import Mutex from "../utils/mutex.js";
import linkService from "../services/link.js";
import type { EventData } from "./app_context.js";
import type FNote from "../entities/fnote.js";
interface TabState {
contexts: NoteContext[];
position: number;
}
interface NoteContextState {
ntxId: string;
mainNtxId: string | null;
notePath: string | null;
hoistedNoteId: string;
active: boolean;
viewScope: Record<string, any>;
}
export default class TabManager extends Component {
public children: NoteContext[];
public mutex: Mutex;
public activeNtxId: string | null;
public recentlyClosedTabs: TabState[];
public tabsUpdate: SpacedUpdate;
constructor() {
super();
/** @property {NoteContext[]} */
this.children = [];
this.mutex = new Mutex();
this.activeNtxId = null;
// elements are arrays of {contexts, position}, storing note contexts for each tab (one main context + subcontexts [splits]), and the original position of the tab
this.recentlyClosedTabs = [];
this.tabsUpdate = new SpacedUpdate(async () => {
@@ -28,7 +45,9 @@ export default class TabManager extends Component {
return;
}
const openNoteContexts = this.noteContexts.map((nc) => nc.getPojoState()).filter((t) => !!t);
const openNoteContexts = this.noteContexts
.map((nc) => nc.getPojoState())
.filter((t) => !!t);
await server.put("options", {
openNoteContexts: JSON.stringify(openNoteContexts)
@@ -38,13 +57,11 @@ export default class TabManager extends Component {
appContext.addBeforeUnloadListener(this);
}
/** @returns {NoteContext[]} */
get noteContexts() {
get noteContexts(): NoteContext[] {
return this.children;
}
/** @type {NoteContext[]} */
get mainNoteContexts() {
get mainNoteContexts(): NoteContext[] {
return this.noteContexts.filter((nc) => !nc.mainNtxId);
}
@@ -53,11 +70,12 @@ export default class TabManager extends Component {
const noteContextsToOpen = (appContext.isMainWindow && options.getJson("openNoteContexts")) || [];
// preload all notes at once
await froca.getNotes([...noteContextsToOpen.flatMap((tab) => [treeService.getNoteIdFromUrl(tab.notePath), tab.hoistedNoteId])], true);
await froca.getNotes([...noteContextsToOpen.flatMap((tab: NoteContextState) =>
[treeService.getNoteIdFromUrl(tab.notePath), tab.hoistedNoteId])], true);
const filteredNoteContexts = noteContextsToOpen.filter((openTab) => {
const filteredNoteContexts = noteContextsToOpen.filter((openTab: NoteContextState) => {
const noteId = treeService.getNoteIdFromUrl(openTab.notePath);
if (!(noteId in froca.notes)) {
if (noteId && !(noteId in froca.notes)) {
// note doesn't exist so don't try to open tab for it
return false;
}
@@ -82,7 +100,7 @@ export default class TabManager extends Component {
hoistedNoteId: parsedFromUrl.hoistedNoteId || "root",
viewScope: parsedFromUrl.viewScope || {}
});
} else if (!filteredNoteContexts.find((tab) => tab.active)) {
} else if (!filteredNoteContexts.find((tab: NoteContextState) => tab.active)) {
filteredNoteContexts[0].active = true;
}
@@ -101,21 +119,30 @@ export default class TabManager extends Component {
// if there's a notePath in the URL, make sure it's open and active
// (useful, for e.g., opening clipped notes from clipper or opening link in an extra window)
if (parsedFromUrl.notePath) {
await appContext.tabManager.switchToNoteContext(parsedFromUrl.ntxId, parsedFromUrl.notePath, parsedFromUrl.viewScope, parsedFromUrl.hoistedNoteId);
await appContext.tabManager.switchToNoteContext(
parsedFromUrl.ntxId,
parsedFromUrl.notePath,
parsedFromUrl.viewScope,
parsedFromUrl.hoistedNoteId
);
} else if (parsedFromUrl.searchString) {
await appContext.triggerCommand("searchNotes", {
searchString: parsedFromUrl.searchString
});
}
} catch (e) {
logError(`Loading note contexts '${options.get("openNoteContexts")}' failed: ${e.message} ${e.stack}`);
} catch (e: unknown) {
if (e instanceof Error) {
logError(`Loading note contexts '${options.get("openNoteContexts")}' failed: ${e.message} ${e.stack}`);
} else {
logError(`Loading note contexts '${options.get("openNoteContexts")}' failed: ${String(e)}`);
}
// try to recover
await this.openEmptyTab();
}
}
noteSwitchedEvent({ noteContext }) {
noteSwitchedEvent({ noteContext }: EventData<"noteSwitched">) {
if (noteContext.isActive()) {
this.setCurrentNavigationStateToHash();
}
@@ -135,10 +162,10 @@ export default class TabManager extends Component {
const activeNoteContext = this.getActiveContext();
this.updateDocumentTitle(activeNoteContext);
this.triggerEvent("activeNoteChanged"); // trigger this even in on popstate event
this.triggerEvent("activeNoteChanged", {}); // trigger this even in on popstate event
}
calculateHash() {
calculateHash(): string {
const activeNoteContext = this.getActiveContext();
if (!activeNoteContext) {
return "";
@@ -152,21 +179,15 @@ export default class TabManager extends Component {
});
}
/** @returns {NoteContext[]} */
getNoteContexts() {
getNoteContexts(): NoteContext[] {
return this.noteContexts;
}
/**
* Main context is essentially a tab (children are splits), so this returns tabs.
* @returns {NoteContext[]}
*/
getMainNoteContexts() {
getMainNoteContexts(): NoteContext[] {
return this.noteContexts.filter((nc) => nc.isMainContext());
}
/** @returns {NoteContext} */
getNoteContextById(ntxId) {
getNoteContextById(ntxId: string | null): NoteContext {
const noteContext = this.noteContexts.find((nc) => nc.ntxId === ntxId);
if (!noteContext) {
@@ -176,58 +197,47 @@ export default class TabManager extends Component {
return noteContext;
}
/**
* Get active context which represents the visible split with focus. Active context can, but doesn't have to be "main".
*
* @returns {NoteContext}
*/
getActiveContext() {
getActiveContext(): NoteContext | null {
return this.activeNtxId ? this.getNoteContextById(this.activeNtxId) : null;
}
/**
* Get active main context which corresponds to the active tab.
*
* @returns {NoteContext}
*/
getActiveMainContext() {
getActiveMainContext(): NoteContext | null {
return this.activeNtxId ? this.getNoteContextById(this.activeNtxId).getMainContext() : null;
}
/** @returns {string|null} */
getActiveContextNotePath() {
getActiveContextNotePath(): string | null {
const activeContext = this.getActiveContext();
return activeContext ? activeContext.notePath : null;
return activeContext?.notePath ?? null;
}
/** @returns {FNote} */
getActiveContextNote() {
getActiveContextNote(): FNote | null {
const activeContext = this.getActiveContext();
return activeContext ? activeContext.note : null;
}
/** @returns {string|null} */
getActiveContextNoteId() {
getActiveContextNoteId(): string | null {
const activeNote = this.getActiveContextNote();
return activeNote ? activeNote.noteId : null;
}
/** @returns {string|null} */
getActiveContextNoteType() {
getActiveContextNoteType(): string | null {
const activeNote = this.getActiveContextNote();
return activeNote ? activeNote.type : null;
}
/** @returns {string|null} */
getActiveContextNoteMime() {
const activeNote = this.getActiveContextNote();
getActiveContextNoteMime(): string | null {
const activeNote = this.getActiveContextNote();
return activeNote ? activeNote.mime : null;
}
async switchToNoteContext(ntxId, notePath, viewScope = {}, hoistedNoteId = null) {
const noteContext = this.noteContexts.find((nc) => nc.ntxId === ntxId) || (await this.openEmptyTab());
async switchToNoteContext(
ntxId: string | null,
notePath: string,
viewScope: Record<string, any> = {},
hoistedNoteId: string | null = null
) {
const noteContext = this.noteContexts.find((nc) => nc.ntxId === ntxId) ||
await this.openEmptyTab();
await this.activateNoteContext(noteContext.ntxId);
@@ -242,20 +252,21 @@ export default class TabManager extends Component {
async openAndActivateEmptyTab() {
const noteContext = await this.openEmptyTab();
await this.activateNoteContext(noteContext.ntxId);
await noteContext.setEmpty();
noteContext.setEmpty();
}
async openEmptyTab(ntxId = null, hoistedNoteId = "root", mainNtxId) {
async openEmptyTab(
ntxId: string | null = null,
hoistedNoteId: string = "root",
mainNtxId: string | null = null
): Promise<NoteContext> {
const noteContext = new NoteContext(ntxId, hoistedNoteId, mainNtxId);
const existingNoteContext = this.children.find((nc) => nc.ntxId === noteContext.ntxId);
if (existingNoteContext) {
await existingNoteContext.setHoistedNoteId(hoistedNoteId);
return existingNoteContext;
}
@@ -266,29 +277,37 @@ export default class TabManager extends Component {
return noteContext;
}
async openInNewTab(targetNoteId, hoistedNoteId = null) {
const noteContext = await this.openEmptyTab(null, hoistedNoteId || this.getActiveContext().hoistedNoteId);
async openInNewTab(targetNoteId: string, hoistedNoteId: string | null = null) {
const noteContext = await this.openEmptyTab(null, hoistedNoteId || this.getActiveContext()?.hoistedNoteId);
await noteContext.setNote(targetNoteId);
}
async openInSameTab(targetNoteId, hoistedNoteId = null) {
async openInSameTab(targetNoteId: string, hoistedNoteId: string | null = null) {
const activeContext = this.getActiveContext();
if (!activeContext) return;
await activeContext.setHoistedNoteId(hoistedNoteId || activeContext.hoistedNoteId);
await activeContext.setNote(targetNoteId);
}
/**
* If the requested notePath is within current note hoisting scope then keep the note hoisting also for the new tab.
*/
async openTabWithNoteWithHoisting(notePath, opts = {}) {
async openTabWithNoteWithHoisting(
notePath: string,
opts: {
activate?: boolean | null;
ntxId?: string | null;
mainNtxId?: string | null;
hoistedNoteId?: string | null;
viewScope?: Record<string, any> | null;
} = {}
): Promise<NoteContext> {
const noteContext = this.getActiveContext();
let hoistedNoteId = "root";
if (noteContext) {
const resolvedNotePath = await treeService.resolveNotePath(notePath, noteContext.hoistedNoteId);
if (resolvedNotePath.includes(noteContext.hoistedNoteId) || resolvedNotePath.includes("_hidden")) {
if (resolvedNotePath?.includes(noteContext.hoistedNoteId) || resolvedNotePath?.includes("_hidden")) {
hoistedNoteId = noteContext.hoistedNoteId;
}
}
@@ -298,7 +317,16 @@ export default class TabManager extends Component {
return this.openContextWithNote(notePath, opts);
}
async openContextWithNote(notePath, opts = {}) {
async openContextWithNote(
notePath: string | null,
opts: {
activate?: boolean | null;
ntxId?: string | null;
mainNtxId?: string | null;
hoistedNoteId?: string | null;
viewScope?: Record<string, any> | null;
} = {}
): Promise<NoteContext> {
const activate = !!opts.activate;
const ntxId = opts.ntxId || null;
const mainNtxId = opts.mainNtxId || null;
@@ -306,7 +334,6 @@ export default class TabManager extends Component {
const viewScope = opts.viewScope || { viewMode: "default" };
const noteContext = await this.openEmptyTab(ntxId, hoistedNoteId, mainNtxId);
if (notePath) {
await noteContext.setNote(notePath, {
// if activate is false, then send normal noteSwitched event
@@ -315,7 +342,7 @@ export default class TabManager extends Component {
});
}
if (activate) {
if (activate && noteContext.notePath) {
this.activateNoteContext(noteContext.ntxId, false);
await this.triggerEvent("noteSwitchedAndActivated", {
@@ -327,21 +354,24 @@ export default class TabManager extends Component {
return noteContext;
}
async activateOrOpenNote(noteId) {
async activateOrOpenNote(noteId: string) {
for (const noteContext of this.getNoteContexts()) {
if (noteContext.note && noteContext.note.noteId === noteId) {
this.activateNoteContext(noteContext.ntxId);
return;
}
}
// if no tab with this note has been found we'll create new tab
await this.openContextWithNote(noteId, { activate: true });
}
async activateNoteContext(ntxId, triggerEvent = true) {
async activateNoteContext(ntxId: string | null, triggerEvent: boolean = true) {
if (!ntxId) {
logError("activateNoteContext: ntxId is null");
return;
}
if (ntxId === this.activeNtxId) {
return;
}
@@ -359,14 +389,10 @@ export default class TabManager extends Component {
this.setCurrentNavigationStateToHash();
}
/**
* @param ntxId
* @returns {Promise<boolean>} true if note context has been removed, false otherwise
*/
async removeNoteContext(ntxId) {
async removeNoteContext(ntxId: string | null): Promise<boolean> {
// removing note context is an async process which can take some time, if users presses CTRL-W quickly, two
// close events could interleave which would then lead to attempting to activate already removed context.
return await this.mutex.runExclusively(async () => {
return await this.mutex.runExclusively(async (): Promise<boolean> => {
let noteContextToRemove;
try {
@@ -399,7 +425,7 @@ export default class TabManager extends Component {
const noteContextsToRemove = noteContextToRemove.getSubContexts();
const ntxIdsToRemove = noteContextsToRemove.map((nc) => nc.ntxId);
await this.triggerEvent("beforeNoteContextRemove", { ntxIds: ntxIdsToRemove });
await this.triggerEvent("beforeNoteContextRemove", { ntxIds: ntxIdsToRemove.filter((id) => id !== null) });
if (!noteContextToRemove.isMainContext()) {
const siblings = noteContextToRemove.getMainContext().getSubContexts();
@@ -421,12 +447,11 @@ export default class TabManager extends Component {
}
this.removeNoteContexts(noteContextsToRemove);
return true;
});
}
removeNoteContexts(noteContextsToRemove) {
removeNoteContexts(noteContextsToRemove: NoteContext[]) {
const ntxIdsToRemove = noteContextsToRemove.map((nc) => nc.ntxId);
const position = this.noteContexts.findIndex((nc) => ntxIdsToRemove.includes(nc.ntxId));
@@ -435,12 +460,12 @@ export default class TabManager extends Component {
this.addToRecentlyClosedTabs(noteContextsToRemove, position);
this.triggerEvent("noteContextRemoved", { ntxIds: ntxIdsToRemove });
this.triggerEvent("noteContextRemoved", { ntxIds: ntxIdsToRemove.filter((id) => id !== null) });
this.tabsUpdate.scheduleUpdate();
}
addToRecentlyClosedTabs(noteContexts, position) {
addToRecentlyClosedTabs(noteContexts: NoteContext[], position: number) {
if (noteContexts.length === 1 && noteContexts[0].isEmpty()) {
return;
}
@@ -448,26 +473,42 @@ export default class TabManager extends Component {
this.recentlyClosedTabs.push({ contexts: noteContexts, position: position });
}
tabReorderEvent({ ntxIdsInOrder }) {
const order = {};
tabReorderEvent({ ntxIdsInOrder }: { ntxIdsInOrder: string[] }) {
const order: Record<string, number> = {};
let i = 0;
for (const ntxId of ntxIdsInOrder) {
for (const noteContext of this.getNoteContextById(ntxId).getSubContexts()) {
order[noteContext.ntxId] = i++;
if (noteContext.ntxId) {
order[noteContext.ntxId] = i++;
}
}
}
this.children.sort((a, b) => (order[a.ntxId] < order[b.ntxId] ? -1 : 1));
this.children.sort((a, b) => {
if (!a.ntxId || !b.ntxId) return 0;
return (order[a.ntxId] ?? 0) < (order[b.ntxId] ?? 0) ? -1 : 1;
});
this.tabsUpdate.scheduleUpdate();
}
noteContextReorderEvent({ ntxIdsInOrder, oldMainNtxId, newMainNtxId }) {
noteContextReorderEvent({
ntxIdsInOrder,
oldMainNtxId,
newMainNtxId
}: {
ntxIdsInOrder: string[];
oldMainNtxId?: string;
newMainNtxId?: string;
}) {
const order = Object.fromEntries(ntxIdsInOrder.map((v, i) => [v, i]));
this.children.sort((a, b) => (order[a.ntxId] < order[b.ntxId] ? -1 : 1));
this.children.sort((a, b) => {
if (!a.ntxId || !b.ntxId) return 0;
return (order[a.ntxId] ?? 0) < (order[b.ntxId] ?? 0) ? -1 : 1;
});
if (oldMainNtxId && newMainNtxId) {
this.children.forEach((c) => {
@@ -485,7 +526,8 @@ export default class TabManager extends Component {
}
async activateNextTabCommand() {
const activeMainNtxId = this.getActiveMainContext().ntxId;
const activeMainNtxId = this.getActiveMainContext()?.ntxId;
if (!activeMainNtxId) return;
const oldIdx = this.mainNoteContexts.findIndex((nc) => nc.ntxId === activeMainNtxId);
const newActiveNtxId = this.mainNoteContexts[oldIdx === this.mainNoteContexts.length - 1 ? 0 : oldIdx + 1].ntxId;
@@ -494,7 +536,8 @@ export default class TabManager extends Component {
}
async activatePreviousTabCommand() {
const activeMainNtxId = this.getActiveMainContext().ntxId;
const activeMainNtxId = this.getActiveMainContext()?.ntxId;
if (!activeMainNtxId) return;
const oldIdx = this.mainNoteContexts.findIndex((nc) => nc.ntxId === activeMainNtxId);
const newActiveNtxId = this.mainNoteContexts[oldIdx === 0 ? this.mainNoteContexts.length - 1 : oldIdx - 1].ntxId;
@@ -506,9 +549,8 @@ export default class TabManager extends Component {
await this.removeNoteContext(this.activeNtxId);
}
beforeUnloadEvent() {
beforeUnloadEvent(): boolean {
this.tabsUpdate.updateNowIfNecessary();
return true; // don't block closing the tab, this metadata is not that important
}
@@ -522,7 +564,7 @@ export default class TabManager extends Component {
}
}
async closeOtherTabsCommand({ ntxId }) {
async closeOtherTabsCommand({ ntxId }: { ntxId: string }) {
for (const ntxIdToRemove of this.mainNoteContexts.map((nc) => nc.ntxId)) {
if (ntxIdToRemove !== ntxId) {
await this.removeNoteContext(ntxIdToRemove);
@@ -530,7 +572,7 @@ export default class TabManager extends Component {
}
}
async closeRightTabsCommand({ ntxId }) {
async closeRightTabsCommand({ ntxId }: { ntxId: string }) {
const ntxIds = this.mainNoteContexts.map((nc) => nc.ntxId);
const index = ntxIds.indexOf(ntxId);
@@ -542,11 +584,11 @@ export default class TabManager extends Component {
}
}
async closeTabCommand({ ntxId }) {
async closeTabCommand({ ntxId }: { ntxId: string }) {
await this.removeNoteContext(ntxId);
}
async moveTabToNewWindowCommand({ ntxId }) {
async moveTabToNewWindowCommand({ ntxId }: { ntxId: string }) {
const { notePath, hoistedNoteId } = this.getNoteContextById(ntxId);
const removed = await this.removeNoteContext(ntxId);
@@ -556,17 +598,16 @@ export default class TabManager extends Component {
}
}
async copyTabToNewWindowCommand({ ntxId }) {
async copyTabToNewWindowCommand({ ntxId }: { ntxId: string }) {
const { notePath, hoistedNoteId } = this.getNoteContextById(ntxId);
this.triggerCommand("openInWindow", { notePath, hoistedNoteId });
}
async reopenLastTabCommand() {
let closeLastEmptyTab = null;
await this.mutex.runExclusively(async () => {
const closeLastEmptyTab: NoteContext | undefined = await this.mutex.runExclusively(async () => {
let closeLastEmptyTab
if (this.recentlyClosedTabs.length === 0) {
return;
return closeLastEmptyTab;
}
if (this.noteContexts.length === 1 && this.noteContexts[0].isEmpty()) {
@@ -575,6 +616,8 @@ export default class TabManager extends Component {
}
const lastClosedTab = this.recentlyClosedTabs.pop();
if (!lastClosedTab) return closeLastEmptyTab;
const noteContexts = lastClosedTab.contexts;
for (const noteContext of noteContexts) {
@@ -589,7 +632,7 @@ export default class TabManager extends Component {
...this.noteContexts.slice(-noteContexts.length),
...this.noteContexts.slice(lastClosedTab.position, -noteContexts.length)
];
await this.noteContextReorderEvent({ ntxIdsInOrder: ntxsInOrder.map((nc) => nc.ntxId) });
this.noteContextReorderEvent({ ntxIdsInOrder: ntxsInOrder.map((nc) => nc.ntxId).filter((id) => id !== null) });
let mainNtx = noteContexts.find((nc) => nc.isMainContext());
if (mainNtx) {
@@ -601,13 +644,14 @@ export default class TabManager extends Component {
} else {
// reopened a single split, need to reorder the pane widget in split note container
await this.triggerEvent("contextsReopened", {
ntxId: ntxsInOrder[lastClosedTab.position].ntxId,
mainNtxId: ntxsInOrder[lastClosedTab.position].ntxId,
// this is safe since lastClosedTab.position can never be 0 in this case
afterNtxId: ntxsInOrder[lastClosedTab.position - 1].ntxId
tabPosition: lastClosedTab.position - 1
});
}
const noteContextToActivate = noteContexts.length === 1 ? noteContexts[0] : noteContexts.find((nc) => nc.isMainContext());
if (!noteContextToActivate) return closeLastEmptyTab;
await this.activateNoteContext(noteContextToActivate.ntxId);
@@ -615,6 +659,7 @@ export default class TabManager extends Component {
noteContext: noteContextToActivate,
notePath: noteContextToActivate.notePath
});
return closeLastEmptyTab;
});
if (closeLastEmptyTab) {
@@ -626,7 +671,9 @@ export default class TabManager extends Component {
this.tabsUpdate.scheduleUpdate();
}
async updateDocumentTitle(activeNoteContext) {
async updateDocumentTitle(activeNoteContext: NoteContext | null) {
if (!activeNoteContext) return;
const titleFragments = [
// it helps to navigate in history if note title is included in the title
await activeNoteContext.getNavigationTitle(),
@@ -636,7 +683,7 @@ export default class TabManager extends Component {
document.title = titleFragments.join(" - ");
}
async entitiesReloadedEvent({ loadResults }) {
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
const activeContext = this.getActiveContext();
if (activeContext && loadResults.isNoteReloaded(activeContext.noteId)) {
@@ -646,7 +693,6 @@ export default class TabManager extends Component {
async frocaReloadedEvent() {
const activeContext = this.getActiveContext();
if (activeContext) {
await this.updateDocumentTitle(activeContext);
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -18,40 +18,49 @@
</p>
<p>The Calendar view of Book notes will display each child note in a calendar
that has a start date and optionally an end date, as an event.</p>
<p>The Calendar view has multiple display modes:</p>
<ul>
<li>Week view, where all the 7 days of the week (or 5 if the weekends are
hidden) are displayed in columns. This mode allows entering and displaying
time-specific events, not just all-day events.</li>
<li>Month view, where the entire month is displayed and all-day events can
be inserted. Both time-specific events and all-day events are listed.</li>
<li>Year view, which displays the entire year for quick reference.</li>
<li>List view, which displays all the events of a given month in sequence.</li>
</ul>
<p>Unlike other Book view types, the Calendar view also allows some kind
of interaction, such as moving events around as well as creating new ones.</p>
<h2>Creating a calendar</h2>
<figure class="table">
<table>
<thead>
<tr>
<th>&nbsp;</th>
<th>&nbsp;</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>
<img src="2_Calendar View_image.png">
</td>
<td>The Calendar View works only for Book note types. To create a new note,
right click on the note tree on the left and select Insert note after,
or Insert child note and then select <em>Book</em>.</td>
</tr>
<tr>
<td>2</td>
<td>
<img src="3_Calendar View_image.png">
</td>
<td>Once created, the “View type” of the Book needs changed to “Calendar”,
by selecting the “Book Properties” tab in the ribbon.</td>
</tr>
</tbody>
</table>
</figure>
<h2>Creating a new event/note</h2>
<table>
<thead>
<tr>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>
<img src="2_Calendar View_image.png">
</td>
<td>The Calendar View works only for Book note types. To create a new note,
right click on the note tree on the left and select Insert note after,
or Insert child note and then select <em>Book</em>.</td>
</tr>
<tr>
<td>2</td>
<td>
<img src="3_Calendar View_image.png">
</td>
<td>Once created, the “View type” of the Book needs changed to “Calendar”,
by selecting the Book Properties” tab in the ribbon.</td>
</tr>
</tbody>
</table>
<h2>Creating a new event/note</h2>
<ul>
<li>Clicking on a day will create a new child note and assign it to that particular
day.
@@ -72,7 +81,7 @@
<ul>
<li>Hovering the mouse over an event will display information about the note.
<br>
<img src="9_Calendar View_image.png">
<img src="7_Calendar View_image.png">
</li>
<li>Left clicking the event will go to that note. Middle clicking will open
the note in a new tab and right click will offer more options including
@@ -83,284 +92,272 @@
</ul>
<h2>Configuring the calendar</h2>
<p>The following attributes can be added to the book type:</p>
<figure class="table">
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>#calendar:hideWeekends</code>
</td>
<td>When present (regardless of value), it will hide Saturday and Sundays
from the calendar.</td>
</tr>
<tr>
<td><code>#calendar:weekNumbers</code>
</td>
<td>When present (regardless of value), it will show the number of the week
on the calendar.</td>
</tr>
<tr>
<td><code>#calendar:view</code>
</td>
<td>
<p>Which view to display in the calendar:</p>
<ul>
<li><code>timeGridWeek</code> for the <em>week</em> view;</li>
<li><code>dayGridMonth</code> for the <em>month</em> view;</li>
<li><code>multiMonthYear</code> for the <em>year</em> view;</li>
<li><code>listMonth</code> for the <em>list</em> view.</li>
</ul>
<p>Any other value will be dismissed and the default view (month) will be
used instead.</p>
<p>The value of this label is automatically updated when changing the view
using the UI buttons.</p>
</td>
</tr>
<tr>
<td><code>~child:template</code>
</td>
<td>Defines the template for newly created notes in the calendar (via dragging
or clicking).</td>
</tr>
</tbody>
</table>
</figure>
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>#calendar:hideWeekends</code>
</td>
<td>When present (regardless of value), it will hide Saturday and Sundays
from the calendar.</td>
</tr>
<tr>
<td><code>#calendar:weekNumbers</code>
</td>
<td>When present (regardless of value), it will show the number of the week
on the calendar.</td>
</tr>
<tr>
<td><code>#calendar:view</code>
</td>
<td>
<p>Which view to display in the calendar:</p>
<ul>
<li><code>timeGridWeek</code> for the <em>week</em> view;</li>
<li><code>dayGridMonth</code> for the <em>month</em> view;</li>
<li><code>multiMonthYear</code> for the <em>year</em> view;</li>
<li><code>listMonth</code> for the <em>list</em> view.</li>
</ul>
<p>Any other value will be dismissed and the default view (month) will be
used instead.</p>
<p>The value of this label is automatically updated when changing the view
using the UI buttons.</p>
</td>
</tr>
<tr>
<td><code>~child:template</code>
</td>
<td>Defines the template for newly created notes in the calendar (via dragging
or clicking).</td>
</tr>
</tbody>
</table>
<p>In addition, the first day of the week can be either Sunday or Monday
and can be adjusted from the application settings.</p>
<h2>Configuring the calendar events</h2>
<p>For each note of the calendar, the following attributes can be used:</p>
<figure
class="table">
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>#startDate</code>
</td>
<td>The date the event starts, which will display it in the calendar. The
format is <code>YYYY-MM-DD</code> (year, month and day separated by a minus
sign).</td>
</tr>
<tr>
<td><code>#endDate</code>
</td>
<td>Similar to <code>startDate</code>, mentions the end date if the event spans
across multiple days. The date is inclusive, so the end day is also considered.
The attribute can be missing for single-day events.</td>
</tr>
<tr>
<td><code>#startTime</code>
</td>
<td>The time the event starts at. If this value is missing, then the event
is considered a full-day event. The format is <code>HH:MM</code> (hours in
24-hour format and minutes).</td>
</tr>
<tr>
<td><code>#endTime</code>
</td>
<td>Similar to <code>startTime</code>, it mentions the time at which the event
ends (in relation with <code>endDate</code> if present, or <code>startDate</code>).</td>
</tr>
<tr>
<td><code>#color</code>
</td>
<td>Displays the event with a specified color (named such as <code>red</code>, <code>gray</code> or
hex such as <code>#FF0000</code>). This will also change the color of the
note in other places such as the note tree.</td>
</tr>
<tr>
<td><code>#calendar:color</code>
</td>
<td>Similar to <code>#color</code>, but applies the color only for the event
in the calendar and not for other places such as the note tree.</td>
</tr>
<tr>
<td><code>#iconClass</code>
</td>
<td>If present, the icon of the note will be displayed to the left of the
event title.</td>
</tr>
<tr>
<td><code>#calendar:title</code>
</td>
<td>Changes the title of an event to point to an attribute of the note other
than the title, either a label (e.g. <code>#assignee</code>) or a relation
(e.g. <code>~for</code>). See <em>Advanced use-cases</em> for more information.</td>
</tr>
<tr>
<td><code>#calendar:displayedAttributes</code>
</td>
<td>Allows displaying the value of one or more attributes in the calendar
like this:&nbsp;&nbsp;
<br>
<br>
<img src="11_Calendar View_image.png">&nbsp;
<br>
<br><code>#weight="70" #Mood="Good" #calendar:displayedAttributes="weight,Mood"</code>
<br>
<br>It can also be used with relations, case in which it will display the
title of the target note:&nbsp;
<br>
<br><code>~assignee=@My assignee #calendar:displayedAttributes="assignee"</code>
</td>
</tr>
<tr>
<td><code>#calendar:startDate</code>
</td>
<td>Allows using a different label to represent the start date, other than <code>startDate</code> (e.g. <code>expiryDate</code>).
The label name <strong>must not be</strong> prefixed with <code>#</code>.
If the label is not defined for a note, the default will be used instead.</td>
</tr>
<tr>
<td><code>#calendar:endDate</code>
</td>
<td>Similar to <code>#calendar:startDate</code>, allows changing the attribute
which is being used to read the end date.</td>
</tr>
<tr>
<td><code>#calendar:startTime</code>
</td>
<td>Similar to <code>#calendar:startDate</code>, allows changing the attribute
which is being used to read the start time.</td>
</tr>
<tr>
<td><code>#calendar:endTime</code>
</td>
<td>Similar to <code>#calendar:startDate</code>, allows changing the attribute
which is being used to read the end time.</td>
</tr>
</tbody>
</table>
</figure>
<h2>How the calendar works</h2>
<p>
<img src="14_Calendar View_image.png">
</p>
<p>The calendar displays all the child notes of the book that have a <code>#startDate</code>.
An <code>#endDate</code> can optionally be added.</p>
<p>If editing the start date and end date from the note itself is desirable,
the following attributes can be added to the book note:</p><pre><code class="language-text-x-trilium-auto">#viewType=calendar #label:startDate(inheritable)="promoted,alias=Start Date,single,date"
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>#startDate</code>
</td>
<td>The date the event starts, which will display it in the calendar. The
format is <code>YYYY-MM-DD</code> (year, month and day separated by a minus
sign).</td>
</tr>
<tr>
<td><code>#endDate</code>
</td>
<td>Similar to <code>startDate</code>, mentions the end date if the event spans
across multiple days. The date is inclusive, so the end day is also considered.
The attribute can be missing for single-day events.</td>
</tr>
<tr>
<td><code>#startTime</code>
</td>
<td>The time the event starts at. If this value is missing, then the event
is considered a full-day event. The format is <code>HH:MM</code> (hours in
24-hour format and minutes).</td>
</tr>
<tr>
<td><code>#endTime</code>
</td>
<td>Similar to <code>startTime</code>, it mentions the time at which the event
ends (in relation with <code>endDate</code> if present, or <code>startDate</code>).</td>
</tr>
<tr>
<td><code>#color</code>
</td>
<td>Displays the event with a specified color (named such as <code>red</code>, <code>gray</code> or
hex such as <code>#FF0000</code>). This will also change the color of the
note in other places such as the note tree.</td>
</tr>
<tr>
<td><code>#calendar:color</code>
</td>
<td>Similar to <code>#color</code>, but applies the color only for the event
in the calendar and not for other places such as the note tree.</td>
</tr>
<tr>
<td><code>#iconClass</code>
</td>
<td>If present, the icon of the note will be displayed to the left of the
event title.</td>
</tr>
<tr>
<td><code>#calendar:title</code>
</td>
<td>Changes the title of an event to point to an attribute of the note other
than the title, can either a label or a relation (without the <code>#</code> or <code>~</code> symbol).
See <em>Use-cases</em> for more information.</td>
</tr>
<tr>
<td><code>#calendar:displayedAttributes</code>
</td>
<td>Allows displaying the value of one or more attributes in the calendar
like this:   
<br>
<br>
<img src="9_Calendar View_image.png"> 
<br>
<br><code>#weight="70" #Mood="Good" #calendar:displayedAttributes="weight,Mood"</code> 
<br>
<br>It can also be used with relations, case in which it will display the
title of the target note:  
<br>
<br><code>~assignee=@My assignee #calendar:displayedAttributes="assignee"</code>
</td>
</tr>
<tr>
<td><code>#calendar:startDate</code>
</td>
<td>Allows using a different label to represent the start date, other than <code>startDate</code> (e.g. <code>expiryDate</code>).
The label name <strong>must not be</strong> prefixed with <code>#</code>.
If the label is not defined for a note, the default will be used instead.</td>
</tr>
<tr>
<td><code>#calendar:endDate</code>
</td>
<td>Similar to <code>#calendar:startDate</code>, allows changing the attribute
which is being used to read the end date.</td>
</tr>
<tr>
<td><code>#calendar:startTime</code>
</td>
<td>Similar to <code>#calendar:startDate</code>, allows changing the attribute
which is being used to read the start time.</td>
</tr>
<tr>
<td><code>#calendar:endTime</code>
</td>
<td>Similar to <code>#calendar:startDate</code>, allows changing the attribute
which is being used to read the end time.</td>
</tr>
</tbody>
</table>
<h2>How the calendar works</h2>
<p>
<img src="11_Calendar View_image.png">
</p>
<p>The calendar displays all the child notes of the book that have a <code>#startDate</code>.
An <code>#endDate</code> can optionally be added.</p>
<p>If editing the start date and end date from the note itself is desirable,
the following attributes can be added to the book note:</p><pre><code class="language-text-x-trilium-auto">#viewType=calendar #label:startDate(inheritable)="promoted,alias=Start Date,single,date"
#label:endDate(inheritable)="promoted,alias=End Date,single,date"
#hidePromotedAttributes </code></pre>
<p>This will result in:</p>
<p>
<img src="12_Calendar View_image.png">
</p>
<p>When not used in a Journal, the calendar is recursive. That is, it will
look for events not just in its child notes but also in the children of
these child notes.</p>
<h2>Use-cases</h2>
<h3>Using with the Journal / calendar</h3>
<p>It is possible to integrate the calendar view into the Journal with day
notes. In order to do so change the note type of the Journal note (calendar
root) to Book and then select the Calendar View.</p>
<p>Based on the <code>#calendarRoot</code> (or <code>#workspaceCalendarRoot</code>)
attribute, the calendar will know that it's in a calendar and apply the
following:</p>
<ul>
<li>The calendar events are now rendered based on their <code>dateNote</code> attribute
rather than <code>startDate</code>.</li>
<li>Interactive editing such as dragging over an empty era or resizing an
event is no longer possible.</li>
<li>Clicking on the empty space on a date will automatically open that day's
note or create it if it does not exist.</li>
<li>Direct children of a day note will be displayed on the calendar despite
not having a <code>dateNote</code> attribute. Children of the child notes
will not be displayed.</li>
</ul>
<p>
<img src="10_Calendar View_image.png">
</p>
<h3>Using a different attribute as event title</h3>
<p>By default, events are displayed on the calendar by their note title.
However, it is possible to configure a different attribute to be displayed
instead.</p>
<p>To do so, assign <code>#calendar:title</code> to the child note (not the
calendar/book note), with the value being <code>#name</code> where <code>name</code> can
be any label. The attribute can also come through inheritance such as a
template attribute. If the note does not have the requested label, the
title of the note will be used instead.</p>
<figure class="table">
<table>
<thead>
<tr>
<th>&nbsp;</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<img src="5_Calendar View_image.png">
</td>
<td>
<img src="7_Calendar View_image.png">
</td>
</tr>
</tbody>
</table>
</figure>
<h3>Using a relation attribute as event title</h3>
<p>Similarly to using an attribute, use <code>#calendar:title</code> and set
it to <code>~name</code> where <code>name</code> is the name of the relation
to use.</p>
<p>Moreover, if there are more relations of the same name, they will be displayed
as multiple events coming from the same note.</p>
<figure class="table">
<table>
<thead>
<tr>
<th>&nbsp;</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<img src="6_Calendar View_image.png">
</td>
<td>
<img src="8_Calendar View_image.png">
</td>
</tr>
</tbody>
</table>
</figure>
<p>Note that it's even possible to have a <code>#calendar:title</code> on the
target note (e.g. “John Smith”) which will try to render an attribute of
it. Note that it's not possible to use a relation here as well for safety
reasons (an accidental recursion &nbsp;of attributes could cause the application
to loop infinitely).</p>
<figure class="table">
<table>
<thead>
<tr>
<th>&nbsp;</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<img src="13_Calendar View_image.png">
</td>
<td>
<img src="1_Calendar View_image.png">
</td>
</tr>
</tbody>
</table>
</figure>
<p>This will result in:</p>
<p>
<img src="10_Calendar View_image.png">
</p>
<p>When not used in a Journal, the calendar is recursive. That is, it will
look for events not just in its child notes but also in the children of
these child notes.</p>
<h2>Use-cases</h2>
<h3>Using with the Journal / calendar</h3>
<p>It is possible to integrate the calendar view into the Journal with day
notes. In order to do so change the note type of the Journal note (calendar
root) to Book and then select the Calendar View.</p>
<p>Based on the <code>#calendarRoot</code> (or <code>#workspaceCalendarRoot</code>)
attribute, the calendar will know that it's in a calendar and apply the
following:</p>
<ul>
<li>The calendar events are now rendered based on their <code>dateNote</code> attribute
rather than <code>startDate</code>.</li>
<li>Interactive editing such as dragging over an empty era or resizing an
event is no longer possible.</li>
<li>Clicking on the empty space on a date will automatically open that day's
note or create it if it does not exist.</li>
<li>Direct children of a day note will be displayed on the calendar despite
not having a <code>dateNote</code> attribute. Children of the child notes
will not be displayed.</li>
</ul>
<p>
<img src="8_Calendar View_image.png">
</p>
<h3>Using a different attribute as event title</h3>
<p>By default, events are displayed on the calendar by their note title.
However, it is possible to configure a different attribute to be displayed
instead.</p>
<p>To do so, assign <code>#calendar:title</code> to the child note (not the
calendar/book note), with the value being <code>name</code> where <code>name</code> can
be any label (make not to add the <code>#</code> prefix). The attribute can
also come through inheritance such as a template attribute. If the note
does not have the requested label, the title of the note will be used instead.</p>
<table>
<thead>
<tr>
<th> </th>
<th> </th>
</tr>
</thead>
<tbody>
<tr>
<td><pre><code class="language-text-x-trilium-auto">#startDate=2025-02-11 #endDate=2025-02-13 #name="My vacation" #calendar:title="name"</code></pre>
</td>
<td>
<img src="5_Calendar View_image.png">
</td>
</tr>
</tbody>
</table>
<h3>Using a relation attribute as event title</h3>
<p>Similarly to using an attribute, use <code>#calendar:title</code> and set
it to <code>name</code> where <code>name</code> is the name of the relation
to use.</p>
<p>Moreover, if there are more relations of the same name, they will be displayed
as multiple events coming from the same note.</p>
<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>#startDate=2025-02-14 #endDate=2025-02-15 ~for=@John Smith ~for=@Jane Doe #calendar:title="for"</code>
</td>
<td>
<img src="6_Calendar View_image.png">
</td>
</tr>
</tbody>
</table>
<p>Note that it's even possible to have a <code>#calendar:title</code> on the
target note (e.g. “John Smith”) which will try to render an attribute of
it. Note that it's not possible to use a relation here as well for safety
reasons (an accidental recursion  of attributes could cause the application
to loop infinitely).</p>
<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>#calendar:title="shortName" #shortName="John S."</code>
</td>
<td>
<img src="1_Calendar View_image.png">
</td>
</tr>
</tbody>
</table>
</div>
</div>
</body>

View File

@@ -13,62 +13,59 @@
<h1 data-trilium-h1>Admonitions</h1>
<div class="ck-content">
<div>
<div>
<figure class="image">
<img style="aspect-ratio:959/547;" src="1_Admonitions_image.png" width="959"
height="547">
</figure>
<p>Admonitions are a way to highlight information to the reader. Other names
for it include <em>call-outs</em> and <em>info/warning/alert boxes</em>.</p>
<h2>Inserting a new admonition</h2>
<h3>From the UI</h3>
<p>In the Formatting toolbar:</p>
<p>
<img src="Admonitions_image.png" width="202" height="194">
</p>
<h3>Via the keyboard</h3>
<p>It's possible to insert an admonition simply by typing:</p>
<ul>
<li><code>!!! note</code>
</li>
<li><code>!!! tip</code>
</li>
<li><code>!!! important</code>
</li>
<li><code>!!! caution</code>
</li>
<li><code>!!! warning</code>
</li>
</ul>
<p>In addition to that, it's also possible to type <code>!!!&nbsp;</code> followed
by any text, case in which a default admonition type will appear (note)
with the entered text inside it.</p>
<h2>Interaction</h2>
<p>By design, admonitions act very similar to block quotes.</p>
<ul>
<li>Selecting a text and pressing the admonition button will turn that text
into an admonition.</li>
<li>If selecting multiple admonitions, pressing the admonition button will
automatically merge them into one.</li>
</ul>
<p>Inside an admonition:</p>
<ul>
<li>Pressing <kbd>Backspace</kbd> while the admonition is empty will remove
it.</li>
<li>Pressing <kbd>Enter</kbd> will start a new paragraph. Pressing it twice
will exit out of the admonition.</li>
<li>Headings and other block content including tables can be inserted inside
the admonition.</li>
</ul>
<h2>Types of admonitions</h2>
<p>There are currently five types of admonitions: <em>Note</em>, <em>Tip</em>, <em>Important</em>, <em>Caution</em>, <em>Warning</em>.</p>
<p>These types were inspired by GitHub's support for this feature and there
are currently no plans for adjusting it or allowing the user to customize
them.</p>
<h2>Markdown support</h2>
<p>The Markdown syntax for admonitions as supported by Trilium is the one
that GitHub uses, which is as follows:</p><pre><code class="language-text-x-markdown">&gt; [!NOTE]
<p>
<img src="1_Admonitions_image.png">
</p>
<p>Admonitions are a way to highlight information to the reader. Other names
for it include <em>call-outs</em> and <em>info/warning/alert boxes</em>.</p>
<h2>Inserting a new admonition</h2>
<h3>From the UI</h3>
<p>In the Formatting toolbar:</p>
<p>
<img src="Admonitions_image.png">
</p>
<h3>Via the keyboard</h3>
<p>It's possible to insert an admonition simply by typing:</p>
<ul>
<li><code>!!! note</code>
</li>
<li><code>!!! tip</code>
</li>
<li><code>!!! important</code>
</li>
<li><code>!!! caution</code>
</li>
<li><code>!!! warning</code>
</li>
</ul>
<p>In addition to that, it's also possible to type <code>!!!</code>  followed
by any text, case in which a default admonition type will appear (note)
with the entered text inside it.</p>
<h2>Interaction</h2>
<p>By design, admonitions act very similar to block quotes.</p>
<ul>
<li>Selecting a text and pressing the admonition button will turn that text
into an admonition.</li>
<li>If selecting multiple admonitions, pressing the admonition button will
automatically merge them into one.</li>
</ul>
<p>Inside an admonition:</p>
<ul>
<li>Pressing <kbd>Backspace</kbd> while the admonition is empty will remove
it.</li>
<li>Pressing <kbd>Enter</kbd> will start a new paragraph. Pressing it twice
will exit out of the admonition.</li>
<li>Headings and other block content including tables can be inserted inside
the admonition.</li>
</ul>
<h2>Types of admonitions</h2>
<p>There are currently five types of admonitions: <em>Note</em>, <em>Tip</em>, <em>Important</em>, <em>Caution</em>, <em>Warning</em>.</p>
<p>These types were inspired by GitHub's support for this feature and there
are currently no plans for adjusting it or allowing the user to customize
them.</p>
<h2>Markdown support</h2>
<p>The Markdown syntax for admonitions as supported by Trilium is the one
that GitHub uses, which is as follows:</p><pre><code class="language-text-x-trilium-auto">&gt; [!NOTE]
&gt; This is a note.
&gt; [!TIP]
@@ -79,10 +76,8 @@
&gt; [!CAUTION]
&gt; This is a caution.</code></pre>
<p>There are currently no plans of supporting alternative admonition syntaxes
such as <code>!!! note</code>.</p>
</div>
</div>
<p>There are currently no plans of supporting alternative admonition syntaxes
such as <code>!!! note</code>.</p>
</div>
</div>
</body>

View File

@@ -9,6 +9,73 @@
<ul>
<li><a href="User%20Guide.html" target="detail">User Guide</a>
<ul>
<li>Advanced Usage
<ul>
<li><a href="User%20Guide/Advanced%20Usage/Attributes.html" target="detail">Attributes</a>
<ul>
<li><a href="User%20Guide/Advanced%20Usage/Attributes/Attribute%20Inheritance.html"
target="detail">Attribute Inheritance</a>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Attributes/Promoted%20Attributes.html"
target="detail">Promoted Attributes</a>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Attributes/Template.html" target="detail">Template</a>
</li>
</ul>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Relation%20Map.html" target="detail">Relation Map</a>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Note%20Map.html" target="detail">Note Map</a>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Sharing.html" target="detail">Sharing</a>
<ul>
<li><a href="User%20Guide/Advanced%20Usage/Sharing/Serving%20directly%20the%20content%20o.html"
target="detail">Serving directly the content of a note</a>
</li>
</ul>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Advanced%20Showcases.html" target="detail">Advanced Showcases</a>
<ul>
<li><a href="User%20Guide/Advanced%20Usage/Advanced%20Showcases/Day%20Notes.html"
target="detail">Day Notes</a>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Advanced%20Showcases/Weight%20Tracker.html"
target="detail">Weight Tracker</a>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Advanced%20Showcases/Task%20Manager.html"
target="detail">Task Manager</a>
</li>
</ul>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Custom%20Request%20Handler.html"
target="detail">Custom Request Handler</a>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Custom%20Resource%20Providers.html"
target="detail">Custom Resource Providers</a>
</li>
<li><a href="User%20Guide/Advanced%20Usage/ETAPI%20(REST%20API).html" target="detail">ETAPI (REST API)</a>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Default%20Note%20Title.html" target="detail">Default Note Title</a>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Database.html" target="detail">Database</a>
<ul>
<li><a href="User%20Guide/Advanced%20Usage/Database/Manually%20altering%20the%20database.html"
target="detail">Manually altering the database</a>
<ul>
<li><a href="User%20Guide/Advanced%20Usage/Database/Manually%20altering%20the%20database/SQL%20Console.html"
target="detail">SQL Console</a>
</li>
</ul>
</li>
</ul>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Configuration%20(config.ini%20or%20e.html"
target="detail">Configuration (config.ini or environment variables)</a>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Bulk%20actions.html" target="detail">Bulk actions</a>
</li>
</ul>
</li>
<li>Installation &amp; Setup
<ul>
<li><a href="User%20Guide/Installation%20%26%20Setup/Desktop%20Installation.html"
@@ -235,73 +302,6 @@
</li>
</ul>
</li>
<li>Advanced Usage
<ul>
<li><a href="User%20Guide/Advanced%20Usage/Attributes.html" target="detail">Attributes</a>
<ul>
<li><a href="User%20Guide/Advanced%20Usage/Attributes/Attribute%20Inheritance.html"
target="detail">Attribute Inheritance</a>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Attributes/Promoted%20Attributes.html"
target="detail">Promoted Attributes</a>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Attributes/Template.html" target="detail">Template</a>
</li>
</ul>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Relation%20Map.html" target="detail">Relation Map</a>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Note%20Map.html" target="detail">Note Map</a>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Sharing.html" target="detail">Sharing</a>
<ul>
<li><a href="User%20Guide/Advanced%20Usage/Sharing/Serving%20directly%20the%20content%20o.html"
target="detail">Serving directly the content of a note</a>
</li>
</ul>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Advanced%20Showcases.html" target="detail">Advanced Showcases</a>
<ul>
<li><a href="User%20Guide/Advanced%20Usage/Advanced%20Showcases/Day%20Notes.html"
target="detail">Day Notes</a>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Advanced%20Showcases/Weight%20Tracker.html"
target="detail">Weight Tracker</a>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Advanced%20Showcases/Task%20Manager.html"
target="detail">Task Manager</a>
</li>
</ul>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Custom%20Request%20Handler.html"
target="detail">Custom Request Handler</a>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Custom%20Resource%20Providers.html"
target="detail">Custom Resource Providers</a>
</li>
<li><a href="User%20Guide/Advanced%20Usage/ETAPI%20(REST%20API).html" target="detail">ETAPI (REST API)</a>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Default%20Note%20Title.html" target="detail">Default Note Title</a>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Database.html" target="detail">Database</a>
<ul>
<li><a href="User%20Guide/Advanced%20Usage/Database/Manually%20altering%20the%20database.html"
target="detail">Manually altering the database</a>
<ul>
<li><a href="User%20Guide/Advanced%20Usage/Database/Manually%20altering%20the%20database/SQL%20Console.html"
target="detail">SQL Console</a>
</li>
</ul>
</li>
</ul>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Configuration%20(config.ini%20or%20e.html"
target="detail">Configuration (config.ini or environment variables)</a>
</li>
<li><a href="User%20Guide/Advanced%20Usage/Bulk%20actions.html" target="detail">Bulk actions</a>
</li>
</ul>
</li>
<li>Theme development
<ul>
<li><a href="User%20Guide/Theme%20development/Creating%20a%20custom%20theme.html"

View File

@@ -25,7 +25,6 @@
border-radius: 0.5em;
padding: 1em;
margin: 1.25em 0;
margin-right: 14px;
position: relative;
overflow: hidden;
}

View File

@@ -22,13 +22,19 @@ function getItems(): MenuItem<CommandNames>[] {
function handleLinkContextMenuItem(command: string | undefined, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) {
if (!hoistedNoteId) {
hoistedNoteId = appContext.tabManager.getActiveContext().hoistedNoteId;
hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId ?? null;
}
if (command === "openNoteInNewTab") {
appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope });
} else if (command === "openNoteInNewSplit") {
const subContexts = appContext.tabManager.getActiveContext().getSubContexts();
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
if (!subContexts) {
logError("subContexts is null");
return;
}
const { ntxId } = subContexts[subContexts.length - 1];
appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope });

View File

@@ -288,11 +288,15 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent
const noteContext = ntxId ? appContext.tabManager.getNoteContextById(ntxId) : appContext.tabManager.getActiveContext();
noteContext.setNote(notePath, { viewScope }).then(() => {
if (noteContext !== appContext.tabManager.getActiveContext()) {
appContext.tabManager.activateNoteContext(noteContext.ntxId);
}
});
if (noteContext) {
noteContext.setNote(notePath, { viewScope }).then(() => {
if (noteContext !== appContext.tabManager.getActiveContext()) {
appContext.tabManager.activateNoteContext(noteContext.ntxId);
}
});
} else {
appContext.tabManager.openContextWithNote(notePath, { viewScope, activate: true });
}
}
} else if (hrefLink) {
const withinEditLink = $link?.hasClass("ck-link-actions__preview");

View File

@@ -1,6 +1,8 @@
import options from "./options.js";
import Split from "split.js"
export const DEFAULT_GUTTER_SIZE = 5;
let leftInstance: ReturnType<typeof Split> | null;
let rightInstance: ReturnType<typeof Split> | null;
@@ -26,7 +28,7 @@ function setupLeftPaneResizer(leftPaneVisible: boolean) {
if (leftPaneVisible) {
leftInstance = Split(["#left-pane", "#rest-pane"], {
sizes: [leftPaneWidth, 100 - leftPaneWidth],
gutterSize: 5,
gutterSize: DEFAULT_GUTTER_SIZE,
onDragEnd: (sizes) => options.save("leftPaneWidth", Math.round(sizes[0]))
});
}
@@ -54,7 +56,7 @@ function setupRightPaneResizer() {
if (rightPaneVisible) {
rightInstance = Split(["#center-pane", "#right-pane"], {
sizes: [100 - rightPaneWidth, rightPaneWidth],
gutterSize: 5,
gutterSize: DEFAULT_GUTTER_SIZE,
minSize: [ 300, 180 ],
onDragEnd: (sizes) => options.save("rightPaneWidth", Math.round(sizes[1]))
});

View File

@@ -138,7 +138,7 @@ function getParentProtectedStatus(node: Fancytree.FancytreeNode) {
return hoistedNoteService.isHoistedNode(node) ? false : node.getParent().data.isProtected;
}
function getNoteIdFromUrl(urlOrNotePath: string | undefined) {
function getNoteIdFromUrl(urlOrNotePath: string | null | undefined) {
if (!urlOrNotePath) {
return null;
}

View File

@@ -411,7 +411,11 @@ async function openInAppHelp($button: JQuery<HTMLElement>) {
if (inAppHelpPage) {
// Dynamic import to avoid import issues in tests.
const appContext = (await import("../components/app_context.js")).default;
const subContexts = appContext.tabManager.getActiveContext().getSubContexts();
const activeContext = appContext.tabManager.getActiveContext();
if (!activeContext) {
return;
}
const subContexts = activeContext.getSubContexts();
const targetNote = `_help_${inAppHelpPage}`;
const helpSubcontext = subContexts.find((s) => s.viewScope?.viewMode === "contextual-help");
const viewScope: ViewScope = {

View File

@@ -16,7 +16,7 @@ export default class Mutex {
return newPromise;
}
async runExclusively(cb: () => Promise<void>) {
async runExclusively<T>(cb: () => Promise<T>) {
const unlock = await this.lock();
try {

View File

@@ -12,7 +12,7 @@ export default class ClosePaneButton extends OnClickButtonWidget {
);
}
async noteContextReorderEvent({ ntxIdsInOrder }: EventData<"noteContextReorderEvent">) {
async noteContextReorderEvent({ ntxIdsInOrder }: EventData<"noteContextReorder">) {
this.refresh();
}

View File

@@ -24,7 +24,7 @@ export default class ScrollingContainer extends Container<BasicWidget> {
this.$widget.scrollTop(0);
}
async noteSwitchedAndActivatedEvent({ noteContext, notePath }: EventData<"noteSwitchedAndActivatedEvent">) {
async noteSwitchedAndActivatedEvent({ noteContext, notePath }: EventData<"noteSwitchedAndActivated">) {
this.noteContext = noteContext;
this.$widget.scrollTop(0);

View File

@@ -63,7 +63,7 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
hoistedNoteId?: string;
viewScope?: any;
}) {
const mainNtxId = appContext.tabManager.getActiveMainContext().ntxId;
const mainNtxId = appContext.tabManager.getActiveMainContext()?.ntxId;
if (!mainNtxId) {
logError("empty mainNtxId!");
@@ -76,7 +76,7 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
ntxId = mainNtxId;
}
hoistedNoteId = hoistedNoteId || appContext.tabManager.getActiveContext().hoistedNoteId;
hoistedNoteId = hoistedNoteId || appContext.tabManager.getActiveContext()?.hoistedNoteId;
const noteContext = await appContext.tabManager.openEmptyTab(null, hoistedNoteId, mainNtxId);
@@ -199,7 +199,7 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
return Promise.resolve();
}
if (widget.hasBeenAlreadyShown || name === "noteSwitchedAndActivatedEvent" || appContext.tabManager.getActiveMainContext() === noteContext.getMainContext()) {
if (widget.hasBeenAlreadyShown || name === "noteSwitchedAndActivated" || appContext.tabManager.getActiveMainContext() === noteContext.getMainContext()) {
widget.hasBeenAlreadyShown = true;
return Promise.all([

View File

@@ -8,8 +8,6 @@ import appContext from "../../components/app_context.js";
import { t } from "../../services/i18n.js";
import { Modal } from "bootstrap";
let branchId;
const TPL = `<div class="branch-prefix-dialog modal fade mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<form class="branch-prefix-form">
@@ -25,7 +23,7 @@ const TPL = `<div class="branch-prefix-dialog modal fade mx-auto" tabindex="-1"
<div class="input-group">
<input class="branch-prefix-input form-control">
<div class="branch-prefix-note-title input-group-text"></div>
<div class="branch-prefix-note-title input-group-text"></div>
</div>
</div>
</div>
@@ -38,61 +36,70 @@ const TPL = `<div class="branch-prefix-dialog modal fade mx-auto" tabindex="-1"
</div>`;
export default class BranchPrefixDialog extends BasicWidget {
private modal!: Modal;
private $form!: JQuery<HTMLElement>;
private $treePrefixInput!: JQuery<HTMLElement>;
private $noteTitle!: JQuery<HTMLElement>;
private branchId: string | null = null;
doRender() {
this.$widget = $(TPL);
this.modal = Modal.getOrCreateInstance(this.$widget);
this.modal = Modal.getOrCreateInstance(this.$widget[0]);
this.$form = this.$widget.find(".branch-prefix-form");
this.$treePrefixInput = this.$widget.find(".branch-prefix-input");
this.$noteTitle = this.$widget.find(".branch-prefix-note-title");
this.$form.on("submit", () => {
this.savePrefix();
return false;
});
this.$widget.on("shown.bs.modal", () => this.$treePrefixInput.trigger("focus"));
}
async refresh(notePath) {
async refresh(notePath: string) {
const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath);
if (!noteId || !parentNoteId) {
return;
}
branchId = await froca.getBranchId(parentNoteId, noteId);
const branch = froca.getBranch(branchId);
const newBranchId = await froca.getBranchId(parentNoteId, noteId);
if (!newBranchId) {
return;
}
this.branchId = newBranchId;
const branch = froca.getBranch(this.branchId);
if (!branch || branch.noteId === "root") {
return;
}
const parentNote = await froca.getNote(branch.parentNoteId);
if (parentNote.type === "search") {
if (!parentNote || parentNote.type === "search") {
return;
}
this.$treePrefixInput.val(branch.prefix);
this.$treePrefixInput.val(branch.prefix || "");
const noteTitle = await treeService.getNoteTitle(noteId);
this.$noteTitle.text(` - ${noteTitle}`);
}
async editBranchPrefixEvent() {
const notePath = appContext.tabManager.getActiveContextNotePath();
if (!notePath) {
return;
}
await this.refresh(notePath);
utils.openDialog(this.$widget);
}
async savePrefix() {
const prefix = this.$treePrefixInput.val();
await server.put(`branches/${branchId}/set-prefix`, { prefix: prefix });
await server.put(`branches/${this.branchId}/set-prefix`, { prefix: prefix });
this.modal.hide();

View File

@@ -5,6 +5,8 @@ import utils from "../../services/utils.js";
import server from "../../services/server.js";
import toastService from "../../services/toast.js";
import { t } from "../../services/i18n.js";
import type { EventData } from "../../components/app_context.js";
const TPL = `
<div class="bulk-actions-dialog modal mx-auto" tabindex="-1" role="dialog">
@@ -67,6 +69,13 @@ const TPL = `
</div>`;
export default class BulkActionsDialog extends BasicWidget {
private $includeDescendants!: JQuery<HTMLElement>;
private $affectedNoteCount!: JQuery<HTMLElement>;
private $availableActionList!: JQuery<HTMLElement>;
private $existingActionList!: JQuery<HTMLElement>;
private $executeButton!: JQuery<HTMLElement>;
private selectedOrActiveNoteIds: string[] | null = null;
doRender() {
this.$widget = $(TPL);
this.$includeDescendants = this.$widget.find(".include-descendants");
@@ -79,9 +88,11 @@ export default class BulkActionsDialog extends BasicWidget {
this.$widget.on("click", "[data-action-add]", async (event) => {
const actionName = $(event.target).attr("data-action-add");
if (!actionName) {
return;
}
await bulkActionService.addAction("_bulkAction", actionName);
await this.refresh();
});
@@ -93,7 +104,6 @@ export default class BulkActionsDialog extends BasicWidget {
});
toastService.showMessage(t("bulk_actions.bulk_actions_executed"), 3000);
utils.closeActiveDialog();
});
}
@@ -101,21 +111,28 @@ export default class BulkActionsDialog extends BasicWidget {
async refresh() {
this.renderAvailableActions();
if (!this.selectedOrActiveNoteIds) {
return;
}
const { affectedNoteCount } = await server.post("bulk-action/affected-notes", {
noteIds: this.selectedOrActiveNoteIds,
includeDescendants: this.$includeDescendants.is(":checked")
});
}) as { affectedNoteCount: number };
this.$affectedNoteCount.text(affectedNoteCount);
const bulkActionNote = await froca.getNote("_bulkAction");
if (!bulkActionNote) {
return;
}
const actions = bulkActionService.parseActions(bulkActionNote);
this.$existingActionList.empty();
if (actions.length > 0) {
this.$existingActionList.append(...actions.map((action) => action.render()));
this.$existingActionList.append(...actions.map((action) => action.render()).filter((action) => action !== null));
} else {
this.$existingActionList.append($("<p>").text(t("bulk_actions.none_yet")));
}
@@ -138,7 +155,7 @@ export default class BulkActionsDialog extends BasicWidget {
}
}
entitiesReloadedEvent({ loadResults }) {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
// only refreshing deleted attrs, otherwise components update themselves
if (loadResults.getAttributeRows().find((row) => row.type === "label" && row.name === "action" && row.noteId === "_bulkAction" && row.isDeleted)) {
// this may be triggered from e.g., sync without open widget, then no need to refresh the widget
@@ -148,12 +165,11 @@ export default class BulkActionsDialog extends BasicWidget {
}
}
async openBulkActionsDialogEvent({ selectedOrActiveNoteIds }) {
async openBulkActionsDialogEvent({ selectedOrActiveNoteIds }: EventData<"openBulkActionsDialog">) {
this.selectedOrActiveNoteIds = selectedOrActiveNoteIds;
this.$includeDescendants.prop("checked", false);
await this.refresh();
utils.openDialog(this.$widget);
}
}

View File

@@ -7,6 +7,8 @@ import branchService from "../../services/branches.js";
import appContext from "../../components/app_context.js";
import BasicWidget from "../basic_widget.js";
import { t } from "../../services/i18n.js";
import type { EventData } from "../../components/app_context.js";
const TPL = `
<div class="clone-to-dialog modal mx-auto" tabindex="-1" role="dialog">
@@ -48,10 +50,14 @@ const TPL = `
</div>`;
export default class CloneToDialog extends BasicWidget {
private $form!: JQuery<HTMLElement>;
private $noteAutoComplete!: JQuery<HTMLElement>;
private $clonePrefix!: JQuery<HTMLElement>;
private $noteList!: JQuery<HTMLElement>;
private clonedNoteIds: string[] | null = null;
constructor() {
super();
this.clonedNoteIds = null;
}
doRender() {
@@ -66,7 +72,6 @@ export default class CloneToDialog extends BasicWidget {
if (notePath) {
this.$widget.modal("hide");
this.cloneNotesTo(notePath);
} else {
logError(t("clone_to.no_path_to_clone_to"));
@@ -76,9 +81,9 @@ export default class CloneToDialog extends BasicWidget {
});
}
async cloneNoteIdsToEvent({ noteIds }) {
async cloneNoteIdsToEvent({ noteIds }: EventData<"cloneNoteIdsTo">) {
if (!noteIds || noteIds.length === 0) {
noteIds = [appContext.tabManager.getActiveContextNoteId()];
noteIds = [appContext.tabManager.getActiveContextNoteId() ?? ""];
}
this.clonedNoteIds = [];
@@ -90,14 +95,14 @@ export default class CloneToDialog extends BasicWidget {
}
utils.openDialog(this.$widget);
this.$noteAutoComplete.val("").trigger("focus");
this.$noteList.empty();
for (const noteId of this.clonedNoteIds) {
const note = await froca.getNote(noteId);
if (!note) {
continue;
}
this.$noteList.append($("<li>").text(note.title));
}
@@ -105,15 +110,29 @@ export default class CloneToDialog extends BasicWidget {
noteAutocompleteService.showRecentNotes(this.$noteAutoComplete);
}
async cloneNotesTo(notePath) {
async cloneNotesTo(notePath: string) {
const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath);
if (!noteId || !parentNoteId) {
return;
}
const targetBranchId = await froca.getBranchId(parentNoteId, noteId);
if (!targetBranchId || !this.clonedNoteIds) {
return;
}
for (const cloneNoteId of this.clonedNoteIds) {
await branchService.cloneNoteToBranch(cloneNoteId, targetBranchId, this.$clonePrefix.val());
await branchService.cloneNoteToBranch(cloneNoteId, targetBranchId, this.$clonePrefix.val() as string);
const clonedNote = await froca.getNote(cloneNoteId);
const targetNote = await froca.getBranch(targetBranchId).getNote();
const targetBranch = froca.getBranch(targetBranchId);
if (!clonedNote || !targetBranch) {
continue;
}
const targetNote = await targetBranch.getNote();
if (!targetNote) {
continue;
}
toastService.showMessage(t("clone_to.note_cloned", { clonedTitle: clonedNote.title, targetTitle: targetNote.title }));
}

View File

@@ -115,7 +115,10 @@ export default class RecentChangesDialog extends BasicWidget {
await ws.waitForMaxKnownEntityChangeId();
appContext.tabManager.getActiveContext().setNote(change.noteId);
const activeContext = appContext.tabManager.getActiveContext();
if (activeContext) {
activeContext.setNote(change.noteId);
}
}
});
@@ -141,7 +144,10 @@ export default class RecentChangesDialog extends BasicWidget {
// Skip clicks on the link or deleted notes
if (e.target?.nodeName !== "A" && !change.current_isDeleted) {
// Open the current note
appContext.tabManager.getActiveContext().setNote(change.noteId);
const activeContext = appContext.tabManager.getActiveContext();
if (activeContext) {
activeContext.setNote(change.noteId);
}
}
})
.toggleClass("deleted-note", !!change.current_isDeleted)

View File

@@ -85,7 +85,7 @@ export default class CodeButtonsWidget extends NoteContextAwareWidget {
this.$openTriliumApiDocsButton.toggle(note.mime.startsWith("application/javascript;env="));
}
async noteTypeMimeChangedEvent({ noteId }: EventData<"noteTypeMimeChangedEvent">) {
async noteTypeMimeChangedEvent({ noteId }: EventData<"noteTypeMimeChanged">) {
if (this.isNote(noteId)) {
await this.refresh();
}

View File

@@ -0,0 +1,37 @@
import { describe, expect, it } from "vitest";
import { byBookType, byNoteType } from "./help_button.js";
import fs from "fs";
import type { NoteMetaFile } from "../../../../services/meta/note_meta.js";
import type NoteMeta from "../../../../services/meta/note_meta.js";
describe("Help button", () => {
it("All help notes are accessible", () => {
function getNoteIds(item: NoteMeta | NoteMetaFile): string[] {
const items = [];
if ("noteId" in item && item.noteId) {
items.push(item.noteId);
}
const children = "files" in item ? item.files : item.children;
for (const child of children ?? []) {
items.push(getNoteIds(child));
}
return items.flat();
}
const allHelpNotes = [
...Object.values(byNoteType),
...Object.values(byBookType)
].filter((noteId) => noteId) as string[];
const meta: NoteMetaFile = JSON.parse(fs.readFileSync("src/public/app/doc_notes/en/User Guide/!!!meta.json", "utf-8"));
const allNoteIds = new Set(getNoteIds(meta));
for (const helpNote of allHelpNotes) {
if (!allNoteIds.has(helpNote)) {
expect.fail(`Help note with ID ${helpNote} does not exist in the in-app help.`);
}
}
});
});

View File

@@ -12,13 +12,13 @@ const TPL = `
</button>
`;
const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
canvas: null,
code: null,
contentWidget: null,
doc: null,
file: null,
geoMap: "foPEtsL51pD2",
geoMap: "81SGnPGMk7Xc",
image: null,
launcher: null,
mermaid: null,
@@ -31,10 +31,10 @@ const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
webView: null
};
const byBookType: Record<ViewTypeOptions, string | null> = {
export const byBookType: Record<ViewTypeOptions, string | null> = {
list: null,
grid: null,
calendar: "fDGg7QcJg3Xm"
calendar: "xWbu3jpNWapp"
};
export default class ContextualHelpButton extends NoteContextAwareWidget {

View File

@@ -6,17 +6,26 @@ import utils from "../services/utils.js";
import { loadElkIfNeeded, postprocessMermaidSvg } from "../services/mermaid.js";
import type FNote from "../entities/fnote.js";
import type { EventData } from "../components/app_context.js";
import ScrollingContainer from "./containers/scrolling_container.js";
import Split from "split.js";
import { DEFAULT_GUTTER_SIZE } from "../services/resizer.js";
const TPL = `<div class="mermaid-widget">
<style>
.mermaid-widget {
flex-grow: 2;
overflow: auto;
}
body.mobile .mermaid-widget {
min-height: 200px;
border-bottom: 1px solid var(--main-border-color);
padding: 20px;
margin-bottom: 10px;
flex-grow: 2;
flex-basis: 0;
border-bottom: 1px solid var(--main-border-color);
margin-bottom: 10px;
}
body.desktop .mermaid-widget + .gutter {
border-bottom: 1px solid var(--main-border-color);
}
.mermaid-render {
@@ -24,10 +33,6 @@ const TPL = `<div class="mermaid-widget">
height: 100%;
text-align: center;
}
.mermaid-render svg {
width: 95%; /* https://github.com/zadam/trilium/issues/4340 */
}
</style>
<div class="mermaid-error alert alert-warning">
@@ -46,6 +51,10 @@ export default class MermaidWidget extends NoteContextAwareWidget {
private $errorContainer!: JQuery<HTMLElement>;
private $errorMessage!: JQuery<HTMLElement>;
private dirtyAttachment?: boolean;
private zoomHandler?: () => void;
private zoomInstance?: SvgPanZoom.Instance;
private splitInstance?: Split.Instance;
private lastNote?: FNote;
isEnabled() {
return super.isEnabled() && this.note?.type === "mermaid" && this.note.isContentAvailable() && this.noteContext?.viewScope?.viewMode === "default";
@@ -60,6 +69,9 @@ export default class MermaidWidget extends NoteContextAwareWidget {
}
async refreshWithNote(note: FNote) {
const isSameNote = (this.lastNote === note);
this.cleanup();
this.$errorContainer.hide();
await libraryLoader.requireLibrary(libraryLoader.MERMAID);
@@ -69,9 +81,9 @@ export default class MermaidWidget extends NoteContextAwareWidget {
...(getMermaidConfig() as any)
});
this.$display.empty();
const wheelZoomLoaded = libraryLoader.requireLibrary(libraryLoader.WHEEL_ZOOM);
if (!isSameNote) {
this.$display.empty();
}
this.$errorContainer.hide();
@@ -93,21 +105,47 @@ export default class MermaidWidget extends NoteContextAwareWidget {
}
this.$display.html(svg);
await wheelZoomLoaded;
this.$display.attr("id", `mermaid-render-${idCounter}`);
WZoom.create(`#mermaid-render-${idCounter}`, {
maxScale: 50,
speed: 1.3,
zoomOnClick: false
});
// Fit the image to bounds.
const $svg = this.$display.find("svg");
$svg.attr("width", "100%").attr("height", "100%");
// Enable pan to zoom.
this.#setupPanZoom($svg[0], isSameNote);
} catch (e: any) {
console.warn(e);
this.#cleanUpZoom();
this.$display.empty();
this.$errorMessage.text(e.message);
this.$errorContainer.show();
}
this.#setupResizer();
this.lastNote = note;
}
cleanup() {
super.cleanup();
if (this.zoomHandler) {
$(window).off("resize", this.zoomHandler);
this.zoomHandler = undefined;
}
}
#cleanUpZoom() {
if (this.zoomInstance) {
this.zoomInstance.destroy();
this.zoomInstance = undefined;
}
}
toggleInt(show: boolean | null | undefined): void {
super.toggleInt(show);
if (!show) {
this.cleanup();
}
}
async renderSvg() {
@@ -125,6 +163,66 @@ export default class MermaidWidget extends NoteContextAwareWidget {
return postprocessMermaidSvg(svg);
}
async #setupPanZoom(svgEl: SVGElement, isSameNote: boolean) {
// Clean up
let pan = null;
let zoom = null;
if (this.zoomInstance) {
// Store pan and zoom for same note, when the user is editing the note.
if (isSameNote && this.zoomInstance) {
pan = this.zoomInstance.getPan();
zoom = this.zoomInstance.getZoom();
}
this.#cleanUpZoom();
}
const svgPanZoom = (await import("svg-pan-zoom")).default;
const zoomInstance = svgPanZoom(svgEl, {
zoomEnabled: true,
controlIconsEnabled: true
});
if (pan && zoom) {
// Restore the pan and zoom.
zoomInstance.zoom(zoom);
zoomInstance.pan(pan);
} else {
// New instance, reposition properly.
zoomInstance.center();
zoomInstance.fit();
}
this.zoomHandler = () => {
zoomInstance.resize();
zoomInstance.fit();
zoomInstance.center();
};
this.zoomInstance = zoomInstance;
$(window).on("resize", this.zoomHandler);
}
#setupResizer() {
if (!utils.isDesktop()) {
return;
}
const selfEl = this.$widget;
const scrollingContainer = this.parent?.children.find((ch) => ch instanceof ScrollingContainer)?.$widget;
if (!selfEl.length || !scrollingContainer?.length) {
return;
}
if (!this.splitInstance) {
this.splitInstance = Split([ selfEl[0], scrollingContainer[0] ], {
sizes: [ 50, 50 ],
direction: "vertical",
gutterSize: DEFAULT_GUTTER_SIZE,
onDragEnd: () => this.zoomHandler?.()
});
}
}
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (this.noteId && loadResults.isNoteContentReloaded(this.noteId)) {
this.dirtyAttachment = true;

View File

@@ -105,7 +105,7 @@ class NoteContextAwareWidget extends BasicWidget {
}
// when note is both switched and activated, this should not produce a double refresh
async noteSwitchedAndActivatedEvent({ noteContext, notePath }: EventData<"noteSwitchedAndActivatedEvent">) {
async noteSwitchedAndActivatedEvent({ noteContext, notePath }: EventData<"noteSwitchedAndActivated">) {
this.noteContext = noteContext;
// if notePath does not match, then the noteContext has been switched to another note in the meantime
@@ -119,7 +119,7 @@ class NoteContextAwareWidget extends BasicWidget {
this.noteContext = noteContext;
}
async noteTypeMimeChangedEvent({ noteId }: EventData<"noteTypeMimeChangedEvent">) {
async noteTypeMimeChangedEvent({ noteId }: EventData<"noteTypeMimeChanged">) {
if (this.isNote(noteId)) {
await this.refresh();
}

View File

@@ -140,13 +140,19 @@ export default class QuickSearchWidget extends BasicWidget {
if (!e.target || e.target.nodeName !== "A") {
// click on the link is handled by link handling, but we want the whole item clickable
appContext.tabManager.getActiveContext().setNote(note.noteId);
const activeContext = appContext.tabManager.getActiveContext();
if (activeContext) {
activeContext.setNote(note.noteId);
}
}
});
shortcutService.bindElShortcut($link, "return", () => {
this.dropdown.hide();
appContext.tabManager.getActiveContext().setNote(note.noteId);
const activeContext = appContext.tabManager.getActiveContext();
if (activeContext) {
activeContext.setNote(note.noteId);
}
});
this.$dropdownMenu.append($link);

View File

@@ -1,10 +1,10 @@
import Draggabilly, { type DraggabillyCallback, type MoveVector } from "draggabilly";
import Draggabilly, { type MoveVector } from "draggabilly";
import { t } from "../services/i18n.js";
import BasicWidget from "./basic_widget.js";
import contextMenu from "../menus/context_menu.js";
import utils from "../services/utils.js";
import keyboardActionService from "../services/keyboard_actions.js";
import appContext, { type CommandData, type CommandListenerData, type EventData } from "../components/app_context.js";
import appContext, { type CommandListenerData, type EventData } from "../components/app_context.js";
import froca from "../services/froca.js";
import attributeService from "../services/attributes.js";
import type NoteContext from "../components/note_context.js";
@@ -419,13 +419,13 @@ export default class TabRowWidget extends BasicWidget {
closeActiveTabCommand({ $el }: CommandListenerData<"closeActiveTab">) {
const ntxId = $el.closest(".note-tab").attr("data-ntx-id");
appContext.tabManager.removeNoteContext(ntxId);
appContext.tabManager.removeNoteContext(ntxId ?? null);
}
setTabCloseEvent($tab: JQuery<HTMLElement>) {
$tab.on("mousedown", (e) => {
if (e.which === 2) {
appContext.tabManager.removeNoteContext($tab.attr("data-ntx-id"));
appContext.tabManager.removeNoteContext($tab.attr("data-ntx-id") ?? null);
return true; // event has been handled
}
@@ -494,7 +494,7 @@ export default class TabRowWidget extends BasicWidget {
return $tab.attr("data-ntx-id");
}
noteContextRemovedEvent({ ntxIds }: EventData<"noteContextRemovedEvent">) {
noteContextRemovedEvent({ ntxIds }: EventData<"noteContextRemoved">) {
for (const ntxId of ntxIds) {
this.removeTab(ntxId);
}
@@ -516,7 +516,7 @@ export default class TabRowWidget extends BasicWidget {
this.draggabillyDragging.element.style.transform = "";
this.draggabillyDragging.dragEnd();
this.draggabillyDragging.isDragging = false;
this.draggabillyDragging.positionDrag = () => {}; // Prevent Draggabilly from updating tabEl.style.transform in later frames
this.draggabillyDragging.positionDrag = () => { }; // Prevent Draggabilly from updating tabEl.style.transform in later frames
this.draggabillyDragging.destroy();
this.draggabillyDragging = null;
}
@@ -628,7 +628,7 @@ export default class TabRowWidget extends BasicWidget {
return closestIndex;
}
noteSwitchedAndActivatedEvent({ noteContext }: EventData<"noteSwitchedAndActivatedEvent">) {
noteSwitchedAndActivatedEvent({ noteContext }: EventData<"noteSwitchedAndActivated">) {
this.activeContextChangedEvent();
this.updateTabById(noteContext.mainNtxId || noteContext.ntxId);
@@ -638,7 +638,7 @@ export default class TabRowWidget extends BasicWidget {
this.updateTabById(noteContext.mainNtxId || noteContext.ntxId);
}
noteContextReorderEvent({ oldMainNtxId, newMainNtxId }: EventData<"noteContextReorderEvent">) {
noteContextReorderEvent({ oldMainNtxId, newMainNtxId }: EventData<"noteContextReorder">) {
if (!oldMainNtxId || !newMainNtxId) {
// no need to update tab row
return;
@@ -649,8 +649,8 @@ export default class TabRowWidget extends BasicWidget {
this.updateTabById(newMainNtxId);
}
contextsReopenedEvent({ mainNtxId, tabPosition }: EventData<"contextsReopenedEvent">) {
if (mainNtxId === undefined || tabPosition === undefined) {
contextsReopenedEvent({ mainNtxId, tabPosition }: EventData<"contextsReopened">) {
if (!mainNtxId || !tabPosition) {
// no tab reopened
return;
}
@@ -748,7 +748,7 @@ export default class TabRowWidget extends BasicWidget {
hoistedNoteChangedEvent({ ntxId }: EventData<"hoistedNoteChanged">) {
const $tab = this.getTabById(ntxId);
if ($tab) {
if ($tab && ntxId) {
const noteContext = appContext.tabManager.getNoteContextById(ntxId);
this.updateTab($tab, noteContext);

View File

@@ -10,12 +10,17 @@ const TPL = `<div class="note-detail-doc note-detail-printable">
}
.note-detail-doc-content pre {
background-color: var(--accented-background-color);
border: 1px solid var(--main-border-color);
border: 0;
box-shadow: var(--code-block-box-shadow);
padding: 15px;
border-radius: 5px;
}
.note-detail-doc-content pre:not(.hljs) {
background-color: var(--accented-background-color);
border: 1px solid var(--main-border-color);
}
.note-detail-doc.contextual-help {
padding-bottom: 0;
}

View File

@@ -87,7 +87,10 @@ export default class EmptyTypeWidget extends TypeWidget {
return false;
}
appContext.tabManager.getActiveContext().setNote(suggestion.notePath);
const activeContext = appContext.tabManager.getActiveContext();
if (activeContext) {
activeContext.setNote(suggestion.notePath);
}
});
this.$workspaceNotes = this.$widget.find(".workspace-notes");

View File

@@ -5,6 +5,9 @@ import nodeMenu from "@mind-elixir/node-menu";
import type FNote from "../../entities/fnote.js";
import type { EventData } from "../../components/app_context.js";
// allow node-menu plugin css to be bundled by webpack
import "@mind-elixir/node-menu/dist/style.css";
const NEW_TOPIC_NAME = "";
const TPL = `
@@ -22,7 +25,7 @@ const TPL = `
height: 100%;
}
.mind-elixir .node-menu {
.map-container .node-menu {
position: absolute;
top: 60px;
right: 20px;
@@ -38,28 +41,28 @@ const TPL = `
transition: .3s all
}
.mind-elixir .node-menu.close {
.map-container .node-menu.close {
height: 29px;
width: 46px;
overflow: hidden
}
.mind-elixir .node-menu .button-container {
.map-container .node-menu .button-container {
padding: 3px 0;
direction: rtl
}
.mind-elixir .node-menu #nm-tag {
.map-container .node-menu #nm-tag {
margin-top: 20px
}
.mind-elixir .node-menu .nm-fontsize-container {
.map-container .node-menu .nm-fontsize-container {
display: flex;
justify-content: space-around;
margin-bottom: 20px
}
.mind-elixir .node-menu .nm-fontsize-container div {
.map-container .node-menu .nm-fontsize-container div {
height: 36px;
width: 36px;
display: flex;
@@ -71,12 +74,12 @@ const TPL = `
border-radius: 100%
}
.mind-elixir .node-menu .nm-fontcolor-container {
.map-container .node-menu .nm-fontcolor-container {
margin-bottom: 10px
}
.mind-elixir .node-menu input,
.mind-elixir .node-menu textarea {
.map-container .node-menu input,
.map-container .node-menu textarea {
background: var(--input-background-color);
border: 1px solid var(--panel-border-color);
border-radius: var(--bs-border-radius);
@@ -87,17 +90,17 @@ const TPL = `
box-sizing: border-box;
}
.mind-elixir .node-menu textarea {
.map-container .node-menu textarea {
resize: none
}
.mind-elixir .node-menu .split6 {
.map-container .node-menu .split6 {
display: inline-block;
width: 16.66%;
margin-bottom: 5px
}
.mind-elixir .node-menu .palette {
.map-container .node-menu .palette {
border-radius: 100%;
width: 21px;
height: 21px;
@@ -105,35 +108,35 @@ const TPL = `
margin: auto
}
.mind-elixir .node-menu .nmenu-selected,
.mind-elixir .node-menu .palette:hover {
.map-container .node-menu .nmenu-selected,
.map-container .node-menu .palette:hover {
box-shadow: tomato 0 0 0 2px;
background-color: #c7e9fa
}
.mind-elixir .node-menu .size-selected {
.map-container .node-menu .size-selected {
background-color: tomato !important;
border-color: tomato;
fill: #fff;
color: #fff
}
.mind-elixir .node-menu .size-selected svg {
.map-container .node-menu .size-selected svg {
color: #fff
}
.mind-elixir .node-menu .bof {
.map-container .node-menu .bof {
text-align: center
}
.mind-elixir .node-menu .bof span {
.map-container .node-menu .bof span {
display: inline-block;
font-size: 14px;
border-radius: 4px;
padding: 2px 5px
}
.mind-elixir .node-menu .bof .selected {
.map-container .node-menu .bof .selected {
background-color: tomato;
color: #fff
}

View File

@@ -65,10 +65,10 @@ const TPL = `
<div class="col-6">
<label for="main-font-size">${t("fonts.size")}</label>
<div class="input-group main-font-size-input-group">
<label class="input-group tn-number-unit-pair main-font-size-input-group">
<input id="main-font-size" type="number" class="main-font-size form-control options-number-input" min="50" max="200" step="10"/>
<span class="input-group-text">%</span>
</div>
</label>
</div>
</div>
@@ -83,10 +83,10 @@ const TPL = `
<div class="col-6">
<label for="tree-font-size">${t("fonts.size")}</label>
<div class="input-group tree-font-size-input-group">
<label class="input-group tn-number-unit-pair tree-font-size-input-group">
<input id="tree-font-size" type="number" class="tree-font-size form-control options-number-input" min="50" max="200" step="10"/>
<span class="input-group-text">%</span>
</div>
</label>
</div>
</div>
@@ -101,10 +101,10 @@ const TPL = `
<div class="col-6">
<label for="detail-font-size">${t("fonts.size")}</label>
<div class="input-group detail-font-size-input-group">
<label class="input-group tn-number-unit-pair detail-font-size-input-group">
<input id="detail-font-size" type="number" class="detail-font-size form-control options-number-input" min="50" max="200" step="10"/>
<span class="input-group-text">%</span>
</div>
</label>
</div>
</div>
@@ -119,10 +119,10 @@ const TPL = `
<div class="col-6">
<label for="monospace-font-size">${t("fonts.size")}</label>
<div class="input-group monospace-font-size-input-group">
<label class="input-group tn-number-unit-pair monospace-font-size-input-group">
<input id="monospace-font-size" type="number" class="monospace-font-size form-control options-number-input" min="50" max="200" step="10"/>
<span class="input-group-text">%</span>
</div>
</label>
</div>
</div>

View File

@@ -14,7 +14,10 @@ const TPL = `
<div class="form-group row">
<div class="col-md-6">
<label for="max-content-width">${t("max_content_width.max_width_label")}</label>
<input id="max-content-width" type="number" min="${MIN_VALUE}" step="10" class="max-content-width form-control options-number-input">
<label class="input-group tn-number-unit-pair">
<input id="max-content-width" type="number" min="${MIN_VALUE}" step="10" class="max-content-width form-control options-number-input">
<span class="input-group-text">${t("max_content_width.max_width_unit")}</span>
</label>
</div>
</div>

View File

@@ -10,7 +10,10 @@ const TPL = `
<div class="form-group">
<label for="auto-readonly-size-code">${t("code_auto_read_only_size.label")}</label>
<input id="auto-readonly-size-code" class="auto-readonly-size-code form-control options-number-input" type="number" min="0">
<label class="input-group tn-number-unit-pair">
<input id="auto-readonly-size-code" class="auto-readonly-size-code form-control options-number-input" type="number" min="0">
<span class="input-group-text">${t("code_auto_read_only_size.unit")}</span>
</label>
</div>
</div>`;

View File

@@ -30,12 +30,18 @@ const TPL = `
<div class="image-compression-enabled-wraper">
<div class="form-group">
<label>${t("images.max_image_dimensions")}</label>
<input class="image-max-width-height form-control options-number-input" type="number" min="1">
<label class="input-group tn-number-unit-pair">
<input class="image-max-width-height form-control options-number-input" type="number" min="1">
<span class="input-group-text">${t("images.max_image_dimensions_unit")}</span>
</label>
</div>
<div class="form-group">
<label>${t("images.jpeg_quality_description")}</label>
<input class="image-jpeg-quality form-control options-number-input" min="10" max="100" type="number">
<label class="input-group tn-number-unit-pair">
<input class="image-jpeg-quality form-control options-number-input" min="10" max="100" type="number">
<span class="input-group-text">%</span>
</label>
</div>
</div>
</div>

View File

@@ -12,7 +12,10 @@ const TPL = `
<div class="form-group">
<label>${t("revisions_snapshot_limit.snapshot_number_limit_label")}</label>
<input class="revision-snapshot-number-limit form-control options-number-input" type="number" min="-1">
<label class="input-group tn-number-unit-pair">
<input class="revision-snapshot-number-limit form-control options-number-input" type="number" min="-1">
<span class="input-group-text">${t("revisions_snapshot_limit.snapshot_number_limit_unit")}</span>
</label>
</div>
<button class="erase-excess-revision-snapshots-now-button btn btn-sm">

View File

@@ -14,11 +14,6 @@ const TPL = `
<input id="sync-server-host" class="sync-server-host form-control" placeholder="https://<host>:<port>">
</div>
<div class="form-group">
<label for="sync-server-timeout" >${t("sync_2.timeout")}</label>
<input id="sync-server-timeout" class="sync-server-timeout form-control" min="1" max="10000000" type="number" style="text-align: left;">
</div>
<div class="form-group">
<label for="sync-proxy form-control" >${t("sync_2.proxy_label")}</label>
<input id="sync-proxy form-control" class="sync-proxy form-control" placeholder="https://<host>:<port>">
@@ -27,6 +22,14 @@ const TPL = `
<p class="form-text">${t("sync_2.special_value_description")}</p>
</div>
<div class="form-group">
<label for="sync-server-timeout">${t("sync_2.timeout")}</label>
<label class="input-group tn-number-unit-pair">
<input id="sync-server-timeout" class="sync-server-timeout form-control" min="1" max="10000000" type="number" style="text-align: left;">
<span class="input-group-text">${t("sync_2.timeout_unit")}</span>
</label>
</div>
<div style="display: flex; justify-content: space-between;">
<button class="btn btn-primary">${t("sync_2.save")}</button>

View File

@@ -9,7 +9,10 @@ const TPL = `
${t("table_of_contents.description")}
<div class="form-group">
<input type="number" class="min-toc-headings form-control options-number-input options-number-input" min="0" max="9999999999999999" step="1" />
<label class="input-group tn-number-unit-pair">
<input type="number" class="min-toc-headings form-control options-number-input options-number-input" min="0" max="9999999999999999" step="1" />
<span class="input-group-text">${t("table_of_contents.unit")}</span>
</label>
</div>
<p class="form-text">${t("table_of_contents.disable_info")}</p>

View File

@@ -10,7 +10,10 @@ const TPL = `
<div class="form-group">
<label for="auto-readonly-size-text">${t("text_auto_read_only_size.label")}</label>
<input id="auto-readonly-size-text" class="auto-readonly-size-text form-control options-number-input" type="number" min="0">
<label class="input-group tn-number-unit-pair">
<input id="auto-readonly-size-text" class="auto-readonly-size-text form-control options-number-input" type="number" min="0">
<span class="input-group-text">${t("text_auto_read_only_size.unit")}</span>
</label>
</div>
</div>`;

View File

@@ -1730,7 +1730,6 @@ footer.file-footer button {
border-radius: 0.5em;
padding: 1em;
margin: 1.25em 0;
margin-right: 14px;
position: relative;
padding-left: 2.5em;
overflow: hidden;

View File

@@ -127,13 +127,24 @@
--left-pane-item-action-button-hover-shadow: 2px 2px 3px rgba(0, 0, 0, 0.15);
--left-pane-item-selected-action-button-hover-shadow: 2px 2px 10px rgba(0, 0, 0, 0.25);
--launcher-pane-background-color: #1a1a1a;
--launcher-pane-horizontal-background-color: #282828;
--launcher-pane-horizontal-border-color: rgb(22, 22, 22);
--launcher-pane-text-color: #909090;
--launcher-pane-button-hover-color: #ffffff;
--launcher-pane-button-hover-background: #ffffff1c;
--launcher-pane-button-hover-shadow: 4px 4px 4px rgba(0, 0, 0, 0.2);
/* Deprecated: now local variables in #launcher, with the values dependent on the current layout. */
--launcher-pane-background-color: unset;
--launcher-pane-text-color: unset;
--launcher-pane-vert-background-color: #1a1a1a;
--launcher-pane-vert-text-color: #909090;
--launcher-pane-vert-button-hover-color: #ffffff;
--launcher-pane-vert-button-hover-background: #ffffff1c;
--launcher-pane-vert-button-hover-shadow: 4px 4px 4px rgba(0, 0, 0, 0.2);
--launcher-pane-vert-button-focus-outline-color: var(--input-focus-outline-color);
--launcher-pane-horiz-border-color: rgb(22, 22, 22);
--launcher-pane-horiz-background-color: #282828;
--launcher-pane-horiz-text-color: #909090;
--launcher-pane-horiz-button-hover-color: #ffffff;
--launcher-pane-horiz-button-hover-background: #ffffff1c;
--launcher-pane-horiz-button-hover-shadow: unset;
--launcher-pane-horiz-button-focus-outline-color: var(--input-focus-outline-color);
--protected-session-active-icon-color: #8edd8e;
--sync-status-error-pulse-color: #f47871;
@@ -163,6 +174,14 @@
--promoted-attribute-card-background-color: var(--card-background-color);
--promoted-attribute-card-shadow-color: #000000b3;
--floating-button-shadow-color: #00000080;
--floating-button-background-color: #494949d2;
--floating-button-color: var(--button-text-color);
--floating-button-hover-background: #ffffff20;
--floating-button-hover-color: white;
--floating-button-hide-button-background: gray;
--floating-button-separator-color: #00000080;
--right-pane-item-hover-background: #ffffff26;
--right-pane-item-hover-color: white;

View File

@@ -121,13 +121,23 @@
--left-pane-item-action-button-hover-shadow: 2px 2px 3px rgba(0, 0, 0, 0.15);
--left-pane-item-selected-action-button-hover-shadow: 2px 2px 10px rgba(0, 0, 0, 0.25);
--launcher-pane-background-color: #e8e8e8;
--launcher-pane-horizontal-background-color: #fafafa;
--launcher-pane-horizontal-border-color: rgba(0, 0, 0, 0.1);
--launcher-pane-text-color: #000000bd;
--launcher-pane-button-hover-color: black;
--launcher-pane-button-hover-background: white;
--launcher-pane-button-hover-shadow: 4px 4px 4px rgba(0, 0, 0, 0.075);
/* Deprecated: now local variables in #launcher, with the values dependent on the current layout. */
--launcher-pane-background-color: unset;
--launcher-pane-text-color: unset;
--launcher-pane-vert-background-color: #e8e8e8;
--launcher-pane-vert-text-color: #000000bd;
--launcher-pane-vert-button-hover-color: black;
--launcher-pane-vert-button-hover-background: white;
--launcher-pane-vert-button-hover-shadow: 4px 4px 4px rgba(0, 0, 0, 0.075);
--launcher-pane-vert-button-focus-outline-color: var(--input-focus-outline-color);
--launcher-pane-horiz-border-color: rgba(0, 0, 0, 0.1);
--launcher-pane-horiz-background-color: #fafafa;
--launcher-pane-horiz-button-hover-color: black;
--launcher-pane-horiz-button-hover-background: var(--icon-button-hover-background);
--launcher-pane-horiz-button-hover-shadow: unset;
--launcher-pane-horiz-button-focus-outline-color: var(--input-focus-outline-color);
--protected-session-active-icon-color: #16b516;
--sync-status-error-pulse-color: #ff5528;
@@ -157,6 +167,14 @@
--promoted-attribute-card-background-color: var(--card-background-color);
--promoted-attribute-card-shadow-color: #00000033;
--floating-button-shadow-color: #0000001f;
--floating-button-background-color: #e4e4e4cc;
--floating-button-color: var(--button-text-color);
--floating-button-hover-background: #00000017;
--floating-button-hover-color: black;
--floating-button-hide-button-background: gray;
--floating-button-separator-color: #c0c0c0d1;
--new-tab-button-background: #d8d8d8;
--new-tab-button-color: #3a3a3a;
--new-tab-button-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);

View File

@@ -1,8 +1,8 @@
@import url(./forms.css);
@import url(./shell.css);
@import url(./dialogs.css);
@import url(./pages.css);
@import url(./ribbon.css);
@import url(./settings.css);
@import url(./notes/empty.css);
@import url(./notes/text.css);
@font-face {
@@ -10,6 +10,10 @@
src: url(../../fonts/Inter/Inter-VariableFont_opsz\,wght.ttf);
}
/*
* GLOBAL VARIABLES
*/
:root {
--main-font-family: "Inter", sans-serif;
@@ -26,11 +30,15 @@
--left-pane-item-selected-shadow-size: 2px;
--launcher-pane-size: 58px;
--launcher-pane-horizontal-size: 54px;
--launcher-pane-horizontal-icon-size: 20px;
--launcher-pane-button-margin: 6px;
--launcher-pane-button-gap: 3px;
--launcher-pane-vert-size: 58px;
--launcher-pane-vert-icon-size: 150%;
--launcher-pane-vert-button-margin: 6px;
--launcher-pane-vert-button-gap: 3px;
--launcher-pane-horiz-size: 54px;
--launcher-pane-horiz-icon-size: 20px;
--launcher-pane-horiz-button-margin: 8px;
--launcher-pane-horiz-button-gap: 3px;
--tree-actions-toolbar-horizontal-margin: 8px;
--tree-actions-toolbar-vertical-margin: 8px;
@@ -45,6 +53,11 @@
--center-pane-border-radius: 10px;
--floating-button-height: 34px;
--floating-button-width: 40px;
--floating-button-icon-size: 20px;
--floating-button-show-hide-button-size: 26px;
--menu-padding-size: 8px;
--menu-item-icon-vert-offset: -2px;
@@ -69,45 +82,196 @@
--tab-note-icons: true;
}
/* Tool dialogs - small dialogs without a backdrop */
div.tn-tool-dialog {
border-radius: 10px;
background: var(--tool-dialog-background-color) !important;
user-select: none;
box-shadow: 10px 10px 93px -25px var(--tool-dialog-shadow-color);
}
/*
* Note search suggestions
* MENUS
*
* Note: apply the "tn-dropdown-list" class for scrollable dropdown menus. Submenus are not
* supported when this class is used.
*/
/* List body */
.jump-to-note-dialog .jump-to-note-results .aa-suggestions,
.note-detail-empty .aa-suggestions {
padding: 0;
.dropdown-menu:not(.static) {
border-radius: var(--dropdown-border-radius);
padding: var(--menu-padding-size) !important;
font-size: 0.9rem !important;
}
/* List item */
.jump-to-note-dialog .aa-suggestions div,
.note-detail-empty .aa-suggestions div {
.dropdown-menu::-webkit-scrollbar-track {
background: var(--menu-background-color);
}
body.mobile .dropdown-menu {
backdrop-filter: var(--dropdown-backdrop-filter);
border-radius: var(--dropdown-border-radius);
position: relative;
}
body.mobile .dropdown-menu .dropdown-menu {
backdrop-filter: unset !important;
border-radius: unset !important;
}
body.desktop .dropdown-menu::before {
content: "";
backdrop-filter: var(--dropdown-backdrop-filter);
border-radius: var(--dropdown-border-radius);
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
}
body.desktop .dropdown-menu.tn-dropdown-list {
backdrop-filter: var(--dropdown-backdrop-filter);
}
body.desktop .dropdown-menu.tn-dropdown-list::before {
display: none;
}
body.desktop .dropdown-submenu .dropdown-menu::before {
content: unset;
}
body.mobile .dropdown-submenu .dropdown-menu {
background: transparent !important;
}
body.desktop .dropdown-submenu .dropdown-menu {
backdrop-filter: var(--dropdown-backdrop-filter);
background: transparent;
}
.dropdown-item,
body.mobile .dropdown-submenu .dropdown-toggle {
padding: 2px 2px 2px 8px !important;
padding-inline-end: 16px !important;
/* Note: the right padding should also accommodate the submenu arrow. */
border-radius: 6px;
padding: 6px 12px;
color: var(--menu-text-color);
cursor: default;
cursor: default !important;
}
/* Selected list item */
.jump-to-note-dialog .aa-suggestions div.aa-cursor,
.note-detail-empty .aa-suggestions div.aa-cursor {
background: var(--hover-item-background-color);
color: var(--hover-item-text-color);
body.mobile .dropdown-submenu {
padding: 0 !important;
}
body.mobile .dropdown-item:not(:last-of-type) {
margin-bottom: 0.5em;
}
body.mobile .dropdown-submenu:hover {
background: transparent !important;
}
html body .dropdown-item.disabled,
html body .dropdown-item[disabled] {
color: var(--menu-text-color) !important;
opacity: var(--menu-item-disabled-opacity);
}
/* Menu item icon */
.dropdown-item .bx {
transform: translateY(var(--menu-item-icon-vert-offset));
color: var(--menu-item-icon-color) !important;
font-size: 1.1em;
}
/* Menu item keyboard shortcut */
.dropdown-item kbd {
margin-left: 16px;
font-family: unset !important;
font-size: unset !important;
color: var(--menu-item-keyboard-shortcut-color) !important;
padding-top: 0;
}
.dropdown-divider {
position: relative;
border-color: transparent !important;
overflow: visible;
}
.dropdown-divider::after {
position: absolute;
content: "";
top: -1px;
left: calc(0px - var(--menu-padding-size));
right: calc(0px - var(--menu-padding-size));
border-top: 1px solid var(--menu-item-delimiter-color);
}
/* Menu item arrow */
.dropdown-menu .dropdown-toggle::after {
content: "\ed3b" !important;
position: absolute;
display: flex !important;
align-items: center;
justify-content: center;
top: 0;
right: 0;
margin: unset !important;
border: unset !important;
padding: 0 4px;
font-family: boxicons;
font-size: 1.2em;
color: var(--menu-item-arrow-color) !important;
}
/* Menu item group heading */
/* The heading body */
.dropdown-menu h6 {
position: relative;
background: transparent;
padding: 1em 8px 14px 8px;
text-transform: uppercase;
font-size: 0.8em;
letter-spacing: 1pt;
color: var(--menu-item-group-header-color) !important;
}
/* The delimiter line */
.dropdown-menu h6::before {
content: "";
position: absolute;
bottom: 8px;
left: calc(0px - var(--menu-padding-size));
right: calc(0px - var(--menu-padding-size));
border-top: 1px solid var(--menu-item-delimiter-color);
}
/* Static menus (used as a list, such as on the note revisions dialog) */
body.desktop .dropdown-menu.static {
box-shadow: unset;
border-radius: 4px;
border: unset;
background-color: var(--card-background-color) !important;
padding: var(--menu-padding-size) !important;
user-select: none;
}
body.desktop .dropdown-menu.static .dropdown-item.active {
--active-item-text-color: var(--menu-text-color);
}
body.desktop .dropdown-menu .dropdown-toggle::after {
height: 100%;
}
body.mobile .dropdown-menu .dropdown-toggle::after {
transform: rotate(90deg);
}
body.mobile .dropdown-menu .dropdown-item.submenu-open .dropdown-toggle::after {
transform: rotate(270deg);
}
/*
* Toasts
* TOASTS
*/
#toast-container {
#toast-container {
/* The vertical gap between toasts */
gap: 10px;
}
@@ -143,4 +307,223 @@ div.tn-tool-dialog {
#toast-container .toast .toast-body {
flex-grow: 1;
}
/*
* NOTE TOOLTIPS
*/
.tooltip .tooltip-inner:has(.note-tooltip-content) {
border-radius: 8px;
}
.note-tooltip-content {
padding: 8px;
min-height: 56px;
}
.note-tooltip-title .note-title-with-path {
display: flex;
flex-direction: column-reverse;
}
.note-tooltip-title a {
color: inherit !important;
}
.note-tooltip-title.note-no-content {
margin: 0;
}
.note-tooltip-title:not(.note-no-content) .note-title-with-path {
padding-bottom: 6px;
border-bottom: 2px solid currentColor;
}
.note-tooltip-content .note-path {
display: block;
color: var(--muted-text-color);
font-size: 0.75em;
}
.note-tooltip-content .note-tooltip-attributes {
margin-top: -4px;
font-size: 0.75em;
}
.note-tooltip-content .rendered-content {
padding-top: 12px;
}
/* NOTE PATHS */
.note-path .path-bracket {
/* Hide the leading and trailing bracket from the path */
display: none;
}
.note-path .path-delimiter {
/* Hide the path delimiters (slashes) */
display: none;
}
.note-path .path-delimiter + span::before {
/* Replace the path delimiters with arrows */
display: inline-block;
content: "\ed3b";
padding: 0 0.25em;
font-family: boxicons;
opacity: 0.75;
transform: translateY(4%);
}
/*
* NOTE LIST
*/
.note-list .note-book-card {
--note-list-horizontal-padding: 22px;
--note-list-vertical-padding: 15px;
background-color: var(--card-background-color);
border: 1px solid var(--card-border-color) !important;
box-shadow: 2px 3px 4px var(--card-shadow-color);
border-radius: 12px;
user-select: none;
padding: 0;
margin: 5px 10px 5px 0;
}
.note-list.list-view .note-book-card {
box-shadow: 0 0 3px var(--card-shadow-color);
}
.note-list.list-view .note-book-card .note-book-header .note-icon {
vertical-align: middle;
}
.note-list-wrapper .note-book-card:active {
background-color: var(--card-background-press-color);
}
.note-list-wrapper .note-book-card a {
color: inherit !important;
}
.note-list-wrapper .note-book-card .note-book-header {
font-size: 1em;
font-weight: bold;
padding: 0.5em 1rem;
border-bottom-color: var(--card-border-color);
}
.note-list-wrapper .note-book-card .note-book-header .note-icon {
font-size: 17px;
vertical-align: text-bottom;
}
.note-list-wrapper .note-book-card .note-book-header .note-book-title {
font-size: 1em;
color: var(--active-item-text-color);
vertical-align: middle;
}
.note-list-wrapper .note-book-card .note-book-header .rendered-note-attributes {
font-size: 0.7em;
font-weight: normal;
margin-bottom: 0;
}
.note-list-wrapper .note-book-card .note-book-header:last-child {
border-bottom: 0;
}
.note-list-wrapper .note-book-card .note-book-content {
padding: 0 !important;
font-size: 0.8rem;
}
.note-list-wrapper .note-book-card .note-book-content .rendered-content {
padding: 1rem;
}
.note-list-wrapper .note-book-card .note-book-content .rendered-content.text-with-ellipsis {
padding: 1rem !important;
}
.note-list-wrapper .note-book-card .note-book-content h1,
.note-list-wrapper .note-book-card .note-book-content h2,
.note-list-wrapper .note-book-card .note-book-content h3,
.note-list-wrapper .note-book-card .note-book-content h4,
.note-list-wrapper .note-book-card .note-book-content h5,
.note-list-wrapper .note-book-card .note-book-content h6 {
font-size: 1rem;
color: var(--active-item-text-color);
}
.note-list-wrapper .note-book-card .note-book-content p:last-child {
margin-bottom: 0;
}
.note-list-wrapper .note-book-card .note-book-content.type-canvas .rendered-content,
.note-list-wrapper .note-book-card .note-book-content.type-mindMap .rendered-content,
.note-list-wrapper .note-book-card .note-book-content.type-code .rendered-content,
.note-list-wrapper .note-book-card .note-book-content.type-video .rendered-content {
padding: 0;
}
.note-list-wrapper .note-book-card .note-book-content.type-code pre {
height: 100%;
padding: 1em;
}
.note-list-wrapper .note-book-card .bx {
color: var(--left-pane-icon-color) !important;
}
.note-list.grid-view .note-book-card:hover {
background: var(--card-background-color) !important;
filter: contrast(105%);
}
.note-list.grid-view .note-book-card img {
object-fit: cover !important;
width: 100%;
}
.note-list.grid-view .ck-content {
line-height: 1.3;
}
.note-list.grid-view .ck-content p {
margin-bottom: 0.5em;
}
.note-list.grid-view .ck-content figure.image {
width: 25%;
}
/*
* NOTE SEARCH SUGGESTIONS
*/
/* List body */
.jump-to-note-dialog .jump-to-note-results .aa-suggestions,
.note-detail-empty .aa-suggestions {
padding: 0;
}
/* List item */
.jump-to-note-dialog .aa-suggestions div,
.note-detail-empty .aa-suggestions div {
border-radius: 6px;
padding: 6px 12px;
color: var(--menu-text-color);
cursor: default;
}
/* Selected list item */
.jump-to-note-dialog .aa-suggestions div.aa-cursor,
.note-detail-empty .aa-suggestions div.aa-cursor {
background: var(--hover-item-background-color);
color: var(--hover-item-text-color);
}

View File

@@ -0,0 +1,398 @@
/*
* MODALS
*/
/* Modal body */
.modal .modal-content {
box-shadow: 0 .5em 3em .5em var(--modal-shadow-color);
border: 1px solid var(--modal-border-color);
background: var(--modal-background-color);
}
/* Modal header */
.modal .modal-header {
border-bottom: unset;
user-select: none;
}
.modal .modal-header .modal-title {
flex-grow: 1;
font-size: 1.2em;
color: var(--modal-title-color);
}
/* Modal and toast control buttons (close and help) */
.modal .modal-header .btn-close,
.modal .modal-header .help-button,
#toast-container .toast .toast-header .btn-close {
display: flex;
justify-content: center;
align-items: center;
margin-left: 8px;
border: 0;
border-radius: 50%;
padding: 0;
width: var(--modal-control-button-size);
height: var(--modal-control-button-size);
background: var(--modal-control-button-background);
font-size: var(--modal-control-button-size);
line-height: normal;
font-weight: normal;
color: var(--modal-control-button-color);
opacity: 1;
cursor: default;
}
.modal .modal-header .btn-close,
#toast-container .toast .toast-header .btn-close {
--modal-control-button-hover-background: var(--modal-close-button-hover-background);
}
.modal .modal-header .btn-close::after,
#toast-container .toast .toast-header .btn-close::after {
content: "\ec8d";
font-family: boxicons;
}
.modal .modal-header .help-button {
margin-right: 0;
font-size: calc(var(--modal-control-button-size) * .75);
font-family: unset;
font-weight: bold;
}
.modal .modal-header .btn-close:hover,
.modal .modal-header .help-button:hover,
#toast-container .toast .toast-header .btn-close:hover {
background: var(--modal-control-button-hover-background);
color: var(--modal-control-button-hover-color);
}
.modal .modal-header .btn-close:active,
.modal .modal-header .help-button:active,
#toast-container .toast .toast-header .btn-close:active {
transform: scale(.85);
}
.modal .modal-header .btn-close:focus,
.modal .modal-header .help-button:focus,
#toast-container .toast .toast-header .btn-close:focus {
box-shadow: none !important;
}
.modal .modal-header .btn-close:focus-visible,
.modal .modal-header .help-button:focus-visible,
#toast-container .toast .toast-header .btn-close:focus-visible {
outline: 2px solid var(--input-focus-outline-color);
outline-offset: 2px;
}
/* Modal footer */
.modal .modal-footer {
background: var(--modal-footer-background);
color: var(--modal-footer-color);
border-top: unset;
}
/* Tool dialogs - small dialogs without a backdrop */
div.tn-tool-dialog {
border-radius: 10px;
background: var(--tool-dialog-background-color) !important;
user-select: none;
box-shadow: 10px 10px 93px -25px var(--tool-dialog-shadow-color);
}
/*
* JUMP TO NOTE DIALOG
*/
.jump-to-note-dialog .modal-dialog {
--modal-background-color: var(--menu-background-color);
--modal-footer-background: transparent;
--bs-modal-header-border-width: 0;
--bs-modal-footer-border-width: 0;
--bs-modal-footer-gap: 0;
backdrop-filter: var(--dropdown-backdrop-filter);
}
.jump-to-note-dialog .modal-content {
--bs-modal-header-padding-x: 0;
box-shadow: 0 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
border: 1px solid var(--dropdown-border-color);
padding: 20px;
border-radius: 8px;
}
.jump-to-note-dialog .modal-header {
padding: unset !important;
}
.jump-to-note-dialog .modal-body {
padding: 26px 0 !important;
}
/* Search box wrapper */
.jump-to-note-dialog .input-group {
margin-right: 16px;
}
.jump-to-note-dialog .input-group:hover {
background: var(--quick-search-hover-background);
}
/* Focused search box */
.jump-to-note-dialog .input-group:focus-within {
border-color: var(--quick-search-focus-border);
background: var(--quick-search-focus-background);
color: var(--quick-search-focus-color);
}
.jump-to-note-dialog .input-clearer-button {
background: transparent !important;
}
/*
* RECENT CHANGES DIALOG
*/
.recent-changes-dialog .modal-body {
padding: 0;
}
.recent-changes-content {
margin: var(--bs-modal-padding);
}
/* Date headings */
.recent-changes-content > div > b {
position: sticky;
display: block;
top: 0;
background: var(--modal-background-color);
padding: 10px 0 10px calc(var(--timeline-left-gap) + var(--timeline-right-gap));
font-size: 1.25em;
font-weight: 300;
z-index: 10;
}
.recent-changes-content ul {
list-style: none;
margin: 0;
padding: 0;
}
/* Timeline items */
.recent-changes-content ul li {
display: flex;
position: relative;
margin: 0;
border: unset;
padding-top: var(--timeline-item-top-padding);
padding-bottom: var(--timeline-item-bottom-padding);
padding-left: calc(var(--timeline-left-gap) + var(--timeline-right-gap));
padding-right: var(--timeline-left-gap);
color: var(--active-item-text-color);
}
.recent-changes-content li > span:first-child::after {
/* Remove the dash between time and note title */
content: "" !important;
}
.recent-changes-content ul li:not(.deleted-note):hover {
border-radius: 8px;
background: var(--hover-item-background-color);
color: var(--hover-item-text-color);
}
.recent-changes-content ul li .note-path {
color: var(--muted-text-color);
font-size: 0.75em;
}
/* Item time */
.recent-changes-content ul li > span:first-child {
display: inline-block;
min-width: 80px;
vertical-align: top;
color: var(--muted-text-color);
}
/* Item title & path container */
.recent-changes-content ul li > span:nth-child(2) {
display: inline-block;
}
/* Item title link */
.recent-changes-content ul li .note-title a {
color: currentColor;
}
.recent-changes-content ul li .note-title a:hover {
text-decoration: underline;
}
/* Item title for deleted notes */
.recent-changes-content ul li.deleted-note .note-title > .note-title {
text-decoration: line-through;
}
/* Item path */
.recent-changes-content ul li > span:nth-child(2) small {
display: block;
line-height: 1;
opacity: 0.75;
}
/* Timeline connector */
.recent-changes-content ul li::before,
.recent-changes-content > div > b::before {
position: absolute;
content: "";
top: var(--connector-top, 0);
left: calc(var(--timeline-left-gap) + ((var(--timeline-bullet-size) - var(--timeline-connector-size)) / 2));
bottom: var(--connector-bottom, 0);
width: var(--timeline-connector-size);
border-radius: var(--connector-radius, 0) var(--connector-radius, 0) 0 0;
background: var(--timeline-connector-color);
transition: background-color 400ms ease-in-out;
}
.recent-changes-content ul li:hover:before {
mix-blend-mode: var(--timeline-connector-hover-blend-mode);
}
.recent-changes-content > div:hover {
--timeline-connector-color: var(--timeline-connector-active-color);
}
/* The first item of the timeline */
.recent-changes-content > div:first-child > *:first-child {
--connector-top: 50%;
--connector-radius: calc(var(--timeline-connector-size) / 2);
}
/* The last item of the timeline */
.recent-changes-content > div:last-child li:last-child {
--connector-bottom: 50%;
}
/* Timeline bullet */
.recent-changes-content ul li::after {
position: absolute;
content: "";
top: calc(var(--timeline-item-top-padding) + var(--timeline-bullet-vertical-pos));
left: var(--timeline-left-gap);
width: var(--timeline-bullet-size);
height: var(--timeline-bullet-size);
border-radius: 50%;
background: var(--timeline-bullet-color);
transform: translateY(-50%);
}
/* Hovered timeline bullet */
.recent-changes-content ul li:hover::after {
background: var(--timeline-bullet-hover-color);
}
/*
* CHEATSHEET DIALOG
*/
.help-dialog .modal-content {
--modal-background-color: var(--help-background-color);
backdrop-filter: blur(var(--help-backdrop-blur));
}
.help-dialog .help-cards {
display: block;
columns: 3;
column-gap: 20px;
}
.help-dialog .card {
margin: 0;
width: auto;
border: none;
background: unset;
padding: 16px 8px;
break-inside: avoid-column;
}
.help-dialog .card-body {
box-shadow: var(--help-card-shadow);
border-radius: 6px;
background: var(--help-card-background);
}
.help-dialog .card-body h5,
.help-dialog .card-body h6 {
color: var(--help-card-heading-color);
font-weight: 600;
}
.help-dialog .card-body h5 {
font-size: 20px;
}
.help-dialog .card-body h6 {
font-size: 15px;
padding-bottom: 0.5em;
}
/* Help card item */
.help-dialog .help-cards ul {
list-style-type: none;
padding: 0;
}
.help-dialog .help-cards li + li {
margin-top: 0.4em;
}
/* Keyboard shortcut */
.help-dialog .help-cards kbd,
.ck-content kbd {
box-shadow: var(--help-kbd-shortcut-shadow);
margin: 0 4px;
border: none;
border-radius: 4px;
padding: 2px 10px;
background: var(--help-kbd-shortcut-background);
color: var(--help-kbd-shortcut-color);
font-weight: 500;
letter-spacing: 0.5pt;
}
.help-dialog .help-cards kbd:first-child {
margin-left: 0;
}
/* Inline code - used for Markdown samples */
.help-dialog .help-cards code {
border-radius: 4px;
background: var(--help-code-background);
padding: 0 8px;
color: var(--help-code-color);
}
/* DELETE NOTE PREVIEW DIALOG */
.delete-notes-list .note-path {
padding-left: 8px;
}
/*
* ATTRIBUTE DETAIL DIALOG
*/
/* Labels */
.attr-edit-table th {
padding-right: 12px;
font-weight: normal;
white-space: nowrap;
}

View File

@@ -220,6 +220,7 @@ input::selection,
border-radius: 6px;
padding-right: 8px;
color: var(--quick-search-color);
flex-wrap: nowrap;
}
.input-group:hover {
@@ -302,6 +303,24 @@ input::selection,
font-style: italic;
}
/*
Numeric input with measurement unit as a sufix
<label class="input-group tn-number-unit-pair">
<input ... >
<span class="input-group-text">meters</span>
</label>
*/
label.input-group.tn-number-unit-pair {
width: fit-content;
}
label.input-group.tn-number-unit-pair input {
width: 120px !important;
padding-inline-end: 0;
}
/* Combo box-like dropdown buttons */
.select-button.dropdown-toggle::after {

View File

@@ -1,11 +0,0 @@
/* The container */
div.note-detail-empty {
max-width: 70%;
margin: 50px auto;
}
/* The search results list */
.note-detail-empty span.aa-dropdown-menu {
margin-top: 1em;
border: unset;
}

View File

@@ -132,105 +132,4 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
.ck-content .table > figcaption {
background: var(--accented-background-color);
color: var(--main-text-color);
}
/*
* Search in text panel
*/
.find-replace-widget {
container-type: inline-size;
border-top: 3px solid var(--root-background) !important;
}
.find-replace-widget > div {
padding: 8px;
}
.find-replace-widget > div + div {
padding-top: 0;
}
div.find-replace-widget div.find-widget-found-wrapper > span {
min-width: 50px;
font-style: normal;
font-weight: normal;
}
/* The up / down buttons of the "Find in text" input */
.find-replace-widget .input-group button {
font-size: 1.3em;
}
.find-replace-widget .form-check {
padding-left: 0;
white-space: nowrap;
}
.find-replace-widget .form-check .form-check-input {
margin-left: 0;
}
/* Narrow version */
@container (max-width: 600px) {
.find-replace-widget > *,
.find-replace-widget input,
.find-replace-widget button.btn.btn-sm {
font-size: .9em;
}
.find-widget-box {
position: relative;
justify-content: center;
flex-wrap: wrap;
gap: 8px;
}
.find-widget-box,
.replace-widget-box {
padding-right: 3em !important;
}
.find-widget-close-button {
position: absolute;
top: .85em;
right: .5em;
}
.find-widget-box > * {
margin: unset !important;
}
div.find-widget-search-term-input-group {
margin-bottom: 8px;
max-width: unset;
width: 100%;
}
.find-widget-found-wrapper,
.find-widget-found-wrapper > span {
min-width: 0 !important;
}
.find-widget-spacer {
display: none;
}
.form-check {
min-height: unset;
margin-bottom: unset;
}
.replace-widget-box {
gap: 8px;
}
.replace-widget-box > * {
margin-right: unset !important;
}
div.replace-widget-box button.btn.btn-sm {
min-width: unset;
white-space: nowrap;
}
}
}

View File

@@ -1,7 +1,127 @@
/*
* Settings
* LOG IN PAGE
*/
:root {
.login-page {
display: flex; /* Note: the login page contents is hidden before this property is applied */
height: 100%;
flex-direction: column;
justify-content: center;
}
.login-page > div {
padding-top: 0 !important;
padding-bottom: 20vh;
}
.login-page h1 {
margin-bottom: .5em;
font-weight: 300;
color: var(--muted-text-color);
}
.login-page .form-group {
margin-top: 8px;
}
.login-page .alert {
margin: 0;
border: unset;
padding: 8px 0 0 0;
font-size: .85em;
color: var(--dropdown-item-icon-destructive-color) !important;
}
/*
* SEARCH PAGE
*/
/* Button bar */
.search-definition-widget .search-setting-table tbody:last-child div {
justify-content: flex-end !important;
gap: 8px;
}
.search-result-widget-content .note-path .path-bracket {
display: inline;
}
.search-result-widget-content .note-path {
opacity: 0.75;
font-weight: normal;
}
/*
* SQL CONSOLE
*/
/* Table buttons */
.sql-table-schemas-widget .sql-table-schemas button {
--color: var(--main-text-color);
--background: var(--card-background-color);
display: inline-block;
box-shadow: 2px 2px 2px var(--card-shadow-color);
margin-top: 4px;
vertical-align: baseline;
border: unset;
border-radius: 12px;
padding: 2px 12px;
background: var(--background) !important;
color: var(--color) !important;
line-height: unset;
cursor: help;
}
.sql-table-schemas-widget .sql-table-schemas button:hover,
.sql-table-schemas-widget .sql-table-schemas button:active,
.sql-table-schemas-widget .sql-table-schemas button:focus-visible {
--background: var(--card-background-press-color);
--color: var(--main-text-color);
}
/* Tooltip */
.tooltip .table-schema {
font-family: var(--monospace-font-family);
font-size: .85em;
}
/* Data type */
.tooltip .table-schema td:nth-child(2) {
color: var(--muted-text-color);
}
/*
* NOTE MAP
*/
.note-detail-note-map .fixnodes-type-switcher .tn-tool-button.toggled {
color: var(--tab-close-button-hover-background);
}
/*
* EMPTY NOTE PAGE
*/
/* The container */
div.note-detail-empty {
max-width: 70%;
margin: 50px auto;
}
/* The search results list */
.note-detail-empty span.aa-dropdown-menu {
margin-top: 1em;
border: unset;
}
/*
* OPTIONS PAGES
*/
:root {
--options-card-min-width: 500px;
--options-card-max-width: 900px;
--options-card-padding: 17px;
@@ -20,6 +140,11 @@
height: 1em;
}
/* Add a gap between consecutive buttons */
.note-detail-content-widget-content.options button.btn + button.btn {
margin-inline-start: 8px;
}
.note-detail-content-widget-content.options:has(.shortcuts-options-section)::after {
height: 0;
}
@@ -107,15 +232,6 @@
font-size: .85em;
}
/* Appeareance */
.main-font-size-input-group,
.tree-font-size-input-group,
.detail-font-size-input-group,
.monospace-font-size-input-group {
width: fit-content;
}
/* Shortcuts */
.note-detail-content-widget-content:has(.shortcuts-options-section) {

View File

@@ -153,15 +153,4 @@ div.editability-dropdown a.dropdown-item {
/* Narrow width layout */
.note-info-widget {
container: info-section / inline-size;
}
/*
* Attribute detail dialog
*/
/* Labels */
.attr-edit-table th {
padding-right: 12px;
font-weight: normal;
white-space: nowrap;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1090,7 +1090,8 @@
"max_content_width": {
"title": "Content Width",
"default_description": "Trilium by default limits max content width to improve readability for maximized screens on wide screens.",
"max_width_label": "Max content width in pixels",
"max_width_label": "Max content width",
"max_width_unit": "pixels",
"apply_changes_description": "To apply content width changes, click on",
"reload_button": "reload frontend",
"reload_description": "changes from appearance options"
@@ -1128,7 +1129,8 @@
"code_auto_read_only_size": {
"title": "Automatic Read-Only Size",
"description": "Automatic read-only note size is the size after which notes will be displayed in a read-only mode (for performance reasons).",
"label": "Automatic read-only size (code notes)"
"label": "Automatic read-only size (code notes)",
"unit": "characters"
},
"code-editor-options": {
"title": "Editor"
@@ -1149,7 +1151,8 @@
"download_images_automatically": "Download images automatically for offline use.",
"download_images_description": "Pasted HTML can contain references to online images, Trilium will find those references and download the images so that they are available offline.",
"enable_image_compression": "Enable image compression",
"max_image_dimensions": "Max width / height of an image in pixels (image will be resized if it exceeds this setting).",
"max_image_dimensions": "Max width / height of an image (image will be resized if it exceeds this setting).",
"max_image_dimensions_unit": "pixels",
"jpeg_quality_description": "JPEG quality (10 - worst quality, 100 - best quality, 50 - 85 is recommended)"
},
"attachment_erasure_timeout": {
@@ -1181,6 +1184,7 @@
"note_revisions_snapshot_limit_title": "Note Revision Snapshot Limit",
"note_revisions_snapshot_limit_description": "The note revision snapshot number limit refers to the maximum number of revisions that can be saved for each note. Where -1 means no limit, 0 means delete all revisions. You can set the maximum revisions for a single note through the #versioningLimit label.",
"snapshot_number_limit_label": "Note revision snapshot number limit:",
"snapshot_number_limit_unit": "snapshots",
"erase_excess_revision_snapshots": "Erase excess revision snapshots now",
"erase_excess_revision_snapshots_prompt": "Excess revision snapshots have been erased."
},
@@ -1223,13 +1227,15 @@
"table_of_contents": {
"title": "Table of Contents",
"description": "Table of contents will appear in text notes when the note has more than a defined number of headings. You can customize this number:",
"unit": "headings",
"disable_info": "You can also use this option to effectively disable TOC by setting a very high number.",
"shortcut_info": "You can configure a keyboard shortcut for quickly toggling the right pane (including TOC) in the Options -> Shortcuts (name 'toggleRightPane')."
},
"text_auto_read_only_size": {
"title": "Automatic Read-Only Size",
"description": "Automatic read-only note size is the size after which notes will be displayed in a read-only mode (for performance reasons).",
"label": "Automatic read-only size (text notes)"
"label": "Automatic read-only size (text notes)",
"unit": "characters"
},
"i18n": {
"title": "Localization",
@@ -1329,7 +1335,8 @@
"sync_2": {
"config_title": "Sync Configuration",
"server_address": "Server instance address",
"timeout": "Sync timeout (milliseconds)",
"timeout": "Sync timeout",
"timeout_unit": "milliseconds",
"proxy_label": "Sync proxy server (optional)",
"note": "Note",
"note_description": "If you leave the proxy setting blank, the system proxy will be used (applies to desktop/electron build only).",

View File

@@ -353,7 +353,8 @@
"code_auto_read_only_size": {
"description": "Marchează pragul în care o notiță de o anumită dimensiune va fi afișată în mod de citire (pentru motive de performanță).",
"label": "Pragul de dimensiune pentru setarea modului de citire automat (la notițe de cod)",
"title": "Pragul de mod de citire automat"
"title": "Pragul de mod de citire automat",
"unit": "caractere"
},
"code_buttons": {
"execute_button_title": "Execută scriptul",
@@ -697,7 +698,8 @@
"enable_image_compression": "Activează compresia imaginilor",
"images_section_title": "Imagini",
"jpeg_quality_description": "Calitatea JPEG (10 - cea mai slabă calitate, 100 - cea mai bună calitate, se recomandă între 50 și 85)",
"max_image_dimensions": "Lungimea/lățimea maximă a unei imagini în pixeli (imaginea va fi redimensionată dacă depășește acest prag)."
"max_image_dimensions": "Lungimea/lățimea maximă a unei imagini (imaginea va fi redimensionată dacă depășește acest prag).",
"max_image_dimensions_unit": "pixeli"
},
"import": {
"chooseImportFile": "Selectați fișierul de importat",
@@ -772,7 +774,8 @@
"max_content_width": {
"apply_changes_description": "Pentru a aplica schimbările de lățime a conținutului, dați click pe",
"default_description": "În mod implicit Trilium limitează lățimea conținutului pentru a îmbunătăți lizibilitatea pentru ferestrele maximizate pe ecrane late.",
"max_width_label": "Lungimea maximă a conținutului în pixeli",
"max_width_label": "Lungimea maximă a conținutului",
"max_width_unit": "pixeli",
"reload_button": "reîncarcă interfața",
"reload_description": "schimbări din opțiunile de afișare",
"title": "Lățime conținut"
@@ -1215,10 +1218,12 @@
"test_button": "Probează sincronizarea",
"test_description": "Această opțiune va testa conexiunea și comunicarea cu serverul de sincronizare. Dacă serverul de sincronizare nu este inițializat, acest lucru va rula și o sincronizare cu documentul local.",
"test_title": "Probează sincronizarea",
"timeout": "Timp limită de sincronizare (millisecunde)"
"timeout": "Timp limită de sincronizare",
"timeout_unit": "milisecunde"
},
"table_of_contents": {
"description": "Tabela de conținut va apărea în notițele de tip text atunci când notița are un număr de titluri mai mare decât cel definit. Acest număr se poate personaliza:",
"unit": "titluri",
"disable_info": "De asemenea se poate dezactiva tabela de conținut setând o valoare foarte mare.",
"shortcut_info": "Se poate configura și o scurtatură pentru a comuta rapid vizibilitatea panoului din dreapta (inclusiv tabela de conținut) în Opțiuni -> Scurtături (denumirea „toggleRightPane”).",
"title": "Tabelă de conținut"
@@ -1226,7 +1231,8 @@
"text_auto_read_only_size": {
"description": "Marchează pragul în care o notiță de o anumită dimensiune va fi afișată în mod de citire (pentru motive de performanță).",
"label": "Pragul de dimensiune pentru setarea modului de citire automat (la notițe text)",
"title": "Pragul de mod de citire automat"
"title": "Pragul de mod de citire automat",
"unit": "caractere"
},
"theme": {
"auto_theme": "Temă auto (se adaptează la schema de culori a sistemului)",
@@ -1481,7 +1487,8 @@
"erase_excess_revision_snapshots_prompt": "Reviziile excesive au fost șterse.",
"note_revisions_snapshot_limit_description": "Limita numărului de revizii se referă la numărul maxim de revizii pentru fiecare notiță. -1 reprezintă nicio limită, 0 înseamnă ștergerea tuturor reviziilor. Se poate seta valoarea individual pentru o notiță prin eticheta #versioningLimit.",
"note_revisions_snapshot_limit_title": "Limita de revizii a notițelor",
"snapshot_number_limit_label": "Numărul maxim de revizii pentru notițe:"
"snapshot_number_limit_label": "Numărul maxim de revizii pentru notițe:",
"snapshot_number_limit_unit": "revizii"
},
"search_result": {
"no_notes_found": "Nu au fost găsite notițe pentru parametrii de căutare dați.",

View File

@@ -96,8 +96,6 @@ async function register(app: express.Application) {
app.use(`/${assetPath}/node_modules/codemirror/mode/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/codemirror/mode/")));
app.use(`/${assetPath}/node_modules/codemirror/keymap/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/codemirror/keymap/")));
app.use(`/${assetPath}/node_modules/mind-elixir/dist/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/mind-elixir/dist/")));
app.use(`/${assetPath}/node_modules/@mind-elixir/node-menu/dist/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/@mind-elixir/node-menu/dist/")));
app.use(`/${assetPath}/node_modules/@highlightjs/cdn-assets/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/@highlightjs/cdn-assets/")));
app.use(`/${assetPath}/node_modules/leaflet/dist/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/leaflet/dist/")));

View File

@@ -193,4 +193,37 @@ describe("Markdown export", () => {
expect(markdownExportService.toMarkdown(html)).toBe(expected);
});
it("exports code in tables properly", () => {
const html = trimIndentation`\
<table>
<tr>
<td>
Row 1
</td>
<td>
<p>Allows displaying the value of one or more attributes in the calendar
like this:&nbsp;</p>
<p>
<img src="13_Calendar View_image.png" alt="">
</p>
<pre><code class="language-text-x-trilium-auto">#weight="70"
#Mood="Good"
#calendar:displayedAttributes="weight,Mood"</code></pre>
<p>It can also be used with relations, case in which it will display the
title of the target note:</p><pre><code class="language-text-x-trilium-auto">~assignee=@My assignee
#calendar:displayedAttributes="assignee"</code></pre>
</td>
</tr>
</table>
`;
const expected = trimIndentation`\
<table><tbody><tr><td>Row 1</td><td><p>Allows displaying the value of one or more attributes in the calendar like this:&nbsp;</p><p><img src="13_Calendar View_image.png" alt=""></p><pre><code class="language-text-x-trilium-auto">#weight="70"
#Mood="Good"
#calendar:displayedAttributes="weight,Mood"</code></pre><p>It can also be used with relations, case in which it will display the title of the target note:</p><pre><code class="language-text-x-trilium-auto">~assignee=@My assignee
#calendar:displayedAttributes="assignee"</code></pre></td></tr></tbody></table>`;
expect(markdownExportService.toMarkdown(html)).toBe(expected);
});
});

View File

@@ -1,7 +1,7 @@
"use strict";
import TurndownService from "turndown";
import turndownPluginGfm from "@joplin/turndown-plugin-gfm";
import { gfm } from "../../../packages/turndown-plugin-gfm/src/gfm.js";
let instance: TurndownService | null = null;
@@ -43,7 +43,7 @@ function toMarkdown(content: string) {
instance.addRule("fencedCodeBlock", fencedCodeBlockFilter);
instance.addRule("img", buildImageFilter());
instance.addRule("admonition", buildAdmonitionFilter());
instance.use(turndownPluginGfm.gfm);
instance.use(gfm);
instance.keep([ "kbd" ]);
}