Merge pull request #1124 from TriliumNext/feature/in_app_help

In-app help
This commit is contained in:
Elian Doran 2025-02-07 22:56:06 +02:00 committed by GitHub
commit f7397dc2f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
72 changed files with 2352 additions and 181 deletions

View File

@ -8,7 +8,7 @@ test("Help popup", async ({ page, context }) => {
await app.goto();
const popupPromise = page.waitForEvent("popup");
await app.currentNoteSplit.press("F1");
await app.currentNoteSplit.press("Shift+F1");
await page.getByRole("link", { name: "online" }).click();
const popup = await popupPromise;
expect(popup.url()).toBe("https://triliumnext.github.io/Docs/");

View File

@ -24,6 +24,7 @@ import type { Attribute } from "../services/attribute_parser.js";
import type NoteTreeWidget from "../widgets/note_tree.js";
import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js";
import type { ContextMenuEvent } from "../menus/context_menu.js";
import type TypeWidget from "../widgets/type_widgets/type_widget.js";
interface Layout {
getRootWidget: (appContext: AppContext) => RootWidget;
@ -61,8 +62,8 @@ export interface NoteCommandData extends CommandData {
viewScope?: ViewScope;
}
export interface ExecuteCommandData extends CommandData {
resolve: unknown;
export interface ExecuteCommandData<T> extends CommandData {
resolve: (data: T) => void
}
/**
@ -77,6 +78,7 @@ export type CommandMappings = {
searchString?: string;
ancestorNoteId?: string | null;
};
closeTocCommand: CommandData;
showLaunchBarSubtree: CommandData;
showOptions: CommandData & {
section: string;
@ -151,12 +153,16 @@ export type CommandMappings = {
callback: (value: NoteDetailWidget | PromiseLike<NoteDetailWidget>) => void;
};
executeWithTextEditor: CommandData &
ExecuteCommandData & {
ExecuteCommandData<TextEditor> & {
callback?: GetTextEditorCallback;
};
executeWithCodeEditor: CommandData & ExecuteCommandData;
executeWithContentElement: CommandData & ExecuteCommandData;
executeWithTypeWidget: CommandData & ExecuteCommandData;
executeWithCodeEditor: CommandData & ExecuteCommandData<null>;
/**
* Called upon when attempting to retrieve the content element of a {@link NoteContext}.
* Generally should not be invoked manually, as it is used by {@link NoteContext.getContentElement}.
*/
executeWithContentElement: CommandData & ExecuteCommandData<JQuery<HTMLElement>>;
executeWithTypeWidget: CommandData & ExecuteCommandData<TypeWidget | null>;
addTextToActiveEditor: CommandData & {
text: string;
};
@ -202,6 +208,9 @@ export type CommandMappings = {
zoomFactor: string;
}
reEvaluateRightPaneVisibility: CommandData;
runActiveNote: CommandData;
// Geomap
deleteFromMap: { noteId: string },
openGeoLocation: { noteId: string, event: JQuery.MouseDownEvent }
@ -247,7 +256,7 @@ type EventMappings = {
};
noteSwitched: {
noteContext: NoteContext;
notePath: string | null;
notePath?: string | null;
};
noteSwitchedAndActivatedEvent: {
noteContext: NoteContext;
@ -262,6 +271,9 @@ type EventMappings = {
reEvaluateHighlightsListWidgetVisibility: {
noteId: string | undefined;
};
reEvaluateTocWidgetVisibility: {
noteId: string | undefined;
};
showHighlightsListWidget: {
noteId: string;
};
@ -297,7 +309,12 @@ type EventMappings = {
};
refreshNoteList: {
noteId: string;
}
};
showToc: {
noteId: string;
};
scrollToEnd: { ntxId: string };
noteTypeMimeChanged: { noteId: string };
};
export type EventListener<T extends EventNames> = {

View File

@ -9,6 +9,7 @@ import hoistedNoteService from "../services/hoisted_note.js";
import options from "../services/options.js";
import type { ViewScope } from "../services/link.js";
import type FNote from "../entities/fnote.js";
import type TypeWidget from "../widgets/type_widgets/type_widget.js";
interface SetNoteOpts {
triggerSwitchEvent?: unknown;
@ -288,7 +289,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
hasNoteList() {
return (
this.note &&
this.viewScope?.viewMode === "default" &&
["default", "contextual-help"].includes(this.viewScope?.viewMode ?? "") &&
this.note.hasChildren() &&
["book", "text", "code"].includes(this.note.type) &&
this.note.mime !== "text/x-sqlite;schema=trilium" &&
@ -319,6 +320,15 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
);
}
/**
* Returns a promise which will retrieve the JQuery element of the content of this note context.
*
* Do note that retrieving the content element needs to be handled by the type widget, which is the one which
* provides the content element by listening to the `executeWithContentElement` event. Not all note types support
* this.
*
* If no content could be determined `null` is returned instead.
*/
async getContentElement() {
return this.timeout<JQuery<HTMLElement>>(
new Promise((resolve) =>
@ -332,7 +342,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
async getTypeWidget() {
return this.timeout(
new Promise((resolve) =>
new Promise<TypeWidget | null>((resolve) =>
appContext.triggerCommand("executeWithTypeWidget", {
resolve,
ntxId: this.ntxId

View File

@ -90,6 +90,10 @@ export default class RootCommandExecutor extends Component {
await appContext.tabManager.openTabWithNoteWithHoisting("_backendLog", { activate: true });
}
async showHelpCommand() {
await this.showAndHoistSubtree("_help");
}
async showLaunchBarSubtreeCommand() {
const rootNote = utils.isMobile() ? "_lbMobileRoot" : "_lbRoot";
await this.showAndHoistSubtree(rootNote);

View File

@ -0,0 +1,500 @@
{
"formatVersion": 2,
"appVersion": "0.91.5",
"files": [
{
"isClone": false,
"noteId": "OkOZllzB3fqN",
"notePath": [
"OkOZllzB3fqN"
],
"title": "User Guide",
"notePosition": 20,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "label",
"name": "iconClass",
"value": "bx bx-help-circle",
"isInheritable": false,
"position": 10
}
],
"format": "html",
"attachments": [],
"dirFileName": "User Guide",
"children": [
{
"isClone": false,
"noteId": "wmegHv51MJMd",
"notePath": [
"OkOZllzB3fqN",
"wmegHv51MJMd"
],
"title": "Types of notes",
"notePosition": 20,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [],
"format": "html",
"attachments": [],
"dirFileName": "Types of notes",
"children": [
{
"isClone": false,
"noteId": "foPEtsL51pD2",
"notePath": [
"OkOZllzB3fqN",
"wmegHv51MJMd",
"foPEtsL51pD2"
],
"title": "Geo map",
"notePosition": 10,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "label",
"name": "iconClass",
"value": "bx bx-map-alt",
"isInheritable": false,
"position": 10
}
],
"format": "html",
"dataFileName": "Geo map.html",
"attachments": [
{
"attachmentId": "viN50n5G4kB0",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Geo map_image.png"
},
{
"attachmentId": "eUrcqc8RRuZG",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "1_Geo map_image.png"
},
{
"attachmentId": "1quk4yxJpeHZ",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "2_Geo map_image.png"
},
{
"attachmentId": "mgwGrtQZjxxb",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "3_Geo map_image.png"
},
{
"attachmentId": "JULizn130rVI",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "4_Geo map_image.png"
},
{
"attachmentId": "kcYjOvJDFkbS",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "5_Geo map_image.png"
},
{
"attachmentId": "ut6vm2aXVfXI",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "6_Geo map_image.png"
},
{
"attachmentId": "0AwaQMqt3FVA",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "7_Geo map_image.png"
},
{
"attachmentId": "gFR2Izzp18LQ",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "8_Geo map_image.png"
},
{
"attachmentId": "PMqmCbNLlZOG",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "9_Geo map_image.png"
},
{
"attachmentId": "pKdtiq4r0eFY",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "10_Geo map_image.png"
},
{
"attachmentId": "FXRVvYpOxWyR",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "11_Geo map_image.png"
},
{
"attachmentId": "42AncDs7SSAf",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "12_Geo map_image.png"
},
{
"attachmentId": "gR2c2Thmfy3I",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "13_Geo map_image.png"
},
{
"attachmentId": "FDP3JzIVSnuJ",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "14_Geo map_image.png"
},
{
"attachmentId": "GhHYO2LteDmZ",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "15_Geo map_image.png"
},
{
"attachmentId": "J0baLTpafs7C",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "16_Geo map_image.png"
},
{
"attachmentId": "uYdb9wWf5Nuv",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "17_Geo map_image.png"
},
{
"attachmentId": "iSpyhQ5Ya6Nk",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "18_Geo map_image.png"
},
{
"attachmentId": "MdC0DpifJwu4",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "19_Geo map_image.png"
}
]
}
]
},
{
"isClone": false,
"noteId": "BDEpqZHDS51s",
"notePath": [
"OkOZllzB3fqN",
"BDEpqZHDS51s"
],
"title": "Working with notes",
"notePosition": 30,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [],
"format": "html",
"attachments": [],
"dirFileName": "Working with notes",
"children": [
{
"isClone": false,
"noteId": "13D1lOc9sqmZ",
"notePath": [
"OkOZllzB3fqN",
"BDEpqZHDS51s",
"13D1lOc9sqmZ"
],
"title": "Exporting as PDF",
"notePosition": 10,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [],
"format": "html",
"dataFileName": "Exporting as PDF.html",
"attachments": [
{
"attachmentId": "b3v1pLE6TF1Y",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Exporting as PDF_image.png"
},
{
"attachmentId": "xsGM34t8ssKV",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "1_Exporting as PDF_image.png"
},
{
"attachmentId": "cvyes4f1Vhmm",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "2_Exporting as PDF_image.png"
}
]
}
]
},
{
"isClone": false,
"noteId": "XUG1egT28FBk",
"notePath": [
"OkOZllzB3fqN",
"XUG1egT28FBk"
],
"title": "Power users",
"notePosition": 50,
"prefix": null,
"isExpanded": true,
"type": "text",
"mime": "text/html",
"attributes": [],
"format": "html",
"attachments": [],
"dirFileName": "Power users",
"children": [
{
"isClone": false,
"noteId": "DtJJ20yEozPA",
"notePath": [
"OkOZllzB3fqN",
"XUG1egT28FBk",
"DtJJ20yEozPA"
],
"title": "Theme development",
"notePosition": 10,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "label",
"name": "iconClass",
"value": "bx bx-palette",
"isInheritable": false,
"position": 10
}
],
"format": "html",
"attachments": [],
"dirFileName": "Theme development",
"children": [
{
"isClone": false,
"noteId": "5HH79ztN0fZA",
"notePath": [
"OkOZllzB3fqN",
"XUG1egT28FBk",
"DtJJ20yEozPA",
"5HH79ztN0fZA"
],
"title": "Creating a custom theme",
"notePosition": 10,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "relation",
"name": "internalLink",
"value": "aH8Dk5aMiq7R",
"isInheritable": false,
"position": 10
}
],
"format": "html",
"dataFileName": "Creating a custom theme.html",
"attachments": [
{
"attachmentId": "bn93hwF7C8sR",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Creating a custom theme_im.png"
},
{
"attachmentId": "17p6z24yW5eP",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "1_Creating a custom theme_im.png"
},
{
"attachmentId": "gXLyv5KXjfxg",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "2_Creating a custom theme_im.png"
},
{
"attachmentId": "AJHVfQtIQgJ7",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "3_Creating a custom theme_im.png"
},
{
"attachmentId": "on1gD7BzCWdN",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "4_Creating a custom theme_im.png"
},
{
"attachmentId": "K3cdwj8f90m0",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "5_Creating a custom theme_im.png"
}
]
},
{
"isClone": false,
"noteId": "aH8Dk5aMiq7R",
"notePath": [
"OkOZllzB3fqN",
"XUG1egT28FBk",
"DtJJ20yEozPA",
"aH8Dk5aMiq7R"
],
"title": "Theme base (legacy vs. next)",
"notePosition": 20,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [],
"format": "html",
"dataFileName": "Theme base (legacy vs. next).html",
"attachments": [
{
"attachmentId": "u0zkXkD7rGXA",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Theme base (legacy vs. nex.png"
},
{
"attachmentId": "5z4bC0x0eH0P",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "1_Theme base (legacy vs. nex.png"
}
]
},
{
"isClone": false,
"noteId": "pMq6N1oBV9oo",
"notePath": [
"OkOZllzB3fqN",
"XUG1egT28FBk",
"DtJJ20yEozPA",
"pMq6N1oBV9oo"
],
"title": "Reference",
"notePosition": 30,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [],
"format": "html",
"dataFileName": "Reference.html",
"attachments": []
}
]
}
]
}
]
},
{
"noImport": true,
"dataFileName": "navigation.html"
},
{
"noImport": true,
"dataFileName": "index.html"
},
{
"noImport": true,
"dataFileName": "style.css"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View File

@ -0,0 +1,94 @@
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../../../style.css">
<base target="_parent">
<title data-trilium-title>Creating a custom theme</title>
</head>
<body>
<div class="content">
<h1 data-trilium-h1>Creating a custom theme</h1>
<div class="ck-content">
<h2>Step 1. Find a place to place the themes</h2>
<p>Organization is an important aspect of managing a knowledge base. When
developing a new theme or importing an existing one it's a good idea to
keep them into one place.</p>
<p>As such, the first step is to create a new note to gather all the themes.</p>
<p>
<img src="Creating a custom theme_im.png" width="181" height="84">
</p>
<h2>Step 2. Create the theme</h2>
<figure class="table" style="width:100%;">
<table class="ck-table-resized">
<colgroup>
<col style="width:32.47%;">
<col style="width:67.53%;">
</colgroup>
<tbody>
<tr>
<td>
<figure class="image">
<img style="aspect-ratio:651/220;" src="1_Creating a custom theme_im.png"
width="651" height="220">
</figure>
</td>
<td style="vertical-align:top;">Themes are code notes with a special attribute. Start by creating a new
code note.</td>
</tr>
<tr>
<td>
<figure class="image">
<img style="aspect-ratio:302/349;" src="2_Creating a custom theme_im.png"
width="302" height="349">
</figure>
</td>
<td style="vertical-align:top;">Then change the note type to a CSS code.</td>
</tr>
<tr>
<td>
<figure class="image">
<img style="aspect-ratio:316/133;" src="3_Creating a custom theme_im.png"
width="316" height="133">
</figure>
</td>
<td style="vertical-align:top;">In the <i>Owned Attributes</i> section define the <code>#appTheme</code> attribute
to point to any desired name. This is the name that will show up in the
appearance section in settings.</td>
</tr>
</tbody>
</table>
</figure>
<h2>Step 3. Define the theme's CSS</h2>
<p>As a very simple example we will change the background color of the launcher
pane to a shade of blue.</p>
<p>To alter the different variables of the theme:</p><pre><code class="language-text-css">:root {
--launcher-pane-background-color: #0d6efd;
}</code></pre>
<h2>Step 4. Activating the theme</h2>
<p>Refresh the application (Ctrl+Shift+R is a good way to do so) and go to
settings. You should see the newly created theme:</p>
<p>
<img src="4_Creating a custom theme_im.png" width="631" height="481">
</p>
<p>Afterwards the application will refresh itself with the new theme:</p>
<p>
<img src="5_Creating a custom theme_im.png" width="653" height="554">
</p>
<p>Do note that the theme will be based off of the legacy theme. To override
that and base the theme on the new TriliumNext theme, see:&nbsp;<a class="reference-link"
href="Theme%20base%20(legacy%20vs.%20next).html">Theme base (legacy vs. next)</a>
</p>
<h2>Step 5. Making changes</h2>
<p>Simply go back to the note and change according to needs. To apply the
changes to the current window, press Ctrl+Shift+R to refresh.</p>
<p>It's a good idea to keep two windows, one for editing and the other one
for previewing the changes.</p>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -0,0 +1,129 @@
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../../../style.css">
<base target="_parent">
<title data-trilium-title>Reference</title>
</head>
<body>
<div class="content">
<h1 data-trilium-h1>Reference</h1>
<div class="ck-content">
<h2>Detecting horizontal vs. vertical layout</h2>
<p>The user can select between vertical layout (the classical one, where
the launcher bar is on the left) and a horizontal layout (where the launcher
bar is on the top and tabs are full-width).</p>
<p>Different styles can be applied by using classes at <code>body</code> level:</p><pre><code class="language-text-x-trilium-auto">body.layout-vertical #left-pane {
/* Do something */
}
body.layout-horizontal #center-pane {
/* Do something else */
}</code></pre>
<p>The two different layouts use different containers (but they are present
in the DOM regardless of the user's choice), for example <code>#horizontal-main-container</code> and <code>#vertical-main-container</code> can
be used to customize the background of the content section.</p>
<h2>Detecting platform (Windows, macOS) or Electron</h2>
<p>It is possible to add particular styles that only apply to a given platform
by using the classes in <code>body</code>:</p>
<figure class="table">
<table>
<thead>
<tr>
<th>Windows</th>
<th>macOS</th>
</tr>
</thead>
<tbody>
<tr>
<td><pre><code class="language-text-x-trilium-auto">body.platform-win32 {
background: red;
}</code></pre>
</td>
<td><pre><code class="language-text-x-trilium-auto">body.platform-darwin {
background: red;
}</code></pre>
</td>
</tr>
</tbody>
</table>
</figure>
<p>It is also possible to only apply a style if running under Electron (desktop
application):</p><pre><code class="language-text-x-trilium-auto">body.electron {
background: blue;
}</code></pre>
<h3>Native title bar</h3>
<p>It's possible to detect if the user has selected the native title bar
or the custom title bar by querying against <code>body</code>:</p><pre><code class="language-text-x-trilium-auto">body.electron.native-titlebar {
/* Do something */
}
body.electron:not(.native-titlebar) {
/* Do something else */
}</code></pre>
<h3>Native window buttons</h3>
<p>When running under Electron with native title bar off, a feature was introduced
to use the platform-specific window buttons such as the semaphore on macOS.</p>
<p>See <a href="https://github.com/TriliumNext/Notes/pull/702">Native title bar buttons by eliandoran · Pull Request #702 · TriliumNext/Notes</a> for
the original implementation of this feature, including screenshots.</p>
<h4>On Windows</h4>
<p>The colors of the native window button area can be adjusted using a RGB
hex color:</p><pre><code class="language-text-x-trilium-auto">body {
--native-titlebar-foreground: #ffffff;
--native-titlebar-background: #ff0000;
}</code></pre>
<p>It is also possible to use transparency at the cost of reduced hover colors
using a RGBA hex color:</p><pre><code class="language-text-x-trilium-auto">body {
--native-titlebar-background: #ff0000aa;
}</code></pre>
<p>Note that the value is read when the window is initialized and then it
is refreshed only when the user changes their light/dark mode preference.</p>
<h4>On macOS</h4>
<p>On macOS the semaphore window buttons are enabled by default when the
native title bar is disabled. The offset of the buttons can be adjusted
using:</p><pre><code class="language-text-x-trilium-auto">body {
--native-titlebar-darwin-x-offset: 12;
--native-titlebar-darwin-y-offset: 14 !important;
}</code></pre>
<h3>Background/transparency effects on Windows (Mica)</h3>
<p>Windows 11 offers a special background/transparency effect called Mica,
which can be enabled by themes by setting the <code>--background-material</code> variable
at <code>body</code> level:</p><pre><code class="language-text-x-trilium-auto">body.electron.platform-win32 {
--background-material: tabbed;
}</code></pre>
<p>The value can be either <code>tabbed</code> (especially useful for the horizontal
layout) or <code>mica</code> (ideal for the vertical layout).</p>
<p>Do note that the Mica effect is applied at <code>body</code> level and the
theme needs to make the entire hierarchy (semi-)transparent in order for
it to be visible. Use the TrilumNext theme as an inspiration.</p>
<h2>Note icons, tab workspace accent color</h2>
<p>Theme capabilities are small adjustments done through CSS variables that
can affect the layout or the visual aspect of the application.</p>
<p>In the tab bar, to display the icons of notes instead of the icon of the
workspace:</p><pre><code class="language-text-x-trilium-auto">:root {
--tab-note-icons: true;
}</code></pre>
<p>When a workspace is hoisted for a given tab, it is possible to get the
background color of that workspace, for example to apply a small strip
on the tab instead of the whole background color:</p><pre><code class="language-text-x-trilium-auto">.note-tab .note-tab-wrapper {
--tab-background-color: initial !important;
}
.note-tab .note-tab-wrapper::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background-color: var(--workspace-tab-background-color);
}</code></pre>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,36 @@
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../../../style.css">
<base target="_parent">
<title data-trilium-title>Theme base (legacy vs. next)</title>
</head>
<body>
<div class="content">
<h1 data-trilium-h1>Theme base (legacy vs. next)</h1>
<div class="ck-content">
<p>By default, any custom theme will be based on the legacy light theme.
To change the TriliumNext theme instead, add the <code>#appThemeBase=next</code> attribute
onto the existing theme. The <code>appTheme</code> attribute must also be
present.</p>
<p>
<img src="1_Theme base (legacy vs. nex.png" width="424" height="140">
</p>
<p>When <code>appThemeBase</code> is set to <code>next</code> it will use the
“TriliumNext (auto)” theme. Any other value is ignored and will use the
legacy white theme instead.</p>
<h2>Overrides</h2>
<p>Do note that the TriliumNext theme has a few more overrides than the legacy
theme, so you might need to suffix <code>!important</code> if the style changes
are not applied.</p><pre><code class="language-text-css">:root {
--launcher-pane-background-color: #0d6efd !important;
}</code></pre>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -0,0 +1,295 @@
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../../style.css">
<base target="_parent">
<title data-trilium-title>Geo map</title>
</head>
<body>
<div class="content">
<h1 data-trilium-h1>Geo map</h1>
<div class="ck-content">
<h2>Creating a new geo map</h2>
<figure class="table" style="width:100%;">
<table class="ck-table-resized">
<colgroup>
<col style="width:4.67%;">
<col style="width:57.81%;">
<col style="width:37.52%;">
</colgroup>
<tbody>
<tr>
<th>1</th>
<td>
<figure class="image image_resized" style="width:100%;">
<img style="aspect-ratio:1256/1044;" src="Geo map_image.png" width="1256"
height="1044">
</figure>
</td>
<td style="vertical-align:top;">Right click on any note on the note tree and select <i>Insert child note</i><i>Geo Map (beta)</i>.</td>
</tr>
<tr>
<th>2</th>
<td>
<figure class="image image_resized" style="width:100%;">
<img style="aspect-ratio:1720/1396;" src="1_Geo map_image.png" width="1720"
height="1396">
</figure>
</td>
<td style="vertical-align:top;">By default the map will be empty and will show the entire world.</td>
</tr>
</tbody>
</table>
</figure>
<h2>Repositioning the map</h2>
<ul>
<li>Click and drag the map in order to move across the map.</li>
<li>Use the mouse wheel, two-finger gesture on a touchpad or the +/- buttons
on the top-left to adjust the zoom.</li>
</ul>
<p>The position on the map and the zoom are saved inside the map note. When
visting again the note it will restore this position.</p>
<h2>Adding a marker using the map</h2>
<figure class="table" style="width:100%;">
<table class="ck-table-resized">
<colgroup>
<col style="width:5.05%;">
<col style="width:49.62%;">
<col style="width:45.33%;">
</colgroup>
<tbody>
<tr>
<th>1</th>
<td>&nbsp;</td>
<td style="vertical-align:top;">
<p>To create a marker, first navigate to the desired point on the map. Then
press the
<img class="image_resized" style="aspect-ratio:72/66;width:7.37%;"
src="2_Geo map_image.png" width="72" height="66">button on the top-right of the map.</p>
<p>If the button is not visible, make sure the button section is visible
by pressing the chevron button (
<img class="image_resized" style="aspect-ratio:72/66;width:7.51%;"
src="3_Geo map_image.png" width="72" height="66">) in the top-right of the map.</p>
</td>
</tr>
<tr>
<th>2</th>
<td>
<figure class="image image_resized" style="width:100%;">
<img style="aspect-ratio:1730/416;" src="4_Geo map_image.png" width="1730"
height="416">
</figure>
<p>&nbsp;</p>
</td>
<td style="vertical-align:top;">
<p>Once pressed, the map will enter in the insert mode, as illustrated by
the notification.</p>
<p>Simply click the point on the map where to place the marker, or the Escape
key to cancel.</p>
</td>
</tr>
<tr>
<th>3</th>
<td>
<figure class="image">
<img style="aspect-ratio:1586/404;" src="5_Geo map_image.png" width="1586"
height="404">
</figure>
<p>&nbsp;</p>
</td>
<td>Enter the name of the marker/note to be created.&nbsp;</td>
</tr>
<tr>
<th>4</th>
<td>
<figure class="image">
<img style="aspect-ratio:1696/608;" src="6_Geo map_image.png" width="1696"
height="608">
</figure>
<p>&nbsp;</p>
</td>
<td>Once confirmed, the marker will show up on the map and it will also be
displayed as a child note of the map.</td>
</tr>
</tbody>
</table>
</figure>
<h2>Repositioning markers</h2>
<p>It's possible to reposition existing markers by simply drag and dropping
them to the new destination.</p>
<p>As soon as the mouse is released, the new position is saved.</p>
<p>If moved by mistake, there is currently no way to undo the change. If
the mouse was not yet released, it's possible to force a refresh of the
page (Ctrl+R or Meta+R) to cancel it.</p>
<h2>Adding the geolocation manually</h2>
<p>The location of a marker is stored in the <code>#geolocation</code> attribute
of the child notes:</p>
<figure class="image">
<img style="aspect-ratio:1288/278;" src="7_Geo map_image.png" width="1288"
height="278">
</figure>
<p>The value of the attribute is made up of the latitude and longitude separated
by a comma.</p>
<h3>Adding from Google Maps</h3>
<figure class="table">
<table>
<tbody>
<tr>
<th>1</th>
<td>
<figure class="image image-style-align-center image_resized" style="width:100%;">
<img style="aspect-ratio:732/918;" src="8_Geo map_image.png" width="732"
height="918">
</figure>
</td>
<td style="vertical-align:top;">
<p>Go to Google Maps on the web and look for a desired location, right click
on it and a context menu will show up.</p>
<p>Simply click on the first item displaying the coordinates and they will
be copied to clipboard.</p>
<p>Then paste the value inside the text box into the <code>#geolocation</code> attribute
of a child note of the map (don't forget to surround the value with a <code>"</code> character).</p>
</td>
</tr>
<tr>
<th>2</th>
<td>
<figure class="image image_resized" style="width:100%;">
<img style="aspect-ratio:518/84;" src="11_Geo map_image.png" width="518"
height="84">
</figure>
</td>
<td style="vertical-align:top;">
<p>In Trilium, create a child note under the map.</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
</td>
</tr>
<tr>
<th>3</th>
<td>
<figure class="image image_resized" style="width:100%;">
<img style="aspect-ratio:1074/276;" src="9_Geo map_image.png" width="1074"
height="276">
</figure>
</td>
<td style="vertical-align:top;">And then go to Owned Attributes and type <code>#geolocation="</code>, then
paste from the clipboard as-is and then add the ending <code>"</code> character.
Press Enter to confirm and the map should now be updated to contain the
new note.</td>
</tr>
</tbody>
</table>
</figure>
<h3>Adding from OpenStreetMap</h3>
<p>Similarly to the Google Maps approach:</p>
<figure class="table" style="width:100%;">
<table class="ck-table-resized">
<colgroup>
<col style="width:4.65%;">
<col style="width:36.01%;">
<col style="width:59.34%;">
</colgroup>
<tbody>
<tr>
<th>1</th>
<td>
<figure class="image image_resized" style="width:100%;">
<img style="aspect-ratio:562/454;" src="12_Geo map_image.png" width="562"
height="454">
</figure>
</td>
<td style="vertical-align:top;">Go to any location on openstreetmap.org and right click to bring up the
context menu. Select the “Show address” item.</td>
</tr>
<tr>
<th>2</th>
<td>
<figure class="image image_resized" style="width:100%;">
<img style="aspect-ratio:696/480;" src="13_Geo map_image.png" width="696"
height="480">
</figure>
</td>
<td style="vertical-align:top;">
<p>The address will be visible in the top-left of the screen, in the place
of the search bar.</p>
<p>Select the coordinates and copy them into the clipboard.</p>
</td>
</tr>
<tr>
<th>3</th>
<td>
<figure class="image">
<img style="aspect-ratio:640/276;" src="14_Geo map_image.png" width="640"
height="276">
</figure>
</td>
<td style="vertical-align:top;">Simply paste the value inside the text box into the <code>#geolocation</code> attribute
of a child note of the map and then it should be displayed on the map.</td>
</tr>
</tbody>
</table>
</figure>
<h2>Adding GPS tracks (.gpx)</h2>
<p>Trilium has basic support for displaying GPS tracks on the geo map.</p>
<figure
class="table" style="width:100%;">
<table class="ck-table-resized">
<colgroup>
<col style="width:4.66%;">
<col style="width:36.79%;">
<col style="width:58.55%;">
</colgroup>
<tbody>
<tr>
<th>1</th>
<td>
<figure class="image">
<img style="aspect-ratio:226/74;" src="17_Geo map_image.png" width="226"
height="74">
</figure>
</td>
<td style="vertical-align:top;">To add a track, simply drag &amp; drop a .gpx file inside the geo map
in the note tree.</td>
</tr>
<tr>
<th>2</th>
<td>
<figure class="image">
<img style="aspect-ratio:322/222;" src="18_Geo map_image.png" width="322"
height="222">
</figure>
</td>
<td style="vertical-align:top;">In order for the file to be recognized as a GPS track, it needs to show
up as <code>application/gpx+xml</code> in the <i>File type</i> field.</td>
</tr>
<tr>
<th>3</th>
<td>
<figure class="image image_resized" style="width:100%;">
<img style="aspect-ratio:620/530;" src="19_Geo map_image.png" width="620"
height="530">
</figure>
</td>
<td style="vertical-align:top;">
<p>When going back to the map, the track should now be visible.</p>
<p>The start and end points of the track are indicated by the two blue markers.</p>
<p>&nbsp;</p>
</td>
</tr>
</tbody>
</table>
</figure>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,52 @@
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../../style.css">
<base target="_parent">
<title data-trilium-title>Exporting as PDF</title>
</head>
<body>
<div class="content">
<h1 data-trilium-h1>Exporting as PDF</h1>
<div class="ck-content">
<figure class="image image-style-align-right image_resized" style="width:47.17%;">
<img style="aspect-ratio:951/432;" src="1_Exporting as PDF_image.png"
width="951" height="432">
</figure>
<p>On the desktop application of Trilium it is possible to export a note
as PDF. On the server or PWA (mobile), the option is not available due
to technical constraints and it will be hidden.</p>
<p>To print a note, select the
<img src="Exporting as PDF_image.png" width="29"
height="31">button to the right of the note and select <i>Export as PDF</i>.</p>
<p>Afterwards you will be prompted to select where to save the PDF file.
Upon confirmation, the resulting PDF will be opened automatically.</p>
<p>Should you encounter any visual issues in the resulting PDF file (e.g.
a table does not fit properly, there is cut off text, etc.) feel free to
<a
href="#root/OeKBfN6JbMIq/jRV1MPt4mNSP/hrC6xn7hnDq5">report the issue</a>. In this case, it's best to offer a sample note (click
on the
<img src="Exporting as PDF_image.png" width="29" height="31">button, select Export note → This note and all of its descendants → HTML
in ZIP archive). Make sure not to accidentally leak any personal information.</p>
<h2>Landscape mode</h2>
<p>When exporting to PDF, there are no customizable settings such as page
orientation, size, etc. However, it is possible to specify a given note
to be printed as a PDF in landscape mode by adding the <code>#printLandscape</code> attribute
to it (see&nbsp;<a class="reference-link" href="#root/9QRytp0ZYFIf/PnO38wN0ffOA">Adding an attribute to a note</a>).</p>
<h2>Page size</h2>
<p>By default, the resulting PDF will be in Letter format. It is possible
to adjust it to another page size via the <code>#printPageSize</code> attribute,
with one of the following values: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>.</p>
<h2>Keyboard shortcut</h2>
<p>It's possible to trigger the export to PDF from the keyboard by going
to&nbsp;<i>Keyboard shortcuts</i>&nbsp;and assigning a key combination
for the <code>exportAsPdf</code> action.</p>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

View File

@ -0,0 +1,11 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<frameset cols="25%,75%">
<frame name="navigation" src="navigation.html">
<frame name="detail" src="User%20Guide/Types%20of%20notes/Geo%20map.html">
</frameset>
</html>

View File

@ -0,0 +1,47 @@
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<ul>
<li>User Guide
<ul>
<li>Types of notes
<ul>
<li><a href="User%20Guide/Types%20of%20notes/Geo%20map.html" target="detail">Geo map</a>
</li>
</ul>
</li>
<li>Working with notes
<ul>
<li><a href="User%20Guide/Working%20with%20notes/Exporting%20as%20PDF.html"
target="detail">Exporting as PDF</a>
</li>
</ul>
</li>
<li>Power users
<ul>
<li>Theme development
<ul>
<li><a href="User%20Guide/Power%20users/Theme%20development/Creating%20a%20custom%20theme.html"
target="detail">Creating a custom theme</a>
</li>
<li><a href="User%20Guide/Power%20users/Theme%20development/Theme%20base%20(legacy%20vs.%20next).html"
target="detail">Theme base (legacy vs. next)</a>
</li>
<li><a href="User%20Guide/Power%20users/Theme%20development/Reference.html"
target="detail">Reference</a>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</body>
</html>

View File

@ -0,0 +1,567 @@
/* !!!!!! TRILIUM CUSTOM CHANGES !!!!!! */
.printed-content .ck-widget__selection-handle, .printed-content .ck-widget__type-around { /* gets rid of triangles: https://github.com/zadam/trilium/issues/1129 */
display: none;
}
.page-break {
page-break-after: always;
}
.printed-content .page-break:after,
.printed-content .page-break > * {
display: none !important;
}
.ck-content li p {
margin: 0 !important;
}
/*
* CKEditor 5 (v41.0.0) content styles.
* Generated on Fri, 26 Jan 2024 10:23:49 GMT.
* For more information, check out https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/content-styles.html
*/
:root {
--ck-color-image-caption-background: hsl(0, 0%, 97%);
--ck-color-image-caption-text: hsl(0, 0%, 20%);
--ck-color-mention-background: hsla(341, 100%, 30%, 0.1);
--ck-color-mention-text: hsl(341, 100%, 30%);
--ck-color-selector-caption-background: hsl(0, 0%, 97%);
--ck-color-selector-caption-text: hsl(0, 0%, 20%);
--ck-highlight-marker-blue: hsl(201, 97%, 72%);
--ck-highlight-marker-green: hsl(120, 93%, 68%);
--ck-highlight-marker-pink: hsl(345, 96%, 73%);
--ck-highlight-marker-yellow: hsl(60, 97%, 73%);
--ck-highlight-pen-green: hsl(112, 100%, 27%);
--ck-highlight-pen-red: hsl(0, 85%, 49%);
--ck-image-style-spacing: 1.5em;
--ck-inline-image-style-spacing: calc(var(--ck-image-style-spacing) / 2);
--ck-todo-list-checkmark-size: 16px;
}
/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */
.ck-content .table .ck-table-resized {
table-layout: fixed;
}
/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */
.ck-content .table table {
overflow: hidden;
}
/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */
.ck-content .table td,
.ck-content .table th {
overflow-wrap: break-word;
position: relative;
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content .table {
margin: 0.9em auto;
display: table;
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content .table table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
height: 100%;
border: 1px double hsl(0, 0%, 70%);
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content .table table td,
.ck-content .table table th {
min-width: 2em;
padding: .4em;
border: 1px solid hsl(0, 0%, 75%);
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content .table table th {
font-weight: bold;
background: hsla(0, 0%, 0%, 5%);
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content[dir="rtl"] .table th {
text-align: right;
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content[dir="ltr"] .table th {
text-align: left;
}
/* @ckeditor/ckeditor5-table/theme/tablecaption.css */
.ck-content .table > figcaption {
display: table-caption;
caption-side: top;
word-break: break-word;
text-align: center;
color: var(--ck-color-selector-caption-text);
background-color: var(--ck-color-selector-caption-background);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
}
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break {
position: relative;
clear: both;
padding: 5px 0;
display: flex;
align-items: center;
justify-content: center;
}
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break::after {
content: '';
position: absolute;
border-bottom: 2px dashed hsl(0, 0%, 77%);
width: 100%;
}
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break__label {
position: relative;
z-index: 1;
padding: .3em .6em;
display: block;
text-transform: uppercase;
border: 1px solid hsl(0, 0%, 77%);
border-radius: 2px;
font-family: Helvetica, Arial, Tahoma, Verdana, Sans-Serif;
font-size: 0.75em;
font-weight: bold;
color: hsl(0, 0%, 20%);
background: hsl(0, 0%, 100%);
box-shadow: 2px 2px 1px hsla(0, 0%, 0%, 0.15);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* @ckeditor/ckeditor5-media-embed/theme/mediaembed.css */
.ck-content .media {
clear: both;
margin: 0.9em 0;
display: block;
min-width: 15em;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list {
list-style: none;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list li {
position: relative;
margin-bottom: 5px;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list li .todo-list {
margin-top: 5px;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input {
-webkit-appearance: none;
display: inline-block;
position: relative;
width: var(--ck-todo-list-checkmark-size);
height: var(--ck-todo-list-checkmark-size);
vertical-align: middle;
border: 0;
left: -25px;
margin-right: -15px;
right: 0;
margin-left: 0;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content[dir=rtl] .todo-list .todo-list__label > input {
left: 0;
margin-right: 0;
right: -25px;
margin-left: -15px;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input::before {
display: block;
position: absolute;
box-sizing: border-box;
content: '';
width: 100%;
height: 100%;
border: 1px solid hsl(0, 0%, 20%);
border-radius: 2px;
transition: 250ms ease-in-out box-shadow;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input::after {
display: block;
position: absolute;
box-sizing: content-box;
pointer-events: none;
content: '';
left: calc( var(--ck-todo-list-checkmark-size) / 3 );
top: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
width: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
height: calc( var(--ck-todo-list-checkmark-size) / 2.6 );
border-style: solid;
border-color: transparent;
border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0;
transform: rotate(45deg);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input[checked]::before {
background: hsl(126, 64%, 41%);
border-color: hsl(126, 64%, 41%);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input[checked]::after {
border-color: hsl(0, 0%, 100%);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label .todo-list__label__description {
vertical-align: middle;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type=checkbox] {
position: absolute;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > input,
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input {
cursor: pointer;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > input:hover::before, .ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input:hover::before {
box-shadow: 0 0 0 5px hsla(0, 0%, 0%, 0.1);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input {
-webkit-appearance: none;
display: inline-block;
position: relative;
width: var(--ck-todo-list-checkmark-size);
height: var(--ck-todo-list-checkmark-size);
vertical-align: middle;
border: 0;
left: -25px;
margin-right: -15px;
right: 0;
margin-left: 0;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content[dir=rtl] .todo-list .todo-list__label > span[contenteditable=false] > input {
left: 0;
margin-right: 0;
right: -25px;
margin-left: -15px;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input::before {
display: block;
position: absolute;
box-sizing: border-box;
content: '';
width: 100%;
height: 100%;
border: 1px solid hsl(0, 0%, 20%);
border-radius: 2px;
transition: 250ms ease-in-out box-shadow;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input::after {
display: block;
position: absolute;
box-sizing: content-box;
pointer-events: none;
content: '';
left: calc( var(--ck-todo-list-checkmark-size) / 3 );
top: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
width: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
height: calc( var(--ck-todo-list-checkmark-size) / 2.6 );
border-style: solid;
border-color: transparent;
border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0;
transform: rotate(45deg);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input[checked]::before {
background: hsl(126, 64%, 41%);
border-color: hsl(126, 64%, 41%);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input[checked]::after {
border-color: hsl(0, 0%, 100%);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type=checkbox] {
position: absolute;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol {
list-style-type: decimal;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol {
list-style-type: lower-latin;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol ol {
list-style-type: lower-roman;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol ol ol {
list-style-type: upper-latin;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol ol ol ol {
list-style-type: upper-roman;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul {
list-style-type: disc;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul ul {
list-style-type: circle;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul ul ul {
list-style-type: square;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul ul ul ul {
list-style-type: square;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image {
display: table;
clear: both;
text-align: center;
margin: 0.9em auto;
min-width: 50px;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image img {
display: block;
margin: 0 auto;
max-width: 100%;
min-width: 100%;
height: auto;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image-inline {
/*
* Normally, the .image-inline would have "display: inline-block" and "img { width: 100% }" (to follow the wrapper while resizing).;
* Unfortunately, together with "srcset", it gets automatically stretched up to the width of the editing root.
* This strange behavior does not happen with inline-flex.
*/
display: inline-flex;
max-width: 100%;
align-items: flex-start;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image-inline picture {
display: flex;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image-inline picture,
.ck-content .image-inline img {
flex-grow: 1;
flex-shrink: 1;
max-width: 100%;
}
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
.ck-content img.image_resized {
height: auto;
}
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized {
max-width: 100%;
display: block;
box-sizing: border-box;
}
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized img {
width: 100%;
}
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized > figcaption {
display: block;
}
/* @ckeditor/ckeditor5-image/theme/imagecaption.css */
.ck-content .image > figcaption {
display: table-caption;
caption-side: bottom;
word-break: break-word;
color: var(--ck-color-image-caption-text);
background-color: var(--ck-color-image-caption-background);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-block-align-left,
.ck-content .image-style-block-align-right {
max-width: calc(100% - var(--ck-image-style-spacing));
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-left,
.ck-content .image-style-align-right {
clear: none;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-side {
float: right;
margin-left: var(--ck-image-style-spacing);
max-width: 50%;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-left {
float: left;
margin-right: var(--ck-image-style-spacing);
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-center {
margin-left: auto;
margin-right: auto;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-right {
float: right;
margin-left: var(--ck-image-style-spacing);
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-block-align-right {
margin-right: 0;
margin-left: auto;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-block-align-left {
margin-left: 0;
margin-right: auto;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content p + .image-style-align-left,
.ck-content p + .image-style-align-right,
.ck-content p + .image-style-side {
margin-top: 0;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-inline.image-style-align-left,
.ck-content .image-inline.image-style-align-right {
margin-top: var(--ck-inline-image-style-spacing);
margin-bottom: var(--ck-inline-image-style-spacing);
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-inline.image-style-align-left {
margin-right: var(--ck-inline-image-style-spacing);
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-inline.image-style-align-right {
margin-left: var(--ck-inline-image-style-spacing);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-yellow {
background-color: var(--ck-highlight-marker-yellow);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-green {
background-color: var(--ck-highlight-marker-green);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-pink {
background-color: var(--ck-highlight-marker-pink);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-blue {
background-color: var(--ck-highlight-marker-blue);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-red {
color: var(--ck-highlight-pen-red);
background-color: transparent;
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-green {
color: var(--ck-highlight-pen-green);
background-color: transparent;
}
/* @ckeditor/ckeditor5-block-quote/theme/blockquote.css */
.ck-content blockquote {
overflow: hidden;
padding-right: 1.5em;
padding-left: 1.5em;
margin-left: 0;
margin-right: 0;
font-style: italic;
border-left: solid 5px hsl(0, 0%, 80%);
}
/* @ckeditor/ckeditor5-block-quote/theme/blockquote.css */
.ck-content[dir="rtl"] blockquote {
border-left: 0;
border-right: solid 5px hsl(0, 0%, 80%);
}
/* @ckeditor/ckeditor5-basic-styles/theme/code.css */
.ck-content code {
background-color: hsla(0, 0%, 78%, 0.3);
padding: .15em;
border-radius: 2px;
}
/* @ckeditor/ckeditor5-font/theme/fontsize.css */
.ck-content .text-tiny {
font-size: .7em;
}
/* @ckeditor/ckeditor5-font/theme/fontsize.css */
.ck-content .text-small {
font-size: .85em;
}
/* @ckeditor/ckeditor5-font/theme/fontsize.css */
.ck-content .text-big {
font-size: 1.4em;
}
/* @ckeditor/ckeditor5-font/theme/fontsize.css */
.ck-content .text-huge {
font-size: 1.8em;
}
/* @ckeditor/ckeditor5-mention/theme/mention.css */
.ck-content .mention {
background: var(--ck-color-mention-background);
color: var(--ck-color-mention-text);
}
/* @ckeditor/ckeditor5-horizontal-line/theme/horizontalline.css */
.ck-content hr {
margin: 15px 0;
height: 4px;
background: hsl(0, 0%, 87%);
border: 0;
}
/* @ckeditor/ckeditor5-code-block/theme/codeblock.css */
.ck-content pre {
padding: 1em;
text-align: left;
direction: ltr;
tab-size: 4;
white-space: pre-wrap;
font-style: normal;
min-width: 200px;
border: 0px;
border-radius: 6px;
box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.2);
}
.ck-content pre:not(.hljs) {
color: hsl(0, 0%, 20.8%);
background: hsla(0, 0%, 78%, 0.3);
}
/* @ckeditor/ckeditor5-code-block/theme/codeblock.css */
.ck-content pre code {
background: unset;
padding: 0;
border-radius: 0;
}
@media print {
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break {
padding: 0;
}
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break::after {
display: none;
}
}

View File

@ -86,6 +86,7 @@ import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolb
import options from "../services/options.js";
import utils from "../services/utils.js";
import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js";
import ContextualHelpButton from "../widgets/floating_buttons/help_button.js";
export default class DesktopLayout {
constructor(customWidgets) {
@ -205,6 +206,7 @@ export default class DesktopLayout {
.child(new CopyImageReferenceButton())
.child(new SvgExportButton())
.child(new BacklinksWidget())
.child(new ContextualHelpButton())
.child(new HideFloatingButtonsButton())
)
.child(new MermaidWidget())

View File

@ -52,7 +52,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
const noSelectedNotes = selNodes.length === 0 || (selNodes.length === 1 && selNodes[0] === this.node);
const notSearch = note?.type !== "search";
const notOptions = !note?.noteId.startsWith("_options");
const notOptionsOrHelp = !note?.noteId.startsWith("_options") && !note?.noteId.startsWith("_help");
const parentNotSearch = !parentNote || parentNote.type !== "search";
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
@ -80,7 +80,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
command: "insertNoteAfter",
uiIcon: "bx bx-plus",
items: insertNoteAfterEnabled ? await noteTypesService.getNoteTypeItems("insertNoteAfter") : null,
enabled: insertNoteAfterEnabled && noSelectedNotes && notOptions
enabled: insertNoteAfterEnabled && noSelectedNotes && notOptionsOrHelp
},
{
@ -88,7 +88,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
command: "insertChildNote",
uiIcon: "bx bx-plus",
items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null,
enabled: notSearch && noSelectedNotes && notOptions
enabled: notSearch && noSelectedNotes && notOptionsOrHelp
},
{ title: "----" },
@ -112,14 +112,14 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
title: `${t("tree-context-menu.edit-branch-prefix")} <kbd data-command="editBranchPrefix"></kbd>`,
command: "editBranchPrefix",
uiIcon: "bx bx-rename",
enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptions
enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptionsOrHelp
},
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptions },
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptionsOrHelp },
{
title: `${t("tree-context-menu.duplicate-subtree")} <kbd data-command="duplicateSubtree">`,
command: "duplicateSubtree",
uiIcon: "bx bx-outline",
enabled: parentNotSearch && isNotRoot && !isHoisted && notOptions
enabled: parentNotSearch && isNotRoot && !isHoisted && notOptionsOrHelp
},
{ title: "----" },
@ -136,7 +136,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
{ title: "----" },
{ title: t("tree-context-menu.copy-note-path-to-clipboard"), command: "copyNotePathToClipboard", uiIcon: "bx bx-directions", enabled: true },
{ title: t("tree-context-menu.recent-changes-in-subtree"), command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes && notOptions }
{ title: t("tree-context-menu.recent-changes-in-subtree"), command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes && notOptionsOrHelp }
]
},
@ -178,14 +178,14 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
title: `${t("tree-context-menu.delete")} <kbd data-command="deleteNotes"></kbd>`,
command: "deleteNotes",
uiIcon: "bx bx-trash destructive-action-icon",
enabled: isNotRoot && !isHoisted && parentNotSearch && notOptions
enabled: isNotRoot && !isHoisted && parentNotSearch && notOptionsOrHelp
},
{ title: "----" },
{ title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-import", enabled: notSearch && noSelectedNotes && notOptions },
{ title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-import", enabled: notSearch && noSelectedNotes && notOptionsOrHelp },
{ title: t("tree-context-menu.export"), command: "exportNote", uiIcon: "bx bx-export", enabled: notSearch && noSelectedNotes && notOptions },
{ title: t("tree-context-menu.export"), command: "exportNote", uiIcon: "bx bx-export", enabled: notSearch && noSelectedNotes && notOptionsOrHelp },
{ title: "----" },

View File

@ -79,7 +79,7 @@ async function renderAttributes(attributes: FAttribute[], renderIsInheritable: b
return $container;
}
const HIDDEN_ATTRIBUTES = ["originalFileName", "fileSize", "template", "inherit", "cssClass", "iconClass", "pageSize", "viewType", "geolocation"];
const HIDDEN_ATTRIBUTES = [ "originalFileName", "fileSize", "template", "inherit", "cssClass", "iconClass", "pageSize", "viewType", "geolocation", "docName" ];
async function renderNormalAttributes(note: FNote) {
const promotedDefinitionAttributes = note.getPromotedDefinitionAttributes();

View File

@ -25,14 +25,29 @@ async function getLinkIcon(noteId: string, viewMode: ViewMode | undefined) {
return icon;
}
type ViewMode = "default" | "source" | "attachments" | string;
// TODO: Remove `string` once all the view modes have been mapped.
type ViewMode = "default" | "source" | "attachments" | "contextual-help" | string;
export interface ViewScope {
/**
* - "source", when viewing the source code of a note.
* - "attachments", when viewing the attachments of a note.
* - "contextual-help", if the current view represents a help window that was opened to the side of the main content.
* - "default", otherwise.
*/
viewMode?: ViewMode;
attachmentId?: string;
readOnlyTemporarilyDisabled?: boolean;
highlightsListPreviousVisible?: boolean;
highlightsListTemporarilyHidden?: boolean;
tocTemporarilyHidden?: boolean;
/*
* The reason for adding tocPreviousVisible is to record whether the previous state of the toc is hidden or displayed,
* and then let it be displayed/hidden at the initial time. If there is no such value,
* when the right panel needs to display highlighttext but not toc, every time the note content is changed,
* toc will appear and then close immediately, because getToc(html) function will consume time
*/
tocPreviousVisible?: boolean;
}
interface CreateLinkOptions {

View File

@ -22,11 +22,7 @@ interface CreateNoteOpts {
focus?: "title" | "content";
target?: string;
targetBranchId?: string;
textEditor?: {
// TODO: Replace with interface once note_context.js is converted.
getSelectedHtml(): string;
removeSelection(): void;
};
textEditor?: TextEditor;
}
interface Response {

View File

@ -239,6 +239,9 @@ declare global {
},
getData(): string;
setData(data: string): void;
getSelectedHtml(): string;
removeSelection(): void;
sourceElement: HTMLElement;
}
interface MentionItem {

View File

@ -226,6 +226,12 @@ const TPL = `
<kbd data-command="showHelp"></kbd>
</li>
<li class="dropdown-item show-help-button" data-trigger-command="showCheatsheet">
<span class="bx bxs-keyboard"></span>
${t("global_menu.show-cheatsheet")}
<kbd data-command="showCheatsheet"></kbd>
</li>
<li class="dropdown-item show-about-dialog-button">
<span class="bx bx-info-circle"></span>
${t("global_menu.about")}

View File

@ -61,7 +61,7 @@ export default class SplitNoteContainer extends FlexContainer {
await appContext.tabManager.activateNoteContext(noteContext.ntxId);
if (notePath) {
await noteContext.setNote(notePath, viewScope);
await noteContext.setNote(notePath, { viewScope });
} else {
await noteContext.setEmpty();
}

View File

@ -153,7 +153,7 @@ export default class HelpDialog extends BasicWidget {
this.$widget = $(TPL);
}
showHelpEvent() {
showCheatsheetEvent() {
utils.openDialog(this.$widget);
}
}

View File

@ -22,7 +22,7 @@ const TPL = `
}
.floating-buttons-children > *:not(.hidden-int):not(.no-content-hidden) {
margin-left: 10px;
margin: 2px;
}
.floating-buttons-children > button, .floating-buttons-children .floating-button {

View File

@ -5,6 +5,7 @@ const TPL = `\
<div class="geo-map-buttons">
<style>
.geo-map-buttons {
contain: none;
display: flex;
gap: 10px;
}
@ -12,13 +13,6 @@ const TPL = `\
.leaflet-pane {
z-index: 50;
}
.geo-map-buttons {
contain: none;
background: var(--main-background-color);
box-shadow: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
border-radius: 4px;
}
</style>
<button type="button"

View File

@ -0,0 +1,76 @@
import appContext from "../../components/app_context.js";
import type { NoteType } from "../../entities/fnote.js";
import { t } from "../../services/i18n.js";
import type { ViewScope } from "../../services/link.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
const TPL = `
<button class="open-contextual-help-button" title="${t("help-button.title")}">
<span class="bx bx-help-circle"></span>
</button>
`;
const byNoteType: Record<NoteType, string | null> = {
book: null,
canvas: null,
code: null,
contentWidget: null,
doc: null,
file: null,
geoMap: "foPEtsL51pD2",
image: null,
launcher: null,
mermaid: null,
mindMap: null,
noteMap: null,
relationMap: null,
render: null,
search: null,
text: null,
webView: null
};
export default class ContextualHelpButton extends NoteContextAwareWidget {
private helpNoteIdToOpen?: string | null;
isEnabled() {
this.helpNoteIdToOpen = null;
if (!super.isEnabled()) {
return false;
}
if (this.note && byNoteType[this.note.type]) {
this.helpNoteIdToOpen = byNoteType[this.note.type];
}
return !!this.helpNoteIdToOpen;
}
doRender() {
this.$widget = $(TPL);
this.$widget.on("click", () => {
const subContexts = appContext.tabManager.getActiveContext().getSubContexts();
const targetNote = `_help_${this.helpNoteIdToOpen}`;
const helpSubcontext = subContexts.find((s) => s.viewScope?.viewMode === "contextual-help");
const viewScope: ViewScope = {
viewMode: "contextual-help",
};
if (!helpSubcontext) {
// The help is not already open, open a new split with it.
const { ntxId } = subContexts[subContexts.length - 1];
this.triggerCommand("openNewNoteSplit", {
ntxId,
notePath: targetNote,
hoistedNoteId: "_help",
viewScope
})
} else {
// There is already a help window open, make sure it opens on the right note.
helpSubcontext.setNote(targetNote, { viewScope });
}
});
}
}

View File

@ -4,7 +4,7 @@ import protectedSessionHolder from "../services/protected_session_holder.js";
import SpacedUpdate from "../services/spaced_update.js";
import server from "../services/server.js";
import libraryLoader from "../services/library_loader.js";
import appContext from "../components/app_context.js";
import appContext, { type CommandListenerData, type EventData } from "../components/app_context.js";
import keyboardActionsService from "../services/keyboard_actions.js";
import noteCreateService from "../services/note_create.js";
import attributeService from "../services/attributes.js";
@ -33,6 +33,8 @@ import MindMapWidget from "./type_widgets/mind_map.js";
import { getStylesheetUrl, isSyntaxHighlightEnabled } from "../services/syntax_highlight.js";
import GeoMapTypeWidget from "./type_widgets/geo_map.js";
import utils from "../services/utils.js";
import type { NoteType } from "../entities/fnote.js";
import type TypeWidget from "./type_widgets/type_widget.js";
const TPL = `
<div class="note-detail">
@ -73,14 +75,34 @@ const typeWidgetClasses = {
geoMap: GeoMapTypeWidget
};
/**
* A `NoteType` altered by the note detail widget, taking into consideration whether the note is editable or not and adding special note types such as an empty one,
* for protected session or attachment information.
*/
type ExtendedNoteType = Exclude<NoteType, "mermaid" | "launcher" | "text" | "code"> | "empty" | "readOnlyCode" | "readOnlyText" | "editableText" | "editableCode" | "attachmentDetail" | "attachmentList" | "protectedSession";
export default class NoteDetailWidget extends NoteContextAwareWidget {
private typeWidgets: Record<string, TypeWidget>;
private spacedUpdate: SpacedUpdate;
private type?: ExtendedNoteType;
private mime?: string;
constructor() {
super();
this.typeWidgets = {};
this.spacedUpdate = new SpacedUpdate(async () => {
if (!this.noteContext) {
return;
}
const { note } = this.noteContext;
if (!note) {
return;
}
const { noteId } = note;
const data = await this.getTypeWidget().getData();
@ -94,7 +116,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
await server.put(`notes/${noteId}/data`, data, this.componentId);
this.getTypeWidget().dataSaved?.();
this.getTypeWidget().dataSaved();
});
appContext.addBeforeUnloadListener(this);
@ -129,13 +151,17 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
this.$widget.append($renderedWidget);
if (this.noteContext) {
await typeWidget.handleEvent("setNoteContext", { noteContext: this.noteContext });
}
// this is happening in update(), so note has been already set, and we need to reflect this
if (this.noteContext) {
await typeWidget.handleEvent("noteSwitched", {
noteContext: this.noteContext,
notePath: this.noteContext.notePath
});
}
this.child(typeWidget);
}
@ -150,57 +176,60 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
// https://github.com/zadam/trilium/issues/2522
const isBackendNote = this.noteContext?.noteId === "_backendLog";
const isSqlNote = this.mime === "text/x-sqlite;schema=trilium";
const isFullHeightNoteType = ["canvas", "webView", "noteMap", "mindMap", "geoMap"].includes(this.type);
const isFullHeight = (!this.noteContext.hasNoteList() && isFullHeightNoteType && !isSqlNote)
|| this.noteContext.viewScope.viewMode === "attachments"
const isFullHeightNoteType = ["canvas", "webView", "noteMap", "mindMap", "geoMap"].includes(this.type ?? "");
const isFullHeight = (!this.noteContext?.hasNoteList() && isFullHeightNoteType && !isSqlNote)
|| this.noteContext?.viewScope?.viewMode === "attachments"
|| isBackendNote;
this.$widget.toggleClass("full-height", isFullHeight);
}
getTypeWidget() {
if (!this.typeWidgets[this.type]) {
if (!this.type || !this.typeWidgets[this.type]) {
throw new Error(t(`note_detail.could_not_find_typewidget`, { type: this.type }));
}
return this.typeWidgets[this.type];
}
async getWidgetType() {
async getWidgetType(): Promise<ExtendedNoteType> {
const note = this.note;
if (!note) {
return "empty";
}
let type = note.type;
const viewScope = this.noteContext.viewScope;
let type: NoteType = note.type;
let resultingType: ExtendedNoteType;
const viewScope = this.noteContext?.viewScope;
if (viewScope.viewMode === "source") {
type = "readOnlyCode";
} else if (viewScope.viewMode === "attachments") {
type = viewScope.attachmentId ? "attachmentDetail" : "attachmentList";
} else if (type === "text" && (await this.noteContext.isReadOnly())) {
type = "readOnlyText";
} else if ((type === "code" || type === "mermaid") && (await this.noteContext.isReadOnly())) {
type = "readOnlyCode";
if (viewScope?.viewMode === "source") {
resultingType = "readOnlyCode";
} else if (viewScope && viewScope.viewMode === "attachments") {
resultingType = viewScope.attachmentId ? "attachmentDetail" : "attachmentList";
} else if (type === "text" && (await this.noteContext?.isReadOnly())) {
resultingType = "readOnlyText";
} else if ((type === "code" || type === "mermaid") && (await this.noteContext?.isReadOnly())) {
resultingType = "readOnlyCode";
} else if (type === "text") {
type = "editableText";
resultingType = "editableText";
} else if (type === "code" || type === "mermaid") {
type = "editableCode";
resultingType = "editableCode";
} else if (type === "launcher") {
type = "doc";
resultingType = "doc";
} else {
resultingType = type;
}
if (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) {
type = "protectedSession";
resultingType = "protectedSession";
}
return type;
return resultingType;
}
async focusOnDetailEvent({ ntxId }) {
if (this.noteContext.ntxId !== ntxId) {
async focusOnDetailEvent({ ntxId }: EventData<"focusOnDetail">) {
if (this.noteContext?.ntxId !== ntxId) {
return;
}
@ -210,8 +239,8 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
widget.focus();
}
async scrollToEndEvent({ ntxId }) {
if (this.noteContext.ntxId !== ntxId) {
async scrollToEndEvent({ ntxId }: EventData<"scrollToEnd">) {
if (this.noteContext?.ntxId !== ntxId) {
return;
}
@ -224,29 +253,29 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
}
}
async beforeNoteSwitchEvent({ noteContext }) {
async beforeNoteSwitchEvent({ noteContext }: EventData<"beforeNoteSwitch">) {
if (this.isNoteContext(noteContext.ntxId)) {
await this.spacedUpdate.updateNowIfNecessary();
}
}
async beforeNoteContextRemoveEvent({ ntxIds }) {
async beforeNoteContextRemoveEvent({ ntxIds }: EventData<"beforeNoteContextRemove">) {
if (this.isNoteContext(ntxIds)) {
await this.spacedUpdate.updateNowIfNecessary();
}
}
async runActiveNoteCommand(params) {
async runActiveNoteCommand(params: CommandListenerData<"runActiveNote">) {
if (this.isNoteContext(params.ntxId)) {
// make sure that script is saved before running it #4028
await this.spacedUpdate.updateNowIfNecessary();
}
return await this.parent.triggerCommand("runActiveNote", params);
return await this.parent?.triggerCommand("runActiveNote", params);
}
async printActiveNoteEvent() {
if (!this.noteContext.isActive()) {
if (!this.noteContext?.isActive()) {
return;
}
@ -254,7 +283,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
}
async exportAsPdfEvent() {
if (!this.noteContext.isActive()) {
if (!this.noteContext?.isActive() || !this.note) {
return;
}
@ -266,18 +295,18 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
});
}
hoistedNoteChangedEvent({ ntxId }) {
hoistedNoteChangedEvent({ ntxId }: EventData<"hoistedNoteChanged">) {
if (this.isNoteContext(ntxId)) {
this.refresh();
}
}
async entitiesReloadedEvent({ loadResults }) {
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
// we're detecting note type change on the note_detail level, but triggering the noteTypeMimeChanged
// globally, so it gets also to e.g. ribbon components. But this means that the event can be generated multiple
// times if the same note is open in several tabs.
if (loadResults.isNoteContentReloaded(this.noteId, this.componentId)) {
if (this.noteId && loadResults.isNoteContentReloaded(this.noteId, this.componentId)) {
// probably incorrect event
// calling this.refresh() is not enough since the event needs to be propagated to children as well
// FIXME: create a separate event to force hierarchical refresh
@ -285,7 +314,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
// this uses handleEvent to make sure that the ordinary content updates are propagated only in the subtree
// to avoid the problem in #3365
this.handleEvent("noteTypeMimeChanged", { noteId: this.noteId });
} else if (loadResults.isNoteReloaded(this.noteId, this.componentId) && (this.type !== (await this.getWidgetType()) || this.mime !== this.note.mime)) {
} else if (this.noteId && loadResults.isNoteReloaded(this.noteId, this.componentId) && (this.type !== (await this.getWidgetType()) || this.mime !== this.note?.mime)) {
// this needs to have a triggerEvent so that e.g., note type (not in the component subtree) is updated
this.triggerEvent("noteTypeMimeChanged", { noteId: this.noteId });
} else {
@ -293,12 +322,12 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
const label = attrs.find(
(attr) =>
attr.type === "label" && ["readOnly", "autoReadOnlyDisabled", "cssClass", "displayRelations", "hideRelations"].includes(attr.name) && attributeService.isAffecting(attr, this.note)
attr.type === "label" && ["readOnly", "autoReadOnlyDisabled", "cssClass", "displayRelations", "hideRelations"].includes(attr.name ?? "") && attributeService.isAffecting(attr, this.note)
);
const relation = attrs.find((attr) => attr.type === "relation" && ["template", "inherit", "renderNote"].includes(attr.name) && attributeService.isAffecting(attr, this.note));
const relation = attrs.find((attr) => attr.type === "relation" && ["template", "inherit", "renderNote"].includes(attr.name ?? "") && attributeService.isAffecting(attr, this.note));
if (label || relation) {
if (this.noteId && (label || relation)) {
// probably incorrect event
// calling this.refresh() is not enough since the event needs to be propagated to children as well
this.triggerEvent("noteTypeMimeChanged", { noteId: this.noteId });
@ -310,13 +339,13 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
return this.spacedUpdate.isAllSavedAndTriggerUpdate();
}
readOnlyTemporarilyDisabledEvent({ noteContext }) {
readOnlyTemporarilyDisabledEvent({ noteContext }: EventData<"readOnlyTemporarilyDisabled">) {
if (this.isNoteContext(noteContext.ntxId)) {
this.refresh();
}
}
async executeInActiveNoteDetailWidgetEvent({ callback }) {
async executeInActiveNoteDetailWidgetEvent({ callback }: EventData<"executeInActiveNoteDetailWidget">) {
if (!this.isActiveNoteContext()) {
return;
}
@ -334,12 +363,15 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
}
// without await as this otherwise causes deadlock through component mutex
noteCreateService.createNote(appContext.tabManager.getActiveContextNotePath(), {
const parentNotePath = appContext.tabManager.getActiveContextNotePath();
if (this.noteContext && parentNotePath) {
noteCreateService.createNote(parentNotePath, {
isProtected: note.isProtected,
saveSelection: true,
textEditor: await this.noteContext.getTextEditor()
});
}
}
// used by cutToNote in CKEditor build
async saveNoteDetailNowCommand() {
@ -347,12 +379,12 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
}
renderActiveNoteEvent() {
if (this.noteContext.isActive()) {
if (this.noteContext?.isActive()) {
this.refresh();
}
}
async executeWithTypeWidgetEvent({ resolve, ntxId }) {
async executeWithTypeWidgetEvent({ resolve, ntxId }: EventData<"executeWithTypeWidget">) {
if (!this.isNoteContext(ntxId)) {
return;
}

View File

@ -368,7 +368,11 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const notePath = treeService.getNotePath(data.node);
const activeNoteContext = appContext.tabManager.getActiveContext();
await activeNoteContext.setNote(notePath);
const opts = {};
if (activeNoteContext.viewScope.viewMode === "contextual-help") {
opts.viewScope = activeNoteContext.viewScope;
}
await activeNoteContext.setNote(notePath, opts);
},
expand: (event, data) => this.setExpanded(data.node.data.branchId, true),
collapse: (event, data) => this.setExpanded(data.node.data.branchId, false),
@ -550,7 +554,12 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
$span.append($refreshSearchButton);
}
if (!["search", "launcher"].includes(note.type) && !note.isOptions() && !note.isLaunchBarConfig()) {
// TODO: Deduplicate with server's notes.ts#getAndValidateParent
if (!["search", "launcher"].includes(note.type)
&& !note.isOptions()
&& !note.isLaunchBarConfig()
&& !note.noteId.startsWith("_help")
) {
const $createChildNoteButton = $(`<span class="tree-item-button add-note-button bx bx-plus" title="${t("note_tree.create-child-note")}"></span>`).on(
"click",
cancelClickPropagation

View File

@ -18,8 +18,9 @@ import attributeService from "../services/attributes.js";
import RightPanelWidget from "./right_panel_widget.js";
import options from "../services/options.js";
import OnClickButtonWidget from "./buttons/onclick_button.js";
import appContext from "../components/app_context.js";
import appContext, { type EventData } from "../components/app_context.js";
import libraryLoader from "../services/library_loader.js";
import type FNote from "../entities/fnote.js";
const TPL = `<div class="toc-widget">
<style>
@ -53,7 +54,16 @@ const TPL = `<div class="toc-widget">
<span class="toc"></span>
</div>`;
interface Toc {
$toc: JQuery<HTMLElement>,
headingCount: number
}
export default class TocWidget extends RightPanelWidget {
private $toc!: JQuery<HTMLElement>;
private tocLabelValue?: string | null;
get widgetTitle() {
return t("toc.table_of_contents");
}
@ -75,7 +85,17 @@ export default class TocWidget extends RightPanelWidget {
}
isEnabled() {
return super.isEnabled() && this.note.type === "text" && !this.noteContext.viewScope.tocTemporarilyHidden && this.noteContext.viewScope.viewMode === "default";
if (!super.isEnabled() || !this.note) {
return false;
}
const isHelpNote = (this.note.type === "doc" && this.note.noteId.startsWith("_help"));
const isTextNote = (this.note.type === "text");
const isNoteSupported = isTextNote || isHelpNote;
return isNoteSupported
&& !this.noteContext?.viewScope?.tocTemporarilyHidden
&& this.noteContext?.viewScope?.viewMode === "default";
}
async doRenderBody() {
@ -83,36 +103,63 @@ export default class TocWidget extends RightPanelWidget {
this.$toc = this.$body.find(".toc");
}
async refreshWithNote(note) {
/*The reason for adding tocPreviousVisible is to record whether the previous state of the toc is hidden or displayed,
* and then let it be displayed/hidden at the initial time. If there is no such value,
* when the right panel needs to display highlighttext but not toc, every time the note content is changed,
* toc will appear and then close immediately, because getToc(html) function will consume time*/
this.toggleInt(!!this.noteContext.viewScope.tocPreviousVisible);
async refreshWithNote(note: FNote) {
const tocLabel = note.getLabel("toc");
this.toggleInt(!!this.noteContext?.viewScope?.tocPreviousVisible);
if (tocLabel?.value === "hide") {
this.tocLabelValue = note.getLabelValue("toc");
if (this.tocLabelValue === "hide") {
this.toggleInt(false);
this.triggerCommand("reEvaluateRightPaneVisibility");
return;
}
let $toc = "",
headingCount = 0;
// Check for type text unconditionally in case alwaysShowWidget is set
if (this.note.type === "text") {
const { content } = await note.getBlob();
({ $toc, headingCount } = await this.getToc(content));
if (!this.note || !this.noteContext?.viewScope) {
return;
}
this.$toc.html($toc);
if (["", "show"].includes(tocLabel?.value) || headingCount >= options.getInt("minTocHeadings")) {
this.toggleInt(true);
this.noteContext.viewScope.tocPreviousVisible = true;
// Check for type text unconditionally in case alwaysShowWidget is set
if (this.note.type === "text") {
const blob = await note.getBlob();
if (blob) {
const toc = await this.getToc(blob.content);
this.#updateToc(toc);
}
return;
}
if (this.note.type === "doc") {
/**
* For document note types, we obtain the content directly from the DOM since it allows us to obtain processed data without
* requesting data twice. However, when immediately navigating to a new note the new document is not yet attached to the hierarchy,
* resulting in an empty TOC. The fix is to simply wait for it to pop up.
*/
setTimeout(async () => {
const $contentEl = await this.noteContext?.getContentElement();
if ($contentEl) {
const content = $contentEl.html();
const toc = await this.getToc(content);
this.#updateToc(toc);
} else {
this.toggleInt(false);
this.noteContext.viewScope.tocPreviousVisible = false;
console.warn("Unable to get content element for doctype");
}
}, 10);
}
}
#updateToc({ $toc, headingCount }: Toc) {
this.$toc.empty();
if ($toc) {
this.$toc.append($toc);
}
const tocLabelValue = this.tocLabelValue;
const visible = (tocLabelValue === "" || tocLabelValue === "show") || headingCount >= (options.getInt("minTocHeadings") ?? 0);
this.toggleInt(visible);
if (this.noteContext?.viewScope) {
this.noteContext.viewScope.tocPreviousVisible = visible;
}
this.triggerCommand("reEvaluateRightPaneVisibility");
@ -121,10 +168,10 @@ export default class TocWidget extends RightPanelWidget {
/**
* Rendering formulas in strings using katex
*
* @param {string} html Note's html content
* @returns {string} The HTML content with mathematical formulas rendered by KaTeX.
* @param html Note's html content
* @returns The HTML content with mathematical formulas rendered by KaTeX.
*/
async replaceMathTextWithKatax(html) {
async replaceMathTextWithKatax(html: string) {
const mathTextRegex = /<span class="math-tex">\\\(([\s\S]*?)\\\)<\/span>/g;
var matches = [...html.matchAll(mathTextRegex)];
let modifiedText = html;
@ -167,12 +214,12 @@ export default class TocWidget extends RightPanelWidget {
/**
* Builds a jquery table of contents.
*
* @param {string} html Note's html content
* @returns {$toc: jQuery, headingCount: integer} ordered list table of headings, nested by heading level
* @param html Note's html content
* @returns ordered list table of headings, nested by heading level
* with an onclick event that will cause the document to scroll to
* the desired position.
*/
async getToc(html) {
async getToc(html: string): Promise<Toc> {
// Regular expression for headings <h1>...</h1> using non-greedy
// matching and backreferences
const headingTagsRegex = /<h(\d+)[^>]*>(.*?)<\/h\1>/gi;
@ -184,12 +231,12 @@ export default class TocWidget extends RightPanelWidget {
// Note heading 2 is the first level Trilium makes available to the note
let curLevel = 2;
const $ols = [$toc];
let headingCount;
let headingCount = 0;
for (let m = null, headingIndex = 0; (m = headingTagsRegex.exec(html)) !== null; headingIndex++) {
//
// Nest/unnest whatever necessary number of ordered lists
//
const newLevel = m[1];
const newLevel = parseInt(m[1]);
const levelDelta = newLevel - curLevel;
if (levelDelta > 0) {
// Open as many lists as newLevel - curLevel
@ -229,7 +276,7 @@ export default class TocWidget extends RightPanelWidget {
/**
* Reduce indent if a larger headings are not being used: https://github.com/zadam/trilium/issues/4363
*/
pullLeft($toc) {
pullLeft($toc: JQuery<HTMLElement>) {
while (true) {
const $children = $toc.children();
@ -248,16 +295,21 @@ export default class TocWidget extends RightPanelWidget {
return $toc;
}
async jumpToHeading(headingIndex) {
async jumpToHeading(headingIndex: number) {
if (!this.note || !this.noteContext) {
return;
}
// A readonly note can change state to "readonly disabled
// temporarily" (ie "edit this note" button) without any
// intervening events, do the readonly calculation at navigation
// time and not at outline creation time
// See https://github.com/zadam/trilium/issues/2828
const isDocNote = this.note.type === "doc";
const isReadOnly = await this.noteContext.isReadOnly();
let $container;
if (isReadOnly) {
if (isReadOnly || isDocNote) {
$container = await this.noteContext.getContentElement();
} else {
const textEditor = await this.noteContext.getTextEditor();
@ -269,26 +321,28 @@ export default class TocWidget extends RightPanelWidget {
}
async closeTocCommand() {
if (this.noteContext?.viewScope) {
this.noteContext.viewScope.tocTemporarilyHidden = true;
}
await this.refresh();
this.triggerCommand("reEvaluateRightPaneVisibility");
appContext.triggerEvent("reEvaluateTocWidgetVisibility", { noteId: this.noteId });
}
async showTocWidgetEvent({ noteId }) {
async showTocWidgetEvent({ noteId }: EventData<"showToc">) {
if (this.noteId === noteId) {
await this.refresh();
this.triggerCommand("reEvaluateRightPaneVisibility");
}
}
async entitiesReloadedEvent({ loadResults }) {
if (loadResults.isNoteContentReloaded(this.noteId)) {
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (this.noteId && loadResults.isNoteContentReloaded(this.noteId)) {
await this.refresh();
} else if (
loadResults
.getAttributeRows()
.find((attr) => attr.type === "label" && (attr.name.toLowerCase().includes("readonly") || attr.name === "toc") && attributeService.isAffecting(attr, this.note))
.find((attr) => attr.type === "label" && ((attr.name ?? "").toLowerCase().includes("readonly") || attr.name === "toc") && attributeService.isAffecting(attr, this.note))
) {
await this.refresh();
}

View File

@ -35,6 +35,8 @@ import RibbonOptions from "./options/appearance/ribbon.js";
import LocalizationOptions from "./options/appearance/i18n.js";
import CodeBlockOptions from "./options/appearance/code_block.js";
import EditorOptions from "./options/text_notes/editor.js";
import type FNote from "../../entities/fnote.js";
import type NoteContextAwareWidget from "../note_context_aware_widget.js";
const TPL = `<div class="note-detail-content-widget note-detail-printable">
<style>
@ -55,7 +57,7 @@ const TPL = `<div class="note-detail-content-widget note-detail-printable">
<div class="note-detail-content-widget-content"></div>
</div>`;
const CONTENT_WIDGETS = {
const CONTENT_WIDGETS: Record<string, (typeof NoteContextAwareWidget)[]> = {
_optionsAppearance: [LocalizationOptions, ThemeOptions, FontsOptions, CodeBlockOptions, ElectronIntegrationOptions, MaxContentWidthOptions, RibbonOptions],
_optionsShortcuts: [KeyboardShortcutsOptions],
_optionsTextNotes: [EditorOptions, HeadingStyleOptions, TableOfContentsOptions, HighlightsListOptions, TextAutoReadOnlySizeOptions],
@ -81,6 +83,9 @@ const CONTENT_WIDGETS = {
};
export default class ContentWidgetTypeWidget extends TypeWidget {
private $content!: JQuery<HTMLElement>;
static getType() {
return "contentWidget";
}
@ -92,7 +97,7 @@ export default class ContentWidgetTypeWidget extends TypeWidget {
super.doRender();
}
async doRefresh(note) {
async doRefresh(note: FNote) {
this.$content.empty();
this.children = [];
@ -103,7 +108,9 @@ export default class ContentWidgetTypeWidget extends TypeWidget {
for (const clazz of contentWidgets) {
const widget = new clazz();
if (this.noteContext) {
await widget.handleEvent("setNoteContext", { noteContext: this.noteContext });
}
this.child(widget);
this.$content.append(widget.render());

View File

@ -1,4 +1,6 @@
import type { EventData } from "../../components/app_context.js";
import type FNote from "../../entities/fnote.js";
import { applySyntaxHighlight } from "../../services/syntax_highlight.js";
import TypeWidget from "./type_widget.js";
const TPL = `<div class="note-detail-doc note-detail-printable">
@ -13,6 +15,24 @@ const TPL = `<div class="note-detail-doc note-detail-printable">
padding: 15px;
border-radius: 5px;
}
.note-detail-doc.contextual-help {
padding-bottom: 15vh;
}
.note-detail-doc.contextual-help h2,
.note-detail-doc.contextual-help h3,
.note-detail-doc.contextual-help h4,
.note-detail-doc.contextual-help h5,
.note-detail-doc.contextual-help h6 {
font-size: 1.25rem;
background-color: var(--main-background-color);
position: sticky;
top: 0;
z-index: 50;
margin: 0;
padding-bottom: 0.25em;
}
</style>
<div class="note-detail-doc-content"></div>
@ -34,19 +54,71 @@ export default class DocTypeWidget extends TypeWidget {
}
async doRefresh(note: FNote) {
const docName = note.getLabelValue("docName");
this.initialized = this.#loadContent(note);
this.$widget.toggleClass("contextual-help", this.noteContext?.viewScope?.viewMode === "contextual-help");
}
#loadContent(note: FNote) {
return new Promise<void>((resolve) => {
let docName = note.getLabelValue("docName");
if (docName) {
// find doc based on language
const lng = i18next.language;
this.$content.load(`${window.glob.appPath}/doc_notes/${lng}/${docName}.html`, (response, status) => {
const url = this.#getUrl(docName, i18next.language);
this.$content.load(url, (response, status) => {
// fallback to english doc if no translation available
if (status === "error") {
this.$content.load(`${window.glob.appPath}/doc_notes/en/${docName}.html`);
const fallbackUrl = this.#getUrl(docName, "en");
this.$content.load(fallbackUrl, () => this.#processContent(fallbackUrl));
resolve();
return;
}
this.#processContent(url);
resolve();
});
} else {
this.$content.empty();
}
});
}
#getUrl(docNameValue: string, language: string) {
// For help notes, we only get the content to avoid loading of styles and meta.
let suffix = "";
if (docNameValue?.startsWith("User Guide")) {
suffix = " .content";
}
// Cannot have spaces in the URL due to how JQuery.load works.
docNameValue = docNameValue.replaceAll(" ", "%20");
return `${window.glob.appPath}/doc_notes/${language}/${docNameValue}.html${suffix}`;
}
#processContent(url: string) {
const dir = url.substring(0, url.lastIndexOf("/"));
// Remove top-level heading since it's already handled by the note title
this.$content.find("h1").remove();
// Images are relative to the docnote but that will not work when rendered in the application since the path breaks.
this.$content.find("img").each((i, el) => {
const $img = $(el);
$img.attr("src", dir + "/" + $img.attr("src"));
});
applySyntaxHighlight(this.$content);
}
async executeWithContentElementEvent({ resolve, ntxId }: EventData<"executeWithContentElement">) {
if (!this.isNoteContext(ntxId)) {
return;
}
await this.initialized;
resolve(this.$content);
}
}

View File

@ -6,7 +6,7 @@ import type SpacedUpdate from "../../services/spaced_update.js";
export default abstract class TypeWidget extends NoteContextAwareWidget {
protected spacedUpdate!: SpacedUpdate;
spacedUpdate!: SpacedUpdate;
// for overriding
static getType() {}
@ -45,6 +45,14 @@ export default abstract class TypeWidget extends NoteContextAwareWidget {
focus() {}
scrollToEnd() {
// Do nothing by default.
}
dataSaved() {
// Do nothing by default.
}
async readOnlyTemporarilyDisabledEvent({ noteContext }: EventData<"readOnlyTemporarilyDisabled">) {
if (this.isNoteContext(noteContext.ntxId)) {
await this.refresh();

View File

@ -1606,3 +1606,9 @@ body.electron.platform-darwin:not(.native-titlebar) .tab-row-container {
background: var(--hover-item-background-color);
color: var(--hover-item-text-color);
}
.note-split.type-geoMap .floating-buttons-children {
background: var(--main-background-color);
box-shadow: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
border-radius: 4px;
}

View File

@ -641,7 +641,8 @@
"show_hidden_subtree": "Show Hidden Subtree",
"show_help": "Show Help",
"about": "About TriliumNext Notes",
"logout": "Logout"
"logout": "Logout",
"show-cheatsheet": "Show Cheatsheet"
},
"sync_status": {
"unknown": "<p>Sync status will be known once the next sync attempt starts.</p><p>Click to trigger sync now.</p>",
@ -1644,5 +1645,8 @@
"geo-map-context": {
"open-location": "Open location",
"remove-from-map": "Remove from map"
},
"help-button": {
"title": "Open the relevant help page"
}
}

View File

@ -21,6 +21,7 @@ import type AttributeMeta from "../meta/attribute_meta.js";
import type BBranch from "../../becca/entities/bbranch.js";
import type { Response } from "express";
import { RESOURCE_DIR } from "../resource_dir.js";
import type { NoteMetaFile } from "../meta/note_meta.js";
async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "html" | "markdown", res: Response | fs.WriteStream, setHeaders = true) {
if (!["html", "markdown"].includes(format)) {
@ -485,8 +486,11 @@ ${markdownContent}`;
const existingFileNames: Record<string, number> = format === "html" ? { navigation: 0, index: 1 } : {};
const rootMeta = createNoteMeta(branch, { notePath: [] }, existingFileNames);
if (!rootMeta) {
throw new Error("Unable to create root meta.");
}
const metaFile = {
const metaFile: NoteMetaFile = {
formatVersion: 2,
appVersion: packageInfo.version,
files: [rootMeta]

View File

@ -6,6 +6,8 @@ import noteService from "./notes.js";
import log from "./log.js";
import migrationService from "./migration.js";
import { t } from "i18next";
import app_path from "./app_path.js";
import { getHelpHiddenSubtreeData } from "./in_app_help.js";
const LBTPL_ROOT = "_lbTplRoot";
const LBTPL_BASE = "_lbTplBase";
@ -16,21 +18,21 @@ const LBTPL_BUILTIN_WIDGET = "_lbTplBuiltinWidget";
const LBTPL_SPACER = "_lbTplSpacer";
const LBTPL_CUSTOM_WIDGET = "_lbTplCustomWidget";
interface Attribute {
interface HiddenSubtreeAttribute {
type: AttributeType;
name: string;
isInheritable?: boolean;
value?: string;
}
interface Item {
export interface HiddenSubtreeItem {
notePosition?: number;
id: string;
title: string;
type: NoteType;
icon?: string;
attributes?: Attribute[];
children?: Item[];
attributes?: HiddenSubtreeAttribute[];
children?: HiddenSubtreeItem[];
isExpanded?: boolean;
baseSize?: string;
growthFactor?: string;
@ -54,9 +56,9 @@ enum Command {
* duplicate subtrees. This way, all instances will generate the same structure with the same IDs.
*/
let hiddenSubtreeDefinition: Item;
let hiddenSubtreeDefinition: HiddenSubtreeItem;
function buildHiddenSubtreeDefinition(): Item {
function buildHiddenSubtreeDefinition(): HiddenSubtreeItem {
return {
id: "_hidden",
title: t("hidden-subtree.root-title"),
@ -345,6 +347,14 @@ function buildHiddenSubtreeDefinition(): Item {
{ id: "_optionsOther", title: t("hidden-subtree.other"), type: "contentWidget", icon: "bx-dots-horizontal" },
{ id: "_optionsAdvanced", title: t("hidden-subtree.advanced-title"), type: "contentWidget" }
]
},
{
id: "_help",
title: t("hidden-subtree.user-guide"),
type: "book",
icon: "bx-help-circle",
children: getHelpHiddenSubtreeData(),
isExpanded: true
}
]
};
@ -368,7 +378,7 @@ function checkHiddenSubtree(force = false, extraOpts: CheckHiddenExtraOpts = {})
checkHiddenSubtreeRecursively("root", hiddenSubtreeDefinition, extraOpts);
}
function checkHiddenSubtreeRecursively(parentNoteId: string, item: Item, extraOpts: CheckHiddenExtraOpts = {}) {
function checkHiddenSubtreeRecursively(parentNoteId: string, item: HiddenSubtreeItem, extraOpts: CheckHiddenExtraOpts = {}) {
if (!item.id || !item.type || !item.title) {
throw new Error(`Item does not contain mandatory properties: ${JSON.stringify(item)}`);
}
@ -449,7 +459,9 @@ function checkHiddenSubtreeRecursively(parentNoteId: string, item: Item, extraOp
for (const attr of attrs) {
const attrId = note.noteId + "_" + attr.type.charAt(0) + attr.name;
if (!note.getAttributes().find((attr) => attr.attributeId === attrId)) {
const existingAttribute = note.getAttributes().find((attr) => attr.attributeId === attrId);
if (!existingAttribute) {
new BAttribute({
attributeId: attrId,
noteId: note.noteId,
@ -458,6 +470,10 @@ function checkHiddenSubtreeRecursively(parentNoteId: string, item: Item, extraOp
value: attr.value,
isInheritable: false
}).save();
} else if (attr.name === "docName") {
// Updating docname
existingAttribute.value = attr.value ?? "";
existingAttribute.save();
}
}

View File

@ -0,0 +1,87 @@
import path from "path";
import fs from "fs";
import type { HiddenSubtreeItem } from "./hidden_subtree.js";
import type NoteMeta from "./meta/note_meta.js";
import type { NoteMetaFile } from "./meta/note_meta.js";
import { fileURLToPath } from "url";
import { isDev } from "./utils.js";
export function getHelpHiddenSubtreeData() {
const srcRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
const appDir = path.join(srcRoot, "public", isDev ? "app" : "app-dist");
const helpDir = path.join(appDir, "doc_notes", "en", "User Guide");
const metaFilePath = path.join(helpDir, "!!!meta.json");
const metaFileContent = JSON.parse(fs.readFileSync(metaFilePath).toString("utf-8"));
try {
return parseNoteMetaFile(metaFileContent as NoteMetaFile);
} catch (e) {
console.warn(e);
return [];
}
}
function parseNoteMetaFile(noteMetaFile: NoteMetaFile): HiddenSubtreeItem[] {
if (!noteMetaFile.files) {
return [];
}
const metaRoot = noteMetaFile.files[0];
const parsedMetaRoot = parseNoteMeta(metaRoot, "/" + (metaRoot.dirFileName ?? ""));
console.log(JSON.stringify(parsedMetaRoot, null, 4));
return parsedMetaRoot.children ?? [];
}
function parseNoteMeta(noteMeta: NoteMeta, docNameRoot: string): HiddenSubtreeItem {
let iconClass: string = "bx bx-file";
const item: HiddenSubtreeItem = {
id: `_help_${noteMeta.noteId}`,
title: noteMeta.title ?? "",
type: "doc", // can change
attributes: []
};
// Handle attributes
for (const attribute of noteMeta.attributes ?? []) {
if (attribute.name === "iconClass") {
iconClass = attribute.value;
}
}
// Handle folder notes
if (!noteMeta.dataFileName) {
iconClass = "bx bx-folder";
item.type = "book";
}
// Handle text notes
if (noteMeta.type === "text" && noteMeta.dataFileName) {
const docPath = `${docNameRoot}/${path.basename(noteMeta.dataFileName, ".html")}`
.substring(1);
item.attributes?.push({
type: "label",
name: "docName",
value: docPath
});
}
// Handle children
if (noteMeta.children) {
const children: HiddenSubtreeItem[] = [];
for (const childMeta of noteMeta.children) {
let newDocNameRoot = (noteMeta.dirFileName ? `${docNameRoot}/${noteMeta.dirFileName}` : docNameRoot);
children.push(parseNoteMeta(childMeta, newDocNameRoot));
}
item.children = children;
}
// Handle note icon
item.attributes?.push({
name: "iconClass",
value: iconClass,
type: "label"
});
return item;
}

View File

@ -344,6 +344,12 @@ function getDefaultKeyboardActions() {
description: t("keyboard_actions.show-help"),
scope: "window"
},
{
actionName: "showCheatsheet",
defaultShortcuts: ["Shift+F1"],
description: t("keyboard_actions.show-cheatsheet"),
scope: "window"
},
{
separator: t("keyboard_actions.text-note-operations")

View File

@ -51,6 +51,7 @@ const enum KeyboardActionNamesEnum {
showRecentChanges,
showSQLConsole,
showBackendLog,
showCheatsheet,
showHelp,
addLinkToText,
followLinkUnderCursor,

View File

@ -1,6 +1,12 @@
import type AttachmentMeta from "./attachment_meta.js";
import type AttributeMeta from "./attribute_meta.js";
export interface NoteMetaFile {
formatVersion: number;
appVersion: string;
files: NoteMeta[];
}
export default interface NoteMeta {
noteId?: string;
notePath?: string[];

View File

@ -146,7 +146,10 @@ function getAndValidateParent(params: GetValidateParams) {
}
if (!params.ignoreForbiddenParents) {
if (["_lbRoot", "_hidden"].includes(parentNote.noteId) || parentNote.noteId.startsWith("_lbTpl") || parentNote.isOptions()) {
if (["_lbRoot", "_hidden"].includes(parentNote.noteId)
|| parentNote.noteId.startsWith("_lbTpl")
|| parentNote.noteId.startsWith("_help")
|| parentNote.isOptions()) {
throw new ValidationError(`Creating child notes into '${parentNote.noteId}' is not allowed.`);
}
}

View File

@ -88,10 +88,11 @@
"reset-zoom-level": "Reset zoom level",
"copy-without-formatting": "Copy selected text without formatting",
"force-save-revision": "Force creating / saving new note revision of the active note",
"show-help": "Shows built-in Help / cheatsheet",
"show-help": "Shows the built-in User Guide",
"toggle-book-properties": "Toggle Book Properties",
"toggle-classic-editor-toolbar": "Toggle the Formatting tab for the editor with fixed toolbar",
"export-as-pdf": "Exports the current note as a PDF"
"export-as-pdf": "Exports the current note as a PDF",
"show-cheatsheet": "Shows a modal with common keyboard operations"
},
"login": {
"title": "Login",
@ -241,7 +242,8 @@
"sync-title": "Sync",
"other": "Other",
"advanced-title": "Advanced",
"visible-launchers-title": "Visible Launchers"
"visible-launchers-title": "Visible Launchers",
"user-guide": "User Guide"
},
"notes": {
"new-note": "New note",