Compare commits

...

92 Commits

Author SHA1 Message Date
Jakob Schlanstedt
7c9bf4a48e
Merge 8ee59e9daa9629c7e568847b811c8e22aa3bdfad into a77e76d5c6d13c7a27b191116d9a6b38abbd5eda 2025-11-30 19:33:19 +00:00
Adorian Doran
a77e76d5c6 Merge branch 'main' of https://github.com/TriliumNext/Trilium
Some checks failed
Checks / main (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Documentation / Build and Deploy Documentation (push) Waiting to run
Dev / Test development (push) Waiting to run
Dev / Build Docker image (push) Blocked by required conditions
Dev / Check Docker build (Dockerfile) (push) Blocked by required conditions
Dev / Check Docker build (Dockerfile.alpine) (push) Blocked by required conditions
/ Check Docker build (Dockerfile) (push) Waiting to run
/ Check Docker build (Dockerfile.alpine) (push) Waiting to run
/ Build Docker images (Dockerfile, ubuntu-24.04-arm, linux/arm64) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.alpine, ubuntu-latest, linux/amd64) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.legacy, ubuntu-24.04-arm, linux/arm/v7) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.legacy, ubuntu-24.04-arm, linux/arm/v8) (push) Blocked by required conditions
/ Merge manifest lists (push) Blocked by required conditions
playwright / E2E tests on linux-arm64 (push) Waiting to run
playwright / E2E tests on linux-x64 (push) Waiting to run
Deploy website / Build & deploy website (push) Has been cancelled
2025-11-30 21:33:08 +02:00
Adorian Doran
ca6660e2ff style/calendar collection: tweak appearance 2025-11-30 21:33:00 +02:00
Elian Doran
af94410c55
Merge branch 'main' of github.com:TriliumNext/Trilium 2025-11-30 21:26:50 +02:00
Elian Doran
b47bc50147
fix(revisions): double scrolling on mobile 2025-11-30 21:24:57 +02:00
Elian Doran
5ff77c16ab
feat(revisions): improve layout on mobile 2025-11-30 21:13:07 +02:00
Adorian Doran
11618260cf style/calendar collection: tweak the appearance of events without a color 2025-11-30 20:18:16 +02:00
Adorian Doran
63f9006d17 style/calendar collection: improve the support for colored notes 2025-11-30 20:08:24 +02:00
Elian Doran
7779acc7bc
refactor(client): split revisions CSS into file 2025-11-30 19:56:13 +02:00
Elian Doran
aacd92eee3
chore(popup-editor): implement switch to full editor button 2025-11-30 19:47:17 +02:00
Adorian Doran
1bf8be2874 Merge branch 'main' of https://github.com/TriliumNext/Trilium 2025-11-30 19:46:26 +02:00
Adorian Doran
66f2d0c7dc style/note colors: use a more elegant way to retrieve the theme-aware note color 2025-11-30 19:46:15 +02:00
Elian Doran
597d952254
Add British English (en-GB) translations (#7904) 2025-11-30 17:37:49 +00:00
Adorian Doran
288595ce5d Merge branch 'main' of https://github.com/TriliumNext/Trilium 2025-11-30 19:34:45 +02:00
Elian Doran
c89e8c78d3
Translations update from Hosted Weblate (#7905) 2025-11-30 17:34:39 +00:00
Adorian Doran
81a37e3fc4 client/CSS class manager: fix a bug when handling red hues 2025-11-30 19:34:31 +02:00
Elian Doran
368c590976
Translated using Weblate (English (United Kingdom))
Currently translated at 0.8% (1 of 118 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/en_GB/
2025-11-30 18:33:26 +01:00
Elian Doran
6e982e646d
Translated using Weblate (English (United Kingdom))
Currently translated at 1.6% (2 of 118 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/en_GB/
2025-11-30 18:29:39 +01:00
Elian Doran
030582b2d5
feat(i18n): add English (United Kingdom) 2025-11-30 19:14:21 +02:00
copilot-swe-agent[bot]
7dd4b10a96 Add British English (en-GB) translations for client
Co-authored-by: eliandoran <21236836+eliandoran@users.noreply.github.com>
2025-11-30 17:10:30 +00:00
copilot-swe-agent[bot]
b055e79b4c Initial plan 2025-11-30 17:02:01 +00:00
Elian Doran
ba980aa93f
Translations update from Hosted Weblate (#7903) 2025-11-30 16:59:52 +00:00
Elian Doran
15baf04ce9
Added translation using Weblate (English (United Kingdom)) 2025-11-30 17:58:03 +01:00
Elian Doran
4dc2587817
Added translation using Weblate (English (United Kingdom)) 2025-11-30 17:58:00 +01:00
Elian Doran
d5e046c289
Added translation using Weblate (English (United Kingdom)) 2025-11-30 17:57:58 +01:00
Elian Doran
ef8073ac58
Added translation using Weblate (English (United Kingdom)) 2025-11-30 17:57:56 +01:00
green
38c9d25214
Translated using Weblate (Japanese)
Currently translated at 100.0% (1637 of 1637 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-11-30 17:57:54 +01:00
Elian Doran
e2f0e4089f
Merge branch 'main' of github.com:TriliumNext/Trilium 2025-11-30 18:41:43 +02:00
Elian Doran
80ce2c04ed
chore(ci): apply platform changes for nightly
See https://github.com/TriliumNext/Trilium/pull/7002/files
2025-11-30 18:41:39 +02:00
Elian Doran
4ebd82beeb
feat(ckeditor5): add formatPainter (#7902) 2025-11-30 13:27:15 +00:00
SiriusXT
8cc43cd9a6 docs(user): add format painter 2025-11-30 20:08:27 +08:00
SiriusXT
15190abb69 feat(ckeditor5): add formatPainter 2025-11-30 16:53:14 +08:00
Jakob Schlanstedt
8ee59e9daa refactor(suggestion): improve naming 2025-11-28 22:53:24 +01:00
Jakob Schlanstedt
4e5c26371e fix(note_create): use correct API for intoDefaultLocation 2025-11-28 22:53:24 +01:00
Jakob Schlanstedt
436146d829 fix(note_create): warn message 2025-11-28 22:53:24 +01:00
Jakob Schlanstedt
315fcecf57 fix(include_note): fix wrongly merged code 2025-11-28 22:53:24 +01:00
Jakob Schlanstedt
0a57e6e154 fix(NoteAutocomplete): use new API 2025-11-28 22:53:24 +01:00
Jakob Schlanstedt
bdcb84a394 fix(EditableTest): cutIntoNoteCommand createNote to new API 2025-11-28 22:53:23 +01:00
Jakob Schlanstedt
213c36ba84 fix(EditableTest): Fix EdtableText to use new API 2025-11-28 22:53:22 +01:00
Jakob Schlanstedt
995e765276 fix(translation): missing ',' 2025-11-28 22:52:03 +01:00
Jakob Schlanstedt
8e81c38c14 fix(translation): restore missing translations 2025-11-28 22:52:03 +01:00
Jakob Schlanstedt
7378fa4cbd fix(createNoteFromAction): don't overwrite promptForType but use the variable instead 2025-11-28 22:52:02 +01:00
Jakob Schlanstedt
af8a5ff0c9 refactor(note_create) use correct terminology link not url 2025-11-28 22:52:02 +01:00
Jakob Schlanstedt
eca5a4a072 refactor(url -> link): let link refer to url and path.
before there was polysemy in word url that is now resolved by making
link hypernym to url and path.
2025-11-28 22:52:02 +01:00
Jakob Schlanstedt
71b3ad5027 refactor(note_create): Inbox -> Default naming in functions, parameters and types 2025-11-28 22:52:02 +01:00
Jakob Schlanstedt
7864168adc fix(AttributeEditor): wrong order of Arguments 2025-11-28 22:52:01 +01:00
Jakob Schlanstedt
f8090d9217 feat(search): add create into inbox to search 2025-11-28 22:52:01 +01:00
Jakob Schlanstedt
09aa22c74b refactor(autocomplete-pipline): refactor autocomplete -> create -> select pipeline 2025-11-28 22:52:01 +01:00
Jakob Schlanstedt
8fc8f97879 refactor(create-note-naming): simplify naming 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
547cdff510 style(jump_to_note): remove dead case to improve readability 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
cdd08d6971 style(row_editing comments): make inline comment proper doc 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
b3cf9c8f2d style(unused-imports): Remove unused-imports 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
feefa389b4 test(server-e2e): fix test for new create note option 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
f65be4f368 fix(note_autocomplete): fix wrong definition of types, and resulting bugs esp. improving row editing 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
77a014109e fix(note_autocomplete): fix attributes not linking 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
d3db48c99b refactor(note-create): remove as typecast 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
7e4833e893 fix(note-autocomplete): logic error hidden by as typecast 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
470d7eee31 fix(type-checker): remove as casts hiding type-errors thus bugs. Solve subsequent found bugs 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
aada631c0f fix(note_autocomplete): fix wrong type of target 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
bc4186d216 fix(note_create): type casting 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
c2a27eff2c refactor(note_autocomplete): simplify big switch statement removing duplicate logic 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
ca24408a13 fix(root-command-executor): fix regression in root_command_executor 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
b9e19e524a fix(typecheck-proven incorrectness): typecheck caught incorrectness through pnpm typecheck 2025-11-28 22:52:00 +01:00
contributor
09c8a778f5 createNote: better typing without cast and never type 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
3438f1103d refactor(note-create): remove small redundancy 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
82a3be06d1 fix(note-create): fix type definition for CreateNoteWithUrlOpts 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
f0dead5390 refactor(note-create): remove unnecessary deep hierarchy 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
b0fdb9fef2 refactor(note-create): replace 'at' with 'with' 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
71009bddc7 refactor(note-create): simplify createNote switch to equivalent small ifs 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
66e499a2e1 refactor(note-create): replace enum with optional fields 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
a5ef5eee2f refactor(note_create): simplify type implementation and documentation 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
bcb29d22f5 docs(note_create): further clarify type system 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
6ad2b49ab3 docs: remove comments duplicating code 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
656e7c069d refactor(create_note): rename types to fit ontological concepts better 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
00aa470bf2 fix(Omit in types): remove Omit from types to make hierarchy logical consistent and resulting errors from corrected logic. 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
c6ed0b43fc docs(note_create): improve clarification of type system 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
3d8a4d2553 docs(note_create): improve documentation for type checking system 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
42ab0eb804 fix(note_create): fix type hierarchy inheriting from wrong type and improve its documentation 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
d80d06a9b8 refactor(note_create): cleanup by removing unused import 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
3c39026739 refactor(note_create): reorder function order to simplify diff 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
72c17b22df refactor(note_create): improve comments for type system 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
dd1aa23cb6 refactor(note_create): clarify type system 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
ecdf243e63 refactor: cleanup comments 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
1e15585a24 refactor(typeerror): resolve typeerrors by refactoring code 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
9d40c0cb26 fix(note-type-chooser): fix note type chooser, unnecessary stick to old chosen path 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
e41e24b044 refactor(create-note): centralize and add advanced type checking to create-note with and without prompt 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
a15d661fd7 fix(jump_to_note): fix enum typescript error in switch statement 2025-11-28 22:52:00 +01:00
contributor
cb5954e8c7 fix typings for creating a note using mentions in ckeditor 2025-11-28 22:52:00 +01:00
contributor
2b55db05e1 rename MentionAction to CreateNoteAction and move to packages/common 2025-11-28 22:52:00 +01:00
Jakob Schlanstedt
74bf93059c refactor(ECMAScript Modules jsimport convention): refactor newly created imports to js convention
https://nodejs.org/api/esm.html#import-specifiers
2025-11-28 22:51:59 +01:00
Jakob Schlanstedt
384d8c9c37 refactor(note-create): change order of noteCreate intoPath and intoInbox to improve code compatibility 2025-11-28 22:51:59 +01:00
Jakob Schlanstedt
1bb6149dbe feat(search): add create into inbox to search 2025-11-28 22:51:59 +01:00
77 changed files with 1854 additions and 631 deletions

View File

@ -45,6 +45,19 @@ jobs:
image: win-signing
shell: cmd
forge_platform: win32
# Exclude ARM64 Linux from default matrix to use native runner
exclude:
- arch: arm64
os:
name: linux
# Add ARM64 Linux with native ubuntu-24.04-arm runner for better-sqlite3 compatibility
include:
- arch: arm64
os:
name: linux
image: ubuntu-24.04-arm
shell: bash
forge_platform: linux
runs-on: ${{ matrix.os.image }}
steps:
- uses: actions/checkout@v6

View File

@ -25,7 +25,7 @@ import TouchBarComponent from "./touch_bar.js";
import type { CKTextEditor } from "@triliumnext/ckeditor5";
import type CodeMirror from "@triliumnext/codemirror";
import { StartupChecks } from "./startup_checks.js";
import type { CreateNoteOpts } from "../services/note_create.js";
import type { CreateNoteOpts, CreateNoteWithLinkOpts } from "../services/note_create.js";
import { ColumnComponent } from "tabulator-tables";
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
import type RootContainer from "../widgets/containers/root_container.js";
@ -359,8 +359,7 @@ export type CommandMappings = {
// Table view
addNewRow: CommandData & {
customOpts: CreateNoteOpts;
parentNotePath?: string;
customOpts?: CreateNoteWithLinkOpts;
};
addNewTableColumn: CommandData & {
columnToEdit?: ColumnComponent;

View File

@ -11,6 +11,7 @@ import froca from "../services/froca.js";
import linkService from "../services/link.js";
import { t } from "../services/i18n.js";
import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons";
import noteCreateService from "../services/note_create.js";
export default class Entrypoints extends Component {
constructor() {
@ -24,23 +25,9 @@ export default class Entrypoints extends Component {
}
async createNoteIntoInboxCommand() {
const inboxNote = await dateNoteService.getInboxNote();
if (!inboxNote) {
console.warn("Missing inbox note.");
return;
}
const { note } = await server.post<CreateChildrenResponse>(`notes/${inboxNote.noteId}/children?target=into`, {
content: "",
type: "text",
isProtected: inboxNote.isProtected && protectedSessionHolder.isProtectedSessionAvailable()
});
await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.openTabWithNoteWithHoisting(note.noteId, { activate: true });
appContext.triggerEvent("focusAndSelectTitle", { isNewNote: true });
await noteCreateService.createNote(
{ target: "default" }
);
}
async toggleNoteHoistingCommand({ noteId = appContext.tabManager.getActiveContextNoteId() }) {

View File

@ -48,10 +48,15 @@ export default class MainTreeExecutors extends Component {
return;
}
await noteCreateService.createNote(activeNoteContext.notePath, {
isProtected: activeNoteContext.note.isProtected,
saveSelection: false
});
await noteCreateService.createNote(
{
target: "into",
parentNoteLink: activeNoteContext.notePath,
isProtected: activeNoteContext.note.isProtected,
saveSelection: false,
promptForType: false,
}
);
}
async createNoteAfterCommand() {
@ -72,11 +77,14 @@ export default class MainTreeExecutors extends Component {
return;
}
await noteCreateService.createNote(parentNotePath, {
target: "after",
targetBranchId: node.data.branchId,
isProtected: isProtected,
saveSelection: false
});
await noteCreateService.createNote(
{
target: "after",
parentNoteLink: parentNotePath,
targetBranchId: node.data.branchId,
isProtected: isProtected,
saveSelection: false
}
);
}
}

View File

@ -45,7 +45,7 @@ export default class RootCommandExecutor extends Component {
}
async searchInSubtreeCommand({ notePath }: CommandListenerData<"searchInSubtree">) {
const noteId = treeService.getNoteIdFromUrl(notePath);
const noteId = treeService.getNoteIdFromLink(notePath);
this.searchNotesCommand({ ancestorNoteId: noteId });
}
@ -240,14 +240,18 @@ export default class RootCommandExecutor extends Component {
// Create a new AI Chat note at the root level
const rootNoteId = "root";
const result = await noteCreateService.createNote(rootNoteId, {
title: "New AI Chat",
type: "aiChat",
content: JSON.stringify({
messages: [],
title: "New AI Chat"
})
});
const result = await noteCreateService.createNote(
{
parentNoteLink: rootNoteId,
target: "into",
title: "New AI Chat",
type: "aiChat",
content: JSON.stringify({
messages: [],
title: "New AI Chat"
}),
}
);
if (!result.note) {
toastService.showError("Failed to create AI Chat note");

View File

@ -74,10 +74,10 @@ export default class TabManager extends Component {
// preload all notes at once
await froca.getNotes([...noteContextsToOpen.flatMap((tab: NoteContextState) =>
[treeService.getNoteIdFromUrl(tab.notePath), tab.hoistedNoteId])], true);
[treeService.getNoteIdFromLink(tab.notePath), tab.hoistedNoteId])], true);
const filteredNoteContexts = noteContextsToOpen.filter((openTab: NoteContextState) => {
const noteId = treeService.getNoteIdFromUrl(openTab.notePath);
const noteId = treeService.getNoteIdFromLink(openTab.notePath);
if (!noteId || !(noteId in froca.notes)) {
// note doesn't exist so don't try to open tab for it
return false;

View File

@ -283,21 +283,31 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
const parentNotePath = treeService.getNotePath(this.node.getParent());
const isProtected = treeService.getParentProtectedStatus(this.node);
noteCreateService.createNote(parentNotePath, {
target: "after",
targetBranchId: this.node.data.branchId,
type: type,
isProtected: isProtected,
templateNoteId: templateNoteId
});
noteCreateService.createNote(
{
target: "after",
parentNoteLink: parentNotePath,
targetBranchId: this.node.data.branchId,
type: type,
isProtected: isProtected,
templateNoteId: templateNoteId,
promptForType: false,
}
);
} else if (command === "insertChildNote") {
const parentNotePath = treeService.getNotePath(this.node);
noteCreateService.createNote(parentNotePath, {
type: type,
isProtected: this.node.data.isProtected,
templateNoteId: templateNoteId
});
noteCreateService.createNote(
{
target: "into",
parentNoteLink: parentNotePath,
type: type,
isProtected: this.node.data.isProtected,
templateNoteId: templateNoteId,
promptForType: false,
}
);
} else if (command === "openNoteInSplit") {
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
const { ntxId } = subContexts?.[subContexts.length - 1] ?? {};

View File

@ -39,12 +39,12 @@ function createClassForColor(colorString: string | null) {
</style>`);
registeredClasses.add(className);
if (hue) {
if (hue !== undefined) {
colorsWithHue.add(className);
}
}
return clsx(className, colorsWithHue.has(className) && "with-hue");
return clsx("use-note-color", className, colorsWithHue.has(className) && "with-hue");
}
function parseColor(color: string) {

View File

@ -50,7 +50,7 @@ async function checkNoteAccess(notePath: string, noteContext: NoteContext) {
const hoistedNoteId = noteContext.hoistedNoteId;
if (!resolvedNotePath.includes(hoistedNoteId) && (!resolvedNotePath.includes("_hidden") || resolvedNotePath.includes("_lbBookmarks"))) {
const noteId = treeService.getNoteIdFromUrl(resolvedNotePath);
const noteId = treeService.getNoteIdFromLink(resolvedNotePath);
if (!noteId) {
return false;
}

View File

@ -261,7 +261,7 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
return {
notePath,
noteId: treeService.getNoteIdFromUrl(notePath),
noteId: treeService.getNoteIdFromLink(notePath),
ntxId,
hoistedNoteId,
viewScope,

View File

@ -5,6 +5,24 @@ import froca from "./froca.js";
import { t } from "./i18n.js";
import commandRegistry from "./command_registry.js";
import type { MentionFeedObjectItem } from "@triliumnext/ckeditor5";
import { CreateNoteAction } from "@triliumnext/commons"
import FNote from "../entities/fnote.js";
/**
* Extends CKEditor's MentionFeedObjectItem with extra fields used by Trilium.
* These additional props (like action, notePath, name, etc.) carry note
* metadata and legacy compatibility info needed for custom autocomplete
* and link insertion behavior beyond CKEditors base mention support.
*/
type ExtendedMentionFeedObjectItem = MentionFeedObjectItem & {
action?: string;
noteTitle?: string;
name?: string;
link?: string;
notePath?: string;
parentNoteId?: string;
highlightedNotePathTitle?: string;
};
// this key needs to have this value, so it's hit by the tooltip
const SELECTED_NOTE_PATH_KEY = "data-note-path";
@ -23,14 +41,39 @@ function getSearchDelay(notesCount: number): number {
}
let searchDelay = getSearchDelay(notesCount);
// TODO: Deduplicate with server.
// String values ensure stable, human-readable identifiers across serialization (JSON, CKEditor, logs).
export enum SuggestionAction {
// These values intentionally mirror CreateNoteAction string values 1:1.
// This overlap ensures that when a suggestion triggers a note creation callback,
// the receiving features (e.g. note creation handlers, CKEditor mentions) can interpret
// the action type consistently
CreateNote = CreateNoteAction.CreateNote,
CreateChildNote = CreateNoteAction.CreateChildNote,
CreateAndLinkNote = CreateNoteAction.CreateAndLinkNote,
CreateAndLinkChildNote = CreateNoteAction.CreateAndLinkChildNote,
SearchNotes = "search-notes",
ExternalLink = "external-link",
Command = "command",
}
export enum SuggestionMode {
SuggestNothing = "nothing",
SuggestCreateOnly = "create-only",
SuggestCreateAndLink = "create-and-link"
}
// NOTE: Previously marked for deduplication with a server-side type,
// but review on 2025-10-12 (using `rg Suggestion`) found no corresponding
// server implementation.
// This interface appears to be client-only.
export interface Suggestion {
noteTitle?: string;
externalLink?: string;
notePathTitle?: string;
notePath?: string;
highlightedNotePathTitle?: string;
action?: string | "create-note" | "search-notes" | "external-link" | "command";
action?: SuggestionAction;
parentNoteId?: string;
icon?: string;
commandId?: string;
@ -43,7 +86,7 @@ export interface Suggestion {
export interface Options {
container?: HTMLElement | null;
fastSearch?: boolean;
allowCreatingNotes?: boolean;
suggestionMode?: SuggestionMode;
allowJumpToSearchNotes?: boolean;
allowExternalLinks?: boolean;
/** If set, hides the right-side button corresponding to go to selected note. */
@ -54,110 +97,160 @@ export interface Options {
isCommandPalette?: boolean;
}
async function autocompleteSourceForCKEditor(queryText: string) {
return await new Promise<MentionFeedObjectItem[]>((res, rej) => {
async function autocompleteSourceForCKEditor(
queryText: string,
suggestionMode: SuggestionMode
): Promise<MentionFeedObjectItem[]> {
// Wrap the callback-based autocompleteSource in a Promise for async/await
const rows = await new Promise<Suggestion[]>((resolve) => {
autocompleteSource(
queryText,
(rows) => {
res(
rows.map((row) => {
return {
action: row.action,
noteTitle: row.noteTitle,
id: `@${row.notePathTitle}`,
name: row.notePathTitle || "",
link: `#${row.notePath}`,
notePath: row.notePath,
highlightedNotePathTitle: row.highlightedNotePathTitle
};
})
);
},
(suggestions) => resolve(suggestions),
{
allowCreatingNotes: true
suggestionMode,
}
);
});
// Map internal suggestions to CKEditor mention feed items
return rows.map((row): ExtendedMentionFeedObjectItem => ({
action: row.action?.toString(),
noteTitle: row.noteTitle,
id: `@${row.notePathTitle}`,
name: row.notePathTitle || "",
link: `#${row.notePath}`,
notePath: row.notePath,
parentNoteId: row.parentNoteId,
highlightedNotePathTitle: row.highlightedNotePathTitle
}));
}
async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void, options: Options = {}) {
async function autocompleteSource(
term: string,
callback: (rows: Suggestion[]) => void,
options: Options = {}
) {
// Check if we're in command mode
if (options.isCommandPalette && term.startsWith(">")) {
const commandQuery = term.substring(1).trim();
// Get commands (all if no query, filtered if query provided)
const commands = commandQuery.length === 0
? commandRegistry.getAllCommands()
: commandRegistry.searchCommands(commandQuery);
const commands =
commandQuery.length === 0
? commandRegistry.getAllCommands()
: commandRegistry.searchCommands(commandQuery);
// Convert commands to suggestions
const commandSuggestions: Suggestion[] = commands.map(cmd => ({
action: "command",
const commandSuggestions: Suggestion[] = commands.map((cmd) => ({
action: SuggestionAction.Command,
commandId: cmd.id,
noteTitle: cmd.name,
notePathTitle: `>${cmd.name}`,
highlightedNotePathTitle: cmd.name,
commandDescription: cmd.description,
commandShortcut: cmd.shortcut,
icon: cmd.icon
icon: cmd.icon,
}));
cb(commandSuggestions);
callback(commandSuggestions);
return;
}
const fastSearch = options.fastSearch === false ? false : true;
if (fastSearch === false) {
if (term.trim().length === 0) {
return;
}
cb([
const fastSearch = options.fastSearch !== false;
const trimmedTerm = term.trim();
const activeNoteId = appContext.tabManager.getActiveContextNoteId();
if (!fastSearch && trimmedTerm.length === 0) return;
if (!fastSearch) {
callback([
{
noteTitle: term,
highlightedNotePathTitle: t("quick-search.searching")
}
noteTitle: trimmedTerm,
highlightedNotePathTitle: t("quick-search.searching"),
},
]);
}
const activeNoteId = appContext.tabManager.getActiveContextNoteId();
const length = term.trim().length;
let results = await server.get<Suggestion[]>(`autocomplete?query=${encodeURIComponent(term)}&activeNoteId=${activeNoteId}&fastSearch=${fastSearch}`);
let results = await server.get<Suggestion[]>(
`autocomplete?query=${encodeURIComponent(trimmedTerm)}&activeNoteId=${activeNoteId}&fastSearch=${fastSearch}`
);
options.fastSearch = true;
if (length >= 1 && options.allowCreatingNotes) {
results = [
{
action: "create-note",
noteTitle: term,
parentNoteId: activeNoteId || "root",
highlightedNotePathTitle: t("note_autocomplete.create-note", { term })
} as Suggestion
].concat(results);
}
if (length >= 1 && options.allowJumpToSearchNotes) {
results = results.concat([
{
action: "search-notes",
noteTitle: term,
highlightedNotePathTitle: `${t("note_autocomplete.search-for", { term })} <kbd style='color: var(--muted-text-color); background-color: transparent; float: right;'>Ctrl+Enter</kbd>`
// --- Create Note suggestions ---
if (trimmedTerm.length >= 1) {
switch (options.suggestionMode) {
case SuggestionMode.SuggestCreateOnly: {
results = [
{
action: SuggestionAction.CreateNote,
noteTitle: trimmedTerm,
parentNoteId: "inbox",
highlightedNotePathTitle: t("note_autocomplete.create-note", { term: trimmedTerm }),
},
{
action: SuggestionAction.CreateChildNote,
noteTitle: trimmedTerm,
parentNoteId: activeNoteId || "root",
highlightedNotePathTitle: t("note_autocomplete.create-child-note", { term: trimmedTerm }),
},
...results,
];
break;
}
]);
case SuggestionMode.SuggestCreateAndLink: {
results = [
{
action: SuggestionAction.CreateAndLinkNote,
noteTitle: trimmedTerm,
parentNoteId: "inbox",
highlightedNotePathTitle: t("note_autocomplete.create-and-link-note", { term: trimmedTerm }),
},
{
action: SuggestionAction.CreateAndLinkChildNote,
noteTitle: trimmedTerm,
parentNoteId: activeNoteId || "root",
highlightedNotePathTitle: t("note_autocomplete.create-and-link-child-note", { term: trimmedTerm }),
},
...results,
];
break;
}
default:
// CreateMode.None or undefined → no creation suggestions
break;
}
}
if (term.match(/^[a-z]+:\/\/.+/i) && options.allowExternalLinks) {
// --- Jump to Search Notes ---
if (trimmedTerm.length >= 1 && options.allowJumpToSearchNotes) {
results = [
...results,
{
action: SuggestionAction.SearchNotes,
noteTitle: trimmedTerm,
highlightedNotePathTitle: `${t("note_autocomplete.search-for", {
term: trimmedTerm,
})} <kbd style='color: var(--muted-text-color); background-color: transparent; float: right;'>Ctrl+Enter</kbd>`,
},
];
}
// --- External Link suggestion ---
if (/^[a-z]+:\/\/.+/i.test(trimmedTerm) && options.allowExternalLinks) {
results = [
{
action: "external-link",
externalLink: term,
highlightedNotePathTitle: t("note_autocomplete.insert-external-link", { term })
} as Suggestion
].concat(results);
action: SuggestionAction.ExternalLink,
externalLink: trimmedTerm,
highlightedNotePathTitle: t("note_autocomplete.insert-external-link", { term: trimmedTerm }),
},
...results,
];
}
cb(results);
callback(results);
}
function clearText($el: JQuery<HTMLElement>) {
@ -198,6 +291,85 @@ function fullTextSearch($el: JQuery<HTMLElement>, options: Options) {
$el.autocomplete("val", searchString);
}
function renderCommandSuggestion(s: Suggestion): string {
const icon = s.icon || "bx bx-terminal";
const shortcut = s.commandShortcut
? `<kbd class="command-shortcut">${s.commandShortcut}</kbd>`
: "";
return `
<div class="command-suggestion">
<span class="command-icon ${icon}"></span>
<div class="command-content">
<div class="command-name">${s.highlightedNotePathTitle}</div>
${s.commandDescription ? `<div class="command-description">${s.commandDescription}</div>` : ""}
</div>
${shortcut}
</div>
`;
}
function renderNoteSuggestion(s: Suggestion): string {
const actionClass =
s.action === SuggestionAction.SearchNotes ? "search-notes-action" : "";
const iconClass = (() => {
switch (s.action) {
case SuggestionAction.SearchNotes:
return "bx bx-search";
case SuggestionAction.CreateAndLinkNote:
case SuggestionAction.CreateNote:
return "bx bx-plus";
case SuggestionAction.CreateAndLinkChildNote:
case SuggestionAction.CreateChildNote:
return "bx bx-plus";
case SuggestionAction.ExternalLink:
return "bx bx-link-external";
default:
return s.icon ?? "bx bx-note";
}
})();
return `
<div class="note-suggestion ${actionClass}" style="display:inline-flex; align-items:center;">
<span class="icon ${iconClass}" style="display:inline-block; vertical-align:middle; line-height:1; margin-right:0.4em;"></span>
<span class="text" style="display:inline-block; vertical-align:middle;">
<span class="search-result-title">${s.highlightedNotePathTitle}</span>
${s.highlightedAttributeSnippet
? `<span class="search-result-attributes">${s.highlightedAttributeSnippet}</span>`
: ""}
</span>
</div>
`;
}
function renderSuggestion(suggestion: Suggestion): string {
return suggestion.action === SuggestionAction.Command
? renderCommandSuggestion(suggestion)
: renderNoteSuggestion(suggestion);
}
function mapSuggestionToCreateNoteAction(
action: SuggestionAction
): CreateNoteAction | null {
switch (action) {
case SuggestionAction.CreateNote:
return CreateNoteAction.CreateNote;
case SuggestionAction.CreateAndLinkNote:
return CreateNoteAction.CreateAndLinkNote;
case SuggestionAction.CreateChildNote:
return CreateNoteAction.CreateChildNote;
case SuggestionAction.CreateAndLinkChildNote:
return CreateNoteAction.CreateAndLinkChildNote;
default:
return null;
}
}
function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
if ($el.hasClass("note-autocomplete-input")) {
// clear any event listener added in previous invocation of this function
@ -283,24 +455,21 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
$el.autocomplete(
{
...autocompleteOptions,
appendTo: document.querySelector("body"),
appendTo: document.body,
hint: false,
autoselect: true,
// openOnFocus has to be false, otherwise re-focus (after return from note type chooser dialog) forces
// re-querying of the autocomplete source which then changes the currently selected suggestion
openOnFocus: false,
minLength: 0,
tabAutocomplete: false
tabAutocomplete: false,
},
[
{
source: (term, cb) => {
source: (term, callback) => {
clearTimeout(debounceTimeoutId);
debounceTimeoutId = setTimeout(() => {
if (isComposingInput) {
return;
if (!isComposingInput) {
autocompleteSource(term, callback, options);
}
autocompleteSource(term, cb, options);
}, searchDelay);
if (searchDelay === 0) {
@ -308,109 +477,85 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
}
},
displayKey: "notePathTitle",
templates: {
suggestion: (suggestion) => {
if (suggestion.action === "command") {
let html = `<div class="command-suggestion">`;
html += `<span class="command-icon ${suggestion.icon || "bx bx-terminal"}"></span>`;
html += `<div class="command-content">`;
html += `<div class="command-name">${suggestion.highlightedNotePathTitle}</div>`;
if (suggestion.commandDescription) {
html += `<div class="command-description">${suggestion.commandDescription}</div>`;
}
html += `</div>`;
if (suggestion.commandShortcut) {
html += `<kbd class="command-shortcut">${suggestion.commandShortcut}</kbd>`;
}
html += '</div>';
return html;
}
// Add special class for search-notes action
const actionClass = suggestion.action === "search-notes" ? "search-notes-action" : "";
// Choose appropriate icon based on action
let iconClass = suggestion.icon ?? "bx bx-note";
if (suggestion.action === "search-notes") {
iconClass = "bx bx-search";
} else if (suggestion.action === "create-note") {
iconClass = "bx bx-plus";
} else if (suggestion.action === "external-link") {
iconClass = "bx bx-link-external";
}
// Simplified HTML structure without nested divs
let html = `<div class="note-suggestion ${actionClass}">`;
html += `<span class="icon ${iconClass}"></span>`;
html += `<span class="text">`;
html += `<span class="search-result-title">${suggestion.highlightedNotePathTitle}</span>`;
// Add attribute snippet inline if available
if (suggestion.highlightedAttributeSnippet) {
html += `<span class="search-result-attributes">${suggestion.highlightedAttributeSnippet}</span>`;
}
html += `</span>`;
html += `</div>`;
return html;
}
},
// we can't cache identical searches because notes can be created / renamed, new recent notes can be added
cache: false
}
templates: { suggestion: renderSuggestion },
cache: false,
},
]
);
// TODO: Types fail due to "autocomplete:selected" not being registered in type definitions.
($el as any).on("autocomplete:selected", async (event: Event, suggestion: Suggestion) => {
if (suggestion.action === "command") {
async function doCommand() {
$el.autocomplete("close");
$el.trigger("autocomplete:commandselected", [suggestion]);
return;
}
if (suggestion.action === "external-link") {
async function doExternalLink() {
$el.setSelectedNotePath(null);
$el.setSelectedExternalLink(suggestion.externalLink);
$el.autocomplete("val", suggestion.externalLink);
$el.autocomplete("close");
$el.trigger("autocomplete:externallinkselected", [suggestion]);
}
async function resolveSuggestionNotePathUnderCurrentHoist(note: FNote) {
const hoisted = appContext.tabManager.getActiveContext()?.hoistedNoteId;
suggestion.notePath = note.getBestNotePathString(hoisted);
}
async function doSearchNotes() {
const searchString = suggestion.noteTitle;
appContext.triggerCommand("searchNotes", { searchString });
}
async function selectNoteFromAutocomplete(suggestion: Suggestion) {
$el.setSelectedNotePath(suggestion.notePath);
$el.setSelectedExternalLink(null);
$el.autocomplete("val", suggestion.noteTitle);
$el.autocomplete("close");
$el.trigger("autocomplete:externallinkselected", [suggestion]);
return;
$el.trigger("autocomplete:noteselected", [suggestion]);
}
if (suggestion.action === "create-note") {
const { success, noteType, templateNoteId, notePath } = await noteCreateService.chooseNoteType();
if (!success) {
switch (suggestion.action) {
case SuggestionAction.Command:
await doCommand();
return;
case SuggestionAction.ExternalLink:
await doExternalLink();
break;
case SuggestionAction.CreateNote:
case SuggestionAction.CreateAndLinkNote:
case SuggestionAction.CreateChildNote:
case SuggestionAction.CreateAndLinkChildNote: {
const createNoteAction = mapSuggestionToCreateNoteAction(
suggestion.action
)!;
const { note } = await noteCreateService.createNoteFromAction(
createNoteAction,
true,
suggestion.noteTitle,
suggestion.parentNoteId,
);
if (!note) break;
await resolveSuggestionNotePathUnderCurrentHoist(note);
await selectNoteFromAutocomplete(suggestion);
break;
}
const { note } = await noteCreateService.createNote( notePath || suggestion.parentNoteId, {
title: suggestion.noteTitle,
activate: false,
type: noteType,
templateNoteId: templateNoteId
});
const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId;
suggestion.notePath = note?.getBestNotePathString(hoistedNoteId);
case SuggestionAction.SearchNotes:
await doSearchNotes();
break;
default:
await selectNoteFromAutocomplete(suggestion);
}
if (suggestion.action === "search-notes") {
const searchString = suggestion.noteTitle;
appContext.triggerCommand("searchNotes", { searchString });
return;
}
$el.setSelectedNotePath(suggestion.notePath);
$el.setSelectedExternalLink(null);
$el.autocomplete("val", suggestion.noteTitle);
$el.autocomplete("close");
$el.trigger("autocomplete:noteselected", [suggestion]);
});
$el.on("autocomplete:closed", () => {

View File

@ -10,8 +10,63 @@ import type FNote from "../entities/fnote.js";
import type FBranch from "../entities/fbranch.js";
import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js";
import type { CKTextEditor } from "@triliumnext/ckeditor5";
import dateNoteService from "../services/date_notes.js";
import { CreateNoteAction } from "@triliumnext/commons";
export interface CreateNoteOpts {
/**
* Defines the type hierarchy and rules for valid argument combinations
* accepted by `note_create`.
*
* ## Overview
* Each variant extends `CreateNoteOpts` and enforces specific constraints to
* ensure only valid note creation options are allowed at compile time.
*
* ## Type Safety
* The `PromptingRule` ensures that `promptForType` and `type` stay mutually
* exclusive (if prompting, `type` is undefined).
*
* The type system prevents invalid argument mixes by design successful type
* checks guarantee a valid state, following CurryHoward correspondence
* principles (types as proofs).
*
* ## Maintenance
* If adding or modifying `Opts`, ensure:
* - All valid combinations are represented (avoid *false negatives*).
* - No invalid ones slip through (avoid *false positives*).
*
* Hierarchy (general specific):
* - CreateNoteOpts
* - CreateNoteWithUrlOpts
* - CreateNoteIntoDefaultOpts
*/
/** enforces a truth rule:
* - If `promptForType` is true `type` must be undefined.
* - If `promptForType` is false `type` must be defined.
*/
type PromptingRule = {
promptForType: true;
type?: never;
} | {
promptForType?: false;
/**
* The note type (e.g. "text", "code", "image", "mermaid", etc.).
*
* If omitted, the server will automatically default to `"text"`.
* TypeScript still enforces explicit typing unless `promptForType` is true,
* to encourage clarity at the call site.
*/
type?: string;
};
/**
* Base type for all note creation options (domain hypernym).
* All specific note option types extend from this.
*
* Combine with `&` to ensure valid logical combinations.
*/
type CreateNoteBase = {
isProtected?: boolean;
saveSelection?: boolean;
title?: string | null;
@ -21,10 +76,34 @@ export interface CreateNoteOpts {
templateNoteId?: string;
activate?: boolean;
focus?: "title" | "content";
target?: string;
targetBranchId?: string;
textEditor?: CKTextEditor;
}
} & PromptingRule;
/*
* Defines options for creating a note at a specific path.
* Serves as a base for "into", "before", and "after" variants,
* sharing common URL-related fields.
*/
export type CreateNoteWithLinkOpts =
| (CreateNoteBase & {
target: "into";
parentNoteLink?: string;
// No branch ID needed for "into"
})
| (CreateNoteBase & {
target: "before" | "after";
// Either an Url or a Path
parentNoteLink?: string;
// Required for "before"/"after"
targetBranchId: string;
});
export type CreateNoteIntoDefaultOpts = CreateNoteBase & {
target: "default";
parentNoteLink?: never;
};
export type CreateNoteOpts = CreateNoteWithLinkOpts | CreateNoteIntoDefaultOpts;
interface Response {
// TODO: Deduplicate with server once we have client/server architecture.
@ -37,7 +116,141 @@ interface DuplicateResponse {
note: FNote;
}
async function createNote(parentNotePath: string | undefined, options: CreateNoteOpts = {}) {
// The low level note creation
async function createNote(
options: CreateNoteOpts
): Promise<{ note: FNote | null; branch: FBranch | undefined }> {
let resolvedOptions = { ...options };
// handle prompts centrally to write once fix for all
if (options.promptForType) {
const maybeResolvedOptions = await promptForType(options);
if (!maybeResolvedOptions) {
return { note: null, branch: undefined };
}
resolvedOptions = maybeResolvedOptions;
}
switch(resolvedOptions.target) {
case "default":
return createNoteIntoDefaultLocation(resolvedOptions);
case "into":
case "before":
case "after":
return createNoteWithLink(resolvedOptions);
}
}
// A wrapper to standardize note creation
async function createNoteFromAction(
action: CreateNoteAction,
promptForType: boolean,
title: string | undefined,
parentNoteLink: string | undefined,
): Promise<{ note: FNote | null; branch: FBranch | undefined }> {
switch (action) {
case CreateNoteAction.CreateNote: {
const resp = await createNote(
{
target: "default",
title: title,
activate: true,
promptForType,
}
);
return resp;
}
case CreateNoteAction.CreateAndLinkNote: {
const resp = await createNote(
{
target: "default",
title,
activate: false,
promptForType,
}
);
return resp;
}
case CreateNoteAction.CreateChildNote: {
if (!parentNoteLink) {
console.warn("createNoteFromAction: Missing parentNoteLink");
return { note: null, branch: undefined };
}
const resp = await createNote(
{
target: "into",
parentNoteLink,
title,
activate: true,
promptForType,
},
);
return resp
}
case CreateNoteAction.CreateAndLinkChildNote: {
if (!parentNoteLink) {
console.warn("createNoteFromAction: Missing parentNoteLink");
return { note: null, branch: undefined };
}
const resp = await createNote(
{
target: "into",
parentNoteLink: parentNoteLink,
title,
activate: false,
promptForType,
},
)
return resp;
}
default:
console.warn("Unknown CreateNoteAction:", action);
return { note: null, branch: undefined };
}
}
async function promptForType(
options: CreateNoteOpts
) : Promise<CreateNoteOpts | null> {
const { success, noteType, templateNoteId, notePath } = await chooseNoteType();
if (!success) {
return null;
}
let resolvedOptions: CreateNoteOpts = {
...options,
promptForType: false,
type: noteType,
templateNoteId,
};
if (notePath) {
resolvedOptions = {
...resolvedOptions,
target: "into",
parentNoteLink: notePath,
};
}
return resolvedOptions;
}
/**
* Creates a new note under a specified parent note path.
*
* @param target - Mirrors the `createNote` API in apps/server/src/routes/api/notes.ts.
* @param options - Note creation options
* @returns A promise resolving with the created note and its branch.
*/
async function createNoteWithLink(
options: CreateNoteWithLinkOpts
): Promise<{ note: FNote | null; branch: FBranch | undefined }> {
options = Object.assign(
{
activate: true,
@ -61,7 +274,8 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot
[options.title, options.content] = parseSelectedHtml(options.textEditor.getSelectedHtml());
}
const parentNoteId = treeService.getNoteIdFromUrl(parentNotePath);
const parentNoteLink = options.parentNoteLink;
const parentNoteId = treeService.getNoteIdFromLink(parentNoteLink);
if (options.type === "mermaid" && !options.content && !options.templateNoteId) {
options.content = `graph TD;
@ -71,7 +285,12 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot
C-->D;`;
}
const { note, branch } = await server.post<Response>(`notes/${parentNoteId}/children?target=${options.target}&targetBranchId=${options.targetBranchId || ""}`, {
const query =
options.target === "into"
? `target=${options.target}`
: `target=${options.target}&targetBranchId=${options.targetBranchId}`;
const { note, branch } = await server.post<Response>(`notes/${parentNoteId}/children?${query}`, {
title: options.title,
content: options.content || "",
isProtected: options.isProtected,
@ -89,7 +308,7 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot
const activeNoteContext = appContext.tabManager.getActiveContext();
if (activeNoteContext && options.activate) {
await activeNoteContext.setNote(`${parentNotePath}/${note.noteId}`);
await activeNoteContext.setNote(`${parentNoteId}/${note.noteId}`);
if (options.focus === "title") {
appContext.triggerEvent("focusAndSelectTitle", { isNewNote: true });
@ -107,25 +326,46 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot
};
}
/**
* Creates a new note inside the user's Inbox.
*
* @param {CreateNoteIntoDefaultOpts} [options] - Optional settings such as title, type, template, or content.
* @returns {Promise<{ note: FNote | null; branch: FBranch | undefined }>}
* Resolves with the created note and its branch, or `{ note: null, branch: undefined }` if the inbox is missing.
*/
async function createNoteIntoDefaultLocation(
options: CreateNoteIntoDefaultOpts
): Promise<{ note: FNote | null; branch: FBranch | undefined }> {
const inboxNote = await dateNoteService.getInboxNote();
if (!inboxNote) {
console.warn("Missing inbox note.");
// always return a defined object
return { note: null, branch: undefined };
}
if (options.isProtected === undefined) {
options.isProtected =
inboxNote.isProtected && protectedSessionHolder.isProtectedSessionAvailable();
}
const result = await createNoteWithLink(
{
...options,
target: "into",
parentNoteLink: inboxNote.getBestNotePathString(),
}
);
return result;
}
async function chooseNoteType() {
return new Promise<ChooseNoteTypeResponse>((res) => {
appContext.triggerCommand("chooseNoteType", { callback: res });
});
}
async function createNoteWithTypePrompt(parentNotePath: string, options: CreateNoteOpts = {}) {
const { success, noteType, templateNoteId, notePath } = await chooseNoteType();
if (!success) {
return;
}
options.type = noteType;
options.templateNoteId = templateNoteId;
return await createNote(notePath || parentNotePath, options);
}
/* If the first element is heading, parse it out and use it as a new heading. */
function parseSelectedHtml(selectedHtml: string) {
const dom = $.parseHTML(selectedHtml);
@ -146,7 +386,7 @@ function parseSelectedHtml(selectedHtml: string) {
}
async function duplicateSubtree(noteId: string, parentNotePath: string) {
const parentNoteId = treeService.getNoteIdFromUrl(parentNotePath);
const parentNoteId = treeService.getNoteIdFromLink(parentNotePath);
const { note } = await server.post<DuplicateResponse>(`notes/${noteId}/duplicate/${parentNoteId}`);
await ws.waitForMaxKnownEntityChangeId();
@ -159,7 +399,6 @@ async function duplicateSubtree(noteId: string, parentNotePath: string) {
export default {
createNote,
createNoteWithTypePrompt,
createNoteFromAction,
duplicateSubtree,
chooseNoteType
};

View File

@ -92,7 +92,7 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
if (effectivePathSegments.includes(hoistedNoteId) && effectivePathSegments.includes('root')) {
return effectivePathSegments;
} else {
const noteId = getNoteIdFromUrl(notePath);
const noteId = getNoteIdFromLink(notePath);
if (!noteId) {
throw new Error(`Unable to find note with ID: ${noteId}.`);
}
@ -129,7 +129,7 @@ function getParentProtectedStatus(node: Fancytree.FancytreeNode) {
return hoistedNoteService.isHoistedNode(node) ? false : node.getParent().data.isProtected;
}
function getNoteIdFromUrl(urlOrNotePath: string | null | undefined) {
function getNoteIdFromLink(urlOrNotePath: string | null | undefined) {
if (!urlOrNotePath) {
return null;
}
@ -306,7 +306,7 @@ export default {
getParentProtectedStatus,
getNotePath,
getNotePathTitleComponents,
getNoteIdFromUrl,
getNoteIdFromLink,
getNoteIdAndParentIdFromUrl,
getBranchIdFromUrl,
getNoteTitle,

View File

@ -1686,46 +1686,6 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
body.mobile .modal-dialog.modal-dialog-scrollable {
height: unset;
}
body.mobile .revisions-dialog .modal-dialog {
height: 95vh;
}
body.mobile .revisions-dialog .modal-body {
height: 100% !important;
flex-direction: column;
padding: 0;
}
body.mobile .revisions-dialog .revision-list {
height: unset;
max-height: 20vh;
border-bottom: 1px solid var(--main-border-color) !important;
padding: 0 1em;
}
body.mobile .revisions-dialog .modal-body > .revision-content-wrapper {
flex-grow: 1;
height: 100%;
overflow: auto;
margin: 0;
}
body.mobile .revisions-dialog .modal-body > .revision-content-wrapper > div:first-of-type {
flex-direction: column;
}
body.mobile .revisions-dialog .revision-title {
font-size: 1rem;
}
body.mobile .revisions-dialog .revision-title-buttons {
text-align: center;
}
body.mobile .revisions-dialog .revision-content {
padding: 0.5em;
}
}
/* Mobile, tablet mode */

View File

@ -109,3 +109,6 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.6) !important;
}
.use-note-color {
--custom-color: var(--dark-theme-custom-color);
}

View File

@ -92,3 +92,7 @@ html {
.board-note {
color: var(--light-theme-custom-color, inherit);
}
.use-note-color {
--custom-color: var(--light-theme-custom-color);
}

View File

@ -270,6 +270,12 @@
--ck-editor-toolbar-button-on-color: white;
--ck-editor-toolbar-button-on-shadow: 1px 1px 2px rgba(0, 0, 0, .75);
--ck-editor-toolbar-dropdown-button-open-background: #ffffff14;
--calendar-coll-event-background-saturation: 12%;
--calendar-coll-event-background-lightness: 21%;
--calendar-coll-event-background-color: #3c3c3c;
--calendar-coll-event-text-color: white;
--calendar-cell-event-hover-filter: brightness(1.25);
}
/*
@ -309,3 +315,7 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
--modal-border-color: hsl(var(--custom-color-hue), 9.4%, 25.1%);
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 13.2%, 20.8%);
}
.use-note-color {
--custom-color: var(--dark-theme-custom-color);
}

View File

@ -268,6 +268,12 @@
--ck-editor-toolbar-button-on-color: black;
--ck-editor-toolbar-button-on-shadow: none;
--ck-editor-toolbar-dropdown-button-open-background: #0000000f;
--calendar-coll-event-background-lightness: 95%;
--calendar-coll-event-background-saturation: 80%;
--calendar-coll-event-background-color: #eaeaea;
--calendar-coll-event-text-color: black;
--calendar-cell-event-hover-filter: brightness(.95) saturate(1.25);
}
#left-pane .fancytree-node.tinted {

View File

@ -0,0 +1,73 @@
{
"import": {
"safeImportTooltip": "Trilium <code>.zip</code> export files can contain executable scripts which may contain harmful behaviour. Safe import will deactivate automatic execution of all imported scripts. Uncheck \"Safe import\" only if the imported archive is supposed to contain executable scripts and you completely trust the contents of the import file.",
"shrinkImagesTooltip": "<p>If you check this option, Trilium will attempt to shrink the imported images by scaling and optimisation which may affect the perceived image quality. If unchecked, images will be imported without changes.</p><p>This doesn't apply to <code>.zip</code> imports with metadata since it is assumed these files are already optimised.</p>",
"codeImportedAsCode": "Import recognised code files (e.g. <code>.json</code>) as code notes if it's unclear from metadata"
},
"upload_attachments": {
"tooltip": "If you check this option, Trilium will attempt to shrink the uploaded images by scaling and optimisation which may affect the perceived image quality. If unchecked, images will be uploaded without changes."
},
"attribute_detail": {
"auto_read_only_disabled": "text/code notes can be set automatically into read mode when they are too large. You can disable this behaviour on per-note basis by adding this label to the note",
"workspace_tab_background_color": "CSS colour used in the note tab when hoisted to this note",
"color": "defines colour of the note in note tree, links etc. Use any valid CSS colour value like 'red' or #a13d5f",
"color_type": "Colour"
},
"mobile_detail_menu": {
"error_unrecognized_command": "Unrecognised command {{command}}"
},
"promoted_attributes": {
"remove_color": "Remove the colour label"
},
"max_content_width": {
"centerContent": "Keep content centred"
},
"theme": {
"auto_theme": "Legacy (Follow system colour scheme)",
"triliumnext": "Trilium (Follow system colour scheme)"
},
"search_engine": {
"custom_name_placeholder": "Customise search engine name",
"custom_url_placeholder": "Customise search engine url"
},
"highlights_list": {
"description": "You can customise the highlights list displayed in the right panel:",
"color": "Coloured text",
"bg_color": "Text with background colour"
},
"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 customise this number:"
},
"custom_date_time_format": {
"description": "Customise the format of the date and time inserted via <shortcut /> or the toolbar. See <doc>Day.js docs</doc> for available format tokens."
},
"i18n": {
"title": "Localisation"
},
"attachment_detail_2": {
"unrecognized_role": "Unrecognised attachment role '{{role}}'."
},
"ai_llm": {
"reprocess_index_started": "Search index optimisation started in the background",
"index_rebuilding": "Optimising index ({{percentage}}%)",
"index_rebuild_complete": "Index optimisation complete"
},
"highlighting": {
"color-scheme": "Colour Scheme"
},
"code_theme": {
"color-scheme": "Colour scheme"
},
"call_to_action": {
"background_effects_message": "On Windows devices, background effects are now fully stable. The background effects adds a touch of colour to the user interface by blurring the background behind it. This technique is also used in other applications such as Windows Explorer."
},
"settings_appearance": {
"related_code_blocks": "Colour scheme for code blocks in text notes",
"related_code_notes": "Colour scheme for code notes"
},
"note-color": {
"clear-color": "Clear note colour",
"set-color": "Set note colour",
"set-custom-color": "Set custom note colour"
}
}

View File

@ -1897,7 +1897,10 @@
},
"note_autocomplete": {
"search-for": "Search for \"{{term}}\"",
"create-note": "Create and link child note \"{{term}}\"",
"create-child-note": "Create child note \"{{term}}\"",
"create-note": "Create note \"{{term}}\"",
"create-and-link-child-note": "Create and link child note \"{{term}}\"",
"create-and-link-note": "Create and link note \"{{term}}\"",
"insert-external-link": "Insert external link to \"{{term}}\"",
"clear-text-field": "Clear text field",
"show-recent-notes": "Show recent notes",
@ -2107,5 +2110,8 @@
"clear-color": "Clear note color",
"set-color": "Set note color",
"set-custom-color": "Set custom note color"
},
"popup-editor": {
"maximize": "Switch to full editor"
}
}

View File

@ -312,7 +312,8 @@
"moveNoteUpDown": "ノートリストでノートを上/下に移動",
"notSet": "未設定",
"goUpDown": "ノートのリストで上下する",
"editBranchPrefix": "アクティブノートのクローンの <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/tree-concepts.html#prefix\">プレフィックス</a> を編集する"
"editBranchPrefix": "アクティブノートのクローンの <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/tree-concepts.html#prefix\">プレフィックス</a> を編集する",
"editShortcuts": "キーボードショートカットを編集"
},
"import": {
"importIntoNote": "ノートにインポート",

View File

@ -3,7 +3,7 @@ import server from "../../services/server.js";
import froca from "../../services/froca.js";
import linkService from "../../services/link.js";
import attributeAutocompleteService from "../../services/attribute_autocomplete.js";
import noteAutocompleteService from "../../services/note_autocomplete.js";
import noteAutocompleteService, { SuggestionMode } from "../../services/note_autocomplete.js";
import promotedAttributeDefinitionParser from "../../services/promoted_attribute_definition_parser.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import SpacedUpdate from "../../services/spaced_update.js";
@ -429,7 +429,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
this.$rowTargetNote = this.$widget.find(".attr-row-target-note");
this.$inputTargetNote = this.$widget.find(".attr-input-target-note");
noteAutocompleteService.initNoteAutocomplete(this.$inputTargetNote, { allowCreatingNotes: true }).on("autocomplete:noteselected", (event, suggestion, dataset) => {
noteAutocompleteService.initNoteAutocomplete(this.$inputTargetNote, { suggestionMode: SuggestionMode.SuggestCreateAndLink }).on("autocomplete:noteselected", (event, suggestion, dataset) => {
if (!suggestion.notePath) {
return false;
}

View File

@ -7,7 +7,7 @@ import branches from "../../../services/branches";
import { executeBulkActions } from "../../../services/bulk_action";
import froca from "../../../services/froca";
import { t } from "../../../services/i18n";
import note_create from "../../../services/note_create";
import note_create from "../../../services/note_create.js";
import server from "../../../services/server";
import { ColumnMap } from "./data";
@ -39,9 +39,11 @@ export default class BoardApi {
const parentNotePath = this.parentNote.noteId;
// Create a new note as a child of the parent note
const { note: newNote, branch: newBranch } = await note_create.createNote(parentNotePath, {
const { note: newNote, branch: newBranch } = await note_create.createNote({
target: "into",
parentNoteLink: parentNotePath,
activate: false,
title
title,
});
if (newNote && newBranch) {
@ -139,13 +141,17 @@ export default class BoardApi {
async insertRowAtPosition(
column: string,
relativeToBranchId: string,
direction: "before" | "after") {
const { note, branch } = await note_create.createNote(this.parentNote.noteId, {
activate: false,
targetBranchId: relativeToBranchId,
target: direction,
title: t("board_view.new-item")
});
direction: "before" | "after"
) {
const { note, branch } = await note_create.createNote(
{
target: direction,
parentNoteLink: this.parentNote.noteId,
activate: false,
targetBranchId: relativeToBranchId,
title: t("board_view.new-item"),
}
);
if (!note || !branch) {
throw new Error("Failed to create note");

View File

@ -57,12 +57,18 @@ export function openNoteContextMenu(api: Api, event: ContextMenuEvent, note: FNo
{
title: t("board_view.insert-above"),
uiIcon: "bx bx-list-plus",
handler: () => api.insertRowAtPosition(column, branchId, "before")
handler: () => api.insertRowAtPosition(
column,
branchId,
"before")
},
{
title: t("board_view.insert-below"),
uiIcon: "bx bx-empty",
handler: () => api.insertRowAtPosition(column, branchId, "after")
handler: () => api.insertRowAtPosition(
column,
branchId,
"after")
},
{ kind: "separator" },
{

View File

@ -15,6 +15,7 @@ import FormTextArea from "../../react/FormTextArea";
import FNote from "../../../entities/fnote";
import NoteAutocomplete from "../../react/NoteAutocomplete";
import toast from "../../../services/toast";
import { SuggestionMode } from "../../../services/note_autocomplete";
export interface BoardViewData {
columns?: BoardColumnData[];
@ -309,7 +310,7 @@ export function TitleEditor({ currentValue, placeholder, save, dismiss, mode, is
noteId={currentValue ?? ""}
opts={{
hideAllButtons: true,
allowCreatingNotes: true
suggestionMode: SuggestionMode.SuggestCreateAndLink
}}
onKeyDown={(e) => {
if (e.key === "Escape") {

View File

@ -3,6 +3,7 @@ import froca from "../../../services/froca";
import { formatDateToLocalISO, getCustomisableLabel, getMonthsInDateRange, offsetDate } from "./utils";
import FNote from "../../../entities/fnote";
import server from "../../../services/server";
import clsx from "clsx";
interface Event {
startDate: string,
@ -81,6 +82,7 @@ export async function buildEvent(note: FNote, { startDate, endDate, startTime, e
const customTitleAttributeName = note.getLabelValue("calendar:title");
const titles = await parseCustomTitle(customTitleAttributeName, note);
const color = note.getLabelValue("calendar:color") ?? note.getLabelValue("color");
const colorClass = note.getColorClass();
const events: EventInput[] = [];
const calendarDisplayedAttributes = note.getLabelValue("calendar:displayedAttributes")?.split(",");
@ -111,7 +113,7 @@ export async function buildEvent(note: FNote, { startDate, endDate, startTime, e
color: color ?? undefined,
iconClass: note.getLabelValue("iconClass"),
promotedAttributes: displayedAttributesData,
className: isArchived ? "archived" : ""
className: clsx({archived: isArchived}, colorClass)
};
if (endDate) {
eventData.end = endDate;

View File

@ -1,4 +1,9 @@
.calendar-view {
--fc-event-border-color: var(--calendar-coll-event-text-color);
--fc-event-bg-color: var(--calendar-coll-event-background-color);
--fc-event-text-color: var(--calendar-coll-event-text-color);
--fc-event-selected-overlay-color: transparent;
overflow: hidden;
position: relative;
outline: 0;
@ -76,3 +81,51 @@ body.desktop:not(.zen) .calendar-view .calendar-header {
padding-inline-end: unset !important;
}
/* #endregion */
/* #region Events */
.calendar-view a.fc-timegrid-event,
.calendar-view a.fc-daygrid-event {
font-weight: 500;
}
.calendar-view a.fc-timegrid-event,
.calendar-view a.fc-daygrid-event:not(.fc-daygrid-dot-event) {
--border-color: transparent;
border-width: 2px 2px 2px 4px;
border-color: var(--border-color) var(--border-color) var(--border-color)
var(--fc-event-text-color) !important;
padding-left: 8px;
}
.calendar-view a.fc-timegrid-event.fc-event-selected,
.calendar-view a.fc-timegrid-event.fc-event:focus,
.calendar-view a.fc-daygrid-event.fc-event-selected,
.calendar-view a.fc-daygrid-event.fc-event:focus {
--border-color: var(--custom-color, var(--input-focus-outline-color));
}
.calendar-view a.fc-timegrid-event:hover,
.calendar-view a.fc-daygrid-event:hover {
filter: var(--calendar-cell-event-hover-filter);
border-color: var(--fc-event-text-color);
text-decoration: none;
color: currentColor;
}
.fc-timegrid-event.with-hue,
.fc-daygrid-event:not(.fc-daygrid-dot-event).with-hue {
--fc-event-text-color: var(--custom-color);
background: hsl(var(--custom-color-hue),
var(--calendar-coll-event-background-saturation),
var(--calendar-coll-event-background-lightness)) !important;
}
.fc-event-time {
opacity: .75;
}
/* #endregion */

View File

@ -77,6 +77,7 @@ export const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, (() => Promise<{ de
"pt_br": () => import("@fullcalendar/core/locales/pt-br"),
uk: () => import("@fullcalendar/core/locales/uk"),
en: null,
"en-GB": () => import("@fullcalendar/core/locales/en-gb"),
"en_rtl": null,
ar: () => import("@fullcalendar/core/locales/ar")
};

View File

@ -6,6 +6,7 @@ import Icon from "../../react/Icon.jsx";
import { useEffect, useRef, useState } from "preact/hooks";
import froca from "../../../services/froca.js";
import NoteAutocomplete from "../../react/NoteAutocomplete.jsx";
import { SuggestionMode } from "../../../services/note_autocomplete.js";
type ColumnType = LabelType | "relation";
@ -227,7 +228,7 @@ function RelationEditor({ cell, success }: EditorOpts) {
inputRef={inputRef}
noteId={cell.getValue()}
opts={{
allowCreatingNotes: true,
suggestionMode: SuggestionMode.SuggestCreateAndLink,
hideAllButtons: true
}}
noteIdChanged={success}

View File

@ -181,8 +181,8 @@ export function showRowContextMenu(parentComponent: Component, e: MouseEvent, ro
uiIcon: "bx bx-horizontal-left bx-rotate-90",
enabled: !sorters.length,
handler: () => parentComponent?.triggerCommand("addNewRow", {
parentNotePath: parentNoteId,
customOpts: {
parentNoteLink: parentNoteId,
target: "before",
targetBranchId: rowData.branchId,
}
@ -194,9 +194,12 @@ export function showRowContextMenu(parentComponent: Component, e: MouseEvent, ro
handler: async () => {
const branchId = row.getData().branchId;
const note = await froca.getBranch(branchId)?.getNote();
if (!note) {
return;
}
parentComponent?.triggerCommand("addNewRow", {
parentNotePath: note?.noteId,
customOpts: {
parentNoteLink: note.noteId,
target: "after",
targetBranchId: branchId,
}
@ -208,8 +211,8 @@ export function showRowContextMenu(parentComponent: Component, e: MouseEvent, ro
uiIcon: "bx bx-horizontal-left bx-rotate-270",
enabled: !sorters.length,
handler: () => parentComponent?.triggerCommand("addNewRow", {
parentNotePath: parentNoteId,
customOpts: {
parentNoteLink: parentNoteId,
target: "after",
targetBranchId: rowData.branchId,
}

View File

@ -1,6 +1,6 @@
import { EventCallBackMethods, RowComponent, Tabulator } from "tabulator-tables";
import { CommandListenerData } from "../../../components/app_context";
import note_create, { CreateNoteOpts } from "../../../services/note_create";
import note_create from "../../../services/note_create";
import { useLegacyImperativeHandlers } from "../../react/hooks";
import { RefObject } from "preact";
import { setAttribute, setLabel } from "../../../services/attributes";
@ -9,17 +9,23 @@ import server from "../../../services/server";
import branches from "../../../services/branches";
import AttributeDetailWidget from "../../attribute_widgets/attribute_detail";
/**
* Hook for handling row table editing, including adding new rows.
*/
export default function useRowTableEditing(api: RefObject<Tabulator>, attributeDetailWidget: AttributeDetailWidget, parentNotePath: string): Partial<EventCallBackMethods> {
// Adding new rows
useLegacyImperativeHandlers({
addNewRowCommand({ customOpts, parentNotePath: customNotePath }: CommandListenerData<"addNewRow">) {
const notePath = customNotePath ?? parentNotePath;
if (notePath) {
const opts: CreateNoteOpts = {
activate: false,
...customOpts
}
note_create.createNote(notePath, opts).then(({ branch }) => {
addNewRowCommand({ customOpts }: CommandListenerData<"addNewRow">) {
if (!customOpts) {
customOpts = {
target: "into",
};
}
const noteUrl = customOpts.parentNoteLink ?? parentNotePath;
if (noteUrl) {
customOpts.parentNoteLink = noteUrl;
customOpts.activate = false;
note_create.createNote(customOpts).then(({ branch }) => {
if (branch) {
setTimeout(() => {
if (!api.current) return;
@ -27,6 +33,7 @@ export default function useRowTableEditing(api: RefObject<Tabulator>, attributeD
}, 100);
}
})
}
}
});

View File

@ -19,6 +19,8 @@ import tree from "../../services/tree";
import froca from "../../services/froca";
import ReadOnlyNoteInfoBar from "../ReadOnlyNoteInfoBar";
import MobileEditorToolbar from "../type_widgets/text/mobile_editor_toolbar";
import { t } from "../../services/i18n";
import appContext from "../../components/app_context";
export default function PopupEditor() {
const [ shown, setShown ] = useState(false);
@ -62,8 +64,13 @@ export default function PopupEditor() {
title={<TitleRow />}
customTitleBarButtons={[{
iconClassName: "bx-expand-alt",
title: "Switch to full editor",
onClick: () => {/* TO DO */}
title: t("popup-editor.maximize"),
onClick: async () => {
if (!noteContext.noteId) return;
const { noteId, hoistedNoteId } = noteContext;
await appContext.tabManager.openInNewTab(noteId, hoistedNoteId, true);
setShown(false);
}
}]}
className="popup-editor-dialog"
size="lg"

View File

@ -5,7 +5,7 @@ import FormRadioGroup from "../react/FormRadioGroup";
import NoteAutocomplete from "../react/NoteAutocomplete";
import { useRef, useState, useEffect } from "preact/hooks";
import tree from "../../services/tree";
import note_autocomplete, { Suggestion } from "../../services/note_autocomplete";
import note_autocomplete, { SuggestionMode, Suggestion } from "../../services/note_autocomplete";
import { logError } from "../../services/ws";
import FormGroup from "../react/FormGroup.js";
import { refToJQuerySelector } from "../react/react_utils";
@ -58,7 +58,7 @@ export default function AddLinkDialog() {
}
if (suggestion.notePath) {
const noteId = tree.getNoteIdFromUrl(suggestion.notePath);
const noteId = tree.getNoteIdFromLink(suggestion.notePath);
if (noteId) {
setDefaultLinkTitle(noteId);
}
@ -133,7 +133,7 @@ export default function AddLinkDialog() {
onChange={setSuggestion}
opts={{
allowExternalLinks: true,
allowCreatingNotes: true
suggestionMode: SuggestionMode.SuggestCreateAndLink,
}}
/>
</FormGroup>

View File

@ -5,7 +5,7 @@ import FormRadioGroup from "../react/FormRadioGroup";
import Modal from "../react/Modal";
import NoteAutocomplete from "../react/NoteAutocomplete";
import Button from "../react/Button";
import { Suggestion, triggerRecentNotes } from "../../services/note_autocomplete";
import { SuggestionMode, Suggestion, triggerRecentNotes } from "../../services/note_autocomplete.js";
import tree from "../../services/tree";
import froca from "../../services/froca";
import { useTriliumEvent } from "../react/hooks";
@ -50,7 +50,7 @@ export default function IncludeNoteDialog() {
inputRef={autoCompleteRef}
opts={{
hideGoToSelectedNoteButton: true,
allowCreatingNotes: true
suggestionMode: SuggestionMode.SuggestCreateOnly,
}}
/>
</FormGroup>
@ -71,7 +71,7 @@ export default function IncludeNoteDialog() {
}
async function includeNote(notePath: string, editorApi: CKEditorApi, boxSize: BoxSize) {
const noteId = tree.getNoteIdFromUrl(notePath);
const noteId = tree.getNoteIdFromLink(notePath);
if (!noteId) {
return;
}

View File

@ -3,7 +3,7 @@ import Button from "../react/Button";
import NoteAutocomplete from "../react/NoteAutocomplete";
import { t } from "../../services/i18n";
import { useRef, useState } from "preact/hooks";
import note_autocomplete, { Suggestion } from "../../services/note_autocomplete";
import note_autocomplete, { SuggestionMode, Suggestion } from "../../services/note_autocomplete.js";
import appContext from "../../components/app_context";
import commandRegistry from "../../services/command_registry";
import { refToJQuerySelector } from "../react/react_utils";
@ -12,34 +12,53 @@ import shortcutService from "../../services/shortcuts";
const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120;
type Mode = "last-search" | "recent-notes" | "commands";
enum Mode {
LastSearch,
RecentNotes,
Commands,
}
export default function JumpToNoteDialogComponent() {
const [ mode, setMode ] = useState<Mode>();
const [ lastOpenedTs, setLastOpenedTs ] = useState<number>(0);
const containerRef = useRef<HTMLDivElement>(null);
const autocompleteRef = useRef<HTMLInputElement>(null);
const [ isCommandMode, setIsCommandMode ] = useState(mode === "commands");
const [ isCommandMode, setIsCommandMode ] = useState(mode === Mode.Commands);
const [ initialText, setInitialText ] = useState(isCommandMode ? "> " : "");
const actualText = useRef<string>(initialText);
const [ shown, setShown ] = useState(false);
async function openDialog(commandMode: boolean) {
async function openDialog(requestedMode: Mode) {
let newMode: Mode;
let initialText = "";
if (commandMode) {
newMode = "commands";
initialText = ">";
} else if (Date.now() - lastOpenedTs <= KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000 && actualText.current) {
// if you open the Jump To dialog soon after using it previously, it can often mean that you
// actually want to search for the same thing (e.g., you opened the wrong note at first try)
// so we'll keep the content.
// if it's outside of this time limit, then we assume it's a completely new search and show recent notes instead.
newMode = "last-search";
initialText = actualText.current;
} else {
newMode = "recent-notes";
switch (requestedMode) {
case Mode.Commands:
newMode = Mode.Commands;
initialText = ">";
break;
case Mode.LastSearch:
// if you open the Jump To dialog soon after using it previously, it can often mean that you
// actually want to search for the same thing (e.g., you opened the wrong note at first try)
// so we'll keep the content.
// if it's outside of this time limit, then we assume it's a completely new search and show recent notes instead.
if (Date.now() - lastOpenedTs <= KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000 && actualText.current) {
newMode = Mode.LastSearch;
initialText = actualText.current;
} else {
newMode = Mode.RecentNotes;
}
break;
default:
if (Date.now() - lastOpenedTs <= KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000 && actualText.current) {
newMode = Mode.LastSearch;
initialText = actualText.current;
} else {
newMode = Mode.RecentNotes;
}
break;
}
if (mode !== newMode) {
@ -51,8 +70,8 @@ export default function JumpToNoteDialogComponent() {
setLastOpenedTs(Date.now());
}
useTriliumEvent("jumpToNote", () => openDialog(false));
useTriliumEvent("commandPalette", () => openDialog(true));
useTriliumEvent("jumpToNote", () => openDialog(Mode.RecentNotes));
useTriliumEvent("commandPalette", () => openDialog(Mode.Commands));
async function onItemSelected(suggestion?: Suggestion | null) {
if (!suggestion) {
@ -70,12 +89,12 @@ export default function JumpToNoteDialogComponent() {
function onShown() {
const $autoComplete = refToJQuerySelector(autocompleteRef);
switch (mode) {
case "last-search":
case Mode.LastSearch:
break;
case "recent-notes":
case Mode.RecentNotes:
note_autocomplete.showRecentNotes($autoComplete);
break;
case "commands":
case Mode.Commands:
note_autocomplete.showAllCommands($autoComplete);
break;
}
@ -116,7 +135,7 @@ export default function JumpToNoteDialogComponent() {
container={containerRef}
text={initialText}
opts={{
allowCreatingNotes: true,
suggestionMode: SuggestionMode.SuggestCreateOnly,
hideGoToSelectedNoteButton: true,
allowJumpToSearchNotes: true,
isCommandPalette: true

View File

@ -7,7 +7,7 @@ import { useEffect, useState } from "preact/hooks";
import note_types from "../../services/note_types";
import { MenuCommandItem, MenuItem } from "../../menus/context_menu";
import { TreeCommandNames } from "../../menus/tree_context_menu";
import { Suggestion } from "../../services/note_autocomplete";
import { SuggestionMode, Suggestion } from "../../services/note_autocomplete.js";
import Badge from "../react/Badge";
import { useTriliumEvent } from "../react/hooks";
@ -76,6 +76,7 @@ export default function NoteTypeChooserDialogComponent() {
onHidden={() => {
callback?.({ success: false });
setShown(false);
setParentNote(null);
}}
show={shown}
stackable
@ -85,7 +86,7 @@ export default function NoteTypeChooserDialogComponent() {
onChange={setParentNote}
placeholder={t("note_type_chooser.search_placeholder")}
opts={{
allowCreatingNotes: false,
suggestionMode: SuggestionMode.SuggestNothing,
hideGoToSelectedNoteButton: true,
allowJumpToSearchNotes: false,
}}

View File

@ -0,0 +1,63 @@
body.mobile .revisions-dialog {
.modal-dialog {
height: 95vh;
}
.modal-header {
display: flex;
flex-wrap: wrap;
gap: 0.25em;
font-size: 0.9em;
}
.modal-title {
flex-grow: 1;
width: 100%;
}
.modal-body {
height: fit-content !important;
flex-direction: column;
padding: 0;
}
.modal-footer {
font-size: 0.9em;
}
.revision-list {
height: fit-content !important;
max-height: 20vh;
border-bottom: 1px solid var(--main-border-color) !important;
padding: 0 1em;
flex-shrink: 0;
}
.modal-body > .revision-content-wrapper {
flex-grow: 1;
max-width: unset !important;
height: 100%;
margin: 0;
display: block !important;
}
.modal-body > .revision-content-wrapper > div:first-of-type {
flex-direction: column;
}
.revision-title {
font-size: 1rem;
}
.revision-title-buttons {
text-align: center;
display: flex;
gap: 0.25em;
flex-wrap: wrap;
}
.revision-content {
padding: 0.5em;
height: fit-content;
}
}

View File

@ -20,6 +20,7 @@ import ActionButton from "../react/ActionButton";
import options from "../../services/options";
import { useTriliumEvent } from "../react/hooks";
import { diffWords } from "diff";
import "./revisions.css";
export default function RevisionsDialog() {
const [ note, setNote ] = useState<FNote>();
@ -137,7 +138,7 @@ export default function RevisionsDialog() {
function RevisionsList({ revisions, onSelect, currentRevision }: { revisions: RevisionItem[], onSelect: (val: string) => void, currentRevision?: RevisionItem }) {
return (
<FormList onSelect={onSelect} fullHeight>
<FormList onSelect={onSelect} fullHeight wrapperClassName="revision-list">
{revisions.map((item) =>
<FormListItem
value={item.revisionId}

View File

@ -5,7 +5,7 @@ import BasicWidget from "../basic_widget.js";
import toastService from "../../services/toast.js";
import appContext from "../../components/app_context.js";
import server from "../../services/server.js";
import noteAutocompleteService from "../../services/note_autocomplete.js";
import noteAutocompleteService, { SuggestionMode } from "../../services/note_autocomplete.js";
import { TPL, addMessageToChat, showSources, hideSources, showLoadingIndicator, hideLoadingIndicator } from "./ui.js";
import { formatMarkdown } from "./utils.js";
@ -163,7 +163,7 @@ export default class LlmChatPanel extends BasicWidget {
const mentionSetup: MentionFeed[] = [
{
marker: "@",
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText, SuggestionMode.SuggestCreateAndLink),
itemRenderer: (item) => {
const suggestion = item as Suggestion;
const itemElement = document.createElement("button");

View File

@ -29,7 +29,14 @@ export default function MobileDetailMenu() {
],
selectMenuItemHandler: async ({ command }) => {
if (command === "insertChildNote") {
note_create.createNote(appContext.tabManager.getActiveContextNotePath() ?? undefined);
const parentNoteUrl = appContext.tabManager.getActiveContextNotePath();
if (parentNoteUrl) {
note_create.createNote({
target: "into",
parentNoteLink: parentNoteUrl,
});
}
} else if (command === "delete") {
const notePath = appContext.tabManager.getActiveContextNotePath();
if (!notePath) {

View File

@ -224,7 +224,13 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
} else if (target.classList.contains("add-note-button")) {
const node = $.ui.fancytree.getNode(e as unknown as Event);
const parentNotePath = treeService.getNotePath(node);
noteCreateService.createNote(parentNotePath, { isProtected: node.data.isProtected });
noteCreateService.createNote(
{
target: "into",
parentNoteLink: parentNotePath,
isProtected: node.data.isProtected
},
);
} else if (target.classList.contains("enter-workspace-button")) {
const node = $.ui.fancytree.getNode(e as unknown as Event);
this.triggerCommand("hoistNote", { noteId: node.data.noteId });
@ -1403,10 +1409,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
let node: Fancytree.FancytreeNode | null | undefined = await this.expandToNote(activeNotePath, false);
if (node && node.data.noteId !== treeService.getNoteIdFromUrl(activeNotePath)) {
if (node && node.data.noteId !== treeService.getNoteIdFromLink(activeNotePath)) {
// if the active note has been moved elsewhere then it won't be found by the path,
// so we switch to the alternative of trying to find it by noteId
const noteId = treeService.getNoteIdFromUrl(activeNotePath);
const noteId = treeService.getNoteIdFromLink(activeNotePath);
if (noteId) {
const notesById = this.getNodesByNoteId(noteId);
@ -1836,9 +1842,13 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const node = this.getActiveNode();
if (!node) return;
const notePath = treeService.getNotePath(node);
noteCreateService.createNote(notePath, {
isProtected: node.data.isProtected
});
noteCreateService.createNote(
{
target: "into",
parentNoteLink: notePath,
isProtected: node.data.isProtected
}
)
}
}),
new TouchBar.TouchBarButton({

View File

@ -6,15 +6,17 @@ import "./FormList.css";
import { CommandNames } from "../../components/app_context";
import { useStaticTooltip } from "./hooks";
import { handleRightToLeftPlacement, isMobile } from "../../services/utils";
import clsx from "clsx";
interface FormListOpts {
children: ComponentChildren;
onSelect?: (value: string) => void;
style?: CSSProperties;
wrapperClassName?: string;
fullHeight?: boolean;
}
export default function FormList({ children, onSelect, style, fullHeight }: FormListOpts) {
export default function FormList({ children, onSelect, style, fullHeight, wrapperClassName }: FormListOpts) {
const wrapperRef = useRef<HTMLDivElement | null>(null);
const triggerRef = useRef<HTMLButtonElement | null>(null);
@ -43,7 +45,7 @@ export default function FormList({ children, onSelect, style, fullHeight }: Form
}, [ fullHeight ]);
return (
<div className="dropdownWrapper" ref={wrapperRef} style={builtinStyles}>
<div className={clsx("dropdownWrapper", wrapperClassName)} ref={wrapperRef} style={builtinStyles}>
<div className="dropdown" style={builtinStyles}>
<button
ref={triggerRef}

View File

@ -2,7 +2,7 @@ import { MutableRef, useEffect, useImperativeHandle, useMemo, useRef, useState }
import { AttributeEditor as CKEditorAttributeEditor, MentionFeed, ModelElement, ModelNode, ModelPosition } from "@triliumnext/ckeditor5";
import { t } from "../../../services/i18n";
import server from "../../../services/server";
import note_autocomplete, { Suggestion } from "../../../services/note_autocomplete";
import note_autocomplete, { SuggestionMode, Suggestion } from "../../../services/note_autocomplete.js";
import CKEditor, { CKEditorApi } from "../../react/CKEditor";
import { useLegacyImperativeHandlers, useLegacyWidget, useTooltip, useTriliumEvent, useTriliumOption } from "../../react/hooks";
import FAttribute from "../../../entities/fattribute";
@ -20,6 +20,7 @@ import type { CommandData, FilteredCommandNames } from "../../../components/app_
import { AttributeType } from "@triliumnext/commons";
import attributes from "../../../services/attributes";
import note_create from "../../../services/note_create";
import { CreateNoteAction } from "@triliumnext/commons";
type AttributeCommandNames = FilteredCommandNames<CommandData>;
@ -33,7 +34,7 @@ const HELP_TEXT = `
const mentionSetup: MentionFeed[] = [
{
marker: "@",
feed: (queryText) => note_autocomplete.autocompleteSourceForCKEditor(queryText),
feed: (queryText) => note_autocomplete.autocompleteSourceForCKEditor(queryText, SuggestionMode.SuggestCreateAndLink),
itemRenderer: (_item) => {
const item = _item as Suggestion;
const itemElement = document.createElement("button");
@ -247,16 +248,18 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI
$el.text(title);
},
createNoteForReferenceLink: async (title: string) => {
let result;
if (notePath) {
result = await note_create.createNoteWithTypePrompt(notePath, {
activate: false,
title: title
});
}
return result?.note?.getBestNotePathString();
createNoteFromCkEditor: async (
title: string,
parentNotePath: string | undefined,
action: CreateNoteAction
): Promise<string> => {
const { note } = await note_create.createNoteFromAction(
action,
true,
title,
parentNotePath,
);
return note?.getBestNotePathString() ?? "";
}
}), [ notePath ]));

View File

@ -4,7 +4,7 @@ import FormGroup from "../react/FormGroup";
import NoteAutocomplete from "../react/NoteAutocomplete";
import "./Empty.css";
import { ParentComponent, refToJQuerySelector } from "../react/react_utils";
import note_autocomplete from "../../services/note_autocomplete";
import note_autocomplete, { SuggestionMode } from "../../services/note_autocomplete";
import appContext from "../../components/app_context";
import FNote from "../../entities/fnote";
import search from "../../services/search";
@ -38,7 +38,7 @@ function NoteSearch({ ntxId }: { ntxId: string | null }) {
inputRef={autocompleteRef}
opts={{
hideGoToSelectedNoteButton: true,
allowCreatingNotes: true,
suggestionMode: SuggestionMode.SuggestCreateOnly,
allowJumpToSearchNotes: true,
}}
onChange={suggestion => {

View File

@ -28,6 +28,7 @@ const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, Options["locale"] | null>
de: null,
en: "en",
en_rtl: "en",
"en-GB": "en",
es: "es",
fr: "fr",
it: "it",

View File

@ -22,6 +22,7 @@ describe("Canvas i18n", () => {
if (locale.contentOnly || locale.devOnly) continue;
const languageCode = LANGUAGE_MAPPINGS[locale.id];
if (!supportedLanguageCodes.has(languageCode)) {
console.log("Supported locales:", Array.from(supportedLanguageCodes.values()).join(", "));
expect.fail(`Unable to find locale for ${locale.id} -> ${languageCode}.`)
}
}

View File

@ -1,10 +1,12 @@
import { Language } from "@excalidraw/excalidraw/i18n";
import type { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
export const LANGUAGE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, string | null> = {
export const LANGUAGE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, Language["code"] | null> = {
ar: "ar-SA",
cn: "zh-CN",
de: "de-DE",
en: "en",
"en-GB": "en",
en_rtl: "en",
es: "es-ES",
fr: "fr-FR",

View File

@ -16,7 +16,7 @@ import note_create from "../../../services/note_create";
import TouchBar, { TouchBarButton, TouchBarGroup, TouchBarSegmentedControl } from "../../react/TouchBar";
import { RefObject } from "preact";
import { buildSelectedBackgroundColor } from "../../../components/touch_bar";
import { deferred } from "@triliumnext/commons";
import { CreateNoteAction, deferred } from "@triliumnext/commons";
import { t } from "../../../services/i18n";
/**
@ -115,17 +115,18 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
},
loadIncludedNote,
// Creating notes in @-completion
async createNoteForReferenceLink(title: string) {
const notePath = noteContext?.notePath;
if (!notePath) return;
const resp = await note_create.createNoteWithTypePrompt(notePath, {
activate: false,
title: title
});
if (!resp || !resp.note) return;
return resp.note.getBestNotePathString();
async createNoteFromCkEditor (
title: string,
parentNotePath: string | undefined,
action: CreateNoteAction
): Promise<string> {
const { note }= await note_create.createNoteFromAction(
action,
true,
title,
parentNotePath,
)
return note?.getBestNotePathString() ?? "";
},
// Keyboard shortcut
async followLinkUnderCursorCommand() {
@ -162,7 +163,9 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
// without await as this otherwise causes deadlock through component mutex
const parentNotePath = appContext.tabManager.getActiveContextNotePath();
if (noteContext && parentNotePath) {
note_create.createNote(parentNotePath, {
note_create.createNote({
parentNoteLink: parentNotePath,
target: "into",
isProtected: note.isProtected,
saveSelection: true,
textEditor: await noteContext?.getTextEditor()

View File

@ -7,7 +7,7 @@ import emojiDefinitionsUrl from "@triliumnext/ckeditor5/src/emoji_definitions/en
import { copyTextWithToast } from "../../../services/clipboard_ext.js";
import { t } from "../../../services/i18n.js";
import { getMermaidConfig } from "../../../services/mermaid.js";
import noteAutocompleteService, { type Suggestion } from "../../../services/note_autocomplete.js";
import noteAutocompleteService, { SuggestionMode, type Suggestion } from "../../../services/note_autocomplete.js";
import mimeTypesService from "../../../services/mime_types.js";
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
import { buildToolbarConfig } from "./toolbar.js";
@ -181,7 +181,7 @@ export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfi
feeds: [
{
marker: "@",
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText, SuggestionMode.SuggestCreateAndLink),
itemRenderer: (item) => {
const itemElement = document.createElement("button");

View File

@ -55,6 +55,7 @@ export function buildClassicToolbar(multilineToolbar: boolean) {
...TEXT_FORMATTING_GROUP,
items: ["underline", "strikethrough", "|", "superscript", "subscript", "|", "kbd"]
},
"formatPainter",
"|",
"fontColor",
"fontBackgroundColor",
@ -104,6 +105,7 @@ export function buildFloatingToolbar() {
...TEXT_FORMATTING_GROUP,
items: [ "strikethrough", "|", "superscript", "subscript", "|", "kbd" ]
},
"formatPainter",
"|",
"fontColor",
"fontBackgroundColor",

View File

@ -8,6 +8,7 @@ interface GotoOpts {
}
const BASE_URL = "http://127.0.0.1:8082";
const NUM_OF_CREATE_NOTE_OPTIONS = 2;
interface DropdownLocator extends Locator {
selectOptionByText: (text: string) => Promise<void>;
@ -73,7 +74,8 @@ export default class App {
const resultsSelector = this.currentNoteSplit.locator(".note-detail-empty-results");
await expect(resultsSelector).toContainText(noteTitle);
await resultsSelector.locator(".aa-suggestion", { hasText: noteTitle })
.nth(1) // Select the second one, as the first one is "Create a new note"
// Select the n+1 one, as the first one is "Create a new note"
.nth(NUM_OF_CREATE_NOTE_OPTIONS)
.click();
}

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,7 @@
<p>Most of the interaction with text notes is done via the built-in toolbars.
Depending on preference, there are two different layouts:</p>
<ul>
<li>The <em>Floating toolbar</em> is hidden by default and only appears when
<li data-list-item-id="eafcb31c309cb140eedbb41048a0e0db1">The <em>Floating toolbar</em> is hidden by default and only appears when
needed. In this mode there are actually two different toolbars:
<br>
<img src="1_Text_image.png" width="496"
@ -12,7 +12,7 @@
<img src="2_Text_image.png" width="812"
height="114">
</li>
<li>A toolbar that appears when text is selected. This provides text-level
<li data-list-item-id="e59aa3103fa4c5de33075e51f8d482164">A toolbar that appears when text is selected. This provides text-level
formatting such as bold, italic, text colors, inline code, etc.
<br><em><img src="Text_image.png" width="1109" height="124"></em>
</li>
@ -20,167 +20,171 @@
<p>Fore more information see&nbsp;<a class="reference-link" href="#root/_help_nRhnJkTT8cPs">Formatting toolbar</a>.</p>
<h2>Features and formatting</h2>
<p>Here's a list of various features supported by text notes:</p>
<table>
<thead>
<tr>
<th>Dedicated article</th>
<th>Feature</th>
</tr>
</thead>
<tbody>
<tr>
<td><a class="reference-link" href="#root/_help_Gr6xFaF6ioJ5">General formatting</a>
</td>
<td>
<ul>
<li>Headings (section titles, paragraph)</li>
<li>Font size</li>
<li>Bold, italic, underline, strike-through</li>
<li>Superscript, subscript</li>
<li>Font color &amp; background color</li>
<li>Remove formatting</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_S6Xx8QIWTV66">Lists</a>
</td>
<td>
<ul>
<li>Bulleted lists</li>
<li>Numbered lists</li>
<li>To-do lists</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_NwBbFdNZ9h7O">Block quotes &amp; admonitions</a>
</td>
<td>
<ul>
<li>Block quotes</li>
<li>Admonitions</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_NdowYOC1GFKS">Tables</a>
</td>
<td>
<ul>
<li>Basic tables</li>
<li>Merging cells</li>
<li>Styling tables and cells.</li>
<li>Table captions</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_UYuUB1ZekNQU">Developer-specific formatting</a>
</td>
<td>
<ul>
<li>Inline code</li>
<li>Code blocks</li>
<li>Keyboard shortcuts</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_AgjCISero73a">Footnotes</a>
</td>
<td>
<ul>
<li>Footnotes</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_mT0HEkOsz6i1">Images</a>
</td>
<td>
<ul>
<li>Images</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_QEAPj01N5f7w">Links</a>
</td>
<td>
<ul>
<li>External links</li>
<li>Internal Trilium links</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_nBAXQFj20hS1">Include Note</a>
</td>
<td>
<ul>
<li>Include note</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_CohkqWQC1iBv">Insert buttons</a>
</td>
<td>
<ul>
<li>Symbols</li>
<li><a class="reference-link" href="#root/_help_YfYAtQBcfo5V">Math Equations</a>
</li>
<li>Mermaid diagrams</li>
<li>Horizontal ruler</li>
<li>Page break</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_dEHYtoWWi8ct">Other features</a>
</td>
<td>
<ul>
<li>Indentation
<ul>
<li>Markdown import</li>
</ul>
</li>
<li><a class="reference-link" href="#root/_help_2x0ZAX9ePtzV">Cut to subnote</a>
</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_gLt3vA97tMcp">Premium features</a>
</td>
<td>
<ul>
<li><a class="reference-link" href="#root/_help_ZlN4nump6EbW">Slash Commands</a>
</li>
<li><a class="reference-link" href="#root/_help_KC1HB96bqqHX">Templates</a>
</li>
</ul>
</td>
</tr>
</tbody>
</table>
<h2>Read-Only vs. Editing Mode</h2>
<p>Text notes are usually opened in edit mode. However, they may open in
read-only mode if the note is too big or the note is explicitly marked
as read-only. For more information, see&nbsp;<a class="reference-link"
href="#root/_help_CoFPLs3dRlXc">Read-Only Notes</a>.</p>
<h2>Keyboard shortcuts</h2>
<p>There are numerous keyboard shortcuts to format the text without having
to use the mouse. For a reference of all the key combinations, see&nbsp;
<a
class="reference-link" href="#root/_help_A9Oc6YKKc65v">Keyboard Shortcuts</a>. In addition, see&nbsp;<a class="reference-link"
href="#root/_help_QrtTYPmdd1qq">Markdown-like formatting</a>&nbsp;as an alternative
to the keyboard shortcuts.</p>
<h2>Technical details</h2>
<p>For the text editing functionality, Trilium uses a commercial product
(with an open-source base) called&nbsp;<a class="reference-link" href="#root/_help_MI26XDLSAlCD">CKEditor</a>.
This brings the benefit of having a powerful WYSIWYG (What You See Is What
You Get) editor.</p>
<figure
class="table">
<table>
<thead>
<tr>
<th>Dedicated article</th>
<th>Feature</th>
</tr>
</thead>
<tbody>
<tr>
<td><a class="reference-link" href="#root/_help_Gr6xFaF6ioJ5">General formatting</a>
</td>
<td>
<ul>
<li data-list-item-id="e04c84d59d44645ee89b2a8541ed99f90">Headings (section titles, paragraph)</li>
<li data-list-item-id="e39d25bd3d8bd06185b9d259e5827d451">Font size</li>
<li data-list-item-id="e1f7e2a2f4b03449d82bdf5b5c6ea8d44">Bold, italic, underline, strike-through</li>
<li data-list-item-id="e3decae72884f65b4d538151b6a297072">Superscript, subscript</li>
<li data-list-item-id="e59adf00fef65304c163ae190fac5e92a">Font color &amp; background color</li>
<li data-list-item-id="ed3f09156147a2769e91db111c76376e2">Remove formatting</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_S6Xx8QIWTV66">Lists</a>
</td>
<td>
<ul>
<li data-list-item-id="ee87806a913900d85d8f018af81f41df8">Bulleted lists</li>
<li data-list-item-id="e3ae314e365fa418ca6e0f061d63834c5">Numbered lists</li>
<li data-list-item-id="ee84e08694165f95430046cb34f4cd123">To-do lists</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_NwBbFdNZ9h7O">Block quotes &amp; admonitions</a>
</td>
<td>
<ul>
<li data-list-item-id="e2892dc35a0d4b7ad65daffb8f9404daa">Block quotes</li>
<li data-list-item-id="e7297e3ad1002f8de15aa0bd66c6f3f22">Admonitions</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_NdowYOC1GFKS">Tables</a>
</td>
<td>
<ul>
<li data-list-item-id="eb358a4567d93f66004f4195df2dda05a">Basic tables</li>
<li data-list-item-id="e6135a555d6c63c30e4b84806a4870830">Merging cells</li>
<li data-list-item-id="e29ac76563d0998b28fb1baf94dbdac8c">Styling tables and cells.</li>
<li data-list-item-id="e372446e81fdedada64b8bed89ca93d1a">Table captions</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_UYuUB1ZekNQU">Developer-specific formatting</a>
</td>
<td>
<ul>
<li data-list-item-id="eb260b76afcbc07bd9d4ceec4e000e8a0">Inline code</li>
<li data-list-item-id="e9864352286369ebe7b41c1599f498de8">Code blocks</li>
<li data-list-item-id="ee62fb9ed7f349178e8f2a2bd9ec8cd74">Keyboard shortcuts</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_AgjCISero73a">Footnotes</a>
</td>
<td>
<ul>
<li data-list-item-id="edf62ec004eff35cfcb7e361deef19aaf">Footnotes</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_mT0HEkOsz6i1">Images</a>
</td>
<td>
<ul>
<li data-list-item-id="ebe6277e643041403489c3ceb30c36f7f">Images</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_QEAPj01N5f7w">Links</a>
</td>
<td>
<ul>
<li data-list-item-id="e3f988be2f259bb40607cb61541955395">External links</li>
<li data-list-item-id="e3f91cc4f0cccd2c077cc306bacd68ef2">Internal Trilium links</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_nBAXQFj20hS1">Include Note</a>
</td>
<td>
<ul>
<li data-list-item-id="eac8015a64bce7b749cc67d1599062007">Include note</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_CohkqWQC1iBv">Insert buttons</a>
</td>
<td>
<ul>
<li data-list-item-id="e5cdf5d3885ec0ea67f924b4b8fe5c483">Symbols</li>
<li data-list-item-id="e95082e6642ed5b1eec6e4e116b899a40"><a class="reference-link" href="#root/_help_YfYAtQBcfo5V">Math Equations</a>
</li>
<li data-list-item-id="ecbef6a358a5b8d27f0d3e08bbc750aa9">Mermaid diagrams</li>
<li data-list-item-id="e6e97ee14dd29b7ccf53227107e5dc72d">Horizontal ruler</li>
<li data-list-item-id="e6198c7c535c249faec2e8906775f11de">Page break</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_dEHYtoWWi8ct">Other features</a>
</td>
<td>
<ul>
<li data-list-item-id="e0c14456cb83d483b07ea432ef9d4728e">Indentation
<ul>
<li data-list-item-id="e2029812c5e105c595590f70ee227631e">Markdown import</li>
</ul>
</li>
<li data-list-item-id="ea1ee012286e05190c89c9f4e64cf2036"><a class="reference-link" href="#root/_help_2x0ZAX9ePtzV">Cut to subnote</a>
</li>
</ul>
</td>
</tr>
<tr>
<td><a class="reference-link" href="#root/_help_gLt3vA97tMcp">Premium features</a>
</td>
<td>
<ul>
<li data-list-item-id="e1ab173193a533ccf33dccfd0cb916f1f"><a class="reference-link" href="#root/_help_ZlN4nump6EbW">Slash Commands</a>
</li>
<li data-list-item-id="e564b978c09fe5adf476b331b1e0640e3"><a class="reference-link" href="#root/_help_KC1HB96bqqHX">Templates</a>
</li>
<li data-list-item-id="e756306c31d9beffbba3820b6d1b9bc61"><a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/iPIMuisry3hd/gLt3vA97tMcp/_help_5wZallV2Qo1t">Format Painter</a>
</li>
</ul>
</td>
</tr>
</tbody>
</table>
</figure>
<h2>Read-Only vs. Editing Mode</h2>
<p>Text notes are usually opened in edit mode. However, they may open in
read-only mode if the note is too big or the note is explicitly marked
as read-only. For more information, see&nbsp;<a class="reference-link"
href="#root/_help_CoFPLs3dRlXc">Read-Only Notes</a>.</p>
<h2>Keyboard shortcuts</h2>
<p>There are numerous keyboard shortcuts to format the text without having
to use the mouse. For a reference of all the key combinations, see&nbsp;
<a
class="reference-link" href="#root/_help_A9Oc6YKKc65v">Keyboard Shortcuts</a>. In addition, see&nbsp;<a class="reference-link"
href="#root/_help_QrtTYPmdd1qq">Markdown-like formatting</a>&nbsp;as an alternative
to the keyboard shortcuts.</p>
<h2>Technical details</h2>
<p>For the text editing functionality, Trilium uses a commercial product
(with an open-source base) called&nbsp;<a class="reference-link" href="#root/_help_MI26XDLSAlCD">CKEditor</a>.
This brings the benefit of having a powerful WYSIWYG (What You See Is What
You Get) editor.</p>

View File

@ -12,11 +12,11 @@
<p>Apart from using the UI, it is also possible to quickly insert headings
using the Markdown-like shortcuts:</p>
<ul>
<li><code>##</code> for Heading 2</li>
<li><code>###</code> for Heading 3</li>
<li><code>####</code> for Heading 4</li>
<li><code>#####</code> for Heading 5</li>
<li><code>######</code> for Heading 6</li>
<li data-list-item-id="e6083df2814e520e138326ef004812a46"><code>##</code> for Heading 2</li>
<li data-list-item-id="e8b8acfd06c340271cc4fe84f65e67375"><code>###</code> for Heading 3</li>
<li data-list-item-id="e19e95a52ddbcd54428ed5ab40df165a7"><code>####</code> for Heading 4</li>
<li data-list-item-id="e69788e0bd8f4459748b8df997a19fa92"><code>#####</code> for Heading 5</li>
<li data-list-item-id="eb7205b764231a314c9e9c1590c12ac1c"><code>######</code> for Heading 6</li>
</ul>
<h2>Font size</h2>
<figure class="image image-style-align-right">
@ -44,17 +44,17 @@
<p>This formatting can be easily removed using the <em>Remove formatting</em> item.</p>
<p>The following keyboard shortcuts can be used here:</p>
<ul>
<li><kbd>Ctrl</kbd>+<kbd>B</kbd> for bold</li>
<li><kbd>Ctrl</kbd>+<kbd>I</kbd> for italic</li>
<li><kbd>Ctrl</kbd>+<kbd>U</kbd> for underline</li>
<li data-list-item-id="ecda6994b8be7fe33daeccbeb8bffe20b"><kbd>Ctrl</kbd>+<kbd>B</kbd> for bold</li>
<li data-list-item-id="eaa20ab7965945937bd10a273e5fc323f"><kbd>Ctrl</kbd>+<kbd>I</kbd> for italic</li>
<li data-list-item-id="e86881b33671d3bd6930d361e4bb76767"><kbd>Ctrl</kbd>+<kbd>U</kbd> for underline</li>
</ul>
<p>Alternatively, Markdown-like formatting can be used:</p>
<ul>
<li><strong>Bold</strong>: Type <code>**text**</code> or <code>__text__</code>
<li data-list-item-id="e90b0f040ab78084c41cd8f44045528ec"><strong>Bold</strong>: Type <code>**text**</code> or <code>__text__</code>
</li>
<li><em>Italic</em>: Type <code>*text*</code> or <code>_text_</code>
<li data-list-item-id="eebc210ee7f2b92e0885cd411c6fb8b80"><em>Italic</em>: Type <code>*text*</code> or <code>_text_</code>
</li>
<li><del>Strikethrough</del>: Type <code>~~text~~</code>
<li data-list-item-id="eb7628d2cb441576b3b3edf6c42ab84b4"><del>Strikethrough</del>: Type <code>~~text~~</code>
</li>
</ul>
<h2>Superscript, subscript</h2>
@ -89,6 +89,11 @@
be manually changed back to a paragraph according to the <em>Headings</em> section.</p>
<p>When pasting content that comes with undesired formatting, an alternative
to pasting and then removing formatting is pasting as plain text via <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>V</kbd>.</p>
<h2>Format painter</h2>
<p>The&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/iPIMuisry3hd/gLt3vA97tMcp/_help_5wZallV2Qo1t">Format Painter</a>&nbsp;allows
users to copy the formatting of text (such as bold, italic, Strikethrough,
etc.) and apply it to other parts of the document. It helps maintain consistent
formatting and accelerates the creation of rich content.</p>
<h2>Support for Markdown</h2>
<p>When exported to&nbsp;<a class="reference-link" href="#root/_help_Oau6X9rCuegd">Markdown</a>,
most of the general formatting is maintained such as headings, bold, italic,

View File

@ -0,0 +1,45 @@
<figure class="image image-style-align-right">
<img style="aspect-ratio:220/76;" src="Format Painter_image.png"
width="220" height="76">
</figure>
<aside class="admonition note">
<p>This is a premium feature of the editor we are using (CKEditor) and we
benefit from it thanks to an written agreement with the team. See &nbsp;
<a
class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/iPIMuisry3hd/_help_gLt3vA97tMcp">Premium features</a>&nbsp;for more information.</p>
</aside>
<p>The Format Painter is a feature in text notes that allows users to copy
the formatting of text (such as <strong>bold</strong>, <em>italic</em>, <del>Strikethrough</del>,
etc.) and apply it to other parts of the document. It helps maintain consistent
formatting and accelerates the creation of rich content.</p>
<h2>Usage Instructions</h2>
<p>Click the text that you want to copy the formatting from and use the paint
formatting toolbar button (<span><img class="image_resized" style="aspect-ratio:150/150;width:2.7%;" src="Format Painter_746436a2e1.svg" alt="Format painter" width="150" height="150">) </span>to
copy the style. Then select the target text with your mouse to apply the
formatting.</p>
<ul>
<li data-list-item-id="e9a728e8f49fb3ecf1202002dfafccabd"><strong>To copy the formatting</strong>: Place the cursor inside a text
with some formatting and click the paint formatting toolbar button. Notice
that the mouse cursor changes to the <span><img class="image_resized" style="aspect-ratio:30/20;width:3.64%;" src="Format Painter_e144e96df9.svg" alt="Format painter text cursor" width="30" height="20"></span>.</li>
<li
data-list-item-id="e745c039a3502d4e979d6dcde7876461b"><strong>To paint with the copied formatting</strong>: Click any word in
the document and the new formatting will be applied. Alternatively, instead
of clicking a single word, you can select a text fragment (like an entire
paragraph). Notice that the cursor will go back to the default one after
the formatting is applied.</li>
<li data-list-item-id="e49e547cfd4e4cbb45712bace9a6e0979"><strong>To keep painting using the same formatting</strong>: Open the
toolbar dropdown and enable the continuous painting mode. Once copied,
the same formatting can be applied multiple times in different places until
the paint formatting button is clicked (the cursor will then revert to
the regular one).</li>
</ul>
<h2>Limitations</h2>
<ol>
<li data-list-item-id="ea3e6a7c317b51341c7a83cee5387ac1e">Painting with block-level formatting (like headings or image styles) is
not supported yet. This is because, in&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/tC7s2alapj8V/1YeN2MzFUluU/_help_MI26XDLSAlCD">CKEditor</a>,
they are considered a part of the content rather than text formatting.</li>
<li
data-list-item-id="edbd74adb8916daaae6024d8b0ae80e32">When applying formatting to words, spaces or other Western punctuation
are used as word boundaries, which prevents proper handling of languages
that do not use space-based word segmentation.</li>
</ol>

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M3 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"/><path d="M16 3.25a1.5 1.5 0 0 1 1.5 1.5v1.7a2.25 2.25 0 0 1-1.932 2.226l-4.424.632a.75.75 0 0 0-.644.743V11a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1v-.95a2.25 2.25 0 0 1 1.932-2.226l4.424-.632A.75.75 0 0 0 16 6.449z"/></svg>

After

Width:  |  Height:  |  Size: 386 B

View File

@ -0,0 +1,7 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 31 20" width="30" height="20">
<path d="M14 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H15a1 1 0 0 1-1-1V3Z" fill="#000"/>
<path d="M27 3.25a1.5 1.5 0 0 1 1.5 1.5v1.7a2.25 2.25 0 0 1-1.932 2.226l-4.424.632a.75.75 0 0 0-.644.743V11a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H20a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1v-.95a2.25 2.25 0 0 1 1.932-2.226l4.424-.632A.75.75 0 0 0 27 6.449V3.25Z" fill="#000"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.855 2.25H27a2.5 2.5 0 0 1 2.5 2.5v1.7a3.25 3.25 0 0 1-2.79 3.216l-4.21.602a2 2 0 0 1 1 1.732v5a2 2 0 0 1-2 2H20a2 2 0 0 1-2-2v-5a2 2 0 0 1 1-1.732v-.217A3.25 3.25 0 0 1 21.129 7H15a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h10a2 2 0 0 1 1.855 1.25ZM20 10.05V11a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h1.5a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1v-.95c0-.016 0-.033.002-.05a.75.75 0 0 1 .642-.692l4.424-.632A2.25 2.25 0 0 0 28.5 6.45V4.75a1.496 1.496 0 0 0-1.5-1.5v3.2a.75.75 0 0 1-.644.742l-4.424.632A2.25 2.25 0 0 0 20 10.05ZM15 2a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H15Z" fill="#fff"/>
<path d="M2.5 2.5A.5.5 0 0 1 3 2h2.5a.5.5 0 0 1 .354.146l.646.647.646-.647A.5.5 0 0 1 7.5 2H10a.5.5 0 0 1 0 1H7.707L7 3.707V10h.5a.5.5 0 0 1 0 1H7v4.793l.707.707H10a.5.5 0 0 1 0 1H7.5a.5.5 0 0 1-.354-.146l-.646-.647-.646.647a.5.5 0 0 1-.354.146H3a.5.5 0 0 1 0-1h2.293L6 15.793V11h-.5a.5.5 0 0 1 0-1H6V3.707L5.293 3H3a.5.5 0 0 1-.5-.5Z" fill="#000"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="m5.793 3.5-.5-.5H3a.5.5 0 0 1 0-1h2.5a.5.5 0 0 1 .354.146l.145.146.501.5.646-.646A.5.5 0 0 1 7.5 2H10a.5.5 0 0 1 0 1H7.707L7 3.707V10h.5a.5.5 0 0 1 0 1H7v4.793l.707.707H10a.5.5 0 0 1 0 1H7.5a.5.5 0 0 1-.354-.146l-.646-.647-.5.5-.146.147a.5.5 0 0 1-.354.146H3a.5.5 0 0 1 0-1h2.293L6 15.793V11h-.5a.5.5 0 0 1 0-1H6V3.707L5.793 3.5Zm-.914.5L5 4.121v4.964a1.5 1.5 0 0 0 0 2.83v3.464l-.121.121H3a1.5 1.5 0 0 0 0 3h2.5a1.5 1.5 0 0 0 1-.382 1.5 1.5 0 0 0 1 .382H10a1.5 1.5 0 0 0 0-3H8.121L8 15.379v-3.464a1.5 1.5 0 0 0 0-2.83V4.121L8.121 4H10a1.5 1.5 0 0 0 0-3H7.5a1.5 1.5 0 0 0-1 .382A1.5 1.5 0 0 0 5.5 1H3a1.5 1.5 0 1 0 0 3h1.879Z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@ -0,0 +1 @@
{}

View File

@ -7,11 +7,13 @@ import hidden_subtree from "./hidden_subtree.js";
import { LOCALES, type Locale, type LOCALE_IDS } from "@triliumnext/commons";
import dayjs, { Dayjs } from "dayjs";
// When adding a new locale, prefer the version with hyphen instead of underscore.
export const DAYJS_LOADER: Record<LOCALE_IDS, () => Promise<typeof import("dayjs/locale/en.js")>> = {
"ar": () => import("dayjs/locale/ar.js"),
"cn": () => import("dayjs/locale/zh-cn.js"),
"de": () => import("dayjs/locale/de.js"),
"en": () => import("dayjs/locale/en.js"),
"en-GB": () => import("dayjs/locale/en-gb.js"),
"en_rtl": () => import("dayjs/locale/en.js"),
"es": () => import("dayjs/locale/es.js"),
"fa": () => import("dayjs/locale/fa.js"),

View File

@ -0,0 +1 @@
{}

332
docs/README-en_GB.md vendored Normal file
View File

@ -0,0 +1,332 @@
<div align="center">
<sup>Special thanks to:</sup><br />
<a href="https://go.warp.dev/Trilium" target="_blank">
<img alt="Warp sponsorship" width="400" src="https://github.com/warpdotdev/brand-assets/blob/main/Github/Sponsor/Warp-Github-LG-03.png"><br />
Warp, built for coding with multiple AI agents<br />
</a>
<sup>Available for macOS, Linux and Windows</sup>
</div>
<hr />
# Trilium Notes
![GitHub Sponsors](https://img.shields.io/github/sponsors/eliandoran)
![LiberaPay patrons](https://img.shields.io/liberapay/patrons/ElianDoran)\
![Docker Pulls](https://img.shields.io/docker/pulls/triliumnext/trilium)
![GitHub Downloads (all assets, all
releases)](https://img.shields.io/github/downloads/triliumnext/trilium/total)\
[![RelativeCI](https://badges.relative-ci.com/badges/Di5q7dz9daNDZ9UXi0Bp?branch=develop)](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp)
[![Translation
status](https://hosted.weblate.org/widget/trilium/svg-badge.svg)](https://hosted.weblate.org/engage/trilium/)
[English](./README.md) | [Chinese (Simplified)](./docs/README-ZH_CN.md) |
[Chinese (Traditional)](./docs/README-ZH_TW.md) | [Russian](./docs/README-ru.md)
| [Japanese](./docs/README-ja.md) | [Italian](./docs/README-it.md) |
[Spanish](./docs/README-es.md)
Trilium Notes is a free and open-source, cross-platform hierarchical note taking
application with focus on building large personal knowledge bases.
See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for
quick overview:
<a href="https://triliumnext.github.io/Docs/Wiki/screenshot-tour"><img src="./docs/app.png" alt="Trilium Screenshot" width="1000"></a>
## ⏬ Download
- [Latest release](https://github.com/TriliumNext/Trilium/releases/latest)
stable version, recommended for most users.
- [Nightly build](https://github.com/TriliumNext/Trilium/releases/tag/nightly)
unstable development version, updated daily with the latest features and
fixes.
## 📚 Documentation
**Visit our comprehensive documentation at
[docs.triliumnotes.org](https://docs.triliumnotes.org/)**
Our documentation is available in multiple formats:
- **Online Documentation**: Browse the full documentation at
[docs.triliumnotes.org](https://docs.triliumnotes.org/)
- **In-App Help**: Press `F1` within Trilium to access the same documentation
directly in the application
- **GitHub**: Navigate through the [User
Guide](./docs/User%20Guide/User%20Guide/) in this repository
### Quick Links
- [Getting Started Guide](https://docs.triliumnotes.org/)
- [Installation
Instructions](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation.md)
- [Docker
Setup](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation/1.%20Installing%20the%20server/Using%20Docker.md)
- [Upgrading
TriliumNext](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Upgrading%20TriliumNext.md)
- [Basic Concepts and
Features](./docs/User%20Guide/User%20Guide/Basic%20Concepts%20and%20Features/Notes.md)
- [Patterns of Personal Knowledge
Base](https://triliumnext.github.io/Docs/Wiki/patterns-of-personal-knowledge)
## 🎁 Features
* Notes can be arranged into arbitrarily deep tree. Single note can be placed
into multiple places in the tree (see
[cloning](https://triliumnext.github.io/Docs/Wiki/cloning-notes))
* Rich WYSIWYG note editor including e.g. tables, images and
[math](https://triliumnext.github.io/Docs/Wiki/text-notes) with markdown
[autoformat](https://triliumnext.github.io/Docs/Wiki/text-notes#autoformat)
* Support for editing [notes with source
code](https://triliumnext.github.io/Docs/Wiki/code-notes), including syntax
highlighting
* Fast and easy [navigation between
notes](https://triliumnext.github.io/Docs/Wiki/note-navigation), full text
search and [note
hoisting](https://triliumnext.github.io/Docs/Wiki/note-hoisting)
* Seamless [note
versioning](https://triliumnext.github.io/Docs/Wiki/note-revisions)
* Note [attributes](https://triliumnext.github.io/Docs/Wiki/attributes) can be
used for note organization, querying and advanced
[scripting](https://triliumnext.github.io/Docs/Wiki/scripts)
* UI available in English, German, Spanish, French, Romanian, and Chinese
(simplified and traditional)
* Direct [OpenID and TOTP
integration](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Server%20Installation/Multi-Factor%20Authentication.md)
for more secure login
* [Synchronization](https://triliumnext.github.io/Docs/Wiki/synchronization)
with self-hosted sync server
* there's a [3rd party service for hosting synchronisation
server](https://trilium.cc/paid-hosting)
* [Sharing](https://triliumnext.github.io/Docs/Wiki/sharing) (publishing) notes
to public internet
* Strong [note
encryption](https://triliumnext.github.io/Docs/Wiki/protected-notes) with
per-note granularity
* Sketching diagrams, based on [Excalidraw](https://excalidraw.com/) (note type
"canvas")
* [Relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map) and
[link maps](https://triliumnext.github.io/Docs/Wiki/link-map) for visualizing
notes and their relations
* Mind maps, based on [Mind Elixir](https://docs.mind-elixir.com/)
* [Geo maps](./docs/User%20Guide/User%20Guide/Note%20Types/Geo%20Map.md) with
location pins and GPX tracks
* [Scripting](https://triliumnext.github.io/Docs/Wiki/scripts) - see [Advanced
showcases](https://triliumnext.github.io/Docs/Wiki/advanced-showcases)
* [REST API](https://triliumnext.github.io/Docs/Wiki/etapi) for automation
* Scales well in both usability and performance upwards of 100 000 notes
* Touch optimized [mobile
frontend](https://triliumnext.github.io/Docs/Wiki/mobile-frontend) for
smartphones and tablets
* Built-in [dark theme](https://triliumnext.github.io/Docs/Wiki/themes), support
for user themes
* [Evernote](https://triliumnext.github.io/Docs/Wiki/evernote-import) and
[Markdown import & export](https://triliumnext.github.io/Docs/Wiki/markdown)
* [Web Clipper](https://triliumnext.github.io/Docs/Wiki/web-clipper) for easy
saving of web content
* Customizable UI (sidebar buttons, user-defined widgets, ...)
* [Metrics](./docs/User%20Guide/User%20Guide/Advanced%20Usage/Metrics.md), along
with a [Grafana
Dashboard](./docs/User%20Guide/User%20Guide/Advanced%20Usage/Metrics/grafana-dashboard.json)
✨ Check out the following third-party resources/communities for more TriliumNext
related goodies:
- [awesome-trilium](https://github.com/Nriver/awesome-trilium) for 3rd party
themes, scripts, plugins and more.
- [TriliumRocks!](https://trilium.rocks/) for tutorials, guides, and much more.
## ❓Why TriliumNext?
The original Trilium developer ([Zadam](https://github.com/zadam)) has
graciously given the Trilium repository to the community project which resides
at https://github.com/TriliumNext
### ⬆Migrating from Zadam/Trilium?
There are no special migration steps to migrate from a zadam/Trilium instance to
a TriliumNext/Trilium instance. Simply [install
TriliumNext/Trilium](#-installation) as usual and it will use your existing
database.
Versions up to and including
[v0.90.4](https://github.com/TriliumNext/Trilium/releases/tag/v0.90.4) are
compatible with the latest zadam/trilium version of
[v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later
versions of TriliumNext/Trilium have their sync versions incremented which
prevents direct migration.
## 💬 Discuss with us
Feel free to join our official conversations. We would love to hear what
features, suggestions, or issues you may have!
- [Matrix](https://matrix.to/#/#triliumnext:matrix.org) (For synchronous
discussions.)
- The `General` Matrix room is also bridged to
[XMPP](xmpp:discuss@trilium.thisgreat.party?join)
- [Github Discussions](https://github.com/TriliumNext/Trilium/discussions) (For
asynchronous discussions.)
- [Github Issues](https://github.com/TriliumNext/Trilium/issues) (For bug
reports and feature requests.)
## 🏗 Installation
### Windows / MacOS
Download the binary release for your platform from the [latest release
page](https://github.com/TriliumNext/Trilium/releases/latest), unzip the package
and run the `trilium` executable.
### Linux
If your distribution is listed in the table below, use your distribution's
package.
[![Packaging
status](https://repology.org/badge/vertical-allrepos/triliumnext.svg)](https://repology.org/project/triliumnext/versions)
You may also download the binary release for your platform from the [latest
release page](https://github.com/TriliumNext/Trilium/releases/latest), unzip the
package and run the `trilium` executable.
TriliumNext is also provided as a Flatpak, but not yet published on FlatHub.
### Browser (any OS)
If you use a server installation (see below), you can directly access the web
interface (which is almost identical to the desktop app).
Currently only the latest versions of Chrome & Firefox are supported (and
tested).
### Mobile
To use TriliumNext on a mobile device, you can use a mobile web browser to
access the mobile interface of a server installation (see below).
See issue https://github.com/TriliumNext/Trilium/issues/4962 for more
information on mobile app support.
If you prefer a native Android app, you can use
[TriliumDroid](https://apt.izzysoft.de/fdroid/index/apk/eu.fliegendewurst.triliumdroid).
Report bugs and missing features at [their
repository](https://github.com/FliegendeWurst/TriliumDroid). Note: It is best to
disable automatic updates on your server installation (see below) when using
TriliumDroid since the sync version must match between Trilium and TriliumDroid.
### Server
To install TriliumNext on your own server (including via Docker from
[Dockerhub](https://hub.docker.com/r/triliumnext/trilium)) follow [the server
installation docs](https://triliumnext.github.io/Docs/Wiki/server-installation).
## 💻 Contribute
### Translations
If you are a native speaker, help us translate Trilium by heading over to our
[Weblate page](https://hosted.weblate.org/engage/trilium/).
Here's the language coverage we have so far:
[![Translation
status](https://hosted.weblate.org/widget/trilium/multi-auto.svg)](https://hosted.weblate.org/engage/trilium/)
### Code
Download the repository, install dependencies using `pnpm` and then run the
server (available at http://localhost:8080):
```shell
git clone https://github.com/TriliumNext/Trilium.git
cd Trilium
pnpm install
pnpm run server:start
```
### Documentation
Download the repository, install dependencies using `pnpm` and then run the
environment required to edit the documentation:
```shell
git clone https://github.com/TriliumNext/Trilium.git
cd Trilium
pnpm install
pnpm edit-docs:edit-docs
```
### Building the Executable
Download the repository, install dependencies using `pnpm` and then build the
desktop app for Windows:
```shell
git clone https://github.com/TriliumNext/Trilium.git
cd Trilium
pnpm install
pnpm run --filter desktop electron-forge:make --arch=x64 --platform=win32
```
For more details, see the [development
docs](https://github.com/TriliumNext/Trilium/tree/main/docs/Developer%20Guide/Developer%20Guide).
### Developer Documentation
Please view the [documentation
guide](https://github.com/TriliumNext/Trilium/blob/main/docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md)
for details. If you have more questions, feel free to reach out via the links
described in the "Discuss with us" section above.
## 👏 Shoutouts
* [zadam](https://github.com/zadam) for the original concept and implementation
of the application.
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
application icon.
* [nriver](https://github.com/nriver) for his work on internationalisation.
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
widget.
* [Dosu](https://dosu.dev/) for providing us with the automated responses to
GitHub issues and discussions.
* [Tabler Icons](https://tabler.io/icons) for the system tray icons.
Trilium would not be possible without the technologies behind it:
* [CKEditor 5](https://github.com/ckeditor/ckeditor5) - the visual editor behind
text notes. We are grateful for being offered a set of the premium features.
* [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with
support for huge amount of languages.
* [Excalidraw](https://github.com/excalidraw/excalidraw) - the infinite
whiteboard used in Canvas notes.
* [Mind Elixir](https://github.com/SSShooter/mind-elixir-core) - providing the
mind map functionality.
* [Leaflet](https://github.com/Leaflet/Leaflet) - for rendering geographical
maps.
* [Tabulator](https://github.com/olifolkerd/tabulator) - for the interactive
table used in collections.
* [FancyTree](https://github.com/mar10/fancytree) - feature-rich tree library
without real competition.
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library.
Used in [relation
maps](https://triliumnext.github.io/Docs/Wiki/relation-map.html) and [link
maps](https://triliumnext.github.io/Docs/Wiki/note-map.html#link-map)
## 🤝 Support
Trilium is built and maintained with [hundreds of hours of
work](https://github.com/TriliumNext/Trilium/graphs/commit-activity). Your
support keeps it open-source, improves features, and covers costs such as
hosting.
Consider supporting the main developer
([eliandoran](https://github.com/eliandoran)) of the application via:
- [GitHub Sponsors](https://github.com/sponsors/eliandoran)
- [PayPal](https://paypal.me/eliandoran)
- [Buy Me a Coffee](https://buymeacoffee.com/eliandoran)
## 🔑 License
Copyright 2017-2025 zadam, Elian Doran, and other contributors
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option) any
later version.

View File

@ -2380,6 +2380,20 @@
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
"value": "IakOLONlIfGI",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "yTjUdsOi4CIE",
"isInheritable": false,
"position": 40
},
{
"type": "label",
"name": "iconClass",
@ -2393,20 +2407,6 @@
"value": "keyboard-shortcuts",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "IakOLONlIfGI",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
"value": "yTjUdsOi4CIE",
"isInheritable": false,
"position": 60
}
],
"format": "markdown",
@ -5207,45 +5207,59 @@
{
"type": "relation",
"name": "internalLink",
"value": "MMiBEQljMQh2",
"value": "DvdZhoQZY9Yd",
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "zEY4DaJG4YT5",
"value": "MMiBEQljMQh2",
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
"value": "iPIMuisry3hd",
"value": "zEY4DaJG4YT5",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "TBwsyfadTA18",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "oiVPnW8QfnvS",
"value": "iPIMuisry3hd",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
"value": "QrtTYPmdd1qq",
"value": "oiVPnW8QfnvS",
"isInheritable": false,
"position": 60
},
{
"type": "relation",
"name": "internalLink",
"value": "eIg8jdvaoNNd",
"value": "QrtTYPmdd1qq",
"isInheritable": false,
"position": 70
},
{
"type": "relation",
"name": "internalLink",
"value": "eIg8jdvaoNNd",
"isInheritable": false,
"position": 80
},
{
"type": "label",
"name": "shareAlias",
@ -5259,20 +5273,6 @@
"value": "bx bxs-keyboard",
"isInheritable": false,
"position": 80
},
{
"type": "relation",
"name": "internalLink",
"value": "DvdZhoQZY9Yd",
"isInheritable": false,
"position": 90
},
{
"type": "relation",
"name": "internalLink",
"value": "TBwsyfadTA18",
"isInheritable": false,
"position": 100
}
],
"format": "markdown",
@ -6244,6 +6244,13 @@
"value": "",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "5wZallV2Qo1t",
"isInheritable": false,
"position": 220
}
],
"format": "markdown",
@ -6914,6 +6921,13 @@
"value": "general-formatting",
"isInheritable": false,
"position": 60
},
{
"type": "relation",
"name": "internalLink",
"value": "5wZallV2Qo1t",
"isInheritable": false,
"position": 70
}
],
"format": "markdown",
@ -8423,6 +8437,81 @@
"dataFileName": "1_Text Snippets_image.png"
}
]
},
{
"isClone": false,
"noteId": "5wZallV2Qo1t",
"notePath": [
"pOsGYCXsbNQG",
"KSZ04uQ2D1St",
"iPIMuisry3hd",
"gLt3vA97tMcp",
"5wZallV2Qo1t"
],
"title": "Format Painter",
"notePosition": 30,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "relation",
"name": "internalLink",
"value": "gLt3vA97tMcp",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "MI26XDLSAlCD",
"isInheritable": false,
"position": 40
},
{
"type": "label",
"name": "shareAlias",
"value": "format-painter",
"isInheritable": false,
"position": 50
},
{
"type": "label",
"name": "iconClass",
"value": "bx bxs-paint-roll",
"isInheritable": false,
"position": 60
}
],
"format": "markdown",
"dataFileName": "Format Painter.md",
"attachments": [
{
"attachmentId": "OY9JmG8zdGm5",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Format Painter_image.png"
},
{
"attachmentId": "qEJy5SJMsPUh",
"title": "e144e96df9.svg",
"role": "image",
"mime": "image/svg+xml",
"position": 10,
"dataFileName": "Format Painter_e144e96df9.svg"
},
{
"attachmentId": "vZqf8QJ80XRF",
"title": "746436a2e1.svg",
"role": "image",
"mime": "image/svg+xml",
"position": 10,
"dataFileName": "Format Painter_746436a2e1.svg"
}
]
}
]
},

View File

@ -16,7 +16,7 @@ Fore more information see <a class="reference-link" href="Text/Formatting%20too
Here's a list of various features supported by text notes:
<table><thead><tr><th>Dedicated article</th><th>Feature</th></tr></thead><tbody><tr><td><a class="reference-link" href="Text/General%20formatting.md">General formatting</a></td><td><ul><li>Headings (section titles, paragraph)</li><li>Font size</li><li>Bold, italic, underline, strike-through</li><li>Superscript, subscript</li><li>Font color &amp; background color</li><li>Remove formatting</li></ul></td></tr><tr><td><a class="reference-link" href="Text/Lists.md">Lists</a></td><td><ul><li>Bulleted lists</li><li>Numbered lists</li><li>To-do lists</li></ul></td></tr><tr><td><a class="reference-link" href="Text/Block%20quotes%20%26%20admonitions.md">Block quotes &amp; admonitions</a></td><td><ul><li>Block quotes</li><li>Admonitions</li></ul></td></tr><tr><td><a class="reference-link" href="Text/Tables.md">Tables</a></td><td><ul><li>Basic tables</li><li>Merging cells</li><li>Styling tables and cells.</li><li>Table captions</li></ul></td></tr><tr><td><a class="reference-link" href="Text/Developer-specific%20formatting.md">Developer-specific formatting</a></td><td><ul><li>Inline code</li><li>Code blocks</li><li>Keyboard shortcuts</li></ul></td></tr><tr><td><a class="reference-link" href="Text/Footnotes.md">Footnotes</a></td><td><ul><li>Footnotes</li></ul></td></tr><tr><td><a class="reference-link" href="Text/Images.md">Images</a></td><td><ul><li>Images</li></ul></td></tr><tr><td><a class="reference-link" href="Text/Links.md">Links</a></td><td><ul><li>External links</li><li>Internal Trilium links</li></ul></td></tr><tr><td><a class="reference-link" href="Text/Include%20Note.md">Include Note</a></td><td><ul><li>Include note</li></ul></td></tr><tr><td><a class="reference-link" href="Text/Insert%20buttons.md">Insert buttons</a></td><td><ul><li>Symbols</li><li><a class="reference-link" href="Text/Math%20Equations.md">Math Equations</a></li><li>Mermaid diagrams</li><li>Horizontal ruler</li><li>Page break</li></ul></td></tr><tr><td><a class="reference-link" href="Text/Other%20features.md">Other features</a></td><td><ul><li>Indentation<ul><li>Markdown import</li></ul></li><li><a class="reference-link" href="Text/Cut%20to%20subnote.md">Cut to subnote</a></li></ul></td></tr><tr><td><a class="reference-link" href="Text/Premium%20features.md">Premium features</a></td><td><ul><li><a class="reference-link" href="Text/Premium%20features/Slash%20Commands.md">Slash Commands</a></li><li><a class="reference-link" href="../Advanced%20Usage/Templates.md">Templates</a></li></ul></td></tr></tbody></table>
<table><thead><tr><th>Dedicated article</th><th>Feature</th></tr></thead><tbody><tr><td><a class="reference-link" href="Text/General%20formatting.md">General formatting</a></td><td><ul><li data-list-item-id="e04c84d59d44645ee89b2a8541ed99f90">Headings (section titles, paragraph)</li><li data-list-item-id="e39d25bd3d8bd06185b9d259e5827d451">Font size</li><li data-list-item-id="e1f7e2a2f4b03449d82bdf5b5c6ea8d44">Bold, italic, underline, strike-through</li><li data-list-item-id="e3decae72884f65b4d538151b6a297072">Superscript, subscript</li><li data-list-item-id="e59adf00fef65304c163ae190fac5e92a">Font color &amp; background color</li><li data-list-item-id="ed3f09156147a2769e91db111c76376e2">Remove formatting</li></ul></td></tr><tr><td><a class="reference-link" href="Text/Lists.md">Lists</a></td><td><ul><li data-list-item-id="ee87806a913900d85d8f018af81f41df8">Bulleted lists</li><li data-list-item-id="e3ae314e365fa418ca6e0f061d63834c5">Numbered lists</li><li data-list-item-id="ee84e08694165f95430046cb34f4cd123">To-do lists</li></ul></td></tr><tr><td><a class="reference-link" href="Text/Block%20quotes%20%26%20admonitions.md">Block quotes &amp; admonitions</a></td><td><ul><li data-list-item-id="e2892dc35a0d4b7ad65daffb8f9404daa">Block quotes</li><li data-list-item-id="e7297e3ad1002f8de15aa0bd66c6f3f22">Admonitions</li></ul></td></tr><tr><td><a class="reference-link" href="Text/Tables.md">Tables</a></td><td><ul><li data-list-item-id="eb358a4567d93f66004f4195df2dda05a">Basic tables</li><li data-list-item-id="e6135a555d6c63c30e4b84806a4870830">Merging cells</li><li data-list-item-id="e29ac76563d0998b28fb1baf94dbdac8c">Styling tables and cells.</li><li data-list-item-id="e372446e81fdedada64b8bed89ca93d1a">Table captions</li></ul></td></tr><tr><td><a class="reference-link" href="Text/Developer-specific%20formatting.md">Developer-specific formatting</a></td><td><ul><li data-list-item-id="eb260b76afcbc07bd9d4ceec4e000e8a0">Inline code</li><li data-list-item-id="e9864352286369ebe7b41c1599f498de8">Code blocks</li><li data-list-item-id="ee62fb9ed7f349178e8f2a2bd9ec8cd74">Keyboard shortcuts</li></ul></td></tr><tr><td><a class="reference-link" href="Text/Footnotes.md">Footnotes</a></td><td><ul><li data-list-item-id="edf62ec004eff35cfcb7e361deef19aaf">Footnotes</li></ul></td></tr><tr><td><a class="reference-link" href="Text/Images.md">Images</a></td><td><ul><li data-list-item-id="ebe6277e643041403489c3ceb30c36f7f">Images</li></ul></td></tr><tr><td><a class="reference-link" href="Text/Links.md">Links</a></td><td><ul><li data-list-item-id="e3f988be2f259bb40607cb61541955395">External links</li><li data-list-item-id="e3f91cc4f0cccd2c077cc306bacd68ef2">Internal Trilium links</li></ul></td></tr><tr><td><a class="reference-link" href="Text/Include%20Note.md">Include Note</a></td><td><ul><li data-list-item-id="eac8015a64bce7b749cc67d1599062007">Include note</li></ul></td></tr><tr><td><a class="reference-link" href="Text/Insert%20buttons.md">Insert buttons</a></td><td><ul><li data-list-item-id="e5cdf5d3885ec0ea67f924b4b8fe5c483">Symbols</li><li data-list-item-id="e95082e6642ed5b1eec6e4e116b899a40"><a class="reference-link" href="Text/Math%20Equations.md">Math Equations</a></li><li data-list-item-id="ecbef6a358a5b8d27f0d3e08bbc750aa9">Mermaid diagrams</li><li data-list-item-id="e6e97ee14dd29b7ccf53227107e5dc72d">Horizontal ruler</li><li data-list-item-id="e6198c7c535c249faec2e8906775f11de">Page break</li></ul></td></tr><tr><td><a class="reference-link" href="Text/Other%20features.md">Other features</a></td><td><ul><li data-list-item-id="e0c14456cb83d483b07ea432ef9d4728e">Indentation<ul><li data-list-item-id="e2029812c5e105c595590f70ee227631e">Markdown import</li></ul></li><li data-list-item-id="ea1ee012286e05190c89c9f4e64cf2036"><a class="reference-link" href="Text/Cut%20to%20subnote.md">Cut to subnote</a></li></ul></td></tr><tr><td><a class="reference-link" href="Text/Premium%20features.md">Premium features</a></td><td><ul><li data-list-item-id="e1ab173193a533ccf33dccfd0cb916f1f"><a class="reference-link" href="Text/Premium%20features/Slash%20Commands.md">Slash Commands</a></li><li data-list-item-id="e564b978c09fe5adf476b331b1e0640e3"><a class="reference-link" href="../Advanced%20Usage/Templates.md">Templates</a></li><li data-list-item-id="e756306c31d9beffbba3820b6d1b9bc61"><a class="reference-link" href="Text/Premium%20features/Format%20Painter.md">Format Painter</a></li></ul></td></tr></tbody></table>
## Read-Only vs. Editing Mode

View File

@ -79,6 +79,10 @@ Note that heading styles are not taken into consideration, these must be manuall
When pasting content that comes with undesired formatting, an alternative to pasting and then removing formatting is pasting as plain text via <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>V</kbd>.
## Format painter
The <a class="reference-link" href="Premium%20features/Format%20Painter.md">Format Painter</a> allows users to copy the formatting of text (such as bold, italic, Strikethrough, etc.) and apply it to other parts of the document. It helps maintain consistent formatting and accelerates the creation of rich content.
## Support for Markdown
When exported to <a class="reference-link" href="../../Basic%20Concepts%20and%20Features/Import%20%26%20Export/Markdown.md">Markdown</a>, most of the general formatting is maintained such as headings, bold, italic, underline, etc.

View File

@ -0,0 +1,20 @@
# Format Painter
<figure class="image image-style-align-right"><img style="aspect-ratio:220/76;" src="Format Painter_image.png" width="220" height="76"></figure>
> [!NOTE]
> This is a premium feature of the editor we are using (CKEditor) and we benefit from it thanks to an written agreement with the team. See  <a class="reference-link" href="../Premium%20features.md">Premium features</a> for more information.
The Format Painter is a feature in text notes that allows users to copy the formatting of text (such as **bold**, _italic_, ~~Strikethrough~~, etc.) and apply it to other parts of the document. It helps maintain consistent formatting and accelerates the creation of rich content.
## Usage Instructions
Click the text that you want to copy the formatting from and use the paint formatting toolbar button (<img class="image_resized" style="aspect-ratio:150/150;width:2.7%;" src="Format Painter_746436a2e1.svg" alt="Format painter" width="150" height="150">) to copy the style. Then select the target text with your mouse to apply the formatting.
* **To copy the formatting**: Place the cursor inside a text with some formatting and click the paint formatting toolbar button. Notice that the mouse cursor changes to the <img class="image_resized" style="aspect-ratio:30/20;width:3.64%;" src="Format Painter_e144e96df9.svg" alt="Format painter text cursor" width="30" height="20">.
* **To paint with the copied formatting**: Click any word in the document and the new formatting will be applied. Alternatively, instead of clicking a single word, you can select a text fragment (like an entire paragraph). Notice that the cursor will go back to the default one after the formatting is applied.
* **To keep painting using the same formatting**: Open the toolbar dropdown and enable the continuous painting mode. Once copied, the same formatting can be applied multiple times in different places until the paint formatting button is clicked (the cursor will then revert to the regular one).
## Limitations
1. Painting with block-level formatting (like headings or image styles) is not supported yet. This is because, in <a class="reference-link" href="../../../Advanced%20Usage/Technologies%20used/CKEditor.md">CKEditor</a>, they are considered a part of the content rather than text formatting.
2. When applying formatting to words, spaces or other Western punctuation are used as word boundaries, which prevents proper handling of languages that do not use space-based word segmentation.

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M3 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"/><path d="M16 3.25a1.5 1.5 0 0 1 1.5 1.5v1.7a2.25 2.25 0 0 1-1.932 2.226l-4.424.632a.75.75 0 0 0-.644.743V11a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1v-.95a2.25 2.25 0 0 1 1.932-2.226l4.424-.632A.75.75 0 0 0 16 6.449z"/></svg>

After

Width:  |  Height:  |  Size: 386 B

View File

@ -0,0 +1,7 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 31 20" width="30" height="20">
<path d="M14 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H15a1 1 0 0 1-1-1V3Z" fill="#000"/>
<path d="M27 3.25a1.5 1.5 0 0 1 1.5 1.5v1.7a2.25 2.25 0 0 1-1.932 2.226l-4.424.632a.75.75 0 0 0-.644.743V11a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H20a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1v-.95a2.25 2.25 0 0 1 1.932-2.226l4.424-.632A.75.75 0 0 0 27 6.449V3.25Z" fill="#000"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.855 2.25H27a2.5 2.5 0 0 1 2.5 2.5v1.7a3.25 3.25 0 0 1-2.79 3.216l-4.21.602a2 2 0 0 1 1 1.732v5a2 2 0 0 1-2 2H20a2 2 0 0 1-2-2v-5a2 2 0 0 1 1-1.732v-.217A3.25 3.25 0 0 1 21.129 7H15a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h10a2 2 0 0 1 1.855 1.25ZM20 10.05V11a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h1.5a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1v-.95c0-.016 0-.033.002-.05a.75.75 0 0 1 .642-.692l4.424-.632A2.25 2.25 0 0 0 28.5 6.45V4.75a1.496 1.496 0 0 0-1.5-1.5v3.2a.75.75 0 0 1-.644.742l-4.424.632A2.25 2.25 0 0 0 20 10.05ZM15 2a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H15Z" fill="#fff"/>
<path d="M2.5 2.5A.5.5 0 0 1 3 2h2.5a.5.5 0 0 1 .354.146l.646.647.646-.647A.5.5 0 0 1 7.5 2H10a.5.5 0 0 1 0 1H7.707L7 3.707V10h.5a.5.5 0 0 1 0 1H7v4.793l.707.707H10a.5.5 0 0 1 0 1H7.5a.5.5 0 0 1-.354-.146l-.646-.647-.646.647a.5.5 0 0 1-.354.146H3a.5.5 0 0 1 0-1h2.293L6 15.793V11h-.5a.5.5 0 0 1 0-1H6V3.707L5.293 3H3a.5.5 0 0 1-.5-.5Z" fill="#000"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="m5.793 3.5-.5-.5H3a.5.5 0 0 1 0-1h2.5a.5.5 0 0 1 .354.146l.145.146.501.5.646-.646A.5.5 0 0 1 7.5 2H10a.5.5 0 0 1 0 1H7.707L7 3.707V10h.5a.5.5 0 0 1 0 1H7v4.793l.707.707H10a.5.5 0 0 1 0 1H7.5a.5.5 0 0 1-.354-.146l-.646-.647-.5.5-.146.147a.5.5 0 0 1-.354.146H3a.5.5 0 0 1 0-1h2.293L6 15.793V11h-.5a.5.5 0 0 1 0-1H6V3.707L5.793 3.5Zm-.914.5L5 4.121v4.964a1.5 1.5 0 0 0 0 2.83v3.464l-.121.121H3a1.5 1.5 0 0 0 0 3h2.5a1.5 1.5 0 0 0 1-.382 1.5 1.5 0 0 0 1 .382H10a1.5 1.5 0 0 0 0-3H8.121L8 15.379v-3.464a1.5 1.5 0 0 0 0-2.83V4.121L8.121 4H10a1.5 1.5 0 0 0 0-3H7.5a1.5 1.5 0 0 0-1 .382A1.5 1.5 0 0 0 5.5 1H3a1.5 1.5 0 1 0 0 3h1.879Z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@ -1,4 +1,5 @@
import "ckeditor5";
import { type CreateNoteAction } from "@triliumnext/commons"
declare global {
interface Component {
@ -7,7 +8,8 @@ declare global {
interface EditorComponent extends Component {
loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string): Promise<void>;
createNoteForReferenceLink(title: string): Promise<string>;
// Must Return Note Path
createNoteFromCkEditor(title: string, parentNotePath: string | undefined, action: CreateNoteAction): Promise<string>;
loadIncludedNote(noteId: string, $el: JQuery<HTMLElement>): void;
}

View File

@ -10,6 +10,11 @@ interface LocaleMapping {
const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, LocaleMapping | null> = {
en: null,
en_rtl: null,
"en-GB": {
languageCode: "en-GB",
coreTranslation: () => import("ckeditor5/translations/en-gb.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/en-gb.js"),
},
ar: {
languageCode: "ar",
coreTranslation: () => import("ckeditor5/translations/ar.js"),

View File

@ -1,4 +1,5 @@
import "ckeditor5/ckeditor5.css";
import 'ckeditor5-premium-features/ckeditor5-premium-features.css';
import "./theme/code_block_toolbar.css";
import { COMMON_PLUGINS, CORE_PLUGINS, POPUP_EDITOR_PLUGINS } from "./plugins.js";
import { BalloonEditor, DecoupledEditor, FindAndReplaceEditing, FindCommand } from "ckeditor5";

View File

@ -1,5 +1,5 @@
import { Autoformat, AutoLink, BlockQuote, BlockToolbar, Bold, CKFinderUploadAdapter, Clipboard, Code, CodeBlock, Enter, FindAndReplace, Font, FontBackgroundColor, FontColor, GeneralHtmlSupport, Heading, HeadingButtonsUI, HorizontalLine, Image, ImageCaption, ImageInline, ImageResize, ImageStyle, ImageToolbar, ImageUpload, Alignment, Indent, IndentBlock, Italic, Link, List, ListProperties, Mention, PageBreak, Paragraph, ParagraphButtonUI, PasteFromOffice, PictureEditing, RemoveFormat, SelectAll, ShiftEnter, SpecialCharacters, SpecialCharactersEssentials, Strikethrough, Style, Subscript, Superscript, Table, TableCaption, TableCellProperties, TableColumnResize, TableProperties, TableSelection, TableToolbar, TextPartLanguage, TextTransformation, TodoList, Typing, Underline, Undo, Bookmark, Emoji, Notification, EmojiMention, EmojiPicker } from "ckeditor5";
import { SlashCommand, Template } from "ckeditor5-premium-features";
import { SlashCommand, Template, FormatPainter } from "ckeditor5-premium-features";
import type { Plugin } from "ckeditor5";
import CutToNotePlugin from "./plugins/cuttonote.js";
import UploadimagePlugin from "./plugins/uploadimage.js";
@ -83,7 +83,8 @@ export const CORE_PLUGINS: typeof Plugin[] = [
*/
export const PREMIUM_PLUGINS: typeof Plugin[] = [
SlashCommand,
Template
Template,
FormatPainter
];
/**

View File

@ -1,4 +1,5 @@
import { Command, Mention, Plugin, ModelRange, type ModelSelectable } from "ckeditor5";
import { CreateNoteAction } from "@triliumnext/commons"
/**
* Overrides the actions taken by the Mentions plugin (triggered by `@` in the text editor, or `~` & `#` in the attribute editor):
@ -9,11 +10,11 @@ import { Command, Mention, Plugin, ModelRange, type ModelSelectable } from "cked
*/
export default class MentionCustomization extends Plugin {
static get requires() {
static get requires() {
return [ Mention ];
}
public static get pluginName() {
public static get pluginName() {
return "MentionCustomization" as const;
}
@ -25,20 +26,21 @@ export default class MentionCustomization extends Plugin {
}
interface MentionOpts {
mention: string | {
id: string;
[key: string]: unknown;
};
marker: string;
text?: string;
range?: ModelRange;
mention: string | {
id: string;
[key: string]: unknown;
};
marker: string;
text?: string;
range?: ModelRange;
}
interface MentionAttribute {
id: string;
action?: "create-note";
noteTitle: string;
notePath: string;
id: string;
action?: CreateNoteAction;
noteTitle: string;
notePath: string;
parentNoteId?: string;
}
class CustomMentionCommand extends Command {
@ -56,14 +58,27 @@ class CustomMentionCommand extends Command {
model.insertContent( writer.createText( mention.id, {} ), range );
});
}
else if (mention.action === 'create-note') {
const editorEl = this.editor.editing.view.getDomRoot();
const component = glob.getComponentByEl<EditorComponent>(editorEl);
else if (
mention.action === CreateNoteAction.CreateNote ||
mention.action === CreateNoteAction.CreateChildNote ||
mention.action === CreateNoteAction.CreateAndLinkNote ||
mention.action === CreateNoteAction.CreateAndLinkChildNote
) {
const editorEl = this.editor.editing.view.getDomRoot();
const component = glob.getComponentByEl<EditorComponent>(editorEl);
component.createNoteForReferenceLink(mention.noteTitle).then(notePath => {
this.insertReference(range, notePath);
});
}
// use parentNoteId as fallback when notePath is missing
const parentNotePath = mention.notePath || mention.parentNoteId;
component
.createNoteFromCkEditor(mention.noteTitle, parentNotePath, mention.action)
.then(notePath => {
if (notePath) {
this.insertReference(range, notePath);
}
})
.catch(err => console.error("Error creating note from CKEditor mention:", err));
}
else {
this.insertReference(range, mention.notePath);
}

View File

@ -10,4 +10,5 @@ export * from "./lib/server_api.js";
export * from "./lib/shared_constants.js";
export * from "./lib/ws_api.js";
export * from "./lib/attribute_names.js";
export * from "./lib/create_note_actions.js";
export * from "./lib/utils.js";

View File

@ -0,0 +1,6 @@
export enum CreateNoteAction {
CreateNote = "create-note",
CreateChildNote = "create-child-note",
CreateAndLinkNote = "create-and-link-note",
CreateAndLinkChildNote = "create-and-link-child-note"
}

View File

@ -15,6 +15,7 @@ const UNSORTED_LOCALES = [
{ id: "cn", name: "简体中文", electronLocale: "zh_CN" },
{ id: "de", name: "Deutsch", electronLocale: "de" },
{ id: "en", name: "English", electronLocale: "en" },
{ id: "en-GB", name: "English (United Kingdom)", electronLocale: "en_GB" },
{ id: "es", name: "Español", electronLocale: "es" },
{ id: "fr", name: "Français", electronLocale: "fr" },
{ id: "it", name: "Italiano", electronLocale: "it" },