Merge branch 'develop' into port_dialogs

This commit is contained in:
JYC333
2025-03-19 19:20:46 +01:00
committed by GitHub
80 changed files with 8178 additions and 3521 deletions

View File

@@ -41,7 +41,8 @@ try {
"./src/public/robots.txt", "./src/public/robots.txt",
"./src/public/fonts", "./src/public/fonts",
"./src/public/stylesheets", "./src/public/stylesheets",
"./src/public/translations" "./src/public/translations",
"./packages/turndown-plugin-gfm/src"
]); ]);
for (const asset of assetsToCopy) { for (const asset of assetsToCopy) {

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -3,6 +3,13 @@
The Calendar view of Book notes will display each child note in a calendar that has a start date and optionally an end date, as an event. The Calendar view of Book notes will display each child note in a calendar that has a start date and optionally an end date, as an event.
The Calendar view has multiple display modes:
* Week view, where all the 7 days of the week (or 5 if the weekends are hidden) are displayed in columns. This mode allows entering and displaying time-specific events, not just all-day events.
* Month view, where the entire month is displayed and all-day events can be inserted. Both time-specific events and all-day events are listed.
* Year view, which displays the entire year for quick reference.
* List view, which displays all the events of a given month in sequence.
Unlike other Book view types, the Calendar view also allows some kind of interaction, such as moving events around as well as creating new ones. Unlike other Book view types, the Calendar view also allows some kind of interaction, such as moving events around as well as creating new ones.
## Creating a calendar ## Creating a calendar
@@ -23,7 +30,7 @@ Unlike other Book view types, the Calendar view also allows some kind of interac
## Interacting with events ## Interacting with events
* Hovering the mouse over an event will display information about the note. * Hovering the mouse over an event will display information about the note.
![](9_Calendar%20View_image.png) ![](7_Calendar%20View_image.png)
* Left clicking the event will go to that note. Middle clicking will open the note in a new tab and right click will offer more options including opening the note in a new split or window. * Left clicking the event will go to that note. Middle clicking will open the note in a new tab and right click will offer more options including opening the note in a new split or window.
* Drag and drop an event on the calendar to move it to another day. * Drag and drop an event on the calendar to move it to another day.
* The length of an event can be changed by placing the mouse to the right edge of the event and dragging the mouse around. * The length of an event can be changed by placing the mouse to the right edge of the event and dragging the mouse around.
@@ -32,7 +39,7 @@ Unlike other Book view types, the Calendar view also allows some kind of interac
The following attributes can be added to the book type: The following attributes can be added to the book type:
<div class="joplin-table-wrapper"><table><thead><tr><th>Name</th><th>Description</th></tr></thead><tbody><tr><td><code>#calendar:hideWeekends</code></td><td>When present (regardless of value), it will hide Saturday and Sundays from the calendar.</td></tr><tr><td><code>#calendar:weekNumbers</code></td><td>When present (regardless of value), it will show the number of the week on the calendar.</td></tr><tr><td><code>#calendar:view</code></td><td><p>Which view to display in the calendar:</p><ul><li><code>timeGridWeek</code> for the <em>week</em> view;</li><li><code>dayGridMonth</code> for the <em>month</em> view;</li><li><code>multiMonthYear</code> for the <em>year</em> view;</li><li><code>listMonth</code> for the <em>list</em> view.</li></ul><p>Any other value will be dismissed and the default view (month) will be used instead.</p><p>The value of this label is automatically updated when changing the view using the UI buttons.</p></td></tr><tr><td><code>~child:template</code></td><td>Defines the template for newly created notes in the calendar (via dragging or clicking).</td></tr></tbody></table></div> <table><thead><tr><th>Name</th><th>Description</th></tr></thead><tbody><tr><td><code>#calendar:hideWeekends</code></td><td>When present (regardless of value), it will hide Saturday and Sundays from the calendar.</td></tr><tr><td><code>#calendar:weekNumbers</code></td><td>When present (regardless of value), it will show the number of the week on the calendar.</td></tr><tr><td><code>#calendar:view</code></td><td><p>Which view to display in the calendar:</p><ul><li><code>timeGridWeek</code> for the <em>week</em> view;</li><li><code>dayGridMonth</code> for the <em>month</em> view;</li><li><code>multiMonthYear</code> for the <em>year</em> view;</li><li><code>listMonth</code> for the <em>list</em> view.</li></ul><p>Any other value will be dismissed and the default view (month) will be used instead.</p><p>The value of this label is automatically updated when changing the view using the UI buttons.</p></td></tr><tr><td><code>~child:template</code></td><td>Defines the template for newly created notes in the calendar (via dragging or clicking).</td></tr></tbody></table>
In addition, the first day of the week can be either Sunday or Monday and can be adjusted from the application settings. In addition, the first day of the week can be either Sunday or Monday and can be adjusted from the application settings.
@@ -49,8 +56,8 @@ For each note of the calendar, the following attributes can be used:
| `#color` | Displays the event with a specified color (named such as `red`, `gray` or hex such as `#FF0000`). This will also change the color of the note in other places such as the note tree. | | `#color` | Displays the event with a specified color (named such as `red`, `gray` or hex such as `#FF0000`). This will also change the color of the note in other places such as the note tree. |
| `#calendar:color` | Similar to `#color`, but applies the color only for the event in the calendar and not for other places such as the note tree. | | `#calendar:color` | Similar to `#color`, but applies the color only for the event in the calendar and not for other places such as the note tree. |
| `#iconClass` | If present, the icon of the note will be displayed to the left of the event title. | | `#iconClass` | If present, the icon of the note will be displayed to the left of the event title. |
| `#calendar:title` | Changes the title of an event to point to an attribute of the note other than the title, either a label (e.g. `#assignee`) or a relation (e.g. `~for`). See _Advanced use-cases_ for more information. | | `#calendar:title` | Changes the title of an event to point to an attribute of the note other than the title, can either a label or a relation (without the `#` or `~` symbol). See _Use-cases_ for more information. |
| `#calendar:displayedAttributes` | Allows displaying the value of one or more attributes in the calendar like this:   <br> <br>![](11_Calendar%20View_image.png)  <br> <br>`#weight="70" #Mood="Good" #calendar:displayedAttributes="weight,Mood"` <br> <br>It can also be used with relations, case in which it will display the title of the target note:  <br> <br>`~assignee=@My assignee #calendar:displayedAttributes="assignee"` | | `#calendar:displayedAttributes` | Allows displaying the value of one or more attributes in the calendar like this:    <br> <br>![](9_Calendar%20View_image.png)   <br> <br>`#weight="70" #Mood="Good" #calendar:displayedAttributes="weight,Mood"`  <br> <br>It can also be used with relations, case in which it will display the title of the target note:   <br> <br>`~assignee=@My assignee #calendar:displayedAttributes="assignee"` |
| `#calendar:startDate` | Allows using a different label to represent the start date, other than `startDate` (e.g. `expiryDate`). The label name **must not be** prefixed with `#`. If the label is not defined for a note, the default will be used instead. | | `#calendar:startDate` | Allows using a different label to represent the start date, other than `startDate` (e.g. `expiryDate`). The label name **must not be** prefixed with `#`. If the label is not defined for a note, the default will be used instead. |
| `#calendar:endDate` | Similar to `#calendar:startDate`, allows changing the attribute which is being used to read the end date. | | `#calendar:endDate` | Similar to `#calendar:startDate`, allows changing the attribute which is being used to read the end date. |
| `#calendar:startTime` | Similar to `#calendar:startDate`, allows changing the attribute which is being used to read the start time. | | `#calendar:startTime` | Similar to `#calendar:startDate`, allows changing the attribute which is being used to read the start time. |
@@ -58,7 +65,7 @@ For each note of the calendar, the following attributes can be used:
## How the calendar works ## How the calendar works
![](14_Calendar%20View_image.png) ![](11_Calendar%20View_image.png)
The calendar displays all the child notes of the book that have a `#startDate`. An `#endDate` can optionally be added. The calendar displays all the child notes of the book that have a `#startDate`. An `#endDate` can optionally be added.
@@ -72,7 +79,7 @@ If editing the start date and end date from the note itself is desirable, the fo
This will result in: This will result in:
![](12_Calendar%20View_image.png) ![](10_Calendar%20View_image.png)
When not used in a Journal, the calendar is recursive. That is, it will look for events not just in its child notes but also in the children of these child notes. When not used in a Journal, the calendar is recursive. That is, it will look for events not just in its child notes but also in the children of these child notes.
@@ -89,30 +96,28 @@ Based on the `#calendarRoot` (or `#workspaceCalendarRoot`) attribute, the calend
* Clicking on the empty space on a date will automatically open that day's note or create it if it does not exist. * Clicking on the empty space on a date will automatically open that day's note or create it if it does not exist.
* Direct children of a day note will be displayed on the calendar despite not having a `dateNote` attribute. Children of the child notes will not be displayed. * Direct children of a day note will be displayed on the calendar despite not having a `dateNote` attribute. Children of the child notes will not be displayed.
![](10_Calendar%20View_image.png) ![](8_Calendar%20View_image.png)
### Using a different attribute as event title ### Using a different attribute as event title
By default, events are displayed on the calendar by their note title. However, it is possible to configure a different attribute to be displayed instead. By default, events are displayed on the calendar by their note title. However, it is possible to configure a different attribute to be displayed instead.
To do so, assign `#calendar:title` to the child note (not the calendar/book note), with the value being `#name` where `name` can be any label. The attribute can also come through inheritance such as a template attribute. If the note does not have the requested label, the title of the note will be used instead. To do so, assign `#calendar:title` to the child note (not the calendar/book note), with the value being `name` where `name` can be any label (make not to add the `#` prefix). The attribute can also come through inheritance such as a template attribute. If the note does not have the requested label, the title of the note will be used instead.
| | | <table><thead><tr><th>&nbsp;</th><th>&nbsp;</th></tr></thead><tbody><tr><td><pre><code class="language-text-x-trilium-auto">#startDate=2025-02-11 #endDate=2025-02-13 #name="My vacation" #calendar:title="name"</code></pre></td><td><img src="5_Calendar View_image.png"></td></tr></tbody></table>
| --- | --- |
| ![](5_Calendar%20View_image.png) | ![](7_Calendar%20View_image.png) |
### Using a relation attribute as event title ### Using a relation attribute as event title
Similarly to using an attribute, use `#calendar:title` and set it to `~name` where `name` is the name of the relation to use. Similarly to using an attribute, use `#calendar:title` and set it to `name` where `name` is the name of the relation to use.
Moreover, if there are more relations of the same name, they will be displayed as multiple events coming from the same note. Moreover, if there are more relations of the same name, they will be displayed as multiple events coming from the same note.
| | | | | |
| --- | --- | | --- | --- |
| ![](6_Calendar%20View_image.png) | ![](8_Calendar%20View_image.png) | | `#startDate=2025-02-14 #endDate=2025-02-15 ~for=@John Smith ~for=@Jane Doe #calendar:title="for"` | ![](6_Calendar%20View_image.png) |
Note that it's even possible to have a `#calendar:title` on the target note (e.g. “John Smith”) which will try to render an attribute of it. Note that it's not possible to use a relation here as well for safety reasons (an accidental recursion  of attributes could cause the application to loop infinitely). Note that it's even possible to have a `#calendar:title` on the target note (e.g. “John Smith”) which will try to render an attribute of it. Note that it's not possible to use a relation here as well for safety reasons (an accidental recursion  of attributes could cause the application to loop infinitely).
| | | | | |
| --- | --- | | --- | --- |
| ![](13_Calendar%20View_image.png) | ![](1_Calendar%20View_image.png) | | `#calendar:title="shortName" #shortName="John S."` | ![](1_Calendar%20View_image.png) |

View File

@@ -46,7 +46,7 @@ These types were inspired by GitHub's support for this feature and there are cur
The Markdown syntax for admonitions as supported by Trilium is the one that GitHub uses, which is as follows: The Markdown syntax for admonitions as supported by Trilium is the one that GitHub uses, which is as follows:
```markdown ```
> [!NOTE] > [!NOTE]
> This is a note. > This is a note.

View File

@@ -61,8 +61,24 @@ test("Displays math popup", async ({ page, context }) => {
const mathForm = page.locator(".ck-math-form"); const mathForm = page.locator(".ck-math-form");
await expect(mathForm).toBeVisible(); await expect(mathForm).toBeVisible();
await mathForm.locator(".ck-input").first().fill("e=mc^2"); const input = mathForm.locator(".ck-input").first();
await input.click();
await input.fill("e=mc^2");
await page.waitForTimeout(100);
const preview = page.locator('[id^="math-preview"]'); const preview = page.locator('[id^="math-preview"]');
await preview.waitFor({
state: 'visible',
timeout: 5000
});
await page.waitForFunction((): boolean => {
const preview = document.querySelector('[id^="math-preview"]');
if (!preview) return false;
const katex = preview.querySelector('.katex');
return !!katex && window.getComputedStyle(preview).display !== 'none';
}, { timeout: 5000 });
await expect(preview.locator('.katex')).toBeVisible();
await expect(preview).toMatchAriaSnapshot("- math: e = m c 2"); await expect(preview).toMatchAriaSnapshot("- math: e = m c 2");
}); });

View File

@@ -75,6 +75,8 @@ export default class App {
*/ */
async closeAllTabs() { async closeAllTabs() {
await this.triggerCommand("closeAllTabs"); await this.triggerCommand("closeAllTabs");
// Page in Playwright is not updated somehow, need to click on the tab to make sure it's rendered
await this.getTab(0).click();
} }
/** /**

17
package-lock.json generated
View File

@@ -12,11 +12,7 @@
"@braintree/sanitize-url": "7.1.1", "@braintree/sanitize-url": "7.1.1",
"@electron/remote": "2.1.2", "@electron/remote": "2.1.2",
"@excalidraw/excalidraw": "0.18.0", "@excalidraw/excalidraw": "0.18.0",
"@fullcalendar/core": "6.1.15",
"@fullcalendar/daygrid": "6.1.15",
"@fullcalendar/interaction": "6.1.15",
"@highlightjs/cdn-assets": "11.11.1", "@highlightjs/cdn-assets": "11.11.1",
"@joplin/turndown-plugin-gfm": "1.0.61",
"@mermaid-js/layout-elk": "0.1.7", "@mermaid-js/layout-elk": "0.1.7",
"@mind-elixir/node-menu": "1.0.4", "@mind-elixir/node-menu": "1.0.4",
"@triliumnext/express-partial-content": "1.0.1", "@triliumnext/express-partial-content": "1.0.1",
@@ -116,6 +112,9 @@
"@electron-forge/plugin-auto-unpack-natives": "7.7.0", "@electron-forge/plugin-auto-unpack-natives": "7.7.0",
"@electron/rebuild": "3.7.1", "@electron/rebuild": "3.7.1",
"@eslint/js": "9.22.0", "@eslint/js": "9.22.0",
"@fullcalendar/core": "6.1.15",
"@fullcalendar/daygrid": "6.1.15",
"@fullcalendar/interaction": "6.1.15",
"@fullcalendar/list": "6.1.15", "@fullcalendar/list": "6.1.15",
"@fullcalendar/multimonth": "6.1.15", "@fullcalendar/multimonth": "6.1.15",
"@fullcalendar/timegrid": "6.1.15", "@fullcalendar/timegrid": "6.1.15",
@@ -2873,6 +2872,7 @@
"version": "6.1.15", "version": "6.1.15",
"resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.15.tgz", "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.15.tgz",
"integrity": "sha512-BuX7o6ALpLb84cMw1FCB9/cSgF4JbVO894cjJZ6kP74jzbUZNjtwffwRdA+Id8rrLjT30d/7TrkW90k4zbXB5Q==", "integrity": "sha512-BuX7o6ALpLb84cMw1FCB9/cSgF4JbVO894cjJZ6kP74jzbUZNjtwffwRdA+Id8rrLjT30d/7TrkW90k4zbXB5Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"preact": "~10.12.1" "preact": "~10.12.1"
@@ -2882,6 +2882,7 @@
"version": "10.12.1", "version": "10.12.1",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz", "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz",
"integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==", "integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
@@ -2892,6 +2893,7 @@
"version": "6.1.15", "version": "6.1.15",
"resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.15.tgz", "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.15.tgz",
"integrity": "sha512-j8tL0HhfiVsdtOCLfzK2J0RtSkiad3BYYemwQKq512cx6btz6ZZ2RNc/hVnIxluuWFyvx5sXZwoeTJsFSFTEFA==", "integrity": "sha512-j8tL0HhfiVsdtOCLfzK2J0RtSkiad3BYYemwQKq512cx6btz6ZZ2RNc/hVnIxluuWFyvx5sXZwoeTJsFSFTEFA==",
"dev": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@fullcalendar/core": "~6.1.15" "@fullcalendar/core": "~6.1.15"
@@ -2901,6 +2903,7 @@
"version": "6.1.15", "version": "6.1.15",
"resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.15.tgz", "resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.15.tgz",
"integrity": "sha512-DOTSkofizM7QItjgu7W68TvKKvN9PSEEvDJceyMbQDvlXHa7pm/WAVtAc6xSDZ9xmB1QramYoWGLHkCYbTW1rQ==", "integrity": "sha512-DOTSkofizM7QItjgu7W68TvKKvN9PSEEvDJceyMbQDvlXHa7pm/WAVtAc6xSDZ9xmB1QramYoWGLHkCYbTW1rQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@fullcalendar/core": "~6.1.15" "@fullcalendar/core": "~6.1.15"
@@ -3573,12 +3576,6 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@joplin/turndown-plugin-gfm": {
"version": "1.0.61",
"resolved": "https://registry.npmjs.org/@joplin/turndown-plugin-gfm/-/turndown-plugin-gfm-1.0.61.tgz",
"integrity": "sha512-m5PNP1OkktlGgmFI7r/HWON/vQA56GCiM1oTWYkY2JFc28Uc8yHj0nT46pahDyU8uRYPj4TXnxLjQzDDJ11i7w==",
"license": "MIT"
},
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8", "version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",

View File

@@ -72,11 +72,7 @@
"@braintree/sanitize-url": "7.1.1", "@braintree/sanitize-url": "7.1.1",
"@electron/remote": "2.1.2", "@electron/remote": "2.1.2",
"@excalidraw/excalidraw": "0.18.0", "@excalidraw/excalidraw": "0.18.0",
"@fullcalendar/core": "6.1.15",
"@fullcalendar/daygrid": "6.1.15",
"@fullcalendar/interaction": "6.1.15",
"@highlightjs/cdn-assets": "11.11.1", "@highlightjs/cdn-assets": "11.11.1",
"@joplin/turndown-plugin-gfm": "1.0.61",
"@mermaid-js/layout-elk": "0.1.7", "@mermaid-js/layout-elk": "0.1.7",
"@mind-elixir/node-menu": "1.0.4", "@mind-elixir/node-menu": "1.0.4",
"@triliumnext/express-partial-content": "1.0.1", "@triliumnext/express-partial-content": "1.0.1",
@@ -173,6 +169,9 @@
"@electron-forge/plugin-auto-unpack-natives": "7.7.0", "@electron-forge/plugin-auto-unpack-natives": "7.7.0",
"@electron/rebuild": "3.7.1", "@electron/rebuild": "3.7.1",
"@eslint/js": "9.22.0", "@eslint/js": "9.22.0",
"@fullcalendar/core": "6.1.15",
"@fullcalendar/daygrid": "6.1.15",
"@fullcalendar/interaction": "6.1.15",
"@fullcalendar/list": "6.1.15", "@fullcalendar/list": "6.1.15",
"@fullcalendar/multimonth": "6.1.15", "@fullcalendar/multimonth": "6.1.15",
"@fullcalendar/timegrid": "6.1.15", "@fullcalendar/timegrid": "6.1.15",

View File

@@ -0,0 +1,5 @@
dist
lib
node_modules
npm-debug.log
test/*browser.js

View File

@@ -0,0 +1,4 @@
language: node_js
node_js:
- "node"
- "6"

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Dom Christie
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,64 @@
# turndown-plugin-gfm
A [Turndown](https://github.com/domchristie/turndown) plugin which adds GitHub Flavored Markdown extensions.
This is a fork of the original [turndown-plugin-gfm](https://github.com/domchristie/turndown-plugin-gfm) for use with [Joplin](https://github.com/laurent22/joplin). The changes are:
- New: Always render tables even if they don't have a header.
- New: Don't render the border of tables that contain other tables (frequent for websites that do the layout using tables). Only render the inner tables, if any, and if they also don't contain other tables.
- New: Replace newlines (`\n`) with `<br>` inside table cells so that multi-line content is displayed correctly as Markdown.
- New: Table cells are at least three characters long (padded with spaces) so that they render correctly in GFM-compliant renderers.
- New: Handle colspan in TD tags
- Fixed: Ensure there are no blank lines inside tables (due for example to an empty `<tr>` tag)
- Fixed: Fixed importing tables that contain pipes.
## Installation
npm:
```
npm install @joplin/turndown-plugin-gfm
```
## Usage
```js
// For Node.js
var TurndownService = require('@joplin/turndown')
var turndownPluginGfm = require('@joplin/turndown-plugin-gfm')
var gfm = turndownPluginGfm.gfm
var turndownService = new TurndownService()
turndownService.use(gfm)
var markdown = turndownService.turndown('<strike>Hello world!</strike>')
```
turndown-plugin-gfm is a suite of plugins which can be applied individually. The available plugins are as follows:
- `strikethrough` (for converting `<strike>`, `<s>`, and `<del>` elements)
- `tables`
- `taskListItems`
- `gfm` (which applies all of the above)
So for example, if you only wish to convert tables:
```js
var tables = require('@joplin/turndown-plugin-gfm').tables
var turndownService = new TurndownService()
turndownService.use(tables)
```
### Typescript
To use this in a typescript project, add this to a `declarations.d.ts` file, as described in https://www.npmjs.com/package/@joplin/turndown, and then add:
```ts
declare module "@joplin/turndown-plugin-gfm" {
export const gfm: any;
// Add other named exports if necessary
}
```
## License
turndown-plugin-gfm is copyright © 2017+ Dom Christie and released under the MIT license.

View File

@@ -0,0 +1,8 @@
#!/bin/bash
set -e
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
ROOT_DIR="$SCRIPT_DIR/../.."
npm run build
cd $ROOT_DIR/packages/app-cli && npm run test -- HtmlToMd

View File

@@ -0,0 +1,8 @@
import config from './rollup.config';
export default config({
output: {
format: 'cjs',
file: 'lib/turndown-plugin-gfm.browser.cjs.js',
},
});

View File

@@ -0,0 +1,8 @@
import config from './rollup.config';
export default config({
output: {
format: 'es',
file: 'lib/turndown-plugin-gfm.browser.es.js',
},
});

View File

@@ -0,0 +1,8 @@
import config from './rollup.config';
export default config({
output: {
format: 'cjs',
file: 'lib/turndown-plugin-gfm.cjs.js',
},
});

View File

@@ -0,0 +1,8 @@
import config from './rollup.config';
export default config({
output: {
format: 'es',
file: 'lib/turndown-plugin-gfm.es.js',
},
});

View File

@@ -0,0 +1,8 @@
import config from './rollup.config';
export default config({
output: {
format: 'iife',
file: 'dist/turndown-plugin-gfm.js',
},
});

View File

@@ -0,0 +1,7 @@
export default function(config) {
return {
name: 'turndownPluginGfm',
input: 'src/gfm.js',
output: config.output,
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
{
"name": "@joplin/turndown-plugin-gfm",
"description": "Turndown plugin to add GitHub Flavored Markdown extensions.",
"publishConfig": {
"access": "public"
},
"version": "1.0.61",
"author": "Dom Christie",
"main": "lib/turndown-plugin-gfm.cjs.js",
"devDependencies": {
"browserify": "14.5.0",
"rollup": "0.50.1",
"standard": "17.1.0",
"turndown": "7.2.0",
"turndown-attendant": "0.0.3"
},
"files": [
"lib",
"dist"
],
"keywords": [
"turndown",
"turndown-plugin",
"html-to-markdown",
"html",
"markdown",
"github-flavored-markdown",
"gfm"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/laurent22/joplin-turndown-plugin-gfm.git"
},
"scripts": {
"build-all": "npm run build-cjs && npm run build-es && npm run build-iife",
"build": "rollup -c config/rollup.config.cjs.js",
"build-cjs": "rollup -c config/rollup.config.cjs.js && rollup -c config/rollup.config.browser.cjs.js",
"build-es": "rollup -c config/rollup.config.es.js && rollup -c config/rollup.config.browser.es.js",
"build-iife": "rollup -c config/rollup.config.iife.js",
"build-test": "browserify test/turndown-plugin-gfm-test.js --outfile test/turndown-plugin-gfm-test.browser.js",
"prepare": "npm run build"
},
"gitHead": "05a29b450962bf05a8642bbd39446a1f679a96ba"
}

View File

@@ -0,0 +1,3 @@
#!/bin/bash
npm version patch
npm publish

View File

@@ -0,0 +1,15 @@
import highlightedCodeBlock from './highlighted-code-block.js'
import strikethrough from './strikethrough.js'
import tables from './tables.js'
import taskListItems from './task-list-items.js'
function gfm (turndownService) {
turndownService.use([
highlightedCodeBlock,
strikethrough,
tables,
taskListItems
])
}
export { gfm, highlightedCodeBlock, strikethrough, tables, taskListItems }

View File

@@ -0,0 +1,25 @@
var highlightRegExp = /highlight-(?:text|source)-([a-z0-9]+)/
export default function highlightedCodeBlock (turndownService) {
turndownService.addRule('highlightedCodeBlock', {
filter: function (node) {
var firstChild = node.firstChild
return (
node.nodeName === 'DIV' &&
highlightRegExp.test(node.className) &&
firstChild &&
firstChild.nodeName === 'PRE'
)
},
replacement: function (content, node, options) {
var className = node.className || ''
var language = (className.match(highlightRegExp) || [null, ''])[1]
return (
'\n\n' + options.fence + language + '\n' +
node.firstChild.textContent +
'\n' + options.fence + '\n\n'
)
}
})
}

View File

@@ -0,0 +1,8 @@
export default function strikethrough (turndownService) {
turndownService.addRule('strikethrough', {
filter: ['del', 's', 'strike'],
replacement: function (content) {
return '~~' + content + '~~'
}
})
}

View File

@@ -0,0 +1,286 @@
var indexOf = Array.prototype.indexOf
var every = Array.prototype.every
var rules = {}
var alignMap = { left: ':---', right: '---:', center: ':---:' };
let isCodeBlock_ = null;
let options_ = null;
// We need to cache the result of tableShouldBeSkipped() as it is expensive.
// Caching it means we went from about 9000 ms for rendering down to 90 ms.
// Fixes https://github.com/laurent22/joplin/issues/6736
const tableShouldBeSkippedCache_ = new WeakMap();
function getAlignment(node) {
return node ? (node.getAttribute('align') || node.style.textAlign || '').toLowerCase() : '';
}
function getBorder(alignment) {
return alignment ? alignMap[alignment] : '---';
}
function getColumnAlignment(table, columnIndex) {
var votes = {
left: 0,
right: 0,
center: 0,
'': 0,
};
var align = '';
for (var i = 0; i < table.rows.length; ++i) {
var row = table.rows[i];
if (columnIndex < row.childNodes.length) {
var cellAlignment = getAlignment(row.childNodes[columnIndex]);
++votes[cellAlignment];
if (votes[cellAlignment] > votes[align]) {
align = cellAlignment;
}
}
}
return align;
}
rules.tableCell = {
filter: ['th', 'td'],
replacement: function (content, node) {
if (tableShouldBeSkipped(nodeParentTable(node))) return content;
return cell(content, node)
}
}
rules.tableRow = {
filter: 'tr',
replacement: function (content, node) {
const parentTable = nodeParentTable(node);
if (tableShouldBeSkipped(parentTable)) return content;
var borderCells = ''
if (isHeadingRow(node)) {
const colCount = tableColCount(parentTable);
for (var i = 0; i < colCount; i++) {
const childNode = i < node.childNodes.length ? node.childNodes[i] : null;
var border = getBorder(getColumnAlignment(parentTable, i));
borderCells += cell(border, childNode, i);
}
}
return '\n' + content + (borderCells ? '\n' + borderCells : '')
}
}
rules.table = {
filter: function (node, options) {
return node.nodeName === 'TABLE';
},
replacement: function (content, node) {
// Only convert tables that can result in valid Markdown
// Other tables are kept as HTML using `keep` (see below).
if (tableShouldBeHtml(node, options_)) {
return node.outerHTML;
} else {
if (tableShouldBeSkipped(node)) return content;
// Ensure there are no blank lines
content = content.replace(/\n+/g, '\n')
// If table has no heading, add an empty one so as to get a valid Markdown table
var secondLine = content.trim().split('\n');
if (secondLine.length >= 2) secondLine = secondLine[1]
var secondLineIsDivider = /\| :?---/.test(secondLine);
var columnCount = tableColCount(node);
var emptyHeader = ''
if (columnCount && !secondLineIsDivider) {
emptyHeader = '|' + ' |'.repeat(columnCount) + '\n' + '|'
for (var columnIndex = 0; columnIndex < columnCount; ++columnIndex) {
emptyHeader += ' ' + getBorder(getColumnAlignment(node, columnIndex)) + ' |';
}
}
const captionContent = node.caption ? node.caption.textContent || '' : '';
const caption = captionContent ? `${captionContent}\n\n` : '';
const tableContent = `${emptyHeader}${content}`.trimStart();
return `\n\n${caption}${tableContent}\n\n`;
}
}
}
rules.tableCaption = {
filter: ['caption'],
replacement: () => '',
};
rules.tableColgroup = {
filter: ['colgroup', 'col'],
replacement: () => '',
};
rules.tableSection = {
filter: ['thead', 'tbody', 'tfoot'],
replacement: function (content) {
return content
}
}
// A tr is a heading row if:
// - the parent is a THEAD
// - or if its the first child of the TABLE or the first TBODY (possibly
// following a blank THEAD)
// - and every cell is a TH
function isHeadingRow (tr) {
var parentNode = tr.parentNode
return (
parentNode.nodeName === 'THEAD' ||
(
parentNode.firstChild === tr &&
(parentNode.nodeName === 'TABLE' || isFirstTbody(parentNode)) &&
every.call(tr.childNodes, function (n) { return n.nodeName === 'TH' })
)
)
}
function isFirstTbody (element) {
var previousSibling = element.previousSibling
return (
element.nodeName === 'TBODY' && (
!previousSibling ||
(
previousSibling.nodeName === 'THEAD' &&
/^\s*$/i.test(previousSibling.textContent)
)
)
)
}
function cell (content, node = null, index = null) {
if (index === null) index = indexOf.call(node.parentNode.childNodes, node)
var prefix = ' '
if (index === 0) prefix = '| '
let filteredContent = content.trim().replace(/\n\r/g, '<br>').replace(/\n/g, "<br>");
filteredContent = filteredContent.replace(/\|+/g, '\\|')
while (filteredContent.length < 3) filteredContent += ' ';
if (node) filteredContent = handleColSpan(filteredContent, node, ' ');
return prefix + filteredContent + ' |'
}
function nodeContainsTable(node) {
if (!node.childNodes) return false;
for (let i = 0; i < node.childNodes.length; i++) {
const child = node.childNodes[i];
if (child.nodeName === 'TABLE') return true;
if (nodeContainsTable(child)) return true;
}
return false;
}
const nodeContains = (node, types) => {
if (!node.childNodes) return false;
for (let i = 0; i < node.childNodes.length; i++) {
const child = node.childNodes[i];
if (types === 'code' && isCodeBlock_ && isCodeBlock_(child)) return true;
if (types.includes(child.nodeName)) return true;
if (nodeContains(child, types)) return true;
}
return false;
}
const tableShouldBeHtml = (tableNode, options) => {
const possibleTags = [
'UL',
'OL',
'H1',
'H2',
'H3',
'H4',
'H5',
'H6',
'HR',
'BLOCKQUOTE',
'PRE'
];
// In general we should leave as HTML tables that include other tables. The
// exception is with the Web Clipper when we import a web page with a layout
// that's made of HTML tables. In that case we have this logic of removing the
// outer table and keeping only the inner ones. For the Rich Text editor
// however we always want to keep nested tables.
if (options.preserveNestedTables) possibleTags.push('TABLE');
return nodeContains(tableNode, 'code') ||
nodeContains(tableNode, possibleTags);
}
// Various conditions under which a table should be skipped - i.e. each cell
// will be rendered one after the other as if they were paragraphs.
function tableShouldBeSkipped(tableNode) {
const cached = tableShouldBeSkippedCache_.get(tableNode);
if (cached !== undefined) return cached;
const result = tableShouldBeSkipped_(tableNode);
tableShouldBeSkippedCache_.set(tableNode, result);
return result;
}
function tableShouldBeSkipped_(tableNode) {
if (!tableNode) return true;
if (!tableNode.rows) return true;
if (tableNode.rows.length === 1 && tableNode.rows[0].childNodes.length <= 1) return true; // Table with only one cell
if (nodeContainsTable(tableNode)) return true;
return false;
}
function nodeParentDiv(node) {
let parent = node.parentNode;
while (parent.nodeName !== 'DIV') {
parent = parent.parentNode;
if (!parent) return null;
}
return parent;
}
function nodeParentTable(node) {
let parent = node.parentNode;
while (parent.nodeName !== 'TABLE') {
parent = parent.parentNode;
if (!parent) return null;
}
return parent;
}
function handleColSpan(content, node, emptyChar) {
const colspan = node.getAttribute('colspan') || 1;
for (let i = 1; i < colspan; i++) {
content += ' | ' + emptyChar.repeat(3);
}
return content
}
function tableColCount(node) {
let maxColCount = 0;
for (let i = 0; i < node.rows.length; i++) {
const row = node.rows[i]
const colCount = row.childNodes.length
if (colCount > maxColCount) maxColCount = colCount
}
return maxColCount
}
export default function tables (turndownService) {
isCodeBlock_ = turndownService.isCodeBlock;
options_ = turndownService.options;
turndownService.keep(function (node) {
if (node.nodeName === 'TABLE' && tableShouldBeHtml(node, turndownService.options)) return true;
return false;
});
for (var key in rules) turndownService.addRule(key, rules[key])
}

View File

@@ -0,0 +1,10 @@
export default function taskListItems (turndownService) {
turndownService.addRule('taskListItems', {
filter: function (node) {
return node.type === 'checkbox' && node.parentNode.nodeName === 'LI'
},
replacement: function (content, node) {
return (node.checked ? '[x]' : '[ ]') + ' '
}
})
}

View File

@@ -0,0 +1,323 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>turndown test runner</title>
<link rel="stylesheet" href="../node_modules/turndown-attendant/dist/styles.css">
</head>
<body>
<!-- TEST CASES -->
<div class="case" data-name="strike">
<div class="input"><strike>Lorem ipsum</strike></div>
<pre class="expected">~Lorem ipsum~</pre>
</div>
<div class="case" data-name="s">
<div class="input"><s>Lorem ipsum</s></div>
<pre class="expected">~Lorem ipsum~</pre>
</div>
<div class="case" data-name="del">
<div class="input"><del>Lorem ipsum</del></div>
<pre class="expected">~Lorem ipsum~</pre>
</div>
<div class="case" data-name="unchecked inputs">
<div class="input"><ul><li><input type=checkbox>Check Me!</li></ul></div>
<pre class="expected">* [ ] Check Me!</pre>
</div>
<div class="case" data-name="checked inputs">
<div class="input"><ul><li><input type=checkbox checked>Checked!</li></ul></div>
<pre class="expected">* [x] Checked!</pre>
</div>
<div class="case" data-name="basic table">
<div class="input">
<table>
<thead>
<tr>
<th>Column 1</th>
<th>Column 2</th>
</tr>
</thead>
<tbody>
<tr>
<td>Row 1, Column 1</td>
<td>Row 1, Column 2</td>
</tr>
<tr>
<td>Row 2, Column 1</td>
<td>Row 2, Column 2</td>
</tr>
</tbody>
</table>
</div>
<pre class="expected">| Column 1 | Column 2 |
| --- | --- |
| Row 1, Column 1 | Row 1, Column 2 |
| Row 2, Column 1 | Row 2, Column 2 |</pre>
</div>
<div class="case" data-name="cell alignment">
<div class="input">
<table>
<thead>
<tr>
<th align="left">Column 1</th>
<th align="center">Column 2</th>
<th align="right">Column 3</th>
<th align="foo">Column 4</th>
</tr>
</thead>
<tbody>
<tr>
<td>Row 1, Column 1</td>
<td>Row 1, Column 2</td>
<td>Row 1, Column 3</td>
<td>Row 1, Column 4</td>
</tr>
<tr>
<td>Row 2, Column 1</td>
<td>Row 2, Column 2</td>
<td>Row 2, Column 3</td>
<td>Row 2, Column 4</td>
</tr>
</tbody>
</table>
</div>
<pre class="expected">| Column 1 | Column 2 | Column 3 | Column 4 |
| :-- | :-: | --: | --- |
| Row 1, Column 1 | Row 1, Column 2 | Row 1, Column 3 | Row 1, Column 4 |
| Row 2, Column 1 | Row 2, Column 2 | Row 2, Column 3 | Row 2, Column 4 |</pre>
</div>
<div class="case" data-name="empty cells">
<div class="input">
<table>
<thead>
<tr>
<th align="left">Column 1</th>
<th align="center">Column 2</th>
<th align="right">Column 3</th>
<th align="foo">Column 4</th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
<td>Row 1, Column 2</td>
<td>Row 1, Column 3</td>
<td>Row 1, Column 4</td>
</tr>
<tr>
<td>Row 2, Column 1</td>
<td></td>
<td>Row 2, Column 3</td>
<td>Row 2, Column 4</td>
</tr>
<tr>
<td>Row 3, Column 1</td>
<td>Row 3, Column 2</td>
<td></td>
<td>Row 3, Column 4</td>
</tr>
<tr>
<td>Row 4, Column 1</td>
<td>Row 4, Column 2</td>
<td>Row 4, Column 3</td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td>Row 5, Column 4</td>
</tr>
</tbody>
</table>
</div>
<pre class="expected">| Column 1 | Column 2 | Column 3 | Column 4 |
| :-- | :-: | --: | --- |
| | Row 1, Column 2 | Row 1, Column 3 | Row 1, Column 4 |
| Row 2, Column 1 | | Row 2, Column 3 | Row 2, Column 4 |
| Row 3, Column 1 | Row 3, Column 2 | | Row 3, Column 4 |
| Row 4, Column 1 | Row 4, Column 2 | Row 4, Column 3 | |
| | | | Row 5, Column 4 |</pre>
</div>
<div class="case" data-name="empty rows">
<div class="input">
<table>
<thead>
<td>Heading 1</td>
<td>Heading 2</td>
</thead>
<tbody>
<tr>
<td>Row 1</td>
<td>Row 1</td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
<tr>
<td>Row 3</td>
<td>Row 3</td>
</tr>
</tbody>
</table>
</div>
<pre class="expected">| Heading 1 | Heading 2 |
| --- | --- |
| Row 1 | Row 1 |
| Row 3 | Row 3 |</pre>
</div>
<div class="case" data-name="th in first row">
<div class="input">
<table>
<tr>
<th>Heading</th>
</tr>
<tr>
<td>Content</td>
</tr>
</table>
</div>
<pre class="expected">| Heading |
| --- |
| Content |</pre>
</div>
<div class="case" data-name="th first row in tbody">
<div class="input">
<table>
<tbody>
<tr>
<th>Heading</th>
</tr>
<tr>
<td>Content</td>
</tr>
</tbody>
</table>
</div>
<pre class="expected">| Heading |
| --- |
| Content |</pre>
</div>
<div class="case" data-name="table with two tbodies">
<div class="input">
<table>
<tbody>
<tr>
<th>Heading</th>
</tr>
<tr>
<td>Content</td>
</tr>
</tbody>
<tbody>
<tr>
<th>Heading</th>
</tr>
<tr>
<td>Content</td>
</tr>
</tbody>
</table>
</div>
<pre class="expected">| Heading |
| --- |
| Content |
| Heading |
| Content |</pre>
</div>
<div class="case" data-name="heading cells in both thead and tbody">
<div class="input">
<table>
<thead><tr><th>Heading</th></tr></thead>
<tbody><tr><th>Cell</th></tr></tbody>
</table>
</div>
<pre class="expected">| Heading |
| --- |
| Cell |</pre>
</div>
<div class="case" data-name="empty head">
<div class="input">
<table>
<thead><tr><th></th></tr></thead>
<tbody><tr><th>Heading</th></tr></tbody>
</table>
</div>
<pre class="expected">| Heading |
| --- |</pre>
</div>
<div class="case" data-name="non-definitive heading row (converted but with empty header)">
<div class="input">
<table>
<tr><td>Row 1 Cell 1</td><td>Row 1 Cell 2</td></tr>
<tr><td>Row 2 Cell 1</td><td>Row 2 Cell 2</td></tr>
</table>
</div>
<pre class="expected">| | |
| --- | --- |
| Row 1 Cell 1 | Row 1 Cell 2 |
| Row 2 Cell 1 | Row 2 Cell 2 |</pre>
</div>
<div class="case" data-name="non-definitive heading row with th (converted but with empty header)">
<div class="input">
<table>
<tr>
<th>Heading</th>
<td>Not a heading</td>
</tr>
<tr>
<td>Heading</td>
<td>Not a heading</td>
</tr>
</table>
</div>
<pre class="expected">| | |
| --- | --- |
| Heading | Not a heading |
| Heading | Not a heading |</pre>
</div>
<div class="case" data-name="highlighted code block with html">
<div class="input">
<div class="highlight highlight-text-html-basic">
<pre>&lt;<span class="pl-ent">p</span>&gt;Hello world&lt;/<span class="pl-ent">p</span>&gt;</pre>
</div>
</div>
<pre class="expected">```html
&lt;p&gt;Hello world&lt;/p&gt;
```</pre>
</div>
<div class="case" data-name="highlighted code block with js">
<div class="input">
<div class="highlight highlight-source-js">
<pre>;(<span class="pl-k">function</span> () {})()</pre>
</div>
</div>
<pre class="expected">```js
;(function () {})()
```</pre>
</div>
<!-- /TEST CASES -->
<script src="turndown-plugin-gfm-test.browser.js"></script>
</body>
</html>

View File

@@ -0,0 +1,13 @@
const Attendant = require('turndown-attendant');
const TurndownService = require('turndown');
const gfm = require('../lib/turndown-plugin-gfm.cjs').gfm;
const attendant = new Attendant({
file: `${__dirname}/index.html`,
TurndownService: TurndownService,
beforeEach: function(turndownService) {
turndownService.use(gfm);
},
});
attendant.run();

View File

@@ -22,7 +22,6 @@ import type LoadResults from "../services/load_results.js";
import type { Attribute } from "../services/attribute_parser.js"; import type { Attribute } from "../services/attribute_parser.js";
import type NoteTreeWidget from "../widgets/note_tree.js"; import type NoteTreeWidget from "../widgets/note_tree.js";
import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js"; import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js";
import type { ContextMenuEvent } from "../menus/context_menu.js";
import type TypeWidget from "../widgets/type_widgets/type_widget.js"; import type TypeWidget from "../widgets/type_widgets/type_widget.js";
import type EditableTextTypeWidget from "../widgets/type_widgets/editable_text.js"; import type EditableTextTypeWidget from "../widgets/type_widgets/editable_text.js";
import type FAttribute from "../entities/fattribute.js"; import type FAttribute from "../entities/fattribute.js";
@@ -58,8 +57,8 @@ export interface ContextMenuCommandData extends CommandData {
} }
export interface NoteCommandData extends CommandData { export interface NoteCommandData extends CommandData {
notePath?: string; notePath?: string | null;
hoistedNoteId?: string; hoistedNoteId?: string | null;
viewScope?: ViewScope; viewScope?: ViewScope;
} }
@@ -297,16 +296,13 @@ type EventMappings = {
noteContext: NoteContext; noteContext: NoteContext;
notePath?: string | null; notePath?: string | null;
}; };
noteSwitchedAndActivatedEvent: { noteSwitchedAndActivated: {
noteContext: NoteContext; noteContext: NoteContext;
notePath: string; notePath: string;
}; };
setNoteContext: { setNoteContext: {
noteContext: NoteContext; noteContext: NoteContext;
}; };
noteTypeMimeChangedEvent: {
noteId: string;
};
reEvaluateHighlightsListWidgetVisibility: { reEvaluateHighlightsListWidgetVisibility: {
noteId: string | undefined; noteId: string | undefined;
}; };
@@ -327,14 +323,14 @@ type EventMappings = {
noteId: string; noteId: string;
ntxId: string | null; ntxId: string | null;
}; };
contextsReopenedEvent: { contextsReopened: {
mainNtxId: string; mainNtxId: string | null;
tabPosition: number; tabPosition: number;
}; };
noteDetailRefreshed: { noteDetailRefreshed: {
ntxId?: string | null; ntxId?: string | null;
}; };
noteContextReorderEvent: { noteContextReorder: {
oldMainNtxId: string; oldMainNtxId: string;
newMainNtxId: string; newMainNtxId: string;
ntxIdsInOrder: string[]; ntxIdsInOrder: string[];
@@ -342,7 +338,7 @@ type EventMappings = {
newNoteContextCreated: { newNoteContextCreated: {
noteContext: NoteContext; noteContext: NoteContext;
}; };
noteContextRemovedEvent: { noteContextRemoved: {
ntxIds: string[]; ntxIds: string[];
}; };
exportSvg: { exportSvg: {
@@ -363,7 +359,7 @@ type EventMappings = {
relationMapResetPanZoom: { ntxId: string | null | undefined }; relationMapResetPanZoom: { ntxId: string | null | undefined };
relationMapResetZoomIn: { ntxId: string | null | undefined }; relationMapResetZoomIn: { ntxId: string | null | undefined };
relationMapResetZoomOut: { ntxId: string | null | undefined }; relationMapResetZoomOut: { ntxId: string | null | undefined };
activeNoteChangedEvent: {}; activeNoteChanged: {};
showAddLinkDialog: { showAddLinkDialog: {
textTypeWidget: EditableTextTypeWidget; textTypeWidget: EditableTextTypeWidget;
text: string; text: string;
@@ -374,7 +370,6 @@ type EventMappings = {
cloneNoteIdsTo: { cloneNoteIdsTo: {
noteIds: string[]; noteIds: string[];
}; };
}; };
export type EventListener<T extends EventNames> = { export type EventListener<T extends EventNames> = {

View File

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

View File

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

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ const TPL = `
height: 100%; height: 100%;
} }
.mind-elixir .node-menu { .map-container .node-menu {
position: absolute; position: absolute;
top: 60px; top: 60px;
right: 20px; right: 20px;
@@ -38,28 +38,28 @@ const TPL = `
transition: .3s all transition: .3s all
} }
.mind-elixir .node-menu.close { .map-container .node-menu.close {
height: 29px; height: 29px;
width: 46px; width: 46px;
overflow: hidden overflow: hidden
} }
.mind-elixir .node-menu .button-container { .map-container .node-menu .button-container {
padding: 3px 0; padding: 3px 0;
direction: rtl direction: rtl
} }
.mind-elixir .node-menu #nm-tag { .map-container .node-menu #nm-tag {
margin-top: 20px margin-top: 20px
} }
.mind-elixir .node-menu .nm-fontsize-container { .map-container .node-menu .nm-fontsize-container {
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
margin-bottom: 20px margin-bottom: 20px
} }
.mind-elixir .node-menu .nm-fontsize-container div { .map-container .node-menu .nm-fontsize-container div {
height: 36px; height: 36px;
width: 36px; width: 36px;
display: flex; display: flex;
@@ -71,12 +71,12 @@ const TPL = `
border-radius: 100% border-radius: 100%
} }
.mind-elixir .node-menu .nm-fontcolor-container { .map-container .node-menu .nm-fontcolor-container {
margin-bottom: 10px margin-bottom: 10px
} }
.mind-elixir .node-menu input, .map-container .node-menu input,
.mind-elixir .node-menu textarea { .map-container .node-menu textarea {
background: var(--input-background-color); background: var(--input-background-color);
border: 1px solid var(--panel-border-color); border: 1px solid var(--panel-border-color);
border-radius: var(--bs-border-radius); border-radius: var(--bs-border-radius);
@@ -87,17 +87,17 @@ const TPL = `
box-sizing: border-box; box-sizing: border-box;
} }
.mind-elixir .node-menu textarea { .map-container .node-menu textarea {
resize: none resize: none
} }
.mind-elixir .node-menu .split6 { .map-container .node-menu .split6 {
display: inline-block; display: inline-block;
width: 16.66%; width: 16.66%;
margin-bottom: 5px margin-bottom: 5px
} }
.mind-elixir .node-menu .palette { .map-container .node-menu .palette {
border-radius: 100%; border-radius: 100%;
width: 21px; width: 21px;
height: 21px; height: 21px;
@@ -105,35 +105,35 @@ const TPL = `
margin: auto margin: auto
} }
.mind-elixir .node-menu .nmenu-selected, .map-container .node-menu .nmenu-selected,
.mind-elixir .node-menu .palette:hover { .map-container .node-menu .palette:hover {
box-shadow: tomato 0 0 0 2px; box-shadow: tomato 0 0 0 2px;
background-color: #c7e9fa background-color: #c7e9fa
} }
.mind-elixir .node-menu .size-selected { .map-container .node-menu .size-selected {
background-color: tomato !important; background-color: tomato !important;
border-color: tomato; border-color: tomato;
fill: #fff; fill: #fff;
color: #fff color: #fff
} }
.mind-elixir .node-menu .size-selected svg { .map-container .node-menu .size-selected svg {
color: #fff color: #fff
} }
.mind-elixir .node-menu .bof { .map-container .node-menu .bof {
text-align: center text-align: center
} }
.mind-elixir .node-menu .bof span { .map-container .node-menu .bof span {
display: inline-block; display: inline-block;
font-size: 14px; font-size: 14px;
border-radius: 4px; border-radius: 4px;
padding: 2px 5px padding: 2px 5px
} }
.mind-elixir .node-menu .bof .selected { .map-container .node-menu .bof .selected {
background-color: tomato; background-color: tomato;
color: #fff color: #fff
} }

View File

@@ -127,13 +127,24 @@
--left-pane-item-action-button-hover-shadow: 2px 2px 3px rgba(0, 0, 0, 0.15); --left-pane-item-action-button-hover-shadow: 2px 2px 3px rgba(0, 0, 0, 0.15);
--left-pane-item-selected-action-button-hover-shadow: 2px 2px 10px rgba(0, 0, 0, 0.25); --left-pane-item-selected-action-button-hover-shadow: 2px 2px 10px rgba(0, 0, 0, 0.25);
--launcher-pane-background-color: #1a1a1a; /* Deprecated: now local variables in #launcher, with the values dependent on the current layout. */
--launcher-pane-horizontal-background-color: #282828; --launcher-pane-background-color: unset;
--launcher-pane-horizontal-border-color: rgb(22, 22, 22); --launcher-pane-text-color: unset;
--launcher-pane-text-color: #909090;
--launcher-pane-button-hover-color: #ffffff; --launcher-pane-vert-background-color: #1a1a1a;
--launcher-pane-button-hover-background: #ffffff1c; --launcher-pane-vert-text-color: #909090;
--launcher-pane-button-hover-shadow: 4px 4px 4px rgba(0, 0, 0, 0.2); --launcher-pane-vert-button-hover-color: #ffffff;
--launcher-pane-vert-button-hover-background: #ffffff1c;
--launcher-pane-vert-button-hover-shadow: 4px 4px 4px rgba(0, 0, 0, 0.2);
--launcher-pane-vert-button-focus-outline-color: var(--input-focus-outline-color);
--launcher-pane-horiz-border-color: rgb(22, 22, 22);
--launcher-pane-horiz-background-color: #282828;
--launcher-pane-horiz-text-color: #909090;
--launcher-pane-horiz-button-hover-color: #ffffff;
--launcher-pane-horiz-button-hover-background: #ffffff1c;
--launcher-pane-horiz-button-hover-shadow: unset;
--launcher-pane-horiz-button-focus-outline-color: var(--input-focus-outline-color);
--protected-session-active-icon-color: #8edd8e; --protected-session-active-icon-color: #8edd8e;
--sync-status-error-pulse-color: #f47871; --sync-status-error-pulse-color: #f47871;

View File

@@ -121,13 +121,23 @@
--left-pane-item-action-button-hover-shadow: 2px 2px 3px rgba(0, 0, 0, 0.15); --left-pane-item-action-button-hover-shadow: 2px 2px 3px rgba(0, 0, 0, 0.15);
--left-pane-item-selected-action-button-hover-shadow: 2px 2px 10px rgba(0, 0, 0, 0.25); --left-pane-item-selected-action-button-hover-shadow: 2px 2px 10px rgba(0, 0, 0, 0.25);
--launcher-pane-background-color: #e8e8e8; /* Deprecated: now local variables in #launcher, with the values dependent on the current layout. */
--launcher-pane-horizontal-background-color: #fafafa; --launcher-pane-background-color: unset;
--launcher-pane-horizontal-border-color: rgba(0, 0, 0, 0.1); --launcher-pane-text-color: unset;
--launcher-pane-text-color: #000000bd;
--launcher-pane-button-hover-color: black; --launcher-pane-vert-background-color: #e8e8e8;
--launcher-pane-button-hover-background: white; --launcher-pane-vert-text-color: #000000bd;
--launcher-pane-button-hover-shadow: 4px 4px 4px rgba(0, 0, 0, 0.075); --launcher-pane-vert-button-hover-color: black;
--launcher-pane-vert-button-hover-background: white;
--launcher-pane-vert-button-hover-shadow: 4px 4px 4px rgba(0, 0, 0, 0.075);
--launcher-pane-vert-button-focus-outline-color: var(--input-focus-outline-color);
--launcher-pane-horiz-border-color: rgba(0, 0, 0, 0.1);
--launcher-pane-horiz-background-color: #fafafa;
--launcher-pane-horiz-button-hover-color: black;
--launcher-pane-horiz-button-hover-background: var(--icon-button-hover-background);
--launcher-pane-horiz-button-hover-shadow: unset;
--launcher-pane-horiz-button-focus-outline-color: var(--input-focus-outline-color);
--protected-session-active-icon-color: #16b516; --protected-session-active-icon-color: #16b516;
--sync-status-error-pulse-color: #ff5528; --sync-status-error-pulse-color: #ff5528;

View File

@@ -26,11 +26,15 @@
--left-pane-item-selected-shadow-size: 2px; --left-pane-item-selected-shadow-size: 2px;
--launcher-pane-size: 58px; --launcher-pane-vert-size: 58px;
--launcher-pane-horizontal-size: 54px; --launcher-pane-vert-icon-size: 150%;
--launcher-pane-horizontal-icon-size: 20px; --launcher-pane-vert-button-margin: 6px;
--launcher-pane-button-margin: 6px; --launcher-pane-vert-button-gap: 3px;
--launcher-pane-button-gap: 3px;
--launcher-pane-horiz-size: 54px;
--launcher-pane-horiz-icon-size: 20px;
--launcher-pane-horiz-button-margin: 8px;
--launcher-pane-horiz-button-gap: 3px;
--tree-actions-toolbar-horizontal-margin: 8px; --tree-actions-toolbar-horizontal-margin: 8px;
--tree-actions-toolbar-vertical-margin: 8px; --tree-actions-toolbar-vertical-margin: 8px;

View File

@@ -26,9 +26,7 @@ body {
} }
body.layout-horizontal { body.layout-horizontal {
--launcher-pane-background-color: var(--launcher-pane-horizontal-background-color); --active-tab-background-color: var(--launcher-pane-horiz-background-color);
--launcher-pane-size: var(--launcher-pane-horizontal-size);
--active-tab-background-color: var(--launcher-pane-background-color);
--active-tab-hover-background-color: var(--active-tab-background-color); --active-tab-hover-background-color: var(--active-tab-background-color);
--new-tab-button-background: transparent; --new-tab-button-background: transparent;
--tab-bar-height: 44px; --tab-bar-height: 44px;
@@ -42,16 +40,19 @@ body.mobile {
/* #region Mica */ /* #region Mica */
body.background-effects.platform-win32 { body.background-effects.platform-win32 {
--launcher-pane-horizontal-border-color: rgba(0, 0, 0, 0.15);
--launcher-pane-background-color: rgba(255, 255, 255, 0.7);
--tab-background-color: transparent; --tab-background-color: transparent;
--new-tab-button-background: transparent; --new-tab-button-background: transparent;
--active-tab-background-color: var(--launcher-pane-background-color); --active-tab-background-color: var(--launcher-pane-background-color); /* TODO: fix */
--background-material: tabbed; --background-material: tabbed;
} }
body.background-effects.platform-win32 #launcher-pane {
--launcher-pane-horizontal-border-color: rgba(0, 0, 0, 0.15);
--launcher-pane-background-color: rgba(255, 255, 255, 0.7);
}
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
body.background-effects.platform-win32 { body.background-effects.platform-win32 #launcher-pane {
--launcher-pane-horizontal-border-color: rgba(0, 0, 0, 0.5); --launcher-pane-horizontal-border-color: rgba(0, 0, 0, 0.5);
--launcher-pane-background-color: rgba(255, 255, 255, 0.09); --launcher-pane-background-color: rgba(255, 255, 255, 0.09);
} }
@@ -59,11 +60,14 @@ body.background-effects.platform-win32 {
body.background-effects.platform-win32.layout-vertical { body.background-effects.platform-win32.layout-vertical {
--left-pane-background-color: transparent; --left-pane-background-color: transparent;
--launcher-pane-background-color: rgba(255, 255, 255, 0.055);
--left-pane-item-hover-background: rgba(127, 127, 127, 0.05); --left-pane-item-hover-background: rgba(127, 127, 127, 0.05);
--background-material: mica; --background-material: mica;
} }
body.background-effects.platform-win32.layout-vertical #launcher-pane {
--launcher-pane-background-color: rgba(255, 255, 255, 0.055);
}
body.background-effects.platform-win32, body.background-effects.platform-win32,
body.background-effects.platform-win32 #root-widget, body.background-effects.platform-win32 #root-widget,
body.background-effects.platform-win32 #launcher-pane .launcher-button { body.background-effects.platform-win32 #launcher-pane .launcher-button {
@@ -141,18 +145,44 @@ body.layout-horizontal > .horizontal {
} }
#launcher-pane.vertical { #launcher-pane.vertical {
--launcher-pane-border-color: unset;
--launcher-pane-background-color: var(--launcher-pane-vert-background-color);
--launcher-pane-text-color: var(--launcher-pane-vert-text-color);
--launcher-pane-button-hover-color: var(--launcher-pane-vert-button-hover-color);
--launcher-pane-button-hover-background: var(--launcher-pane-vert-button-hover-background);
--launcher-pane-button-hover-shadow: var(--launcher-pane-vert-button-hover-shadow);
--launcher-pane-button-focus-outline-color: var(--launcher-pane-vert-button-focus-outline-color);
--launcher-pane-size: var(--launcher-pane-vert-size);
--launcher-pane-icon-size: var(--launcher-pane-vert-icon-size);
--launcher-pane-button-margin: var(--launcher-pane-vert-button-margin);
--launcher-pane-button-gap: var(--launcher-pane-vert-button-gap);
width: var(--launcher-pane-size) !important; width: var(--launcher-pane-size) !important;
padding-bottom: var(--launcher-pane-button-gap); padding-bottom: var(--launcher-pane-button-gap);
} }
#launcher-pane.horizontal { #launcher-pane.horizontal {
--launcher-pane-border-color: var(--launcher-pane-horiz-border-color);
--launcher-pane-background-color: var(--launcher-pane-horiz-background-color);
--launcher-pane-text-color: var(--launcher-pane-horiz-text-color);
--launcher-pane-button-hover-color: var(--launcher-pane-horiz-button-hover-color);
--launcher-pane-button-hover-background: var(--launcher-pane-horiz-button-hover-background);
--launcher-pane-button-hover-shadow: var(--launcher-pane-horiz-button-hover-shadow);
--launcher-pane-button-focus-outline-color: var(--launcher-pane-horiz-button-focus-outline-color);
--launcher-pane-size: var(--launcher-pane-horiz-size);
--launcher-pane-icon-size: var(--launcher-pane-horiz-icon-size);
--launcher-pane-button-margin: var(--launcher-pane-horiz-button-margin);
--launcher-pane-button-gap: var(--launcher-pane-horiz-button-gap);
height: var(--launcher-pane-size) !important; height: var(--launcher-pane-size) !important;
align-items: center; align-items: center;
} }
@media (max-width: 991px) { @media (max-width: 991px) {
#mobile-bottom-bar { #mobile-bottom-bar {
background: var(--launcher-pane-background-color); background: var(--launcher-pane-horiz-background-color);
} }
body.mobile #launcher-pane { body.mobile #launcher-pane {
@@ -163,7 +193,7 @@ body.layout-horizontal > .horizontal {
@media (min-width: 992px) { @media (min-width: 992px) {
#launcher-pane.horizontal { #launcher-pane.horizontal {
border-top: unset; border-top: unset;
border-bottom: 1px solid var(--launcher-pane-horizontal-border-color); border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
} }
} }
@@ -213,6 +243,27 @@ body.layout-horizontal > .horizontal {
align-items: center; align-items: center;
} }
#launcher-pane .launcher-button {
font-size: var(--launcher-pane-icon-size) !important;
}
#launcher-pane .launcher-button:focus,
#launcher-pane .global-menu button:focus {
outline: none;
}
#launcher-pane .launcher-button:focus-visible,
#launcher-pane.horizontal .global-menu button:focus-visible {
outline: 2px solid var(--launcher-pane-button-focus-outline-color);
}
#launcher-pane.vertical .global-menu button:focus-visible svg {
outline-offset: 4px;
outline: 2px solid var(--launcher-pane-button-focus-outline-color);
border-radius: 4px;
overflow: visible;
}
#launcher-pane.vertical .spacer { #launcher-pane.vertical .spacer {
width: var(--launcher-pane-size); width: var(--launcher-pane-size);
} }
@@ -253,10 +304,6 @@ body.layout-horizontal > .horizontal {
animation: sync-status-pulse 1s ease-in-out alternate-reverse infinite; animation: sync-status-pulse 1s ease-in-out alternate-reverse infinite;
} }
#launcher-pane.horizontal .launcher-button {
font-size: var(--launcher-pane-horizontal-icon-size);
}
#launcher-pane .global-menu-button { #launcher-pane .global-menu-button {
--hover-item-background-color: transparent; --hover-item-background-color: transparent;
} }
@@ -711,7 +758,7 @@ body.layout-horizontal .tab-row-container .note-tab[active]:before {
top: var(--tab-height); top: var(--tab-height);
right: calc(100% - 1px); right: calc(100% - 1px);
height: 1px; height: 1px;
border-bottom: 1px solid var(--launcher-pane-horizontal-border-color); border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
} }
body.layout-horizontal .tab-row-container .note-tab[active]:after { body.layout-horizontal .tab-row-container .note-tab[active]:after {
@@ -723,7 +770,7 @@ body.layout-horizontal .tab-row-container .note-tab[active]:after {
right: 0; right: 0;
width: 100vw; width: 100vw;
height: 1px; height: 1px;
border-bottom: 1px solid var(--launcher-pane-horizontal-border-color); border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
} }
/* #endregion */ /* #endregion */
@@ -757,7 +804,7 @@ body.desktop:not(.background-effects.platform-win32) #root-widget.horizontal-lay
} }
#root-widget.horizontal-layout .tab-row-widget .note-tab[active] .note-tab-wrapper { #root-widget.horizontal-layout .tab-row-widget .note-tab[active] .note-tab-wrapper {
border: 1px solid var(--launcher-pane-horizontal-border-color); border: 1px solid var(--launcher-pane-horiz-border-color);
border-bottom-color: transparent; border-bottom-color: transparent;
} }

View File

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

View File

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