mirror of
https://github.com/zadam/trilium.git
synced 2025-12-05 06:54:23 +01:00
Compare commits
92 Commits
a364d1a993
...
7c9bf4a48e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c9bf4a48e | ||
|
|
a77e76d5c6 | ||
|
|
ca6660e2ff | ||
|
|
af94410c55 | ||
|
|
b47bc50147 | ||
|
|
5ff77c16ab | ||
|
|
11618260cf | ||
|
|
63f9006d17 | ||
|
|
7779acc7bc | ||
|
|
aacd92eee3 | ||
|
|
1bf8be2874 | ||
|
|
66f2d0c7dc | ||
|
|
597d952254 | ||
|
|
288595ce5d | ||
|
|
c89e8c78d3 | ||
|
|
81a37e3fc4 | ||
|
|
368c590976 | ||
|
|
6e982e646d | ||
|
|
030582b2d5 | ||
|
|
7dd4b10a96 | ||
|
|
b055e79b4c | ||
|
|
ba980aa93f | ||
|
|
15baf04ce9 | ||
|
|
4dc2587817 | ||
|
|
d5e046c289 | ||
|
|
ef8073ac58 | ||
|
|
38c9d25214 | ||
|
|
e2f0e4089f | ||
|
|
80ce2c04ed | ||
|
|
4ebd82beeb | ||
|
|
8cc43cd9a6 | ||
|
|
15190abb69 | ||
|
|
8ee59e9daa | ||
|
|
4e5c26371e | ||
|
|
436146d829 | ||
|
|
315fcecf57 | ||
|
|
0a57e6e154 | ||
|
|
bdcb84a394 | ||
|
|
213c36ba84 | ||
|
|
995e765276 | ||
|
|
8e81c38c14 | ||
|
|
7378fa4cbd | ||
|
|
af8a5ff0c9 | ||
|
|
eca5a4a072 | ||
|
|
71b3ad5027 | ||
|
|
7864168adc | ||
|
|
f8090d9217 | ||
|
|
09aa22c74b | ||
|
|
8fc8f97879 | ||
|
|
547cdff510 | ||
|
|
cdd08d6971 | ||
|
|
b3cf9c8f2d | ||
|
|
feefa389b4 | ||
|
|
f65be4f368 | ||
|
|
77a014109e | ||
|
|
d3db48c99b | ||
|
|
7e4833e893 | ||
|
|
470d7eee31 | ||
|
|
aada631c0f | ||
|
|
bc4186d216 | ||
|
|
c2a27eff2c | ||
|
|
ca24408a13 | ||
|
|
b9e19e524a | ||
|
|
09c8a778f5 | ||
|
|
3438f1103d | ||
|
|
82a3be06d1 | ||
|
|
f0dead5390 | ||
|
|
b0fdb9fef2 | ||
|
|
71009bddc7 | ||
|
|
66e499a2e1 | ||
|
|
a5ef5eee2f | ||
|
|
bcb29d22f5 | ||
|
|
6ad2b49ab3 | ||
|
|
656e7c069d | ||
|
|
00aa470bf2 | ||
|
|
c6ed0b43fc | ||
|
|
3d8a4d2553 | ||
|
|
42ab0eb804 | ||
|
|
d80d06a9b8 | ||
|
|
3c39026739 | ||
|
|
72c17b22df | ||
|
|
dd1aa23cb6 | ||
|
|
ecdf243e63 | ||
|
|
1e15585a24 | ||
|
|
9d40c0cb26 | ||
|
|
e41e24b044 | ||
|
|
a15d661fd7 | ||
|
|
cb5954e8c7 | ||
|
|
2b55db05e1 | ||
|
|
74bf93059c | ||
|
|
384d8c9c37 | ||
|
|
1bb6149dbe |
13
.github/workflows/nightly.yml
vendored
13
.github/workflows/nightly.yml
vendored
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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() }) {
|
||||
|
||||
@ -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
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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] ?? {};
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -261,7 +261,7 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
|
||||
|
||||
return {
|
||||
notePath,
|
||||
noteId: treeService.getNoteIdFromUrl(notePath),
|
||||
noteId: treeService.getNoteIdFromLink(notePath),
|
||||
ntxId,
|
||||
hoistedNoteId,
|
||||
viewScope,
|
||||
|
||||
@ -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 CKEditor’s 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", () => {
|
||||
|
||||
@ -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 Curry–Howard 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
|
||||
};
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -92,3 +92,7 @@ html {
|
||||
.board-note {
|
||||
color: var(--light-theme-custom-color, inherit);
|
||||
}
|
||||
|
||||
.use-note-color {
|
||||
--custom-color: var(--light-theme-custom-color);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
73
apps/client/src/translations/en-GB/translation.json
Normal file
73
apps/client/src/translations/en-GB/translation.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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": "ノートにインポート",
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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" },
|
||||
{
|
||||
|
||||
@ -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") {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 */
|
||||
@ -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")
|
||||
};
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
}}
|
||||
|
||||
63
apps/client/src/widgets/dialogs/revisions.css
Normal file
63
apps/client/src/widgets/dialogs/revisions.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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}
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 ]));
|
||||
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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}.`)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
File diff suppressed because one or more lines are too long
336
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text.html
generated
vendored
336
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text.html
generated
vendored
@ -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 <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 & 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 & 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 <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
|
||||
<a
|
||||
class="reference-link" href="#root/_help_A9Oc6YKKc65v">Keyboard Shortcuts</a>. In addition, see <a class="reference-link"
|
||||
href="#root/_help_QrtTYPmdd1qq">Markdown-like formatting</a> 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 <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 & 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 & 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 <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
|
||||
<a
|
||||
class="reference-link" href="#root/_help_A9Oc6YKKc65v">Keyboard Shortcuts</a>. In addition, see <a class="reference-link"
|
||||
href="#root/_help_QrtTYPmdd1qq">Markdown-like formatting</a> 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 <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>
|
||||
@ -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 <a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/iPIMuisry3hd/gLt3vA97tMcp/_help_5wZallV2Qo1t">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.</p>
|
||||
<h2>Support for Markdown</h2>
|
||||
<p>When exported to <a class="reference-link" href="#root/_help_Oau6X9rCuegd">Markdown</a>,
|
||||
most of the general formatting is maintained such as headings, bold, italic,
|
||||
|
||||
45
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text/Premium features/Format Painter.html
generated
vendored
Normal file
45
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text/Premium features/Format Painter.html
generated
vendored
Normal 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
|
||||
<a
|
||||
class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/iPIMuisry3hd/_help_gLt3vA97tMcp">Premium features</a> 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 <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>
|
||||
1
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text/Premium features/Format Painter_746436a2e1.svg
generated
vendored
Normal file
1
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text/Premium features/Format Painter_746436a2e1.svg
generated
vendored
Normal 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 |
7
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text/Premium features/Format Painter_e144e96df9.svg
generated
vendored
Normal file
7
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text/Premium features/Format Painter_e144e96df9.svg
generated
vendored
Normal 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 |
BIN
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text/Premium features/Format Painter_image.png
generated
vendored
Normal file
BIN
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text/Premium features/Format Painter_image.png
generated
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
1
apps/server/src/assets/translations/en-GB/server.json
Normal file
1
apps/server/src/assets/translations/en-GB/server.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
@ -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"),
|
||||
|
||||
1
apps/website/src/translations/en-GB/translation.json
Normal file
1
apps/website/src/translations/en-GB/translation.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
332
docs/README-en_GB.md
vendored
Normal file
332
docs/README-en_GB.md
vendored
Normal 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
|
||||
|
||||

|
||||
\
|
||||

|
||||
\
|
||||
[](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp)
|
||||
[](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.
|
||||
|
||||
[](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:
|
||||
|
||||
[](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.
|
||||
157
docs/User Guide/!!!meta.json
vendored
157
docs/User Guide/!!!meta.json
vendored
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@ -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 & 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 & 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 & 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 & 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
|
||||
|
||||
|
||||
@ -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.
|
||||
20
docs/User Guide/User Guide/Note Types/Text/Premium features/Format Painter.md
vendored
Normal file
20
docs/User Guide/User Guide/Note Types/Text/Premium features/Format Painter.md
vendored
Normal 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.
|
||||
1
docs/User Guide/User Guide/Note Types/Text/Premium features/Format Painter_746436a2e1.svg
vendored
Normal file
1
docs/User Guide/User Guide/Note Types/Text/Premium features/Format Painter_746436a2e1.svg
vendored
Normal 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 |
7
docs/User Guide/User Guide/Note Types/Text/Premium features/Format Painter_e144e96df9.svg
vendored
Normal file
7
docs/User Guide/User Guide/Note Types/Text/Premium features/Format Painter_e144e96df9.svg
vendored
Normal 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 |
BIN
docs/User Guide/User Guide/Note Types/Text/Premium features/Format Painter_image.png
vendored
Normal file
BIN
docs/User Guide/User Guide/Note Types/Text/Premium features/Format Painter_image.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
6
packages/commons/src/lib/create_note_actions.ts
Normal file
6
packages/commons/src/lib/create_note_actions.ts
Normal 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"
|
||||
}
|
||||
@ -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" },
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user