Merge pull request #1183 from TriliumNext/feature/calendar_view
Calendar view
40
package-lock.json
generated
@ -12,6 +12,9 @@
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@electron/remote": "2.1.2",
|
||||
"@excalidraw/excalidraw": "0.17.6",
|
||||
"@fullcalendar/core": "6.1.15",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"@fullcalendar/interaction": "6.1.15",
|
||||
"@highlightjs/cdn-assets": "11.11.1",
|
||||
"@joplin/turndown-plugin-gfm": "1.0.61",
|
||||
"@mermaid-js/layout-elk": "0.1.7",
|
||||
@ -2124,6 +2127,43 @@
|
||||
"react-dom": "^17.0.2 || ^18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fullcalendar/core": {
|
||||
"version": "6.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.15.tgz",
|
||||
"integrity": "sha512-BuX7o6ALpLb84cMw1FCB9/cSgF4JbVO894cjJZ6kP74jzbUZNjtwffwRdA+Id8rrLjT30d/7TrkW90k4zbXB5Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"preact": "~10.12.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@fullcalendar/core/node_modules/preact": {
|
||||
"version": "10.12.1",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz",
|
||||
"integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
}
|
||||
},
|
||||
"node_modules/@fullcalendar/daygrid": {
|
||||
"version": "6.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.15.tgz",
|
||||
"integrity": "sha512-j8tL0HhfiVsdtOCLfzK2J0RtSkiad3BYYemwQKq512cx6btz6ZZ2RNc/hVnIxluuWFyvx5sXZwoeTJsFSFTEFA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@fullcalendar/core": "~6.1.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@fullcalendar/interaction": {
|
||||
"version": "6.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.15.tgz",
|
||||
"integrity": "sha512-DOTSkofizM7QItjgu7W68TvKKvN9PSEEvDJceyMbQDvlXHa7pm/WAVtAc6xSDZ9xmB1QramYoWGLHkCYbTW1rQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@fullcalendar/core": "~6.1.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@gar/promisify": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
|
||||
|
@ -70,6 +70,9 @@
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@electron/remote": "2.1.2",
|
||||
"@excalidraw/excalidraw": "0.17.6",
|
||||
"@fullcalendar/core": "6.1.15",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"@fullcalendar/interaction": "6.1.15",
|
||||
"@highlightjs/cdn-assets": "11.11.1",
|
||||
"@joplin/turndown-plugin-gfm": "1.0.61",
|
||||
"@mermaid-js/layout-elk": "0.1.7",
|
||||
|
@ -290,7 +290,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
||||
return (
|
||||
this.note &&
|
||||
["default", "contextual-help"].includes(this.viewScope?.viewMode ?? "") &&
|
||||
this.note.hasChildren() &&
|
||||
(this.note.hasChildren() || this.note.getLabelValue("viewType") === "calendar") &&
|
||||
["book", "text", "code"].includes(this.note.type) &&
|
||||
this.note.mime !== "text/x-sqlite;schema=trilium" &&
|
||||
!this.note.isLabelTruthy("hideChildrenOverview")
|
||||
|
@ -11,7 +11,7 @@
|
||||
"title": "User Guide",
|
||||
"notePosition": 20,
|
||||
"prefix": null,
|
||||
"isExpanded": true,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/html",
|
||||
"attributes": [
|
||||
@ -166,7 +166,7 @@
|
||||
"title": "Features",
|
||||
"notePosition": 40,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"isExpanded": true,
|
||||
"type": "text",
|
||||
"mime": "text/html",
|
||||
"attributes": [],
|
||||
@ -314,7 +314,7 @@
|
||||
"title": "Note Types",
|
||||
"notePosition": 70,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"isExpanded": true,
|
||||
"type": "text",
|
||||
"mime": "text/html",
|
||||
"attributes": [],
|
||||
@ -536,6 +536,208 @@
|
||||
"dataFileName": "19_Geo map_image.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "pSDzQIgLGswQ",
|
||||
"notePath": [
|
||||
"OkOZllzB3fqN",
|
||||
"wmegHv51MJMd",
|
||||
"pSDzQIgLGswQ"
|
||||
],
|
||||
"title": "Book",
|
||||
"notePosition": 30,
|
||||
"prefix": null,
|
||||
"isExpanded": true,
|
||||
"type": "text",
|
||||
"mime": "text/html",
|
||||
"attributes": [
|
||||
{
|
||||
"type": "label",
|
||||
"name": "iconClass",
|
||||
"value": "bx bx-book-alt",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
}
|
||||
],
|
||||
"format": "html",
|
||||
"attachments": [],
|
||||
"dirFileName": "Book",
|
||||
"children": [
|
||||
{
|
||||
"isClone": false,
|
||||
"noteId": "fDGg7QcJg3Xm",
|
||||
"notePath": [
|
||||
"OkOZllzB3fqN",
|
||||
"wmegHv51MJMd",
|
||||
"pSDzQIgLGswQ",
|
||||
"fDGg7QcJg3Xm"
|
||||
],
|
||||
"title": "Calendar View",
|
||||
"notePosition": 10,
|
||||
"prefix": null,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/html",
|
||||
"attributes": [
|
||||
{
|
||||
"type": "label",
|
||||
"name": "iconClass",
|
||||
"value": "bx bx-calendar",
|
||||
"isInheritable": false,
|
||||
"position": 10
|
||||
}
|
||||
],
|
||||
"format": "html",
|
||||
"dataFileName": "Calendar View.html",
|
||||
"attachments": [
|
||||
{
|
||||
"attachmentId": "j1NIQJvjsFrc",
|
||||
"title": "image.png",
|
||||
"role": "image",
|
||||
"mime": "image/png",
|
||||
"position": 10,
|
||||
"dataFileName": "Calendar View_image.png"
|
||||
},
|
||||
{
|
||||
"attachmentId": "9FxGltAPWr9V",
|
||||
"title": "image.png",
|
||||
"role": "image",
|
||||
"mime": "image/png",
|
||||
"position": 10,
|
||||
"dataFileName": "1_Calendar View_image.png"
|
||||
},
|
||||
{
|
||||
"attachmentId": "8kfaJPGjJ1t5",
|
||||
"title": "image.png",
|
||||
"role": "image",
|
||||
"mime": "image/png",
|
||||
"position": 10,
|
||||
"dataFileName": "2_Calendar View_image.png"
|
||||
},
|
||||
{
|
||||
"attachmentId": "GaH4K6lKfcQe",
|
||||
"title": "image.png",
|
||||
"role": "image",
|
||||
"mime": "image/png",
|
||||
"position": 10,
|
||||
"dataFileName": "3_Calendar View_image.png"
|
||||
},
|
||||
{
|
||||
"attachmentId": "xr4c0Mdf7gPm",
|
||||
"title": "image.png",
|
||||
"role": "image",
|
||||
"mime": "image/png",
|
||||
"position": 10,
|
||||
"dataFileName": "4_Calendar View_image.png"
|
||||
},
|
||||
{
|
||||
"attachmentId": "K8NQktF9sCss",
|
||||
"title": "image.png",
|
||||
"role": "image",
|
||||
"mime": "image/png",
|
||||
"position": 10,
|
||||
"dataFileName": "5_Calendar View_image.png"
|
||||
},
|
||||
{
|
||||
"attachmentId": "fFaq1mWTFlJA",
|
||||
"title": "image.png",
|
||||
"role": "image",
|
||||
"mime": "image/png",
|
||||
"position": 10,
|
||||
"dataFileName": "6_Calendar View_image.png"
|
||||
},
|
||||
{
|
||||
"attachmentId": "2CExLYphNtCd",
|
||||
"title": "image.png",
|
||||
"role": "image",
|
||||
"mime": "image/png",
|
||||
"position": 10,
|
||||
"dataFileName": "7_Calendar View_image.png"
|
||||
},
|
||||
{
|
||||
"attachmentId": "UaXBPb7fINm4",
|
||||
"title": "image.png",
|
||||
"role": "image",
|
||||
"mime": "image/png",
|
||||
"position": 10,
|
||||
"dataFileName": "8_Calendar View_image.png"
|
||||
},
|
||||
{
|
||||
"attachmentId": "TIzqtnGIPlxu",
|
||||
"title": "image.png",
|
||||
"role": "image",
|
||||
"mime": "image/png",
|
||||
"position": 10,
|
||||
"dataFileName": "9_Calendar View_image.png"
|
||||
},
|
||||
{
|
||||
"attachmentId": "p7eRe4TFFdIt",
|
||||
"title": "image.png",
|
||||
"role": "image",
|
||||
"mime": "image/png",
|
||||
"position": 10,
|
||||
"dataFileName": "10_Calendar View_image.png"
|
||||
},
|
||||
{
|
||||
"attachmentId": "bnKESYv4Toa1",
|
||||
"title": "image.png",
|
||||
"role": "image",
|
||||
"mime": "image/png",
|
||||
"position": 10,
|
||||
"dataFileName": "11_Calendar View_image.png"
|
||||
},
|
||||
{
|
||||
"attachmentId": "MwECr6EjQjEE",
|
||||
"title": "image.png",
|
||||
"role": "image",
|
||||
"mime": "image/png",
|
||||
"position": 10,
|
||||
"dataFileName": "12_Calendar View_image.png"
|
||||
},
|
||||
{
|
||||
"attachmentId": "0J8MfQPq7E1H",
|
||||
"title": "image.png",
|
||||
"role": "image",
|
||||
"mime": "image/png",
|
||||
"position": 10,
|
||||
"dataFileName": "13_Calendar View_image.png"
|
||||
},
|
||||
{
|
||||
"attachmentId": "0yGXmgB3yfGg",
|
||||
"title": "image.png",
|
||||
"role": "image",
|
||||
"mime": "image/png",
|
||||
"position": 10,
|
||||
"dataFileName": "14_Calendar View_image.png"
|
||||
},
|
||||
{
|
||||
"attachmentId": "XBOyB2RH28OS",
|
||||
"title": "image.png",
|
||||
"role": "image",
|
||||
"mime": "image/png",
|
||||
"position": 10,
|
||||
"dataFileName": "15_Calendar View_image.png"
|
||||
},
|
||||
{
|
||||
"attachmentId": "BsiAqW51VJOz",
|
||||
"title": "image.png",
|
||||
"role": "image",
|
||||
"mime": "image/png",
|
||||
"position": 10,
|
||||
"dataFileName": "16_Calendar View_image.png"
|
||||
},
|
||||
{
|
||||
"attachmentId": "RTFdV19BHn28",
|
||||
"title": "image.png",
|
||||
"role": "image",
|
||||
"mime": "image/png",
|
||||
"position": 10,
|
||||
"dataFileName": "17_Calendar View_image.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -624,7 +826,7 @@
|
||||
"title": "Examples",
|
||||
"notePosition": 10,
|
||||
"prefix": null,
|
||||
"isExpanded": true,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/html",
|
||||
"attributes": [],
|
||||
@ -895,7 +1097,7 @@
|
||||
"title": "ETAPI",
|
||||
"notePosition": 10,
|
||||
"prefix": null,
|
||||
"isExpanded": true,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/html",
|
||||
"attributes": [],
|
||||
@ -945,7 +1147,7 @@
|
||||
"title": "Internal API",
|
||||
"notePosition": 20,
|
||||
"prefix": null,
|
||||
"isExpanded": true,
|
||||
"isExpanded": false,
|
||||
"type": "text",
|
||||
"mime": "text/html",
|
||||
"attributes": [],
|
||||
|
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 9.7 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 7.3 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 9.1 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 7.3 KiB |
After Width: | Height: | Size: 8.4 KiB |
After Width: | Height: | Size: 7.9 KiB |
@ -0,0 +1,192 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="../../../style.css">
|
||||
<base target="_parent">
|
||||
<title data-trilium-title>Calendar View</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="content">
|
||||
<h1 data-trilium-h1>Calendar View</h1>
|
||||
|
||||
<div class="ck-content">
|
||||
<figure class="image">
|
||||
<img style="aspect-ratio:767/606;" src="6_Calendar View_image.png" width="767"
|
||||
height="606">
|
||||
</figure>
|
||||
<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>
|
||||
<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>
|
||||
<h2>Creating a calendar</h2>
|
||||
<figure class="table" style="width:100%;">
|
||||
<table class="ck-table-resized">
|
||||
<colgroup>
|
||||
<col style="width:3.67%;">
|
||||
<col style="width:61.57%;">
|
||||
<col style="width:34.76%;">
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>1</th>
|
||||
<td>
|
||||
<figure class="image">
|
||||
<img style="aspect-ratio:545/299;" src="Calendar View_image.png" width="545"
|
||||
height="299">
|
||||
</figure>
|
||||
<p> </p>
|
||||
</td>
|
||||
<td style="vertical-align:top;">
|
||||
<p>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,
|
||||
or Insert child note and then select <i>Book</i>.</p>
|
||||
<p> </p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>2</th>
|
||||
<td>
|
||||
<figure class="image">
|
||||
<img style="aspect-ratio:314/233;" src="1_Calendar View_image.png" width="314"
|
||||
height="233">
|
||||
</figure>
|
||||
</td>
|
||||
<td style="vertical-align:top;">Once created, the “View type” of the Book needs changed to “Calendar”,
|
||||
by selecting the “Book Properties” tab in the ribbon.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
<h2>Creating a new event/note</h2>
|
||||
<ul>
|
||||
<li>Clicking on a day will create a new child note and assign it to that particular
|
||||
day.
|
||||
<ul>
|
||||
<li>You will be asked for the name of the new note. If the popup is dismissed
|
||||
by pressing the close button or escape, then the note will not be created.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>It's possible to drag across multiple days to set both the start and end
|
||||
date of a particular note.
|
||||
<br>
|
||||
<img src="4_Calendar View_image.png" width="425" height="91">
|
||||
</li>
|
||||
</ul>
|
||||
<h2>Interacting with events</h2>
|
||||
<ul>
|
||||
<li>Hovering the mouse over an event will display information about the note.
|
||||
<br>
|
||||
<img src="5_Calendar View_image.png" width="323" height="160">
|
||||
</li>
|
||||
<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
|
||||
opening the note in a new split or window.</li>
|
||||
<li>Drag and drop an event on the calendar to move it to another day.</li>
|
||||
<li>The length of an event can be changed by placing the mouse to the right
|
||||
edge of the event and dragging the mouse around.</li>
|
||||
</ul>
|
||||
<h2>Configuring the calendar</h2>
|
||||
<ul>
|
||||
<li>The first day of the week can be either Sunday or Monday and can be adjusted
|
||||
from the application settings.</li>
|
||||
</ul>
|
||||
<h2>How the calendar works</h2>
|
||||
<p>
|
||||
<img class="image-style-align-left" src="7_Calendar View_image.png" width="329"
|
||||
height="116">The calendar displays all the child notes of the book that have a <code>#startDate</code>.
|
||||
An <code>#endDate</code> can optionally be added.</p>
|
||||
<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" #hidePromotedAttributes </code></pre>
|
||||
<p>This will result in:</p>
|
||||
<p>
|
||||
<img src="9_Calendar View_image.png" width="264" height="164">
|
||||
</p>
|
||||
<h2>Advanced use-cases</h2>
|
||||
<h3>Using a different attribute as event title</h3>
|
||||
<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
|
||||
instead.</p>
|
||||
<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
|
||||
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.</p>
|
||||
<figure class="table">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<figure class="image">
|
||||
<img style="aspect-ratio:631/115;" src="10_Calendar View_image.png" width="631"
|
||||
height="115">
|
||||
</figure>
|
||||
</td>
|
||||
<td>
|
||||
<figure class="image">
|
||||
<img style="aspect-ratio:445/124;" src="11_Calendar View_image.png" width="445"
|
||||
height="124">
|
||||
</figure>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
<h3>Using a relation attribute as event title</h3>
|
||||
<p>Similarly to using an attribute, use <code>#calendar:title</code> and set
|
||||
it to <code>~name</code> where <code>name</code> is the name of the relation
|
||||
to use.</p>
|
||||
<p>Moreover, if there are more relations of the same name, they will be displayed
|
||||
as multiple events coming from the same note.</p>
|
||||
<figure class="table">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<figure class="image">
|
||||
<img style="aspect-ratio:666/118;" src="15_Calendar View_image.png" width="666"
|
||||
height="118">
|
||||
</figure>
|
||||
</td>
|
||||
<td>
|
||||
<figure class="image">
|
||||
<img style="aspect-ratio:294/151;" src="14_Calendar View_image.png" width="294"
|
||||
height="151">
|
||||
</figure>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
<p>Note that it's even possible to have a <code>#calendar:title</code> 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).</p>
|
||||
<figure class="table">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<figure class="image">
|
||||
<img style="aspect-ratio:364/121;" src="16_Calendar View_image.png" width="364"
|
||||
height="121">
|
||||
</figure>
|
||||
</td>
|
||||
<td>
|
||||
<figure class="image">
|
||||
<img style="aspect-ratio:296/150;" src="17_Calendar View_image.png" width="296"
|
||||
height="150">
|
||||
</figure>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
After Width: | Height: | Size: 31 KiB |
@ -29,6 +29,12 @@
|
||||
</li>
|
||||
<li><a href="User%20Guide/Note%20Types/Geo%20map.html" target="detail">Geo map</a>
|
||||
</li>
|
||||
<li>Book
|
||||
<ul>
|
||||
<li><a href="User%20Guide/Note%20Types/Book/Calendar%20View.html" target="detail">Calendar View</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Shared notes
|
||||
|
@ -31,6 +31,7 @@ import type AppContext from "../components/app_context.js";
|
||||
import TabRowWidget from "../widgets/tab_row.js";
|
||||
import JumpToNoteDialog from "../widgets/dialogs/jump_to_note.js";
|
||||
import RecentChangesDialog from "../widgets/dialogs/recent_changes.js";
|
||||
import PromptDialog from "../widgets/dialogs/prompt.js";
|
||||
|
||||
const MOBILE_CSS = `
|
||||
<style>
|
||||
@ -189,6 +190,7 @@ export default class MobileLayout {
|
||||
.child(new AboutDialog())
|
||||
.child(new HelpDialog())
|
||||
.child(new RecentChangesDialog())
|
||||
.child(new JumpToNoteDialog());
|
||||
.child(new JumpToNoteDialog())
|
||||
.child(new PromptDialog());
|
||||
}
|
||||
}
|
||||
|
@ -1,404 +1,51 @@
|
||||
import linkService from "./link.js";
|
||||
import contentRenderer from "./content_renderer.js";
|
||||
import froca from "./froca.js";
|
||||
import attributeRenderer from "./attribute_renderer.js";
|
||||
import libraryLoader from "./library_loader.js";
|
||||
import treeService from "./tree.js";
|
||||
import utils from "./utils.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import CalendarView from "../widgets/view_widgets/calendar_view.js";
|
||||
import ListOrGridView from "../widgets/view_widgets/list_or_grid_view.js";
|
||||
import type { ViewModeArgs } from "../widgets/view_widgets/view_mode.js";
|
||||
import type ViewMode from "../widgets/view_widgets/view_mode.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-list">
|
||||
<style>
|
||||
.note-list {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
export type ViewTypeOptions = "list" | "grid" | "calendar";
|
||||
|
||||
.note-list.grid-view .note-list-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
export default class NoteListRenderer {
|
||||
|
||||
.note-list.grid-view .note-book-card {
|
||||
flex-basis: 300px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
private viewType: ViewTypeOptions;
|
||||
public viewMode: ViewMode | null;
|
||||
|
||||
.note-list.grid-view .note-expander {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.note-list.grid-view .note-book-card {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.note-list.grid-view .note-book-card img {
|
||||
max-height: 220px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.note-list.grid-view .note-book-card:hover {
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--main-border-color);
|
||||
background: var(--more-accented-background-color);
|
||||
}
|
||||
|
||||
.note-book-card {
|
||||
border-radius: 10px;
|
||||
background-color: var(--accented-background-color);
|
||||
padding: 10px 15px 15px 8px;
|
||||
margin: 5px 5px 5px 5px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.note-book-card:not(.expanded) .note-book-content {
|
||||
display: none !important;
|
||||
padding: 10px
|
||||
}
|
||||
|
||||
.note-book-card.expanded .note-book-content {
|
||||
display: block;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.note-book-content .rendered-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.note-book-header {
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
margin-bottom: 0;
|
||||
padding-bottom: .5rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* not-expanded title is limited to one line only */
|
||||
.note-book-card:not(.expanded) .note-book-header {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.note-book-header .rendered-note-attributes {
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
.note-book-header .rendered-note-attributes:before {
|
||||
content: "\\00a0\\00a0";
|
||||
}
|
||||
|
||||
.note-book-header .note-icon {
|
||||
font-size: 100%;
|
||||
display: inline-block;
|
||||
padding-right: 7px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.note-book-card .note-book-card {
|
||||
border: 1px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
.note-book-content.type-image, .note-book-content.type-file, .note-book-content.type-protectedSession {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.note-book-content.type-image img, .note-book-content.type-canvas svg {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.note-book-card.type-image .note-book-content img,
|
||||
.note-book-card.type-text .note-book-content img,
|
||||
.note-book-card.type-canvas .note-book-content img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.note-book-header {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.note-list-wrapper {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.note-expander {
|
||||
font-size: x-large;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.note-list-pager {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="note-list-wrapper">
|
||||
<div class="note-list-pager"></div>
|
||||
|
||||
<div class="note-list-container use-tn-links"></div>
|
||||
|
||||
<div class="note-list-pager"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
class NoteListRenderer {
|
||||
private $noteList: JQuery<HTMLElement>;
|
||||
|
||||
private parentNote: FNote;
|
||||
private noteIds: string[];
|
||||
private page?: number;
|
||||
private pageSize?: number;
|
||||
private viewType?: string | null;
|
||||
private showNotePath?: boolean;
|
||||
private highlightRegex?: RegExp | null;
|
||||
|
||||
/*
|
||||
* We're using noteIds so that it's not necessary to load all notes at once when paging
|
||||
*/
|
||||
constructor($parent: JQuery<HTMLElement>, parentNote: FNote, noteIds: string[], showNotePath: boolean = false) {
|
||||
this.$noteList = $(TPL);
|
||||
|
||||
// note list must be added to the DOM immediately, otherwise some functionality scripting (canvas) won't work
|
||||
$parent.empty();
|
||||
|
||||
this.parentNote = parentNote;
|
||||
const includedNoteIds = this.getIncludedNoteIds();
|
||||
|
||||
this.noteIds = noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden");
|
||||
|
||||
if (this.noteIds.length === 0) {
|
||||
return;
|
||||
this.viewType = this.#getViewType(parentNote);
|
||||
const args: ViewModeArgs = {
|
||||
$parent,
|
||||
parentNote,
|
||||
noteIds,
|
||||
showNotePath
|
||||
}
|
||||
|
||||
$parent.append(this.$noteList);
|
||||
|
||||
this.page = 1;
|
||||
this.pageSize = parseInt(parentNote.getLabelValue("pageSize") || "");
|
||||
|
||||
if (!this.pageSize || this.pageSize < 1) {
|
||||
this.pageSize = 20;
|
||||
if (this.viewType === "list" || this.viewType === "grid") {
|
||||
this.viewMode = new ListOrGridView(this.viewType, args);
|
||||
} else if (this.viewType === "calendar") {
|
||||
this.viewMode = new CalendarView(args);
|
||||
} else {
|
||||
this.viewMode = null;
|
||||
}
|
||||
|
||||
this.viewType = parentNote.getLabelValue("viewType");
|
||||
|
||||
if (!["list", "grid"].includes(this.viewType || "")) {
|
||||
// when not explicitly set, decide based on the note type
|
||||
this.viewType = parentNote.type === "search" ? "list" : "grid";
|
||||
}
|
||||
|
||||
this.$noteList.addClass(`${this.viewType}-view`);
|
||||
|
||||
this.showNotePath = showNotePath;
|
||||
}
|
||||
|
||||
/** @returns {Set<string>} list of noteIds included (images, included notes) in the parent note and which
|
||||
* don't have to be shown in the note list. */
|
||||
getIncludedNoteIds() {
|
||||
const includedLinks = this.parentNote ? this.parentNote.getRelations().filter((rel) => rel.name === "imageLink" || rel.name === "includeNoteLink") : [];
|
||||
#getViewType(parentNote: FNote): ViewTypeOptions {
|
||||
const viewType = parentNote.getLabelValue("viewType");
|
||||
|
||||
return new Set(includedLinks.map((rel) => rel.value));
|
||||
if (!["list", "grid", "calendar"].includes(viewType || "")) {
|
||||
// when not explicitly set, decide based on the note type
|
||||
return parentNote.type === "search" ? "list" : "grid";
|
||||
} else {
|
||||
return viewType as ViewTypeOptions;
|
||||
}
|
||||
}
|
||||
|
||||
async renderList() {
|
||||
if (this.noteIds.length === 0 || !this.page || !this.pageSize) {
|
||||
this.$noteList.hide();
|
||||
return;
|
||||
if (!this.viewMode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const highlightedTokens = this.parentNote.highlightedTokens || [];
|
||||
if (highlightedTokens.length > 0) {
|
||||
await libraryLoader.requireLibrary(libraryLoader.MARKJS);
|
||||
|
||||
const regex = highlightedTokens.map((token) => utils.escapeRegExp(token)).join("|");
|
||||
|
||||
this.highlightRegex = new RegExp(regex, "gi");
|
||||
} else {
|
||||
this.highlightRegex = null;
|
||||
}
|
||||
|
||||
this.$noteList.show();
|
||||
|
||||
const $container = this.$noteList.find(".note-list-container").empty();
|
||||
|
||||
const startIdx = (this.page - 1) * this.pageSize;
|
||||
const endIdx = startIdx + this.pageSize;
|
||||
|
||||
const pageNoteIds = this.noteIds.slice(startIdx, Math.min(endIdx, this.noteIds.length));
|
||||
const pageNotes = await froca.getNotes(pageNoteIds);
|
||||
|
||||
for (const note of pageNotes) {
|
||||
const $card = await this.renderNote(note, this.parentNote.isLabelTruthy("expanded"));
|
||||
|
||||
$container.append($card);
|
||||
}
|
||||
|
||||
this.renderPager();
|
||||
|
||||
return this.$noteList;
|
||||
return await this.viewMode.renderList();
|
||||
}
|
||||
|
||||
renderPager() {
|
||||
const $pager = this.$noteList.find(".note-list-pager").empty();
|
||||
if (!this.page || !this.pageSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pageCount = Math.ceil(this.noteIds.length / this.pageSize);
|
||||
|
||||
$pager.toggle(pageCount > 1);
|
||||
|
||||
let lastPrinted;
|
||||
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
if (pageCount < 20 || i <= 5 || pageCount - i <= 5 || Math.abs(this.page - i) <= 2) {
|
||||
lastPrinted = true;
|
||||
|
||||
const startIndex = (i - 1) * this.pageSize + 1;
|
||||
const endIndex = Math.min(this.noteIds.length, i * this.pageSize);
|
||||
|
||||
$pager.append(
|
||||
i === this.page
|
||||
? $("<span>").text(i).css("text-decoration", "underline").css("font-weight", "bold")
|
||||
: $('<a href="javascript:">')
|
||||
.text(i)
|
||||
.attr("title", `Page of ${startIndex} - ${endIndex}`)
|
||||
.on("click", () => {
|
||||
this.page = i;
|
||||
this.renderList();
|
||||
}),
|
||||
" "
|
||||
);
|
||||
} else if (lastPrinted) {
|
||||
$pager.append("... ");
|
||||
|
||||
lastPrinted = false;
|
||||
}
|
||||
}
|
||||
|
||||
// no need to distinguish "note" vs "notes" since in case of one result, there's no paging at all
|
||||
$pager.append(`<span class="note-list-pager-total-count">(${this.noteIds.length} notes)</span>`);
|
||||
}
|
||||
|
||||
async renderNote(note: FNote, expand: boolean = false) {
|
||||
const $expander = $('<span class="note-expander bx bx-chevron-right"></span>');
|
||||
|
||||
const { $renderedAttributes } = await attributeRenderer.renderNormalAttributes(note);
|
||||
const notePath =
|
||||
this.parentNote.type === "search"
|
||||
? note.noteId // for search note parent, we want to display a non-search path
|
||||
: `${this.parentNote.noteId}/${note.noteId}`;
|
||||
|
||||
const $card = $('<div class="note-book-card">')
|
||||
.attr("data-note-id", note.noteId)
|
||||
.append(
|
||||
$('<h5 class="note-book-header">')
|
||||
.append($expander)
|
||||
.append($('<span class="note-icon">').addClass(note.getIcon()))
|
||||
.append(
|
||||
this.viewType === "grid"
|
||||
? $('<span class="note-book-title">').text(await treeService.getNoteTitle(note.noteId, this.parentNote.noteId))
|
||||
: (await linkService.createLink(notePath, { showTooltip: false, showNotePath: this.showNotePath })).addClass("note-book-title")
|
||||
)
|
||||
.append($renderedAttributes)
|
||||
);
|
||||
|
||||
if (this.viewType === "grid") {
|
||||
$card
|
||||
.addClass("block-link")
|
||||
.attr("data-href", `#${notePath}`)
|
||||
.on("click", (e) => linkService.goToLink(e));
|
||||
}
|
||||
|
||||
$expander.on("click", () => this.toggleContent($card, note, !$card.hasClass("expanded")));
|
||||
|
||||
if (this.highlightRegex) {
|
||||
$card.find(".note-book-title").markRegExp(this.highlightRegex, {
|
||||
element: "span",
|
||||
className: "ck-find-result",
|
||||
separateWordSearch: false,
|
||||
caseSensitive: false
|
||||
});
|
||||
}
|
||||
|
||||
await this.toggleContent($card, note, expand);
|
||||
|
||||
return $card;
|
||||
}
|
||||
|
||||
async toggleContent($card: JQuery<HTMLElement>, note: FNote, expand: boolean) {
|
||||
if (this.viewType === "list" && ((expand && $card.hasClass("expanded")) || (!expand && !$card.hasClass("expanded")))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $expander = $card.find("> .note-book-header .note-expander");
|
||||
|
||||
if (expand || this.viewType === "grid") {
|
||||
$card.addClass("expanded");
|
||||
$expander.addClass("bx-chevron-down").removeClass("bx-chevron-right");
|
||||
} else {
|
||||
$card.removeClass("expanded");
|
||||
$expander.addClass("bx-chevron-right").removeClass("bx-chevron-down");
|
||||
}
|
||||
|
||||
if ((expand || this.viewType === "grid") && $card.find(".note-book-content").length === 0) {
|
||||
$card.append(await this.renderNoteContent(note));
|
||||
}
|
||||
}
|
||||
|
||||
async renderNoteContent(note: FNote) {
|
||||
const $content = $('<div class="note-book-content">');
|
||||
|
||||
try {
|
||||
const { $renderedContent, type } = await contentRenderer.getRenderedContent(note, {
|
||||
trim: this.viewType === "grid" // for grid only short content is needed
|
||||
});
|
||||
|
||||
if (this.highlightRegex) {
|
||||
$renderedContent.markRegExp(this.highlightRegex, {
|
||||
element: "span",
|
||||
className: "ck-find-result",
|
||||
separateWordSearch: false,
|
||||
caseSensitive: false
|
||||
});
|
||||
}
|
||||
|
||||
$content.append($renderedContent);
|
||||
$content.addClass(`type-${type}`);
|
||||
} catch (e) {
|
||||
console.warn(`Caught error while rendering note '${note.noteId}' of type '${note.type}'`);
|
||||
console.error(e);
|
||||
|
||||
$content.append("rendering error");
|
||||
}
|
||||
|
||||
if (this.viewType === "list") {
|
||||
const imageLinks = note.getRelations("imageLink");
|
||||
|
||||
const childNotes = (await note.getChildNotes()).filter((childNote) => !imageLinks.find((rel) => rel.value === childNote.noteId));
|
||||
|
||||
for (const childNote of childNotes) {
|
||||
$content.append(await this.renderNote(childNote));
|
||||
}
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
|
||||
export default NoteListRenderer;
|
||||
|
@ -365,6 +365,7 @@ export default class GlobalMenuWidget extends BasicWidget {
|
||||
|
||||
this.$zoomState = this.$widget.find(".zoom-state");
|
||||
this.$toggleZenMode = this.$widget.find('[data-trigger-command="toggleZenMode"');
|
||||
this.$toggleZenMode.toggle(!utils.isMobile());
|
||||
this.$widget.on("show.bs.dropdown", () => this.#onShown());
|
||||
if (this.tooltip) {
|
||||
this.$widget.on("hide.bs.dropdown", () => this.tooltip.enable());
|
||||
|
@ -1,7 +1,8 @@
|
||||
import appContext from "../../components/app_context.js";
|
||||
import appContext, { type EventData } from "../../components/app_context.js";
|
||||
import type { NoteType } from "../../entities/fnote.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import type { ViewScope } from "../../services/link.js";
|
||||
import type { ViewTypeOptions } from "../../services/note_list_renderer.js";
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
|
||||
const TPL = `
|
||||
@ -10,8 +11,7 @@ const TPL = `
|
||||
</button>
|
||||
`;
|
||||
|
||||
const byNoteType: Record<NoteType, string | null> = {
|
||||
book: null,
|
||||
const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
|
||||
canvas: null,
|
||||
code: null,
|
||||
contentWidget: null,
|
||||
@ -30,6 +30,12 @@ const byNoteType: Record<NoteType, string | null> = {
|
||||
webView: null
|
||||
};
|
||||
|
||||
const byBookType: Record<ViewTypeOptions, string | null> = {
|
||||
list: null,
|
||||
grid: null,
|
||||
calendar: "fDGg7QcJg3Xm"
|
||||
};
|
||||
|
||||
export default class ContextualHelpButton extends NoteContextAwareWidget {
|
||||
|
||||
private helpNoteIdToOpen?: string | null;
|
||||
@ -41,8 +47,10 @@ export default class ContextualHelpButton extends NoteContextAwareWidget {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.note && byNoteType[this.note.type]) {
|
||||
if (this.note && this.note.type !== "book" && byNoteType[this.note.type]) {
|
||||
this.helpNoteIdToOpen = byNoteType[this.note.type];
|
||||
} else if (this.note && this.note.type === "book") {
|
||||
this.helpNoteIdToOpen = byBookType[this.note.getAttributeValue("label", "viewType") as ViewTypeOptions ?? ""]
|
||||
}
|
||||
|
||||
return !!this.helpNoteIdToOpen;
|
||||
@ -73,4 +81,10 @@ export default class ContextualHelpButton extends NoteContextAwareWidget {
|
||||
});
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (this.note?.type === "book" && loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId && attr.name === "viewType")) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import NoteListRenderer from "../services/note_list_renderer.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import type { EventData } from "../components/app_context.js";
|
||||
import type ViewMode from "./view_widgets/view_mode.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-list-widget">
|
||||
@ -26,6 +27,7 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
||||
private isIntersecting?: boolean;
|
||||
private noteIdRefreshed?: string;
|
||||
private shownNoteId?: string | null;
|
||||
private viewMode?: ViewMode | null;
|
||||
|
||||
isEnabled() {
|
||||
return super.isEnabled() && this.noteContext?.hasNoteList();
|
||||
@ -67,6 +69,7 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
||||
async renderNoteList(note: FNote) {
|
||||
const noteListRenderer = new NoteListRenderer(this.$content, note, note.getChildNoteIds());
|
||||
await noteListRenderer.renderList();
|
||||
this.viewMode = noteListRenderer.viewMode;
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
@ -102,11 +105,14 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId && attr.name && ["viewType", "expanded", "pageSize"].includes(attr.name))) {
|
||||
this.shownNoteId = null; // force render
|
||||
|
||||
entitiesReloadedEvent(e: EventData<"entitiesReloaded">) {
|
||||
if (e.loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId && attr.name && ["viewType", "expanded", "pageSize"].includes(attr.name))) {
|
||||
this.refresh();
|
||||
this.checkRenderStatus();
|
||||
}
|
||||
|
||||
if (this.viewMode) {
|
||||
this.viewMode.entitiesReloadedEvents(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ const TPL = `
|
||||
<select class="view-type-select form-select form-select-sm">
|
||||
<option value="grid">${t("book_properties.grid")}</option>
|
||||
<option value="list">${t("book_properties.list")}</option>
|
||||
<option value="calendar">${t("book_properties.calendar")}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@ -125,7 +126,7 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type !== "list" && type !== "grid") {
|
||||
if (![ "list", "grid", "calendar"].includes(type)) {
|
||||
throw new Error(t("book_properties.invalid_view_type", { type }));
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import TypeWidget from "./type_widget.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-detail-book note-detail-printable">
|
||||
@ -35,6 +36,15 @@ export default class BookTypeWidget extends TypeWidget {
|
||||
}
|
||||
|
||||
async doRefresh(note: FNote) {
|
||||
this.$helpNoChildren.toggle(!this.note?.hasChildren());
|
||||
this.$helpNoChildren.toggle(
|
||||
!this.note?.hasChildren()
|
||||
&& this.note?.getAttributeValue("label", "viewType") !== "calendar");
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId && attr.name === "viewType")) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
284
src/public/app/widgets/view_widgets/calendar_view.ts
Normal file
@ -0,0 +1,284 @@
|
||||
import type { Calendar, DateSelectArg, EventChangeArg, EventDropArg, EventSourceInput, PluginDef } from "@fullcalendar/core";
|
||||
import froca from "../../services/froca.js";
|
||||
import ViewMode, { type ViewModeArgs } from "./view_mode.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import server from "../../services/server.js";
|
||||
import ws from "../../services/ws.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import options from "../../services/options.js";
|
||||
import dialogService from "../../services/dialog.js";
|
||||
import attributes from "../../services/attributes.js";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="calendar-view">
|
||||
<style>
|
||||
.calendar-view {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.calendar-view a {
|
||||
color: unset;
|
||||
}
|
||||
|
||||
.calendar-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.calendar-container .fc-toolbar.fc-header-toolbar {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
body.desktop:not(.zen) .calendar-container .fc-toolbar.fc-header-toolbar {
|
||||
padding-right: 5em;
|
||||
}
|
||||
|
||||
.calendar-container .fc-toolbar-title {
|
||||
font-size: 1.3rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.calendar-container .fc-button {
|
||||
padding: 0.2em 0.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="calendar-container">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// TODO: Deduplicate
|
||||
interface CreateChildResponse {
|
||||
note: {
|
||||
noteId: string;
|
||||
}
|
||||
}
|
||||
|
||||
export default class CalendarView extends ViewMode {
|
||||
|
||||
private $root: JQuery<HTMLElement>;
|
||||
private $calendarContainer: JQuery<HTMLElement>;
|
||||
private noteIds: string[];
|
||||
private parentNote: FNote;
|
||||
private calendar?: Calendar;
|
||||
|
||||
constructor(args: ViewModeArgs) {
|
||||
super(args);
|
||||
|
||||
this.$root = $(TPL);
|
||||
this.$calendarContainer = this.$root.find(".calendar-container");
|
||||
this.noteIds = args.noteIds;
|
||||
this.parentNote = args.parentNote;
|
||||
console.log(args);
|
||||
args.$parent.append(this.$root);
|
||||
}
|
||||
|
||||
async renderList(): Promise<JQuery<HTMLElement> | undefined> {
|
||||
const isEditable = true;
|
||||
|
||||
const { Calendar } = await import("@fullcalendar/core");
|
||||
const plugins: PluginDef[] = [];
|
||||
plugins.push((await import("@fullcalendar/daygrid")).default);
|
||||
|
||||
if (isEditable) {
|
||||
plugins.push((await import("@fullcalendar/interaction")).default);
|
||||
}
|
||||
|
||||
const calendar = new Calendar(this.$calendarContainer[0], {
|
||||
plugins,
|
||||
initialView: "dayGridMonth",
|
||||
events: async () => await CalendarView.#buildEvents(this.noteIds),
|
||||
editable: isEditable,
|
||||
selectable: isEditable,
|
||||
select: (e) => this.#onCalendarSelection(e),
|
||||
eventChange: (e) => this.#onEventMoved(e),
|
||||
firstDay: options.getInt("firstDayOfWeek") ?? 0,
|
||||
locale: await CalendarView.#getLocale()
|
||||
});
|
||||
calendar.render();
|
||||
this.calendar = calendar;
|
||||
|
||||
return this.$root;
|
||||
}
|
||||
|
||||
static async #getLocale() {
|
||||
const locale = options.get("locale");
|
||||
|
||||
// Here we hard-code the imports in order to ensure that they are embedded by webpack without having to load all the languages.
|
||||
switch (locale) {
|
||||
case "de":
|
||||
return (await import("@fullcalendar/core/locales/de")).default;
|
||||
case "es":
|
||||
return (await import("@fullcalendar/core/locales/es")).default;
|
||||
case "fr":
|
||||
return (await import("@fullcalendar/core/locales/fr")).default;
|
||||
case "cn":
|
||||
return (await import("@fullcalendar/core/locales/zh-cn")).default;
|
||||
case "tw":
|
||||
return (await import("@fullcalendar/core/locales/zh-tw")).default;
|
||||
case "ro":
|
||||
return (await import("@fullcalendar/core/locales/ro")).default;
|
||||
case "en":
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async #onCalendarSelection(e: DateSelectArg) {
|
||||
const startDate = CalendarView.#formatDateToLocalISO(e.start);
|
||||
if (!startDate) {
|
||||
return;
|
||||
}
|
||||
|
||||
const endDate = CalendarView.#formatDateToLocalISO(CalendarView.#offsetDate(e.end, -1));
|
||||
|
||||
const title = await dialogService.prompt({ message: t("relation_map.enter_title_of_new_note"), defaultValue: t("relation_map.default_new_note_title") });
|
||||
if (!title?.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { note } = await server.post<CreateChildResponse>(`notes/${this.parentNote.noteId}/children?target=into`, {
|
||||
title,
|
||||
content: "",
|
||||
type: "text"
|
||||
});
|
||||
attributes.setLabel(note.noteId, "startDate", startDate);
|
||||
if (endDate) {
|
||||
attributes.setLabel(note.noteId, "endDate", endDate);
|
||||
}
|
||||
}
|
||||
|
||||
async #onEventMoved(e: EventChangeArg) {
|
||||
const startDate = CalendarView.#formatDateToLocalISO(e.event.start);
|
||||
// Fullcalendar end date is exclusive, not inclusive but we store it the other way around.
|
||||
let endDate = CalendarView.#formatDateToLocalISO(CalendarView.#offsetDate(e.event.end, -1));
|
||||
const noteId = e.event.extendedProps.noteId;
|
||||
|
||||
// Don't store the end date if it's empty.
|
||||
if (endDate === startDate) {
|
||||
endDate = undefined;
|
||||
}
|
||||
|
||||
// Update start date
|
||||
const note = await froca.getNote(noteId);
|
||||
if (!note) {
|
||||
return;
|
||||
}
|
||||
|
||||
CalendarView.#setAttribute(note, "label", "startDate", startDate);
|
||||
CalendarView.#setAttribute(note, "label", "endDate", endDate);
|
||||
}
|
||||
|
||||
entitiesReloadedEvents({ loadResults }: EventData<"entitiesReloaded">): void {
|
||||
// Refresh note IDs if they got changed.
|
||||
if (loadResults.getBranchRows().some((branch) => branch.parentNoteId == this.parentNote.noteId)) {
|
||||
this.noteIds = this.parentNote.getChildNoteIds();
|
||||
}
|
||||
|
||||
if (this.calendar && loadResults.getAttributeRows().some((a) => this.noteIds.includes(a.noteId ?? ""))) {
|
||||
this.calendar.refetchEvents();
|
||||
}
|
||||
}
|
||||
|
||||
static async #buildEvents(noteIds: string[]) {
|
||||
const notes = await froca.getNotes(noteIds);
|
||||
const events: EventSourceInput = [];
|
||||
|
||||
for (const note of notes) {
|
||||
const startDate = note.getAttributeValue("label", "startDate");
|
||||
const customTitle = note.getAttributeValue("label", "calendar:title");
|
||||
|
||||
if (!startDate) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const titles = await CalendarView.#parseCustomTitle(customTitle, note);
|
||||
for (const title of titles) {
|
||||
const eventData: typeof events[0] = {
|
||||
title: title,
|
||||
start: startDate,
|
||||
url: `#${note.noteId}`,
|
||||
noteId: note.noteId
|
||||
};
|
||||
|
||||
const endDate = CalendarView.#offsetDate(note.getAttributeValue("label", "endDate") ?? startDate, 1);
|
||||
if (endDate) {
|
||||
eventData.end = CalendarView.#formatDateToLocalISO(endDate);
|
||||
}
|
||||
|
||||
events.push(eventData);
|
||||
}
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
static async #parseCustomTitle(customTitleValue: string | null, note: FNote, allowRelations = true): Promise<string[]> {
|
||||
if (customTitleValue) {
|
||||
const attributeName = customTitleValue.substring(1);
|
||||
if (customTitleValue.startsWith("#")) {
|
||||
const labelValue = note.getAttributeValue("label", attributeName);
|
||||
if (labelValue) {
|
||||
return [ labelValue ];
|
||||
}
|
||||
} else if (allowRelations && customTitleValue.startsWith("~")) {
|
||||
const relations = note.getRelations(attributeName);
|
||||
if (relations.length > 0) {
|
||||
const noteIds = relations.map((r) => r.targetNoteId);
|
||||
const notesFromRelation = await froca.getNotes(noteIds);
|
||||
const titles = [];
|
||||
|
||||
for (const targetNote of notesFromRelation) {
|
||||
const targetCustomTitleValue = targetNote.getAttributeValue("label", "calendar:title");
|
||||
const targetTitles = await CalendarView.#parseCustomTitle(targetCustomTitleValue, targetNote, false);
|
||||
titles.push(targetTitles.flat());
|
||||
}
|
||||
|
||||
return titles.flat();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [ note.title ];
|
||||
}
|
||||
|
||||
static async #setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) {
|
||||
if (value) {
|
||||
// Create or update the attribute.
|
||||
await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value });
|
||||
} else {
|
||||
// Remove the attribute if it exists on the server but we don't define a value for it.
|
||||
const attributeId = note.getAttribute(type, name)?.attributeId;
|
||||
if (attributeId) {
|
||||
await server.remove(`notes/${note.noteId}/attributes/${attributeId}`);
|
||||
}
|
||||
}
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
}
|
||||
|
||||
static #formatDateToLocalISO(date: Date | null | undefined) {
|
||||
if (!date) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const offset = date.getTimezoneOffset();
|
||||
const localDate = new Date(date.getTime() - offset * 60 * 1000);
|
||||
return localDate.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
static #offsetDate(date: Date | string | null | undefined, offset: number) {
|
||||
if (!date) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const newDate = new Date(date);
|
||||
newDate.setDate(newDate.getDate() + offset);
|
||||
return newDate;
|
||||
}
|
||||
|
||||
}
|
397
src/public/app/widgets/view_widgets/list_or_grid_view.ts
Normal file
@ -0,0 +1,397 @@
|
||||
import linkService from "../../services/link.js";
|
||||
import contentRenderer from "../../services/content_renderer.js";
|
||||
import froca from "../../services/froca.js";
|
||||
import attributeRenderer from "../../services/attribute_renderer.js";
|
||||
import libraryLoader from "../../services/library_loader.js";
|
||||
import treeService from "../../services/tree.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import ViewMode, { type ViewModeArgs } from "./view_mode.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-list">
|
||||
<style>
|
||||
.note-list {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.note-list.grid-view .note-list-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.note-list.grid-view .note-book-card {
|
||||
flex-basis: 300px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.note-list.grid-view .note-expander {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.note-list.grid-view .note-book-card {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.note-list.grid-view .note-book-card img {
|
||||
max-height: 220px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.note-list.grid-view .note-book-card:hover {
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--main-border-color);
|
||||
background: var(--more-accented-background-color);
|
||||
}
|
||||
|
||||
.note-book-card {
|
||||
border-radius: 10px;
|
||||
background-color: var(--accented-background-color);
|
||||
padding: 10px 15px 15px 8px;
|
||||
margin: 5px 5px 5px 5px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.note-book-card:not(.expanded) .note-book-content {
|
||||
display: none !important;
|
||||
padding: 10px
|
||||
}
|
||||
|
||||
.note-book-card.expanded .note-book-content {
|
||||
display: block;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.note-book-content .rendered-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.note-book-header {
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
margin-bottom: 0;
|
||||
padding-bottom: .5rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* not-expanded title is limited to one line only */
|
||||
.note-book-card:not(.expanded) .note-book-header {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.note-book-header .rendered-note-attributes {
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
.note-book-header .rendered-note-attributes:before {
|
||||
content: "\\00a0\\00a0";
|
||||
}
|
||||
|
||||
.note-book-header .note-icon {
|
||||
font-size: 100%;
|
||||
display: inline-block;
|
||||
padding-right: 7px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.note-book-card .note-book-card {
|
||||
border: 1px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
.note-book-content.type-image, .note-book-content.type-file, .note-book-content.type-protectedSession {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.note-book-content.type-image img, .note-book-content.type-canvas svg {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.note-book-card.type-image .note-book-content img,
|
||||
.note-book-card.type-text .note-book-content img,
|
||||
.note-book-card.type-canvas .note-book-content img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.note-book-header {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.note-list-wrapper {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.note-expander {
|
||||
font-size: x-large;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.note-list-pager {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="note-list-wrapper">
|
||||
<div class="note-list-pager"></div>
|
||||
|
||||
<div class="note-list-container use-tn-links"></div>
|
||||
|
||||
<div class="note-list-pager"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
class ListOrGridView extends ViewMode {
|
||||
private $noteList: JQuery<HTMLElement>;
|
||||
|
||||
private parentNote: FNote;
|
||||
private noteIds: string[];
|
||||
private page?: number;
|
||||
private pageSize?: number;
|
||||
private viewType?: string | null;
|
||||
private showNotePath?: boolean;
|
||||
private highlightRegex?: RegExp | null;
|
||||
|
||||
/*
|
||||
* We're using noteIds so that it's not necessary to load all notes at once when paging
|
||||
*/
|
||||
constructor(viewType: string, args: ViewModeArgs) {
|
||||
super(args);
|
||||
this.$noteList = $(TPL);
|
||||
this.viewType = viewType;
|
||||
|
||||
this.parentNote = args.parentNote;
|
||||
const includedNoteIds = this.getIncludedNoteIds();
|
||||
|
||||
this.noteIds = args.noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden");
|
||||
|
||||
if (this.noteIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
args.$parent.append(this.$noteList);
|
||||
|
||||
this.page = 1;
|
||||
this.pageSize = parseInt(args.parentNote.getLabelValue("pageSize") || "");
|
||||
|
||||
if (!this.pageSize || this.pageSize < 1) {
|
||||
this.pageSize = 20;
|
||||
}
|
||||
|
||||
this.$noteList.addClass(`${this.viewType}-view`);
|
||||
|
||||
this.showNotePath = args.showNotePath;
|
||||
}
|
||||
|
||||
/** @returns {Set<string>} list of noteIds included (images, included notes) in the parent note and which
|
||||
* don't have to be shown in the note list. */
|
||||
getIncludedNoteIds() {
|
||||
const includedLinks = this.parentNote ? this.parentNote.getRelations().filter((rel) => rel.name === "imageLink" || rel.name === "includeNoteLink") : [];
|
||||
|
||||
return new Set(includedLinks.map((rel) => rel.value));
|
||||
}
|
||||
|
||||
async renderList() {
|
||||
if (this.noteIds.length === 0 || !this.page || !this.pageSize) {
|
||||
this.$noteList.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
const highlightedTokens = this.parentNote.highlightedTokens || [];
|
||||
if (highlightedTokens.length > 0) {
|
||||
await libraryLoader.requireLibrary(libraryLoader.MARKJS);
|
||||
|
||||
const regex = highlightedTokens.map((token) => utils.escapeRegExp(token)).join("|");
|
||||
|
||||
this.highlightRegex = new RegExp(regex, "gi");
|
||||
} else {
|
||||
this.highlightRegex = null;
|
||||
}
|
||||
|
||||
this.$noteList.show();
|
||||
|
||||
const $container = this.$noteList.find(".note-list-container").empty();
|
||||
|
||||
const startIdx = (this.page - 1) * this.pageSize;
|
||||
const endIdx = startIdx + this.pageSize;
|
||||
|
||||
const pageNoteIds = this.noteIds.slice(startIdx, Math.min(endIdx, this.noteIds.length));
|
||||
const pageNotes = await froca.getNotes(pageNoteIds);
|
||||
|
||||
for (const note of pageNotes) {
|
||||
const $card = await this.renderNote(note, this.parentNote.isLabelTruthy("expanded"));
|
||||
|
||||
$container.append($card);
|
||||
}
|
||||
|
||||
this.renderPager();
|
||||
|
||||
return this.$noteList;
|
||||
}
|
||||
|
||||
renderPager() {
|
||||
const $pager = this.$noteList.find(".note-list-pager").empty();
|
||||
if (!this.page || !this.pageSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pageCount = Math.ceil(this.noteIds.length / this.pageSize);
|
||||
|
||||
$pager.toggle(pageCount > 1);
|
||||
|
||||
let lastPrinted;
|
||||
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
if (pageCount < 20 || i <= 5 || pageCount - i <= 5 || Math.abs(this.page - i) <= 2) {
|
||||
lastPrinted = true;
|
||||
|
||||
const startIndex = (i - 1) * this.pageSize + 1;
|
||||
const endIndex = Math.min(this.noteIds.length, i * this.pageSize);
|
||||
|
||||
$pager.append(
|
||||
i === this.page
|
||||
? $("<span>").text(i).css("text-decoration", "underline").css("font-weight", "bold")
|
||||
: $('<a href="javascript:">')
|
||||
.text(i)
|
||||
.attr("title", `Page of ${startIndex} - ${endIndex}`)
|
||||
.on("click", () => {
|
||||
this.page = i;
|
||||
this.renderList();
|
||||
}),
|
||||
" "
|
||||
);
|
||||
} else if (lastPrinted) {
|
||||
$pager.append("... ");
|
||||
|
||||
lastPrinted = false;
|
||||
}
|
||||
}
|
||||
|
||||
// no need to distinguish "note" vs "notes" since in case of one result, there's no paging at all
|
||||
$pager.append(`<span class="note-list-pager-total-count">(${this.noteIds.length} notes)</span>`);
|
||||
}
|
||||
|
||||
async renderNote(note: FNote, expand: boolean = false) {
|
||||
const $expander = $('<span class="note-expander bx bx-chevron-right"></span>');
|
||||
|
||||
const { $renderedAttributes } = await attributeRenderer.renderNormalAttributes(note);
|
||||
const notePath =
|
||||
this.parentNote.type === "search"
|
||||
? note.noteId // for search note parent, we want to display a non-search path
|
||||
: `${this.parentNote.noteId}/${note.noteId}`;
|
||||
|
||||
const $card = $('<div class="note-book-card">')
|
||||
.attr("data-note-id", note.noteId)
|
||||
.append(
|
||||
$('<h5 class="note-book-header">')
|
||||
.append($expander)
|
||||
.append($('<span class="note-icon">').addClass(note.getIcon()))
|
||||
.append(
|
||||
this.viewType === "grid"
|
||||
? $('<span class="note-book-title">').text(await treeService.getNoteTitle(note.noteId, this.parentNote.noteId))
|
||||
: (await linkService.createLink(notePath, { showTooltip: false, showNotePath: this.showNotePath })).addClass("note-book-title")
|
||||
)
|
||||
.append($renderedAttributes)
|
||||
);
|
||||
|
||||
if (this.viewType === "grid") {
|
||||
$card
|
||||
.addClass("block-link")
|
||||
.attr("data-href", `#${notePath}`)
|
||||
.on("click", (e) => linkService.goToLink(e));
|
||||
}
|
||||
|
||||
$expander.on("click", () => this.toggleContent($card, note, !$card.hasClass("expanded")));
|
||||
|
||||
if (this.highlightRegex) {
|
||||
$card.find(".note-book-title").markRegExp(this.highlightRegex, {
|
||||
element: "span",
|
||||
className: "ck-find-result",
|
||||
separateWordSearch: false,
|
||||
caseSensitive: false
|
||||
});
|
||||
}
|
||||
|
||||
await this.toggleContent($card, note, expand);
|
||||
|
||||
return $card;
|
||||
}
|
||||
|
||||
async toggleContent($card: JQuery<HTMLElement>, note: FNote, expand: boolean) {
|
||||
if (this.viewType === "list" && ((expand && $card.hasClass("expanded")) || (!expand && !$card.hasClass("expanded")))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $expander = $card.find("> .note-book-header .note-expander");
|
||||
|
||||
if (expand || this.viewType === "grid") {
|
||||
$card.addClass("expanded");
|
||||
$expander.addClass("bx-chevron-down").removeClass("bx-chevron-right");
|
||||
} else {
|
||||
$card.removeClass("expanded");
|
||||
$expander.addClass("bx-chevron-right").removeClass("bx-chevron-down");
|
||||
}
|
||||
|
||||
if ((expand || this.viewType === "grid") && $card.find(".note-book-content").length === 0) {
|
||||
$card.append(await this.renderNoteContent(note));
|
||||
}
|
||||
}
|
||||
|
||||
async renderNoteContent(note: FNote) {
|
||||
const $content = $('<div class="note-book-content">');
|
||||
|
||||
try {
|
||||
const { $renderedContent, type } = await contentRenderer.getRenderedContent(note, {
|
||||
trim: this.viewType === "grid" // for grid only short content is needed
|
||||
});
|
||||
|
||||
if (this.highlightRegex) {
|
||||
$renderedContent.markRegExp(this.highlightRegex, {
|
||||
element: "span",
|
||||
className: "ck-find-result",
|
||||
separateWordSearch: false,
|
||||
caseSensitive: false
|
||||
});
|
||||
}
|
||||
|
||||
$content.append($renderedContent);
|
||||
$content.addClass(`type-${type}`);
|
||||
} catch (e) {
|
||||
console.warn(`Caught error while rendering note '${note.noteId}' of type '${note.type}'`);
|
||||
console.error(e);
|
||||
|
||||
$content.append("rendering error");
|
||||
}
|
||||
|
||||
if (this.viewType === "list") {
|
||||
const imageLinks = note.getRelations("imageLink");
|
||||
|
||||
const childNotes = (await note.getChildNotes()).filter((childNote) => !imageLinks.find((rel) => rel.value === childNote.noteId));
|
||||
|
||||
for (const childNote of childNotes) {
|
||||
$content.append(await this.renderNote(childNote));
|
||||
}
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
|
||||
export default ListOrGridView;
|
24
src/public/app/widgets/view_widgets/view_mode.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
|
||||
export interface ViewModeArgs {
|
||||
$parent: JQuery<HTMLElement>;
|
||||
parentNote: FNote;
|
||||
noteIds: string[];
|
||||
showNotePath?: boolean;
|
||||
}
|
||||
|
||||
export default abstract class ViewMode {
|
||||
|
||||
constructor(args: ViewModeArgs) {
|
||||
// note list must be added to the DOM immediately, otherwise some functionality scripting (canvas) won't work
|
||||
args.$parent.empty();
|
||||
}
|
||||
|
||||
abstract renderList(): Promise<JQuery<HTMLElement> | undefined>;
|
||||
|
||||
entitiesReloadedEvents(e: EventData<"entitiesReloaded">) {
|
||||
// Do nothing by default.
|
||||
}
|
||||
|
||||
}
|
@ -752,7 +752,8 @@
|
||||
"collapse": "Collapse",
|
||||
"expand": "Expand",
|
||||
"book_properties": "Book Properties",
|
||||
"invalid_view_type": "Invalid view type '{{type}}'"
|
||||
"invalid_view_type": "Invalid view type '{{type}}'",
|
||||
"calendar": "Calendar"
|
||||
},
|
||||
"edited_notes": {
|
||||
"no_edited_notes_found": "No edited notes on this day yet...",
|
||||
|
@ -281,7 +281,8 @@
|
||||
"grid": "Grilă",
|
||||
"invalid_view_type": "Mod de afișare incorect „{{type}}”",
|
||||
"list": "Listă",
|
||||
"view_type": "Mod de afișare"
|
||||
"view_type": "Mod de afișare",
|
||||
"calendar": "Calendar"
|
||||
},
|
||||
"bookmark_switch": {
|
||||
"bookmark": "Semn de carte",
|
||||
|