Merge pull request #1319 from TriliumNext/feature/rtl

Right-to-left support
This commit is contained in:
Elian Doran 2025-03-06 18:12:44 +02:00 committed by GitHub
commit ddd0c3a878
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
93 changed files with 988 additions and 730 deletions

View File

@ -369,7 +369,8 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
const { note, viewScope } = this;
let title = viewScope?.viewMode === "default" ? note.title : `${note.title}: ${viewScope?.viewMode}`;
const isNormalView = (viewScope?.viewMode === "default" || viewScope?.viewMode === "contextual-help");
let title = (isNormalView ? note.title : `${note.title}: ${viewScope?.viewMode}`);
if (viewScope?.attachmentId) {
// assuming the attachment has been already loaded

View File

@ -1,6 +1,6 @@
{
"formatVersion": 2,
"appVersion": "0.92.0-beta",
"appVersion": "0.92.2-beta",
"files": [
{
"isClone": false,
@ -34,7 +34,7 @@
"OkOZllzB3fqN",
"yoAe4jV2yzbd"
],
"title": "Features",
"title": "New Features",
"notePosition": 40,
"prefix": null,
"isExpanded": false,
@ -47,53 +47,91 @@
"value": "bx bx-star",
"isInheritable": false,
"position": 10
},
{
"type": "label",
"name": "sorted",
"value": "dateCreated",
"isInheritable": false,
"position": 20
},
{
"type": "label",
"name": "sortDirection",
"value": "desc",
"isInheritable": false,
"position": 30
}
],
"format": "html",
"attachments": [],
"dirFileName": "Features",
"dirFileName": "New Features",
"children": [
{
"isClone": false,
"noteId": "13D1lOc9sqmZ",
"noteId": "3I277VKYxWDH",
"notePath": [
"OkOZllzB3fqN",
"yoAe4jV2yzbd",
"13D1lOc9sqmZ"
"3I277VKYxWDH"
],
"title": "Export as PDF",
"notePosition": 20,
"title": "Right-to-left text notes",
"notePosition": 10,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [],
"attributes": [
{
"type": "label",
"name": "iconClass",
"value": "bx bx-align-right",
"isInheritable": false,
"position": 10
}
],
"format": "html",
"dataFileName": "Export as PDF.html",
"dataFileName": "Right-to-left text notes.html",
"attachments": [
{
"attachmentId": "xsGM34t8ssKV",
"attachmentId": "PSBNAvDyj5Vy",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Export as PDF_image.png"
"dataFileName": "Right-to-left text notes_i.png"
},
{
"attachmentId": "cvyes4f1Vhmm",
"attachmentId": "YXYIJznak915",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "1_Export as PDF_image.png"
"dataFileName": "1_Right-to-left text notes_i.png"
},
{
"attachmentId": "b3v1pLE6TF1Y",
"attachmentId": "Do0S17lDl7uu",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "2_Export as PDF_image.png"
"dataFileName": "2_Right-to-left text notes_i.png"
},
{
"attachmentId": "D3lyhPvPvocb",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "3_Right-to-left text notes_i.png"
},
{
"attachmentId": "Tu7llk3GgRkA",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "4_Right-to-left text notes_i.png"
}
]
},
@ -106,12 +144,20 @@
"B3YLYM4erjnW"
],
"title": "Zen mode",
"notePosition": 30,
"notePosition": 20,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [],
"attributes": [
{
"type": "label",
"name": "iconClass",
"value": "bx bxs-yin-yang",
"isInheritable": false,
"position": 10
}
],
"format": "html",
"dataFileName": "Zen mode.html",
"attachments": [
@ -180,6 +226,50 @@
"dataFileName": "7_Zen mode_image.png"
}
]
},
{
"isClone": false,
"noteId": "13D1lOc9sqmZ",
"notePath": [
"OkOZllzB3fqN",
"yoAe4jV2yzbd",
"13D1lOc9sqmZ"
],
"title": "Export as PDF",
"notePosition": 30,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "label",
"name": "iconClass",
"value": "bx bxs-file-pdf",
"isInheritable": false,
"position": 30
}
],
"format": "html",
"dataFileName": "Export as PDF.html",
"attachments": [
{
"attachmentId": "xsGM34t8ssKV",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Export as PDF_image.png"
},
{
"attachmentId": "b3v1pLE6TF1Y",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "1_Export as PDF_image.png"
}
]
}
]
},
@ -233,8 +323,47 @@
}
],
"format": "html",
"dataFileName": "Text.html",
"attachments": []
"attachments": [],
"dirFileName": "Text",
"children": [
{
"isClone": false,
"noteId": "B0lcI9xz1r8K",
"notePath": [
"OkOZllzB3fqN",
"wmegHv51MJMd",
"crJtzsol4olb",
"B0lcI9xz1r8K"
],
"title": "Content language",
"notePosition": 10,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "relation",
"name": "internalLink",
"value": "3I277VKYxWDH",
"isInheritable": false,
"position": 10
}
],
"format": "html",
"dataFileName": "Content language.html",
"attachments": [
{
"attachmentId": "OpIv6CnYCLVa",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Content language_image.png"
}
]
}
]
},
{
"isClone": false,
@ -382,7 +511,7 @@
"title": "Book",
"notePosition": 70,
"prefix": null,
"isExpanded": true,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
@ -576,6 +705,14 @@
"mime": "image/png",
"position": 10,
"dataFileName": "18_Calendar View_image.png"
},
{
"attachmentId": "JM6AU8N4MIgB",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "19_Calendar View_image.png"
}
]
}
@ -697,7 +834,7 @@
"wmegHv51MJMd",
"foPEtsL51pD2"
],
"title": "Geo Map",
"title": "Geo map",
"notePosition": 120,
"prefix": null,
"isExpanded": false,
@ -713,23 +850,15 @@
}
],
"format": "html",
"dataFileName": "Geo Map.html",
"dataFileName": "Geo map.html",
"attachments": [
{
"attachmentId": "J0baLTpafs7C",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Geo Map_image.png"
},
{
"attachmentId": "kcYjOvJDFkbS",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "1_Geo Map_image.png"
"dataFileName": "Geo map_image.png"
},
{
"attachmentId": "FDP3JzIVSnuJ",
@ -737,7 +866,7 @@
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "2_Geo Map_image.png"
"dataFileName": "1_Geo map_image.png"
},
{
"attachmentId": "eUrcqc8RRuZG",
@ -745,7 +874,7 @@
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "3_Geo Map_image.png"
"dataFileName": "2_Geo map_image.png"
},
{
"attachmentId": "1quk4yxJpeHZ",
@ -753,7 +882,7 @@
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "4_Geo Map_image.png"
"dataFileName": "3_Geo map_image.png"
},
{
"attachmentId": "iSpyhQ5Ya6Nk",
@ -761,7 +890,7 @@
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "5_Geo Map_image.png"
"dataFileName": "4_Geo map_image.png"
},
{
"attachmentId": "ut6vm2aXVfXI",
@ -769,7 +898,7 @@
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "6_Geo Map_image.png"
"dataFileName": "5_Geo map_image.png"
},
{
"attachmentId": "uYdb9wWf5Nuv",
@ -777,15 +906,7 @@
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "7_Geo Map_image.png"
},
{
"attachmentId": "GhHYO2LteDmZ",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "8_Geo Map_image.png"
"dataFileName": "6_Geo map_image.png"
},
{
"attachmentId": "viN50n5G4kB0",
@ -793,7 +914,7 @@
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "9_Geo Map_image.png"
"dataFileName": "7_Geo map_image.png"
},
{
"attachmentId": "mgwGrtQZjxxb",
@ -801,7 +922,7 @@
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "10_Geo Map_image.png"
"dataFileName": "8_Geo map_image.png"
},
{
"attachmentId": "PMqmCbNLlZOG",
@ -809,7 +930,7 @@
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "11_Geo Map_image.png"
"dataFileName": "9_Geo map_image.png"
},
{
"attachmentId": "0AwaQMqt3FVA",
@ -817,7 +938,7 @@
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "12_Geo Map_image.png"
"dataFileName": "10_Geo map_image.png"
},
{
"attachmentId": "gR2c2Thmfy3I",
@ -825,7 +946,7 @@
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "13_Geo Map_image.png"
"dataFileName": "11_Geo map_image.png"
},
{
"attachmentId": "JULizn130rVI",
@ -833,7 +954,7 @@
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "14_Geo Map_image.png"
"dataFileName": "12_Geo map_image.png"
},
{
"attachmentId": "MdC0DpifJwu4",
@ -841,7 +962,7 @@
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "15_Geo Map_image.png"
"dataFileName": "13_Geo map_image.png"
},
{
"attachmentId": "gFR2Izzp18LQ",
@ -849,7 +970,7 @@
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "16_Geo Map_image.png"
"dataFileName": "14_Geo map_image.png"
},
{
"attachmentId": "42AncDs7SSAf",
@ -857,15 +978,7 @@
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "17_Geo Map_image.png"
},
{
"attachmentId": "pKdtiq4r0eFY",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "18_Geo Map_image.png"
"dataFileName": "15_Geo map_image.png"
},
{
"attachmentId": "FXRVvYpOxWyR",
@ -873,7 +986,23 @@
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "19_Geo Map_image.png"
"dataFileName": "16_Geo map_image.png"
},
{
"attachmentId": "qudP7UCtwIq3",
"title": "image.png",
"role": "image",
"mime": "image/jpg",
"position": 10,
"dataFileName": "17_Geo map_image.png"
},
{
"attachmentId": "utecGxWk08QY",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "18_Geo map_image.png"
}
]
}
@ -943,173 +1072,6 @@
}
]
},
{
"isClone": false,
"noteId": "DtJJ20yEozPA",
"notePath": [
"OkOZllzB3fqN",
"DtJJ20yEozPA"
],
"title": "Theme development",
"notePosition": 130,
"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",
"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": "AJHVfQtIQgJ7",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Creating a custom theme_im.png"
},
{
"attachmentId": "gXLyv5KXjfxg",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "1_Creating a custom theme_im.png"
},
{
"attachmentId": "on1gD7BzCWdN",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "2_Creating a custom theme_im.png"
},
{
"attachmentId": "17p6z24yW5eP",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "3_Creating a custom theme_im.png"
},
{
"attachmentId": "K3cdwj8f90m0",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "4_Creating a custom theme_im.png"
},
{
"attachmentId": "bn93hwF7C8sR",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "5_Creating a custom theme_im.png"
}
]
},
{
"isClone": false,
"noteId": "aH8Dk5aMiq7R",
"notePath": [
"OkOZllzB3fqN",
"DtJJ20yEozPA",
"aH8Dk5aMiq7R"
],
"title": "Customize the Next theme",
"notePosition": 20,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [],
"format": "html",
"dataFileName": "Customize the Next theme.html",
"attachments": [
{
"attachmentId": "5z4bC0x0eH0P",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Customize the Next theme_i.png"
},
{
"attachmentId": "u0zkXkD7rGXA",
"title": "image.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "1_Customize the Next theme_i.png"
}
]
},
{
"isClone": false,
"noteId": "pMq6N1oBV9oo",
"notePath": [
"OkOZllzB3fqN",
"DtJJ20yEozPA",
"pMq6N1oBV9oo"
],
"title": "Reference",
"notePosition": 30,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "relation",
"name": "internalLink",
"value": "po38jIc0LD2H",
"isInheritable": false,
"position": 10
}
],
"format": "html",
"dataFileName": "Reference.html",
"attachments": []
}
]
},
{
"isClone": false,
"noteId": "LTnkDnYmmZ7s",
@ -1283,7 +1245,7 @@
"title": "ETAPI",
"notePosition": 10,
"prefix": null,
"isExpanded": true,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [],
@ -1333,7 +1295,7 @@
"title": "Internal API",
"notePosition": 20,
"prefix": null,
"isExpanded": true,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [],

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@ -23,7 +23,7 @@
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="2_Export as PDF_image.png" width="29"
<img src="1_Export 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 using
@ -33,7 +33,7 @@
<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="2_Export as PDF_image.png" width="29" height="31">button, select Export note → This note and all of its descendants → HTML
<img src="1_Export 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

View File

@ -0,0 +1,56 @@
<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>Right-to-left text notes</title>
</head>
<body>
<div class="content">
<h1 data-trilium-h1>Right-to-left text notes</h1>
<div class="ck-content">
<p>Trilium now has basic support for right-to-left text, at note level.</p>
<figure
class="table">
<table>
<tbody>
<tr>
<td>
<figure class="image">
<img style="aspect-ratio:906/557;" src="3_Right-to-left text notes_i.png"
width="906" height="557">
</figure>
</td>
<td>
<figure class="image">
<img style="aspect-ratio:906/557;" src="2_Right-to-left text notes_i.png"
width="906" height="557">
</figure>
</td>
</tr>
</tbody>
</table>
</figure>
<p>Note that only the Text note type supports this.</p>
<p>The list of languages is configurable via the a new dedicated settings
page:</p>
<figure class="image">
<img style="aspect-ratio:1248/635;" src="4_Right-to-left text notes_i.png"
width="1248" height="635">
</figure>
<p>To select the corresponding language of the text, go to “Basic Properties”
and select your desired language.</p>
<p>
<img src="1_Right-to-left text notes_i.png" width="635" height="492">
</p>
<p>Feel free to report any issues regarding right to left support.</p>
<p>&nbsp;</p>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -118,6 +118,12 @@
<td>When present (regardless of value), it will show the number of the week
on the calendar.</td>
</tr>
<tr>
<td><code>~child:template</code>
</td>
<td>Defines the template for newly created notes in the calendar (via dragging
or clicking).</td>
</tr>
</tbody>
</table>
</figure>
@ -175,6 +181,36 @@
than the title, either a label (e.g. <code>#assignee</code>) or a relation
(e.g. <code>~for</code>). See <i>Advanced use-cases</i> for more information.</td>
</tr>
<tr>
<td><code>#calendar:promotedAttributes</code>
</td>
<td>
<p>Allows displaying the value of one or more promoted attributes in the
calendar like this:
<img src="19_Calendar View_image.png" width="131" height="113">
</p><pre><code class="language-text-x-trilium-auto">#label:weight="promoted,number,single,precision=1"
#label:mood="promoted,alias=Mood,single,text"
#calendar:promotedAttributes="label:weight,label:mood" </code></pre>
<p>It can also be used with relations, case in which it will display the
title of the target note:</p><pre><code class="language-text-x-trilium-auto">#relation:assignee="promoted,alias=Assignee,single,text"
#calendar:promotedAttributes="relation:assignee"
~assignee=@My assignee</code></pre>
</td>
</tr>
<tr>
<td><code>#calendar:startDate</code>
</td>
<td>Allows using a different label to represent the start date, other than <code>#startDate</code> (e.g. <code>#expiryDate</code>).
The label name must be prefixed with <code>#</code>. If the label is not
defined for a note, the default will be used instead.</td>
</tr>
<tr>
<td><code>#calendar:endDate</code>
</td>
<td>Allows using a different label to represent the start date, other than <code>#endDate</code>.
The label name must be prefixed with <code>#</code>. If the label is not
defined for a note, the default will be used instead.</td>
</tr>
</tbody>
</table>
</figure>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -5,12 +5,12 @@
<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>
<title data-trilium-title>Geo map</title>
</head>
<body>
<div class="content">
<h1 data-trilium-h1>Geo Map</h1>
<h1 data-trilium-h1>Geo map</h1>
<div class="ck-content">
<h2>Creating a new geo map</h2>
@ -26,7 +26,7 @@
<th>1</th>
<td>
<figure class="image image_resized" style="width:100%;">
<img style="aspect-ratio:1256/1044;" src="9_Geo Map_image.png" width="1256"
<img style="aspect-ratio:1256/1044;" src="7_Geo map_image.png" width="1256"
height="1044">
</figure>
</td>
@ -36,7 +36,7 @@
<th>2</th>
<td>
<figure class="image image_resized" style="width:100%;">
<img style="aspect-ratio:1720/1396;" src="3_Geo Map_image.png" width="1720"
<img style="aspect-ratio:1720/1396;" src="2_Geo map_image.png" width="1720"
height="1396">
</figure>
</td>
@ -69,18 +69,18 @@
<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="4_Geo Map_image.png" width="72" height="66">button on the top-right of the map.</p>
src="3_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="10_Geo Map_image.png" width="72" height="66">) in the top-right of the map.</p>
src="8_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="14_Geo Map_image.png" width="1730"
<img style="aspect-ratio:1730/416;" src="12_Geo map_image.png" width="1730"
height="416">
</figure>
<p>&nbsp;</p>
@ -96,7 +96,7 @@
<th>3</th>
<td>
<figure class="image">
<img style="aspect-ratio:1586/404;" src="1_Geo Map_image.png" width="1586"
<img style="aspect-ratio:1586/404;" src="Geo map_image.png" width="1586"
height="404">
</figure>
<p>&nbsp;</p>
@ -107,7 +107,7 @@
<th>4</th>
<td>
<figure class="image">
<img style="aspect-ratio:1696/608;" src="6_Geo Map_image.png" width="1696"
<img style="aspect-ratio:1696/608;" src="5_Geo map_image.png" width="1696"
height="608">
</figure>
<p>&nbsp;</p>
@ -122,7 +122,7 @@
<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="12_Geo Map_image.png" width="1288"
<img style="aspect-ratio:1288/278;" src="10_Geo map_image.png" width="1288"
height="278">
</figure>
<p>This value can be added manually if needed. The value of the attribute
@ -155,6 +155,13 @@
</ul>
</li>
</ul>
<h2>Icon and color of the markers</h2>
<p>
<img src="18_Geo map_image.png" alt="image" width="523" height="295">
</p>
<p>The markers will have the same icon as the note.</p>
<p>It's possible to add a custom color to a marker by assigning them a <code>#color</code> attribute
such as <code>#color=green</code>.</p>
<h2>Adding the coordinates manually</h2>
<p>In a nutshell, create a child note and set the <code>#geolocation</code> attribute
to the coordinates.</p>
@ -168,7 +175,7 @@
<th>1</th>
<td>
<figure class="image image-style-align-center image_resized" style="width:100%;">
<img style="aspect-ratio:732/918;" src="16_Geo Map_image.png" width="732"
<img style="aspect-ratio:732/918;" src="14_Geo map_image.png" width="732"
height="918">
</figure>
</td>
@ -185,7 +192,7 @@
<th>2</th>
<td>
<figure class="image image_resized" style="width:100%;">
<img style="aspect-ratio:518/84;" src="19_Geo Map_image.png" width="518"
<img style="aspect-ratio:518/84;" src="16_Geo map_image.png" width="518"
height="84">
</figure>
</td>
@ -199,7 +206,7 @@
<th>3</th>
<td>
<figure class="image image_resized" style="width:100%;">
<img style="aspect-ratio:1074/276;" src="11_Geo Map_image.png" width="1074"
<img style="aspect-ratio:1074/276;" src="9_Geo map_image.png" width="1074"
height="276">
</figure>
</td>
@ -225,7 +232,7 @@
<th>1</th>
<td>
<figure class="image image_resized" style="width:100%;">
<img style="aspect-ratio:562/454;" src="17_Geo Map_image.png" width="562"
<img style="aspect-ratio:562/454;" src="15_Geo map_image.png" width="562"
height="454">
</figure>
</td>
@ -236,7 +243,7 @@
<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"
<img style="aspect-ratio:696/480;" src="11_Geo map_image.png" width="696"
height="480">
</figure>
</td>
@ -250,7 +257,7 @@
<th>3</th>
<td>
<figure class="image">
<img style="aspect-ratio:640/276;" src="2_Geo Map_image.png" width="640"
<img style="aspect-ratio:640/276;" src="1_Geo map_image.png" width="640"
height="276">
</figure>
</td>
@ -275,7 +282,7 @@
<th>1</th>
<td>
<figure class="image">
<img style="aspect-ratio:226/74;" src="7_Geo Map_image.png" width="226"
<img style="aspect-ratio:226/74;" src="6_Geo map_image.png" width="226"
height="74">
</figure>
</td>
@ -286,7 +293,7 @@
<th>2</th>
<td>
<figure class="image">
<img style="aspect-ratio:322/222;" src="5_Geo Map_image.png" width="322"
<img style="aspect-ratio:322/222;" src="4_Geo map_image.png" width="322"
height="222">
</figure>
</td>
@ -297,7 +304,7 @@
<th>3</th>
<td>
<figure class="image image_resized" style="width:100%;">
<img style="aspect-ratio:620/530;" src="15_Geo Map_image.png" width="620"
<img style="aspect-ratio:620/530;" src="13_Geo map_image.png" width="620"
height="530">
</figure>
</td>
@ -310,9 +317,16 @@
</tbody>
</table>
</figure>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<h2>Troubleshooting</h2>
<h3>Grid-like artifacts on the map</h3>
<p>
<img class="image_resized" style="aspect-ratio:678/499;width:58%;" src="17_Geo map_image.png"
width="678" height="499">
</p>
<p>This occurs if the application is not at 100% zoom which causes the pixels
of the map to not render correctly due to fractional scaling. The only
possible solution i to set the UI zoom at 100% (default keyboard shortcut
is Ctrl+0).</p>
<p>&nbsp;</p>
</div>
</div>

View File

@ -1,19 +0,0 @@
<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>Text</title>
</head>
<body>
<div class="content">
<h1 data-trilium-h1>Text</h1>
<div class="ck-content"></div>
</div>
</body>
</html>

View File

@ -0,0 +1,34 @@
<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>Content language</title>
</head>
<body>
<div class="content">
<h1 data-trilium-h1>Content language</h1>
<div class="ck-content">
<p>A language hint can be provided for text notes. This option informs the
browser or the desktop application about the language the note is written
in (for example this might help with spellchecking), and it also determines
whether the text is displayed from right-to-left for languages such as
Arabic, Hebrew, etc.</p>
<p>For more information about right-to-left support, see&nbsp;<a class="reference-link"
href="../../New%20Features/Right-to-left%20text%20notes.html">Right-to-left text notes</a>.</p>
<p>To set the language of the content, go to “Basic Properties” and look
for the “Language” field. By default there will be no content languages
set, they can be configured by going to settings or by selecting the “Configure
languages” item in the list.</p>
<p>
<img src="Content language_image.png" width="635" height="492">
</p>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View File

@ -1,94 +0,0 @@
<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="5_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="3_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="1_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="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="2_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="4_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="Customize%20the%20Next%20theme.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>

View File

@ -1,36 +0,0 @@
<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>Customize the Next theme</title>
</head>
<body>
<div class="content">
<h1 data-trilium-h1>Customize the Next theme</h1>
<div class="ck-content">
<p>By default, any custom theme will be based on the legacy light theme.
To use 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="Customize the Next theme_i.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>

View File

@ -1,180 +0,0 @@
<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 mobile vs. desktop</h2>
<p>The mobile layout is different than the one on the desktop. Use <code>body.mobile</code> and <code>body.desktop</code> to
differentiate between them.</p><pre><code class="language-text-css">body.mobile #root-widget {
/* Do something on mobile */
}
body.desktop #root-widget {
/* Do something on desktop */
}</code></pre>
<p>Do note that there is also a “tablet mode” in the mobile layout. For that
particular case media queries are required:</p><pre><code class="language-text-css">@media (max-width: 991px) {
#launcher-pane {
/* Do something on mobile layout */
}
}
@media (min-width: 992px) {
#launcher-pane {
/* Do something on mobile tablet + desktop layout */
}
}</code></pre>
<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-css">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-css">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-css">: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-css">.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>
<h2>Custom fonts</h2>
<p>Currently the only way to include a custom font is to use&nbsp;<a class="reference-link"
href="../Advanced%20topics/Custom%20resource%20providers.html">Custom resource providers</a>.
Basically import a font into Trilium and assign it <code>#customResourceProvider=fonts/myfont.ttf</code> and
then import the font in CSS via <code>/custom/fonts/myfont.ttf</code>.</p>
<h2>Dark and light themes</h2>
<p>A light theme needs to have the following CSS:</p><pre><code class="language-text-css">:root {
--theme-style: light;
}</code></pre>
<p>if the theme is dark, then <code>--theme-style</code> needs to be <code>dark</code>.</p>
<p>If the theme is auto (e.g. supports both light or dark based on <code>prefers-color-scheme</code>)
it must also declare (in addition to setting <code>--theme-style</code> to
either <code>light</code> or <code>dark</code>):</p><pre><code class="language-text-css">:root {
--theme-style-auto: true;
}</code></pre>
<p>This will affect the behavior of the Electron application by informing
the operating system of the color preference (e.g. background effects will
appear correct on Windows).</p>
</div>
</div>
</body>
</html>

View File

@ -6,6 +6,6 @@
</head>
<frameset cols="25%,75%">
<frame name="navigation" src="navigation.html">
<frame name="detail" src="User%20Guide/Features/Export%20as%20PDF.html">
<frame name="detail" src="User%20Guide/New%20Features/Right-to-left%20text%20notes.html">
</frameset>
</html>

View File

@ -9,17 +9,24 @@
<ul>
<li>User Guide
<ul>
<li>Features
<li>New Features
<ul>
<li><a href="User%20Guide/Features/Export%20as%20PDF.html" target="detail">Export as PDF</a>
<li><a href="User%20Guide/New%20Features/Right-to-left%20text%20notes.html"
target="detail">Right-to-left text notes</a>
</li>
<li><a href="User%20Guide/Features/Zen%20mode.html" target="detail">Zen mode</a>
<li><a href="User%20Guide/New%20Features/Zen%20mode.html" target="detail">Zen mode</a>
</li>
<li><a href="User%20Guide/New%20Features/Export%20as%20PDF.html" target="detail">Export as PDF</a>
</li>
</ul>
</li>
<li>Note Types
<ul>
<li><a href="User%20Guide/Note%20Types/Text.html" target="detail">Text</a>
<li>Text
<ul>
<li><a href="User%20Guide/Note%20Types/Text/Content%20language.html" target="detail">Content language</a>
</li>
</ul>
</li>
<li><a href="User%20Guide/Note%20Types/Code.html" target="detail">Code</a>
</li>
@ -45,7 +52,7 @@
</li>
<li><a href="User%20Guide/Note%20Types/Mind%20Map.html" target="detail">Mind Map</a>
</li>
<li><a href="User%20Guide/Note%20Types/Geo%20Map.html" target="detail">Geo Map</a>
<li><a href="User%20Guide/Note%20Types/Geo%20map.html" target="detail">Geo map</a>
</li>
</ul>
</li>
@ -56,18 +63,6 @@
</li>
</ul>
</li>
<li>Theme development
<ul>
<li><a href="User%20Guide/Theme%20development/Creating%20a%20custom%20theme.html"
target="detail">Creating a custom theme</a>
</li>
<li><a href="User%20Guide/Theme%20development/Customize%20the%20Next%20theme.html"
target="detail">Customize the Next theme</a>
</li>
<li><a href="User%20Guide/Theme%20development/Reference.html" target="detail">Reference</a>
</li>
</ul>
</li>
<li>Scripting
<ul>
<li>Examples

View File

@ -23,6 +23,28 @@ async function removeAttributeById(noteId: string, attributeId: string) {
await server.remove(`notes/${noteId}/attributes/${attributeId}`);
}
/**
* Sets the attribute of the given note to the provided value if its truthy, or removes the attribute if the value is falsy.
* For an attribute with an empty value, pass an empty string instead.
*
* @param note the note to set the attribute to.
* @param type the type of attribute (label or relation).
* @param name the name of the attribute to set.
* @param value the value of the attribute to set.
*/
async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) {
if (value) {
// Create or update the attribute.
await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value });
} else {
// Remove the attribute if it exists on the server but we don't define a value for it.
const attributeId = note.getAttribute(type, name)?.attributeId;
if (attributeId) {
await server.remove(`notes/${note.noteId}/attributes/${attributeId}`);
}
}
}
/**
* @returns - returns true if this attribute has the potential to influence the note in the argument.
* That can happen in multiple ways:
@ -66,6 +88,7 @@ function isAffecting(attrRow: AttributeRow, affectedNote: FNote | null | undefin
export default {
addLabel,
setLabel,
setAttribute,
removeAttributeById,
isAffecting
};

View File

@ -1,10 +1,16 @@
import options from "./options.js";
import i18next from "i18next";
import i18nextHttpBackend from "i18next-http-backend";
import server from "./server.js";
import type { Locale } from "../../../services/i18n.js";
let locales: Locale[] | null;
export async function initLocale() {
const locale = (options.get("locale") as string) || "en";
locales = await server.get<Locale[]>("options/locales");
await i18next.use(i18nextHttpBackend).init({
lng: locale,
fallbackLng: "en",
@ -15,5 +21,24 @@ export async function initLocale() {
});
}
export function getAvailableLocales() {
if (!locales) {
throw new Error("Tried to load list of locales, but localization is not yet initialized.")
}
return locales;
}
/**
* Finds the given locale by ID.
*
* @param localeId the locale ID to search for.
* @returns the corresponding {@link Locale} or `null` if it was not found.
*/
export function getLocaleById(localeId: string | null | undefined) {
if (!localeId) return null;
return locales?.find((l) => l.id === localeId) ?? null;
}
export const t = i18next.t;
export const getCurrentLanguage = () => i18next.language;

View File

@ -1,5 +1,6 @@
import dayjs from "dayjs";
import { Modal } from "bootstrap";
import type { ViewScope } from "./link.js";
function reloadFrontendApp(reason?: string) {
if (reason) {
@ -388,6 +389,10 @@ function initHelpDropdown($el: JQuery<HTMLElement>) {
const wikiBaseUrl = "https://triliumnext.github.io/Docs/Wiki/";
function openHelp($button: JQuery<HTMLElement>) {
if ($button.length === 0) {
return;
}
const helpPage = $button.attr("data-help-page");
if (helpPage) {
@ -397,12 +402,44 @@ function openHelp($button: JQuery<HTMLElement>) {
}
}
async function openInAppHelp($button: JQuery<HTMLElement>) {
if ($button.length === 0) {
return;
}
const inAppHelpPage = $button.attr("data-in-app-help");
if (inAppHelpPage) {
// Dynamic import to avoid import issues in tests.
const appContext = (await import("../components/app_context.js")).default;
const subContexts = appContext.tabManager.getActiveContext().getSubContexts();
const targetNote = `_help_${inAppHelpPage}`;
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];
appContext.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 });
}
return;
}
}
function initHelpButtons($el: JQuery<HTMLElement> | JQuery<Window>) {
// for some reason, the .on(event, listener, handler) does not work here (e.g. Options -> Sync -> Help button)
// so we do it manually
$el.on("click", (e) => {
const $helpButton = $(e.target).closest("[data-help-page]");
openHelp($helpButton);
openHelp($(e.target).closest("[data-help-page]"));
openInAppHelp($(e.target).closest("[data-in-app-help]"));
});
}

View File

@ -5,6 +5,7 @@ import utils from "./services/utils.ts";
import appContext from "./components/app_context.ts";
import server from "./services/server.ts";
import library_loader, { Library } from "./services/library_loader.ts";
import type { init } from "i18next";
interface ElectronProcess {
type: string;
@ -139,10 +140,26 @@ declare global {
}
interface MermaidLoader {
}
interface MermaidChartConfig {
useMaxWidth: boolean;
}
interface MermaidConfig {
theme: string;
securityLevel: "antiscript",
flow: MermaidChartConfig;
sequence: MermaidChartConfig;
gantt: MermaidChartConfig;
class: MermaidChartConfig;
state: MermaidChartConfig;
pie: MermaidChartConfig;
journey: MermaidChartConfig;
git: MermaidChartConfig;
}
var mermaid: {
mermaidAPI: MermaidApi;
registerLayoutLoaders(loader: MermaidLoader);
init(config: MermaidConfig, el: HTMLElement | JQuery<HTMLElement>);
parse(content: string, opts: {
suppressErrors: true
}): Promise<{

View File

@ -10,7 +10,8 @@ const TPL = `
position: relative;
}
.floating-buttons-children,.show-floating-buttons {
.floating-buttons-children,
.show-floating-buttons {
position: absolute;
top: 10px;
right: 10px;
@ -19,6 +20,21 @@ const TPL = `
z-index: 100;
}
.note-split.rtl .floating-buttons-children,
.note-split.rtl .show-floating-buttons {
right: unset;
left: 10px;
}
.note-split.rtl .close-floating-buttons {
order: -1;
}
.note-split.rtl .close-floating-buttons,
.note-split.rtl .show-floating-buttons {
transform: rotate(180deg);
}
.type-canvas .floating-buttons-children {
top: 70px;
}

View File

@ -1,4 +1,5 @@
import appContext, { type EventData } from "../../components/app_context.js";
import type FNote from "../../entities/fnote.js";
import type { NoteType } from "../../entities/fnote.js";
import { t } from "../../services/i18n.js";
import type { ViewScope } from "../../services/link.js";
@ -39,47 +40,28 @@ const byBookType: Record<ViewTypeOptions, string | null> = {
export default class ContextualHelpButton extends NoteContextAwareWidget {
private helpNoteIdToOpen?: string | null;
isEnabled() {
this.helpNoteIdToOpen = null;
if (!super.isEnabled()) {
return false;
}
if (this.note && this.note.type !== "book" && byNoteType[this.note.type]) {
this.helpNoteIdToOpen = byNoteType[this.note.type];
} else if (this.note && this.note.type === "book") {
this.helpNoteIdToOpen = byBookType[(this.note.getAttributeValue("label", "viewType") as ViewTypeOptions) ?? ""];
}
return !!this.helpNoteIdToOpen;
return !!ContextualHelpButton.#getUrlToOpen(this.note);
}
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 });
}
});
}
static #getUrlToOpen(note: FNote | null | undefined) {
if (note && note.type !== "book" && byNoteType[note.type]) {
return byNoteType[note.type];
} else if (note && note.type === "book") {
return byBookType[note.getAttributeValue("label", "viewType") as ViewTypeOptions ?? ""]
}
}
async refreshWithNote(note: FNote | null | undefined): Promise<void> {
this.$widget.attr("data-in-app-help", ContextualHelpButton.#getUrlToOpen(this.note) ?? "");
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {

View File

@ -143,7 +143,7 @@ export default class MermaidWidget extends NoteContextAwareWidget {
}
}
export function getMermaidConfig() {
export function getMermaidConfig(): MermaidConfig {
const documentStyle = window.getComputedStyle(document.documentElement);
const mermaidTheme = documentStyle.getPropertyValue("--mermaid-theme");

View File

@ -157,6 +157,9 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
typeWidget.spacedUpdate = this.spacedUpdate;
typeWidget.setParent(this);
if (this.noteContext) {
typeWidget.setNoteContextEvent({ noteContext: this.noteContext });
}
const $renderedWidget = typeWidget.render();
keyboardActionsService.updateDisplayedShortcuts($renderedWidget);

View File

@ -0,0 +1,169 @@
import { Dropdown } from "bootstrap";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import { getAvailableLocales, getLocaleById } from "../services/i18n.js";
import { t } from "i18next";
import type { EventData } from "../components/app_context.js";
import type FNote from "../entities/fnote.js";
import attributes from "../services/attributes.js";
import type { Locale } from "../../../services/i18n.js";
import options from "../services/options.js";
import appContext from "../components/app_context.js";
const TPL = `\
<div class="dropdown note-language-widget">
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle select-button note-language-button">
<span class="note-language-desc"></span>
<span class="caret"></span>
</button>
<div class="note-language-dropdown dropdown-menu dropdown-menu-left tn-dropdown-list"></div>
<button class="language-help-button icon-action bx bx-help-circle" type="button" data-in-app-help="B0lcI9xz1r8K" title="${t("open-help-page")}"></button>
<style>
.note-language-widget {
display: flex;
align-items: center;
}
.language-help-button {
margin-left: 4px;
}
.note-language-dropdown [dir=rtl] {
text-align: right;
}
.dropdown-item.rtl > .check {
order: 1;
}
</style>
</div>
`;
const DEFAULT_LOCALE: Locale = {
id: "",
name: t("note_language.not_set")
};
export default class NoteLanguageWidget extends NoteContextAwareWidget {
private dropdown!: Dropdown;
private $noteLanguageDropdown!: JQuery<HTMLElement>;
private $noteLanguageDesc!: JQuery<HTMLElement>;
private locales: (Locale | "---")[];
private currentLanguageId?: string;
constructor() {
super();
this.locales = NoteLanguageWidget.#buildLocales();
}
doRender() {
this.$widget = $(TPL);
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0]);
this.$widget.on("show.bs.dropdown", () => this.renderDropdown());
this.$noteLanguageDropdown = this.$widget.find(".note-language-dropdown")
this.$noteLanguageDesc = this.$widget.find(".note-language-desc");
}
renderDropdown() {
this.$noteLanguageDropdown.empty();
if (!this.note) {
return;
}
for (const locale of this.locales) {
if (typeof locale === "object") {
const $title = $("<span>").text(locale.name);
const $link = $('<a class="dropdown-item">')
.attr("data-language", locale.id)
.append('<span class="check">&check;</span> ')
.append($title)
.on("click", () => {
const languageId = $link.attr("data-language") ?? "";
this.save(languageId);
});
if (locale.rtl) {
$link.attr("dir", "rtl");
}
if (locale.id === this.currentLanguageId) {
$link.addClass("selected");
}
this.$noteLanguageDropdown.append($link);
} else {
this.$noteLanguageDropdown.append('<div class="dropdown-divider"></div>');
}
}
const $configureLink = $('<a class="dropdown-item">')
.append(`<span>${t("note_language.configure-languages")}</span>`)
.on("click", () => appContext.tabManager.openContextWithNote("_optionsLocalization", { activate: true }));
this.$noteLanguageDropdown.append($configureLink);
}
async save(languageId: string) {
if (!this.note) {
return;
}
attributes.setAttribute(this.note, "label", "language", languageId);
}
async refreshWithNote(note: FNote) {
const currentLanguageId = note.getLabelValue("language") ?? "";
const language = getLocaleById(currentLanguageId) ?? DEFAULT_LOCALE;
this.currentLanguageId = currentLanguageId;
this.$noteLanguageDesc.text(language.name);
this.dropdown.hide();
}
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isOptionReloaded("languages")) {
this.locales = NoteLanguageWidget.#buildLocales();
}
if (loadResults.getAttributeRows().find((a) => a.noteId === this.noteId && a.name === "language")) {
this.refresh();
}
}
static #buildLocales() {
const enabledLanguages = JSON.parse(options.get("languages") ?? "[]") as string[];
const filteredLanguages = getAvailableLocales().filter((l) => typeof l !== "object" || enabledLanguages.includes(l.id));
const leftToRightLanguages = filteredLanguages.filter((l) => !l.rtl);
const rightToLeftLanguages = filteredLanguages.filter((l) => l.rtl);
let locales: ("---" | Locale)[] = [
DEFAULT_LOCALE
];
if (leftToRightLanguages.length > 0) {
locales = [
...locales,
"---",
...leftToRightLanguages
];
}
if (rightToLeftLanguages.length > 0) {
locales = [
...locales,
"---",
...rightToLeftLanguages
];
}
// This will separate the list of languages from the "Configure languages" button.
// If there is at least one language.
if (locales.length > 2) {
locales.push("---");
}
return locales;
}
}

View File

@ -89,7 +89,10 @@ export default class NoteTitleWidget extends NoteContextAwareWidget {
}
async refreshWithNote(note: FNote) {
const isReadOnly = (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) || utils.isLaunchBarConfig(note.noteId) || this.noteContext?.viewScope?.viewMode !== "default";
const isReadOnly =
(note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable())
|| utils.isLaunchBarConfig(note.noteId)
|| this.noteContext?.viewScope?.viewMode !== "default";
this.$noteTitle.val(isReadOnly ? (await this.noteContext?.getNavigationTitle()) || "" : note.title);
this.$noteTitle.prop("readonly", isReadOnly);

View File

@ -5,6 +5,7 @@ import type BasicWidget from "./basic_widget.js";
import type { EventData } from "../components/app_context.js";
import type NoteContext from "../components/note_context.js";
import type FNote from "../entities/fnote.js";
import { getLocaleById } from "../services/i18n.js";
export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
@ -56,6 +57,10 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
this.$widget.addClass(utils.getMimeTypeClass(note.mime));
this.$widget.toggleClass("protected", note.isProtected);
const noteLanguage = note?.getLabelValue("language");
const locale = getLocaleById(noteLanguage);
this.$widget.toggleClass("rtl", !!locale?.rtl);
}
#isFullWidthNote(note: FNote) {
@ -76,7 +81,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
const noteId = this.noteContext?.noteId;
if (
loadResults.isNoteReloaded(noteId) ||
loadResults.getAttributeRows().find((attr) => attr.type === "label" && attr.name === "cssClass" && attributeService.isAffecting(attr, this.noteContext?.note))
loadResults.getAttributeRows().find((attr) => attr.type === "label" && ["cssClass", "language"].includes(attr.name ?? "") && attributeService.isAffecting(attr, this.noteContext?.note))
) {
this.refresh();
}

View File

@ -6,6 +6,8 @@ import BookmarkSwitchWidget from "../bookmark_switch.js";
import SharedSwitchWidget from "../shared_switch.js";
import { t } from "../../services/i18n.js";
import TemplateSwitchWidget from "../template_switch.js";
import type FNote from "../../entities/fnote.js";
import NoteLanguageWidget from "../note_language.js";
const TPL = `
<div class="basic-properties-widget">
@ -16,40 +18,55 @@ const TPL = `
align-items: baseline;
flex-wrap: wrap;
}
.basic-properties-widget > * {
.basic-properties-widget > * {
margin-top: 9px;
margin-bottom: 2px;
}
.basic-properties-widget > * > :last-child {
margin-right: 30px;
}
.note-type-container, .editability-select-container {
.note-type-container,
.editability-select-container,
.note-language-container {
display: flex;
align-items: center;
}
</style>
<div class="note-type-container">
<span>${t("basic_properties.note_type")}:</span> &nbsp;
</div>
<div class="protected-note-switch-container"></div>
<div class="editability-select-container">
<span>${t("basic_properties.editable")}:</span> &nbsp;
</div>
<div class="bookmark-switch-container"></div>
<div class="shared-switch-container"></div>
<div class="template-switch-container"></div>
<div class="note-language-container">
<span>${t("basic_properties.language")}:</span> &nbsp;
</div>
</div>`;
export default class BasicPropertiesWidget extends NoteContextAwareWidget {
private noteTypeWidget: NoteTypeWidget;
private protectedNoteSwitchWidget: ProtectedNoteSwitchWidget;
private editabilitySelectWidget: EditabilitySelectWidget;
private bookmarkSwitchWidget: BookmarkSwitchWidget;
private sharedSwitchWidget: SharedSwitchWidget;
private templateSwitchWidget: TemplateSwitchWidget;
private noteLanguageWidget: NoteLanguageWidget;
constructor() {
super();
@ -59,8 +76,16 @@ export default class BasicPropertiesWidget extends NoteContextAwareWidget {
this.bookmarkSwitchWidget = new BookmarkSwitchWidget().contentSized();
this.sharedSwitchWidget = new SharedSwitchWidget().contentSized();
this.templateSwitchWidget = new TemplateSwitchWidget().contentSized();
this.noteLanguageWidget = new NoteLanguageWidget().contentSized();
this.child(this.noteTypeWidget, this.protectedNoteSwitchWidget, this.editabilitySelectWidget, this.bookmarkSwitchWidget, this.sharedSwitchWidget, this.templateSwitchWidget);
this.child(
this.noteTypeWidget,
this.protectedNoteSwitchWidget,
this.editabilitySelectWidget,
this.bookmarkSwitchWidget,
this.sharedSwitchWidget,
this.templateSwitchWidget,
this.noteLanguageWidget);
}
get name() {
@ -73,7 +98,7 @@ export default class BasicPropertiesWidget extends NoteContextAwareWidget {
getTitle() {
return {
show: !this.note.isLaunchBarConfig(),
show: !this.note?.isLaunchBarConfig(),
title: t("basic_properties.basic_properties"),
icon: "bx bx-slider"
};
@ -89,11 +114,16 @@ export default class BasicPropertiesWidget extends NoteContextAwareWidget {
this.$widget.find(".bookmark-switch-container").append(this.bookmarkSwitchWidget.render());
this.$widget.find(".shared-switch-container").append(this.sharedSwitchWidget.render());
this.$widget.find(".template-switch-container").append(this.templateSwitchWidget.render());
this.$widget.find(".note-language-container").append(this.noteLanguageWidget.render());
}
async refreshWithNote(note) {
async refreshWithNote(note: FNote) {
await super.refreshWithNote(note);
if (!this.note) {
return;
}
this.$widget.find(".editability-select-container").toggle(this.note && ["text", "code"].includes(this.note.type));
this.$widget.find(".note-language-container").toggle(this.note && ["text"].includes(this.note.type));
}
}

View File

@ -5,6 +5,7 @@ import linkService from "../../services/link.js";
import contentRenderer from "../../services/content_renderer.js";
import utils from "../../services/utils.js";
import options from "../../services/options.js";
import attributes from "../../services/attributes.js";
export default class AbstractTextTypeWidget extends TypeWidget {
doRender() {
@ -117,5 +118,16 @@ export default class AbstractTextTypeWidget extends TypeWidget {
if (loadResults.isOptionReloaded("codeBlockWordWrap")) {
this.refreshCodeBlockOptions();
}
if (loadResults.getAttributeRows().find((attr) =>
attr.type === "label" &&
attr.name === "language" &&
attributes.isAffecting(attr, this.note)))
{
await this.onLanguageChanged();
}
}
async onLanguageChanged() { }
}

View File

@ -39,6 +39,8 @@ import EditorOptions from "./options/text_notes/editor.js";
import ShareSettingsOptions from "./options/other/share_settings.js";
import type FNote from "../../entities/fnote.js";
import type NoteContextAwareWidget from "../note_context_aware_widget.js";
import { t } from "i18next";
import LanguageOptions from "./options/i18n/language.js";
const TPL = `<div class="note-detail-content-widget note-detail-printable">
<style>
@ -81,6 +83,7 @@ const CONTENT_WIDGETS: Record<string, (typeof NoteContextAwareWidget)[]> = {
HtmlImportTagsOptions,
ShareSettingsOptions
],
_optionsLocalization: [ LanguageOptions ],
_optionsAdvanced: [DatabaseIntegrityCheckOptions, DatabaseAnonymizationOptions, AdvancedSyncOptions, VacuumDatabaseOptions],
_backendLog: [BackendLogWidget]
};
@ -119,7 +122,7 @@ export default class ContentWidgetTypeWidget extends TypeWidget {
await widget.refresh();
}
} else {
this.$content.append(`Unknown widget for "${note.noteId}"`);
this.$content.append(t("content_widget.unknown_widget", { id: note.noteId }));
}
}
}

View File

@ -18,7 +18,7 @@ const TPL = `<div class="note-detail-doc note-detail-printable">
}
.note-detail-doc.contextual-help {
padding-bottom: 15vh;
padding-bottom: 0;
}
.note-detail-doc.contextual-help h2,
@ -36,7 +36,7 @@ const TPL = `<div class="note-detail-doc note-detail-printable">
}
img {
max-width: 90vw;
max-width: 100%;
height: auto;
}

View File

@ -140,8 +140,6 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
const isClassicEditor = utils.isMobile() || options.get("textNoteEditorType") === "ckeditor-classic";
const editorClass = isClassicEditor ? CKEditor.DecoupledEditor : CKEditor.BalloonEditor;
const codeBlockLanguages = buildListOfLanguages();
// CKEditor since version 12 needs the element to be visible before initialization. At the same time,
// we want to avoid flicker - i.e., show editor only once everything is ready. That's why we have separate
// display of $widget in both branches.
@ -185,7 +183,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
this.watchdog.setCreator(async (elementOrData, editorConfig) => {
logInfo("Creating new CKEditor");
const editor = await editorClass.create(elementOrData, {
const finalConfig = {
...editorConfig,
...buildConfig(),
...buildToolbarConfig(isClassicEditor),
@ -195,7 +193,20 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
classes: true,
attributes: true
}
});
};
const contentLanguage = this.note.getLabelValue("language");
if (contentLanguage) {
finalConfig.language = {
ui: (typeof finalConfig.language === "string" ? finalConfig.language : "en"),
content: contentLanguage
}
this.contentLanguage = contentLanguage;
} else {
this.contentLanguage = null;
}
const editor = await editorClass.create(elementOrData, finalConfig);
const notificationsPlugin = editor.plugins.get("Notification");
notificationsPlugin.on("show:warning", (evt, data) => {
@ -242,11 +253,15 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
return editor;
});
await this.createEditor();
}
async createEditor() {
await this.watchdog.create(this.$editor[0], {
placeholder: t("editable_text.placeholder"),
mention: mentionSetup,
codeBlock: {
languages: codeBlockLanguages
languages: buildListOfLanguages()
},
math: {
engine: "katex",
@ -265,7 +280,15 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
async doRefresh(note) {
const blob = await note.getBlob();
await this.spacedUpdate.allowUpdateWithoutChange(() => this.watchdog.editor.setData(blob.content || ""));
await this.spacedUpdate.allowUpdateWithoutChange(async () => {
const data = blob.content || "";
const newContentLanguage = this.note.getLabelValue("language");
if (this.contentLanguage !== newContentLanguage) {
await this.reinitialize(data);
} else {
this.watchdog.editor.setData(data);
}
});
}
getData() {
@ -468,4 +491,20 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
async refreshIncludedNoteEvent({ noteId }) {
this.refreshIncludedNote(this.$editor, noteId);
}
async reinitialize(data) {
if (!this.watchdog) {
return;
}
this.watchdog.destroy();
await this.createEditor();
this.watchdog.editor.setData(data);
}
async onLanguageChanged() {
const data = this.watchdog.editor.getData();
await this.reinitialize(data);
}
}

View File

@ -1,7 +1,7 @@
import OptionsWidget from "../options_widget.js";
import server from "../../../../services/server.js";
import utils from "../../../../services/utils.js";
import { t } from "../../../../services/i18n.js";
import { getAvailableLocales, t } from "../../../../services/i18n.js";
import type { OptionMap } from "../../../../../../services/options_interface.js";
const TPL = `
@ -25,12 +25,6 @@ const TPL = `
</div>
`;
// TODO: Deduplicate with server.
interface Locale {
id: string;
name: string;
}
export default class LocalizationOptions extends OptionsWidget {
private $localeSelect!: JQuery<HTMLElement>;
@ -53,7 +47,7 @@ export default class LocalizationOptions extends OptionsWidget {
}
async optionsLoaded(options: OptionMap) {
const availableLocales = await server.get<Locale[]>("options/locales");
const availableLocales = getAvailableLocales().filter(l => !l.contentOnly);
this.$localeSelect.empty();
for (const locale of availableLocales) {

View File

@ -0,0 +1,63 @@
import OptionsWidget from "../options_widget.js";
import type { OptionMap } from "../../../../../../services/options_interface.js";
import { getAvailableLocales } from "../../../../services/i18n.js";
import { t } from "i18next";
const TPL = `
<div class="options-section">
<h4>${t("content_language.title")}</h4>
<p>${t("content_language.description")}</p>
<ul class="options-languages">
</ul>
<style>
ul.options-languages {
list-style-type: none;
margin-bottom: 0;
column-width: 400px;
}
</style>
</div>
`;
export default class LanguageOptions extends OptionsWidget {
private $languagesContainer!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
this.$languagesContainer = this.$widget.find(".options-languages");
}
async save() {
const enabledLanguages: string[] = [];
this.$languagesContainer.find("input:checked").each((i, el) => {
const languageId = $(el).attr("data-language-id");
if (languageId) {
enabledLanguages.push(languageId);
}
});
await this.updateOption("languages", JSON.stringify(enabledLanguages));
}
async optionsLoaded(options: OptionMap) {
const availableLocales = getAvailableLocales();
const enabledLanguages = (JSON.parse(options.languages) as string[]);
this.$languagesContainer.empty();
for (const locale of availableLocales) {
const checkbox = $('<input type="checkbox" class="form-check-input">')
.attr("data-language-id", locale.id)
.prop("checked", enabledLanguages.includes(locale.id));
const wrapper = $(`<label class="tn-checkbox">`)
.append(checkbox)
.on("change", () => this.save())
.append(locale.name);
this.$languagesContainer.append($("<li>").append(wrapper));
}
}
}

View File

@ -2,6 +2,9 @@ import AbstractTextTypeWidget from "./abstract_text_type_widget.js";
import libraryLoader from "../../services/library_loader.js";
import { applySyntaxHighlight } from "../../services/syntax_highlight.js";
import { getMermaidConfig } from "../mermaid.js";
import type FNote from "../../entities/fnote.js";
import type { EventData } from "../../components/app_context.js";
import { getLocaleById } from "../../services/i18n.js";
const TPL = `
<div class="note-detail-readonly-text note-detail-printable">
@ -29,7 +32,7 @@ const TPL = `
body.heading-style-underline .note-detail-readonly-text h6 { border-bottom: 1px solid var(--main-border-color); }
.note-detail-readonly-text {
padding-left: 24px;
padding-inline-start: 24px;
padding-top: 10px;
font-family: var(--detail-font-family);
min-height: 50px;
@ -71,6 +74,9 @@ const TPL = `
`;
export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
private $content!: JQuery<HTMLElement>;
static getType() {
return "readOnlyText";
}
@ -89,21 +95,23 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
this.$content.html("");
}
async doRefresh(note) {
async doRefresh(note: FNote) {
// we load CKEditor also for read only notes because they contain content styles required for correct rendering of even read only notes
// we could load just ckeditor-content.css but that causes CSS conflicts when both build CSS and this content CSS is loaded at the same time
// (see https://github.com/zadam/trilium/issues/1590 for example of such conflict)
await libraryLoader.requireLibrary(libraryLoader.CKEDITOR);
this.onLanguageChanged();
const blob = await note.getBlob();
this.$content.html(blob.content);
this.$content.html(blob?.content ?? "");
this.$content.find("a.reference-link").each(async (_, el) => {
this.$content.find("a.reference-link").each((_, el) => {
this.loadReferenceLinkTitle($(el));
});
this.$content.find("section").each(async (_, el) => {
this.$content.find("section").each((_, el) => {
const noteId = $(el).attr("data-note-id");
this.loadIncludedNote(noteId, $(el));
@ -135,11 +143,11 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
mermaid.init(getMermaidConfig(), this.$content.find(".mermaid-diagram"));
}
async refreshIncludedNoteEvent({ noteId }) {
async refreshIncludedNoteEvent({ noteId }: EventData<"refreshIncludedNote">) {
this.refreshIncludedNote(this.$content, noteId);
}
async executeWithContentElementEvent({ resolve, ntxId }) {
async executeWithContentElementEvent({ resolve, ntxId }: EventData<"executeWithContentElement">) {
if (!this.isNoteContext(ntxId)) {
return;
}
@ -148,4 +156,12 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
resolve(this.$content);
}
async onLanguageChanged(): Promise<void> {
const languageCode = this.note?.getLabelValue("language");
const correspondingLocale = getLocaleById(languageCode);
const isRtl = correspondingLocale?.rtl;
this.$widget.attr("dir", isRtl ? "rtl" : "ltr");
}
}

View File

@ -229,8 +229,8 @@ export default class CalendarView extends ViewMode {
return;
}
CalendarView.#setAttribute(note, "label", "startDate", startDate);
CalendarView.#setAttribute(note, "label", "endDate", endDate);
attributes.setAttribute(note, "label", "startDate", startDate);
attributes.setAttribute(note, "label", "endDate", endDate);
}
onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) {
@ -435,20 +435,6 @@ export default class CalendarView extends ViewMode {
return [note.title];
}
static async #setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) {
if (value) {
// Create or update the attribute.
await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value });
} else {
// Remove the attribute if it exists on the server but we don't define a value for it.
const attributeId = note.getAttribute(type, name)?.attributeId;
if (attributeId) {
await server.remove(`notes/${note.noteId}/attributes/${attributeId}`);
}
}
await ws.waitForMaxKnownEntityChangeId();
}
static #formatDateToLocalISO(date: Date | null | undefined) {
if (!date) {
return undefined;

View File

@ -552,7 +552,8 @@ optgroup {
/* Switches */
.switch-widget.component {
.switch-widget.component,
.note-language-container {
--icon-button-size: 28px;
}

View File

@ -1025,7 +1025,8 @@ body.desktop .dropdown-submenu .dropdown-menu {
.dropdown-item,
body.mobile .dropdown-submenu .dropdown-toggle {
padding: 2px 16px 2px 8px !important;
padding: 2px 2px 2px 8px !important;
padding-inline-end: 16px;
/* Note: the right padding should also accommodate the submenu arrow. */
border-radius: 6px;
cursor: default !important;

View File

@ -744,7 +744,8 @@
"basic_properties": {
"note_type": "Note type",
"editable": "Editable",
"basic_properties": "Basic Properties"
"basic_properties": "Basic Properties",
"language": "Language"
},
"book_properties": {
"view_type": "View type",
@ -1682,5 +1683,16 @@
"tomorrow": "Tomorrow",
"yesterday": "Yesterday"
}
},
"content_widget": {
"unknown_widget": "Unknown widget for \"{{id}}\"."
},
"note_language": {
"not_set": "Not set",
"configure-languages": "Configure languages..."
},
"content_language": {
"title": "Content languages",
"description": "Select one or more languages that should appear in the language selection in the Basic Properties section of a read-only or editable text note. This will allow features such as spell-checking or right-to-left support."
}
}

View File

@ -1685,5 +1685,8 @@
"tomorrow": "Mâine",
"yesterday": "Ieri"
}
},
"content_widget": {
"unknown_widget": "Nu s-a putut găsi widget-ul corespunzător pentru „{{id}}”."
}
}

View File

@ -71,6 +71,7 @@ const ALLOWED_OPTIONS = new Set([
"editedNotesOpenInRibbon",
"locale",
"firstDayOfWeek",
"languages",
"textNoteEditorType",
"textNoteEditorMultilineToolbar",
"layoutOrientation",

View File

@ -275,6 +275,7 @@ function buildHiddenSubtreeDefinition(helpSubtree: HiddenSubtreeItem[]): HiddenS
{ id: "_optionsBackup", title: t("hidden-subtree.backup-title"), type: "contentWidget", icon: "bx-data" },
{ id: "_optionsSync", title: t("hidden-subtree.sync-title"), type: "contentWidget", icon: "bx-wifi" },
{ id: "_optionsOther", title: t("hidden-subtree.other"), type: "contentWidget", icon: "bx-dots-horizontal" },
{ id: "_optionsLocalization", title: t("hidden-subtree.localization"), type: "contentWidget", icon: "bx-world" },
{ id: "_optionsAdvanced", title: t("hidden-subtree.advanced-title"), type: "contentWidget" }
]
},

View File

@ -7,6 +7,10 @@ function checkTranslations(translationDir: string, translationFileName: string)
const locales = i18n.getLocales();
for (const locale of locales) {
if (locale.contentOnly) {
continue;
}
const translationPath = path.join(translationDir, locale.id, translationFileName);
const translationFile = fs.readFileSync(translationPath, { encoding: "utf-8" });
expect(() => {

View File

@ -6,6 +6,76 @@ import { join } from "path";
import { getResourceDir } from "./utils.js";
import hidden_subtree from "./hidden_subtree.js";
export interface Locale {
id: string;
name: string;
/** `true` if the language is a right-to-left one, or `false` if it's left-to-right. */
rtl?: boolean;
/** `true` if the language is not supported by the application as a display language, but it is selectable by the user for the content. */
contentOnly?: boolean;
}
const LOCALES: Locale[] = [
{
id: "en",
name: "English"
},
{
id: "de",
name: "Deutsch"
},
{
id: "es",
name: "Español"
},
{
id: "fr",
name: "Français"
},
{
id: "cn",
name: "简体中文"
},
{
id: "tw",
name: "繁體中文"
},
{
id: "ro",
name: "Română"
},
/*
* Right to left languages
*
* Currently they are only for setting the language of text notes.
*/
{ // Arabic
id: "ar",
name: "اَلْعَرَبِيَّةُ",
rtl: true,
contentOnly: true
},
{ // Hebrew
id: "he",
name: "עברית",
rtl: true,
contentOnly: true
},
{ // Kurdish
id: "ku",
name: "کوردی",
rtl: true,
contentOnly: true
},
{ // Persian
id: "fa",
name: "فارسی",
rtl: true,
contentOnly: true
}
].sort((a, b) => a.name.localeCompare(b.name));
export async function initializeTranslations() {
const resourceDir = getResourceDir();
@ -20,38 +90,8 @@ export async function initializeTranslations() {
});
}
export function getLocales() {
// TODO: Currently hardcoded, needs to read the list of available languages.
return [
{
id: "en",
name: "English"
},
{
id: "de",
name: "Deutsch"
},
{
id: "es",
name: "Español"
},
{
id: "fr",
name: "Français"
},
{
id: "cn",
name: "简体中文"
},
{
id: "tw",
name: "繁體中文"
},
{
id: "ro",
name: "Română"
}
];
export function getLocales(): Locale[] {
return LOCALES;
}
function getCurrentLanguage() {

View File

@ -134,6 +134,7 @@ const defaultOptions: DefaultOption[] = [
// Internationalization
{ name: "locale", value: "en", isSynced: true },
{ name: "firstDayOfWeek", value: "1", isSynced: true },
{ name: "languages", value: "[]", isSynced: true },
// Code block configuration
{

View File

@ -71,6 +71,7 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi
eraseUnusedAttachmentsAfterSeconds: number;
eraseUnusedAttachmentsAfterTimeScale: number;
firstDayOfWeek: number;
languages: string;
initialized: boolean;
isPasswordSet: boolean;

View File

@ -244,7 +244,8 @@
"other": "Other",
"advanced-title": "Advanced",
"visible-launchers-title": "Visible Launchers",
"user-guide": "User Guide"
"user-guide": "User Guide",
"localization": "Language & Region"
},
"notes": {
"new-note": "New note",