mirror of
https://github.com/zadam/trilium.git
synced 2026-03-01 18:13:39 +01:00
Merge branch 'main' of https://github.com/TriliumNext/Trilium
This commit is contained in:
commit
1d698106da
@ -22,6 +22,7 @@
|
||||
"@fullcalendar/interaction": "6.1.20",
|
||||
"@fullcalendar/list": "6.1.20",
|
||||
"@fullcalendar/multimonth": "6.1.20",
|
||||
"@fullcalendar/rrule": "6.1.20",
|
||||
"@fullcalendar/timegrid": "6.1.20",
|
||||
"@maplibre/maplibre-gl-leaflet": "0.1.3",
|
||||
"@mermaid-js/layout-elk": "0.2.0",
|
||||
@ -63,6 +64,7 @@
|
||||
"react-i18next": "16.5.4",
|
||||
"react-window": "2.2.7",
|
||||
"reveal.js": "5.2.1",
|
||||
"rrule": "2.8.1",
|
||||
"svg-pan-zoom": "3.6.2",
|
||||
"tabulator-tables": "6.3.1",
|
||||
"vanilla-js-wheel-zoom": "9.0.4"
|
||||
|
||||
@ -3,6 +3,8 @@ import options from "../services/options.js";
|
||||
import zoomService from "../components/zoom.js";
|
||||
import contextMenu, { type MenuItem } from "./context_menu.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import server from "../services/server.js";
|
||||
import * as clipboardExt from "../services/clipboard_ext.js";
|
||||
import type { BrowserWindow } from "electron";
|
||||
import type { CommandNames, AppContext } from "../components/app_context.js";
|
||||
|
||||
@ -60,6 +62,33 @@ function setupContextMenu() {
|
||||
uiIcon: "bx bx-copy",
|
||||
handler: () => webContents.copy()
|
||||
});
|
||||
|
||||
items.push({
|
||||
enabled: hasText,
|
||||
title: t("electron_context_menu.copy-as-markdown"),
|
||||
uiIcon: "bx bx-copy-alt",
|
||||
handler: async () => {
|
||||
const selection = window.getSelection();
|
||||
if (!selection || !selection.rangeCount) return '';
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
const div = document.createElement('div');
|
||||
div.appendChild(range.cloneContents());
|
||||
|
||||
const htmlContent = div.innerHTML;
|
||||
if (htmlContent) {
|
||||
try {
|
||||
const { markdownContent } = await server.post<{ markdownContent: string }>(
|
||||
"other/to-markdown",
|
||||
{ htmlContent }
|
||||
);
|
||||
await clipboardExt.copyTextWithToast(markdownContent);
|
||||
} catch (error) {
|
||||
console.error("Failed to copy as markdown:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!["", "javascript:", "about:blank#blocked"].includes(params.linkURL) && params.mediaType === "none") {
|
||||
|
||||
@ -882,6 +882,7 @@
|
||||
"electron_context_menu": {
|
||||
"cut": "قص",
|
||||
"copy": "نسخ",
|
||||
"copy-as-markdown": "نسخ كـ Markdown",
|
||||
"paste": "لصق",
|
||||
"copy-link": "نسخ الرابط",
|
||||
"add-term-to-dictionary": "اضافة \"{{term}}\" الى القاموس",
|
||||
|
||||
@ -1760,6 +1760,7 @@
|
||||
"add-term-to-dictionary": "将 \"{{term}}\" 添加到字典",
|
||||
"cut": "剪切",
|
||||
"copy": "复制",
|
||||
"copy-as-markdown": "复制为 Markdown",
|
||||
"copy-link": "复制链接",
|
||||
"paste": "粘贴",
|
||||
"paste-as-plain-text": "以纯文本粘贴",
|
||||
|
||||
@ -1729,6 +1729,7 @@
|
||||
"add-term-to-dictionary": "Begriff \"{{term}}\" zum Wörterbuch hinzufügen",
|
||||
"cut": "Ausschneiden",
|
||||
"copy": "Kopieren",
|
||||
"copy-as-markdown": "Als Markdown kopieren",
|
||||
"copy-link": "Link kopieren",
|
||||
"paste": "Einfügen",
|
||||
"paste-as-plain-text": "Als unformatierten Text einfügen",
|
||||
|
||||
@ -1810,6 +1810,7 @@
|
||||
"add-term-to-dictionary": "Add \"{{term}}\" to dictionary",
|
||||
"cut": "Cut",
|
||||
"copy": "Copy",
|
||||
"copy-as-markdown": "Copy as Markdown",
|
||||
"copy-link": "Copy link",
|
||||
"paste": "Paste",
|
||||
"paste-as-plain-text": "Paste as plain text",
|
||||
|
||||
@ -1778,6 +1778,7 @@
|
||||
"add-term-to-dictionary": "Agregar \"{{term}}\" al diccionario",
|
||||
"cut": "Cortar",
|
||||
"copy": "Copiar",
|
||||
"copy-as-markdown": "Copiar como markdown",
|
||||
"copy-link": "Copiar enlace",
|
||||
"paste": "Pegar",
|
||||
"paste-as-plain-text": "Pegar como texto plano",
|
||||
|
||||
@ -1689,6 +1689,7 @@
|
||||
"add-term-to-dictionary": "Ajouter «{{term}}» au dictionnaire",
|
||||
"cut": "Couper",
|
||||
"copy": "Copier",
|
||||
"copy-as-markdown": "Copier en markdown",
|
||||
"copy-link": "Copier le lien",
|
||||
"paste": "Coller",
|
||||
"paste-as-plain-text": "Coller comme texte brut",
|
||||
|
||||
@ -1808,6 +1808,7 @@
|
||||
"add-term-to-dictionary": "Cuir \"{{term}}\" leis an bhfoclóir",
|
||||
"cut": "Gearr",
|
||||
"copy": "Cóipeáil",
|
||||
"copy-as-markdown": "Cóipeáil mar markdown",
|
||||
"copy-link": "Cóipeáil nasc",
|
||||
"paste": "Greamaigh",
|
||||
"paste-as-plain-text": "Greamaigh mar théacs simplí",
|
||||
|
||||
@ -1810,6 +1810,7 @@
|
||||
"add-term-to-dictionary": "\"{{term}}\" को डिक्शनरी में जोड़ें",
|
||||
"cut": "कट (Cut)",
|
||||
"copy": "कॉपी (Copy)",
|
||||
"copy-as-markdown": "Markdown के रूप में कॉपी करें",
|
||||
"copy-link": "लिंक कॉपी करें",
|
||||
"paste": "पेस्ट (Paste)",
|
||||
"paste-as-plain-text": "प्लेन टेक्स्ट की तरह पेस्ट करें",
|
||||
|
||||
@ -334,6 +334,7 @@
|
||||
"electron_context_menu": {
|
||||
"cut": "Taglia",
|
||||
"copy": "Copia",
|
||||
"copy-as-markdown": "Copia come markdown",
|
||||
"paste": "Incolla",
|
||||
"copy-link": "Copia collegamento",
|
||||
"paste-as-plain-text": "Incolla come testo semplice",
|
||||
|
||||
@ -1353,6 +1353,7 @@
|
||||
"add-term-to-dictionary": "辞書に \"{{term}}\" を追加",
|
||||
"cut": "切り取り",
|
||||
"copy": "コピー",
|
||||
"copy-as-markdown": "Markdownとしてコピー",
|
||||
"copy-link": "リンクをコピー",
|
||||
"paste": "貼り付け",
|
||||
"paste-as-plain-text": "プレーンテキストで貼り付け",
|
||||
|
||||
@ -614,6 +614,7 @@
|
||||
"electron_context_menu": {
|
||||
"cut": "Wytnij",
|
||||
"copy": "Kopiuj",
|
||||
"copy-as-markdown": "Kopiuj jako markdown",
|
||||
"copy-link": "Kopiuj link",
|
||||
"paste": "Wklej",
|
||||
"paste-as-plain-text": "Wklej jako zwykły tekst",
|
||||
|
||||
@ -1774,6 +1774,7 @@
|
||||
"add-term-to-dictionary": "Adicionar \"{{term}}\" ao dicionário",
|
||||
"cut": "Cortar",
|
||||
"copy": "Copiar",
|
||||
"copy-as-markdown": "Copiar como markdown",
|
||||
"copy-link": "Copiar ligação",
|
||||
"paste": "Colar",
|
||||
"paste-as-plain-text": "Colar como texto sem formatação",
|
||||
|
||||
@ -1637,6 +1637,7 @@
|
||||
"add-term-to-dictionary": "Adicionar \"{{term}}\" ao dicionário",
|
||||
"cut": "Cortar",
|
||||
"copy": "Copiar",
|
||||
"copy-as-markdown": "Copiar como markdown",
|
||||
"copy-link": "Copiar link",
|
||||
"paste": "Colar",
|
||||
"paste-as-plain-text": "Colar como texto sem formatação",
|
||||
|
||||
@ -1724,6 +1724,7 @@
|
||||
"electron_context_menu": {
|
||||
"add-term-to-dictionary": "Adaugă „{{term}}” în dicționar",
|
||||
"copy": "Copiază",
|
||||
"copy-as-markdown": "Copiază ca markdown",
|
||||
"copy-link": "Copiază legătura",
|
||||
"cut": "Decupează",
|
||||
"paste": "Lipește",
|
||||
|
||||
@ -708,6 +708,7 @@
|
||||
"paste": "Вставить",
|
||||
"copy-link": "Скопировать ссылку",
|
||||
"copy": "Скопировать",
|
||||
"copy-as-markdown": "Копировать как Markdown",
|
||||
"cut": "Вырезать",
|
||||
"search_online": "Поиск \"{{term}}\" в {{searchEngine}}",
|
||||
"add-term-to-dictionary": "Добавить \"{{term}}\" в словарь",
|
||||
|
||||
@ -1722,6 +1722,7 @@
|
||||
"add-term-to-dictionary": "將 \"{{term}}\" 新增至字典",
|
||||
"cut": "剪下",
|
||||
"copy": "複製",
|
||||
"copy-as-markdown": "複製為 Markdown",
|
||||
"copy-link": "複製連結",
|
||||
"paste": "貼上",
|
||||
"paste-as-plain-text": "以純文字貼上",
|
||||
|
||||
@ -1533,6 +1533,7 @@
|
||||
"add-term-to-dictionary": "Додати \"{{term}}\" до словника",
|
||||
"cut": "Вирізати",
|
||||
"copy": "Копіювати",
|
||||
"copy-as-markdown": "Копіювати як Markdown",
|
||||
"copy-link": "Копіювати посилання",
|
||||
"paste": "Вставити",
|
||||
"paste-as-plain-text": "Вставити як звичайний текст",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { buildNote, buildNotes } from "../../../test/easy-froca.js";
|
||||
import { buildEvent, buildEvents } from "./event_builder.js";
|
||||
import { LOCALE_MAPPINGS } from "./index.js";
|
||||
@ -148,7 +148,7 @@ describe("Promoted attributes", () => {
|
||||
expect(event).toHaveLength(1);
|
||||
expect(event[0]?.promotedAttributes).toMatchObject([
|
||||
[ "assignee", "Target note" ]
|
||||
])
|
||||
]);
|
||||
});
|
||||
|
||||
it("supports start time and end time", async () => {
|
||||
@ -177,6 +177,86 @@ describe("Promoted attributes", () => {
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe("Recurrence", () => {
|
||||
it("supports valid recurrence without end date", async () => {
|
||||
const noteIds = buildNotes([
|
||||
{
|
||||
title: "Recurring Event",
|
||||
"#startDate": "2025-05-05",
|
||||
"#recurrence": "FREQ=DAILY;COUNT=5"
|
||||
}
|
||||
]);
|
||||
const events = await buildEvents(noteIds);
|
||||
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0]).toMatchObject({
|
||||
title: "Recurring Event",
|
||||
start: "2025-05-05",
|
||||
});
|
||||
expect(events[0].rrule).toContain("DTSTART:20250505");
|
||||
expect(events[0].rrule).toContain("FREQ=DAILY;COUNT=5");
|
||||
expect(events[0].end).toBeUndefined();
|
||||
});
|
||||
|
||||
it("supports recurrence with start and end time (duration calculated)", async () => {
|
||||
const noteIds = buildNotes([
|
||||
{
|
||||
title: "Timed Recurring Event",
|
||||
"#startDate": "2025-05-05",
|
||||
"#startTime": "13:00",
|
||||
"#endTime": "15:30",
|
||||
"#recurrence": "FREQ=WEEKLY;COUNT=3"
|
||||
}
|
||||
]);
|
||||
const events = await buildEvents(noteIds);
|
||||
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0]).toMatchObject({
|
||||
title: "Timed Recurring Event",
|
||||
start: "2025-05-05T13:00:00",
|
||||
duration: "02:30"
|
||||
});
|
||||
expect(events[0].rrule).toContain("DTSTART:20250505T130000");
|
||||
expect(events[0].end).toBeUndefined();
|
||||
});
|
||||
|
||||
it("removes end date when recurrence is valid", async () => {
|
||||
const noteIds = buildNotes([
|
||||
{
|
||||
title: "Recurring With End",
|
||||
"#startDate": "2025-05-05",
|
||||
"#endDate": "2025-05-07",
|
||||
"#recurrence": "FREQ=DAILY;COUNT=2"
|
||||
}
|
||||
]);
|
||||
const events = await buildEvents(noteIds);
|
||||
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0].rrule).toBeDefined();
|
||||
expect(events[0].end).toBeUndefined();
|
||||
});
|
||||
|
||||
it("writes to console on invalid recurrence rule", async () => {
|
||||
const noteIds = buildNotes([
|
||||
{
|
||||
title: "Invalid Recurrence",
|
||||
"#startDate": "2025-05-05",
|
||||
"#recurrence": "RRULE:FREQ=INVALID"
|
||||
}
|
||||
]);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
await buildEvents(noteIds);
|
||||
const calledWithInvalid = consoleSpy.mock.calls.some(call =>
|
||||
call[0].includes("has an invalid #recurrence string")
|
||||
);
|
||||
expect(calledWithInvalid).toBe(true);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("Building locales", () => {
|
||||
it("every language has a locale defined", async () => {
|
||||
for (const { id, contentOnly } of LOCALES) {
|
||||
|
||||
@ -1,17 +1,22 @@
|
||||
import { EventInput, EventSourceFuncArg, EventSourceInput } from "@fullcalendar/core/index.js";
|
||||
import { dayjs } from "@triliumnext/commons";
|
||||
import clsx from "clsx";
|
||||
import { start } from "repl";
|
||||
import * as rruleLib from 'rrule';
|
||||
|
||||
import FNote from "../../../entities/fnote";
|
||||
import froca from "../../../services/froca";
|
||||
import server from "../../../services/server";
|
||||
import { formatDateToLocalISO, getCustomisableLabel, getMonthsInDateRange, offsetDate } from "./utils";
|
||||
import toastService from "../../../services/toast";
|
||||
import { getCustomisableLabel, getMonthsInDateRange } from "./utils";
|
||||
|
||||
interface Event {
|
||||
startDate: string,
|
||||
endDate?: string | null,
|
||||
startTime?: string | null,
|
||||
endTime?: string | null,
|
||||
isArchived?: boolean;
|
||||
isArchived?: boolean,
|
||||
recurrence?: string | null;
|
||||
}
|
||||
|
||||
export async function buildEvents(noteIds: string[]) {
|
||||
@ -28,8 +33,17 @@ export async function buildEvents(noteIds: string[]) {
|
||||
const endDate = getCustomisableLabel(note, "endDate", "calendar:endDate");
|
||||
const startTime = getCustomisableLabel(note, "startTime", "calendar:startTime");
|
||||
const endTime = getCustomisableLabel(note, "endTime", "calendar:endTime");
|
||||
const recurrence = getCustomisableLabel(note, "recurrence", "calendar:recurrence");
|
||||
const isArchived = note.hasLabel("archived");
|
||||
events.push(await buildEvent(note, { startDate, endDate, startTime, endTime, isArchived }));
|
||||
try {
|
||||
events.push(await buildEvent(note, { startDate, endDate, startTime, endTime, recurrence, isArchived }));
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
const errorMessage = error.message;
|
||||
toastService.showError(errorMessage);
|
||||
console.error(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return events.flat();
|
||||
@ -59,6 +73,7 @@ export async function buildEventsForCalendar(note: FNote, e: EventSourceFuncArg)
|
||||
|
||||
events.push(await buildEvent(dateNote, { startDate }));
|
||||
|
||||
|
||||
if (dateNote.hasChildren()) {
|
||||
const childNoteIds = await dateNote.getSubtreeNoteIds();
|
||||
for (const childNoteId of childNoteIds) {
|
||||
@ -79,7 +94,7 @@ export async function buildEventsForCalendar(note: FNote, e: EventSourceFuncArg)
|
||||
return events.flat();
|
||||
}
|
||||
|
||||
export async function buildEvent(note: FNote, { startDate, endDate, startTime, endTime, isArchived }: Event) {
|
||||
export async function buildEvent(note: FNote, { startDate, endDate, startTime, endTime, recurrence, isArchived }: Event) {
|
||||
const customTitleAttributeName = note.getLabelValue("calendar:title");
|
||||
const titles = await parseCustomTitle(customTitleAttributeName, note);
|
||||
const colorClass = note.getColorClass();
|
||||
@ -98,9 +113,10 @@ export async function buildEvent(note: FNote, { startDate, endDate, startTime, e
|
||||
|
||||
startDate = (startTime ? `${startDate}T${startTime}:00` : startDate);
|
||||
if (!startTime) {
|
||||
const endDateOffset = offsetDate(endDate ?? startDate, 1);
|
||||
if (endDateOffset) {
|
||||
endDate = formatDateToLocalISO(endDateOffset);
|
||||
if (endDate) {
|
||||
endDate = dayjs(endDate).add(1, "day").format("YYYY-MM-DD");
|
||||
} else if (startDate) {
|
||||
endDate = dayjs(startDate).add(1, "day").format("YYYY-MM-DD");
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,6 +134,30 @@ export async function buildEvent(note: FNote, { startDate, endDate, startTime, e
|
||||
if (endDate) {
|
||||
eventData.end = endDate;
|
||||
}
|
||||
|
||||
if (recurrence) {
|
||||
// Generate rrule string
|
||||
const rruleString = `DTSTART:${dayjs(startDate).format("YYYYMMDD[T]HHmmss")}\n${recurrence}`;
|
||||
|
||||
// Validate rrule string
|
||||
let rruleValid = true;
|
||||
try {
|
||||
rruleLib.rrulestr(rruleString, { forceset: true }) as rruleLib.RRuleSet;
|
||||
} catch {
|
||||
rruleValid = false;
|
||||
}
|
||||
|
||||
if (rruleValid) {
|
||||
delete eventData.end;
|
||||
eventData.rrule = rruleString;
|
||||
if (endDate){
|
||||
const duration = dayjs.duration(dayjs(endDate).diff(dayjs(startDate)));
|
||||
eventData.duration = duration.format("HH:mm");
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Note "${note.noteId} ${note.title}" has an invalid #recurrence string ${recurrence}. Excluding...`);
|
||||
}
|
||||
}
|
||||
events.push(eventData);
|
||||
}
|
||||
return events;
|
||||
|
||||
@ -252,6 +252,7 @@ function usePlugins(isEditable: boolean, isCalendarRoot: boolean) {
|
||||
plugins.push((await import("@fullcalendar/timegrid")).default);
|
||||
plugins.push((await import("@fullcalendar/list")).default);
|
||||
plugins.push((await import("@fullcalendar/multimonth")).default);
|
||||
plugins.push((await import("@fullcalendar/rrule")).default);
|
||||
if (isEditable || isCalendarRoot) {
|
||||
plugins.push((await import("@fullcalendar/interaction")).default);
|
||||
}
|
||||
|
||||
71
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Collections/Calendar.html
generated
vendored
71
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Collections/Calendar.html
generated
vendored
@ -185,6 +185,18 @@
|
||||
at which the event ends (in relation with <code spellcheck="false">endDate</code> if
|
||||
present, or <code spellcheck="false">startDate</code>).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code spellcheck="false">#recurrence</code>
|
||||
</td>
|
||||
<td>This is an optional CalDAV <code spellcheck="false">RRULE</code> string
|
||||
that if present, determines whether a task should repeat or not. Note that
|
||||
it does not include the <code spellcheck="false">DTSTART</code> attribute,
|
||||
which is derived from the <code spellcheck="false">#startDate</code> and
|
||||
<code
|
||||
spellcheck="false">#startTime</code>directly. For examples of valid <code spellcheck="false">RRULE</code> strings
|
||||
see <a href="https://icalendar.org/rrule-tool.html">https://icalendar.org/rrule-tool.html</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code spellcheck="false">#color</code>
|
||||
</td>
|
||||
@ -282,6 +294,65 @@
|
||||
<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
|
||||
these child notes.</p>
|
||||
<p> </p>
|
||||
<h2>Recurrence</h2>
|
||||
<p>The built in calendar view also supports repeating tasks. If a child note
|
||||
of the calendar has a #recurrence label with a valid recurrence, that event
|
||||
will repeat on the calendar according to the recurrence string. </p>
|
||||
<p>For example, to make a note repeat on the calendar:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>Every Day - <code spellcheck="false">#recurrence="FREQ=DAILY;INTERVAL=1"</code>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Every 3 days - <code spellcheck="false">#recurrence="FREQ=DAILY;INTERVAL=3"</code>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Every week - <code spellcheck="false">#recurrence="FREQ=WEEKLY;INTERVAL=1"</code>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Every 2 weeks on Monday, Wednesday and Friday - <code spellcheck="false">#recurrence="FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE,FR"</code>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Every 3 months - <code spellcheck="false">#recurrence="FREQ=MONTHLY;INTERVAL=3"</code>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Every 2 months on the First Sunday - <code spellcheck="false">#recurrence="FREQ=MONTHLY;INTERVAL=2;BYDAY=1SU"</code>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Every month on the Last Friday - <code spellcheck="false">#recurrence="FREQ=MONTHLY;INTERVAL=1;BYDAY=-1FR"</code>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>And so on.</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>For other examples of valid <code spellcheck="false">RRULE</code> strings
|
||||
see <a href="https://icalendar.org/rrule-tool.html">https://icalendar.org/rrule-tool.html</a>
|
||||
</p>
|
||||
<p>Note that the recurrence string does not include the <code spellcheck="false">DTSTART</code> attribute
|
||||
as defined in the iCAL specifications. This is derived directly from the
|
||||
<code
|
||||
spellcheck="false">startDate</code>and <code spellcheck="false">startTime</code> attributes</p>
|
||||
<p>If you want to override the label the calendar uses to fetch the recurrence
|
||||
string, you can use the <code spellcheck="false">#calendar:recurrence</code> attribute.
|
||||
For example, you can set <code spellcheck="false">#calendar:recurrence=taskRepeats</code>.
|
||||
Then you can set your recurrence string like <code spellcheck="false">#taskRepeats="FREQ=DAILY;INTERVAL=1"</code>
|
||||
</p>
|
||||
<p>Also note that the recurrence label can be made promoted as with the start
|
||||
and end dates. </p>
|
||||
<aside class="admonition warning">
|
||||
<p>If the recurrence string is not valid, a toast will be shown with the
|
||||
note ID and title of the note with the erroneous recurrence message. This
|
||||
note will not be added to the calendar</p>
|
||||
</aside>
|
||||
<p> </p>
|
||||
<h2>Use-cases</h2>
|
||||
<h3>Using with the Journal / calendar</h3>
|
||||
<p>It is possible to integrate the calendar view into the Journal with day
|
||||
|
||||
@ -72,11 +72,12 @@ For each note of the calendar, the following attributes can be used:
|
||||
| `#endDate` | Similar to `startDate`, mentions the end date if the event spans across multiple days. The date is inclusive, so the end day is also considered. The attribute can be missing for single-day events. |
|
||||
| `#startTime` | The time the event starts at. If this value is missing, then the event is considered a full-day event. The format is `HH:MM` (hours in 24-hour format and minutes). |
|
||||
| `#endTime` | Similar to `startTime`, it mentions the time at which the event ends (in relation with `endDate` if present, or `startDate`). |
|
||||
| `#recurrence` | This is an optional CalDAV `RRULE` string that if present, determines whether a task should repeat or not. Note that it does not include the `DTSTART` attribute, which is derived from the `#startDate` and `#startTime` directly. For examples of valid `RRULE` strings see [https://icalendar.org/rrule-tool.html](https://icalendar.org/rrule-tool.html) |
|
||||
| `#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` | **❌️ Removed since v0.100.0. Use** `**#color**` **instead.** <br> <br>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` | **❌️ Removed since v0.100.0. Use** `**#color**` **instead.** <br> <br>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. |
|
||||
| `#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> <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> <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: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. |
|
||||
@ -102,6 +103,32 @@ This will result in:
|
||||
|
||||
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.
|
||||
|
||||
## Recurrence
|
||||
|
||||
The built in calendar view also supports repeating tasks. If a child note of the calendar has a #recurrence label with a valid recurrence, that event will repeat on the calendar according to the recurrence string.
|
||||
|
||||
For example, to make a note repeat on the calendar:
|
||||
|
||||
* Every Day - `#recurrence="FREQ=DAILY;INTERVAL=1"`
|
||||
* Every 3 days - `#recurrence="FREQ=DAILY;INTERVAL=3"`
|
||||
* Every week - `#recurrence="FREQ=WEEKLY;INTERVAL=1"`
|
||||
* Every 2 weeks on Monday, Wednesday and Friday - `#recurrence="FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE,FR"`
|
||||
* Every 3 months - `#recurrence="FREQ=MONTHLY;INTERVAL=3"`
|
||||
* Every 2 months on the First Sunday - `#recurrence="FREQ=MONTHLY;INTERVAL=2;BYDAY=1SU"`
|
||||
* Every month on the Last Friday - `#recurrence="FREQ=MONTHLY;INTERVAL=1;BYDAY=-1FR"`
|
||||
* And so on.
|
||||
|
||||
For other examples of valid `RRULE` strings see [https://icalendar.org/rrule-tool.html](https://icalendar.org/rrule-tool.html)
|
||||
|
||||
Note that the recurrence string does not include the `DTSTART` attribute as defined in the iCAL specifications. This is derived directly from the `startDate` and `startTime` attributes
|
||||
|
||||
If you want to override the label the calendar uses to fetch the recurrence string, you can use the `#calendar:recurrence` attribute. For example, you can set `#calendar:recurrence=taskRepeats`. Then you can set your recurrence string like `#taskRepeats="FREQ=DAILY;INTERVAL=1"`
|
||||
|
||||
Also note that the recurrence label can be made promoted as with the start and end dates.
|
||||
|
||||
> [!WARNING]
|
||||
> If the recurrence string is not valid, a toast will be shown with the note ID and title of the note with the erroneous recurrence message. This note will not be added to the calendar
|
||||
|
||||
## Use-cases
|
||||
|
||||
### Using with the Journal / calendar
|
||||
|
||||
52
pnpm-lock.yaml
generated
52
pnpm-lock.yaml
generated
@ -200,6 +200,9 @@ importers:
|
||||
'@fullcalendar/multimonth':
|
||||
specifier: 6.1.20
|
||||
version: 6.1.20(@fullcalendar/core@6.1.20)
|
||||
'@fullcalendar/rrule':
|
||||
specifier: 6.1.20
|
||||
version: 6.1.20(@fullcalendar/core@6.1.20)(rrule@2.8.1)
|
||||
'@fullcalendar/timegrid':
|
||||
specifier: 6.1.20
|
||||
version: 6.1.20(@fullcalendar/core@6.1.20)
|
||||
@ -323,6 +326,9 @@ importers:
|
||||
reveal.js:
|
||||
specifier: 5.2.1
|
||||
version: 5.2.1
|
||||
rrule:
|
||||
specifier: 2.8.1
|
||||
version: 2.8.1
|
||||
svg-pan-zoom:
|
||||
specifier: 3.6.2
|
||||
version: 3.6.2
|
||||
@ -3392,6 +3398,12 @@ packages:
|
||||
peerDependencies:
|
||||
'@fullcalendar/core': ~6.1.20
|
||||
|
||||
'@fullcalendar/rrule@6.1.20':
|
||||
resolution: {integrity: sha512-5Awk7bmaA97hSZRpIBehenXkYreVIvx8nnaMFZ/LDGRuK1mgbR4vSUrDTvVU+oEqqKnj/rqMBByWqN5NeehQxw==}
|
||||
peerDependencies:
|
||||
'@fullcalendar/core': ~6.1.20
|
||||
rrule: ^2.6.0
|
||||
|
||||
'@fullcalendar/timegrid@6.1.20':
|
||||
resolution: {integrity: sha512-4H+/MWbz3ntA50lrPif+7TsvMeX3R1GSYjiLULz0+zEJ7/Yfd9pupZmAwUs/PBpA6aAcFmeRr0laWfcz1a9V1A==}
|
||||
peerDependencies:
|
||||
@ -13131,6 +13143,9 @@ packages:
|
||||
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
rrule@2.8.1:
|
||||
resolution: {integrity: sha512-hM3dHSBMeaJ0Ktp7W38BJZ7O1zOgaFEsn41PDk+yHoEtfLV+PoJt9E9xAlZiWgf/iqEqionN0ebHFZIDAp+iGw==}
|
||||
|
||||
rrweb-cssom@0.8.0:
|
||||
resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
|
||||
|
||||
@ -16043,6 +16058,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-core': 47.4.0
|
||||
'@ckeditor/ckeditor5-upload': 47.4.0
|
||||
ckeditor5: 47.4.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-ai@47.4.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)':
|
||||
dependencies:
|
||||
@ -16183,6 +16200,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-utils': 47.4.0
|
||||
'@ckeditor/ckeditor5-widget': 47.4.0
|
||||
es-toolkit: 1.39.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-cloud-services@47.4.0':
|
||||
dependencies:
|
||||
@ -16397,6 +16416,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-utils': 47.4.0
|
||||
ckeditor5: 47.4.0
|
||||
es-toolkit: 1.39.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-editor-inline@47.4.0':
|
||||
dependencies:
|
||||
@ -16512,6 +16533,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-utils': 47.4.0
|
||||
ckeditor5: 47.4.0
|
||||
es-toolkit: 1.39.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-font@47.4.0':
|
||||
dependencies:
|
||||
@ -16586,6 +16609,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-utils': 47.4.0
|
||||
'@ckeditor/ckeditor5-widget': 47.4.0
|
||||
ckeditor5: 47.4.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-html-embed@47.4.0':
|
||||
dependencies:
|
||||
@ -16770,8 +16795,6 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-utils': 47.4.0
|
||||
ckeditor5: 47.4.0
|
||||
es-toolkit: 1.39.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-merge-fields@47.4.0':
|
||||
dependencies:
|
||||
@ -16794,8 +16817,6 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-ui': 47.4.0
|
||||
'@ckeditor/ckeditor5-utils': 47.4.0
|
||||
ckeditor5: 47.4.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-operations-compressor@47.4.0':
|
||||
dependencies:
|
||||
@ -16850,8 +16871,6 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-utils': 47.4.0
|
||||
'@ckeditor/ckeditor5-widget': 47.4.0
|
||||
ckeditor5: 47.4.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-pagination@47.4.0':
|
||||
dependencies:
|
||||
@ -16959,8 +16978,6 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-ui': 47.4.0
|
||||
'@ckeditor/ckeditor5-utils': 47.4.0
|
||||
ckeditor5: 47.4.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-slash-command@47.4.0':
|
||||
dependencies:
|
||||
@ -16973,8 +16990,6 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-ui': 47.4.0
|
||||
'@ckeditor/ckeditor5-utils': 47.4.0
|
||||
ckeditor5: 47.4.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-source-editing-enhanced@47.4.0':
|
||||
dependencies:
|
||||
@ -17022,8 +17037,6 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-utils': 47.4.0
|
||||
ckeditor5: 47.4.0
|
||||
es-toolkit: 1.39.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-table@47.4.0':
|
||||
dependencies:
|
||||
@ -17036,8 +17049,6 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-widget': 47.4.0
|
||||
ckeditor5: 47.4.0
|
||||
es-toolkit: 1.39.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-template@47.4.0':
|
||||
dependencies:
|
||||
@ -17150,8 +17161,6 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-engine': 47.4.0
|
||||
'@ckeditor/ckeditor5-utils': 47.4.0
|
||||
es-toolkit: 1.39.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-widget@47.4.0':
|
||||
dependencies:
|
||||
@ -17171,8 +17180,6 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-utils': 47.4.0
|
||||
ckeditor5: 47.4.0
|
||||
es-toolkit: 1.39.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@codemirror/autocomplete@6.18.6':
|
||||
dependencies:
|
||||
@ -18514,6 +18521,11 @@ snapshots:
|
||||
'@fullcalendar/core': 6.1.20
|
||||
'@fullcalendar/daygrid': 6.1.20(@fullcalendar/core@6.1.20)
|
||||
|
||||
'@fullcalendar/rrule@6.1.20(@fullcalendar/core@6.1.20)(rrule@2.8.1)':
|
||||
dependencies:
|
||||
'@fullcalendar/core': 6.1.20
|
||||
rrule: 2.8.1
|
||||
|
||||
'@fullcalendar/timegrid@6.1.20(@fullcalendar/core@6.1.20)':
|
||||
dependencies:
|
||||
'@fullcalendar/core': 6.1.20
|
||||
@ -30286,6 +30298,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
rrule@2.8.1:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
rrweb-cssom@0.8.0:
|
||||
optional: true
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user