@luckydye/calendar 1.1.2 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/calendar.js +1387 -1301
- package/package.json +1 -1
- package/src/CalDAVConfig.ts +25 -0
- package/src/CalendarIntegration.ts +1 -2
- package/src/CalendarInternal.ts +20 -1
- package/src/CalendarView.ts +474 -257
- package/src/GoogleCalendarSource.ts +0 -7
- package/src/InMemorySource.ts +0 -15
- package/src/InhouseBookingSource.ts +331 -46
- package/src/Keybinds.ts +46 -0
- package/src/app.css +28 -0
- package/src/app.ts +173 -63
package/src/app.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { InhouseBookingSource } from "./InhouseBookingSource.js";
|
|
|
15
15
|
import { queueStatus } from "./StatusMessage.js";
|
|
16
16
|
import { activeCalendarStore } from "./ActiveCalendarStore.js";
|
|
17
17
|
import type { CalendarEvent } from "./CalendarInternal.js";
|
|
18
|
+
import { registerKeybinds } from "./Keybinds.js";
|
|
18
19
|
import "./StatusBar.js";
|
|
19
20
|
import { NotificationScheduler } from "./NotificationScheduler.js";
|
|
20
21
|
import serviceWorkerUrl from "./service-worker.js?url";
|
|
@@ -34,7 +35,19 @@ const calendarElement = document.querySelector(
|
|
|
34
35
|
) as CalendarViewElement;
|
|
35
36
|
const calendar = calendarElement.internal;
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
registerKeybinds(
|
|
39
|
+
[
|
|
40
|
+
{ key: "r", cmdOrCtrl: true, shift: true, action: () => calendarElement.forceSync() },
|
|
41
|
+
{ key: "c", cmdOrCtrl: true, action: () => calendarElement.copySelectedEvents() },
|
|
42
|
+
{ key: "Backspace", action: () => calendarElement.deleteSelectedEvents() },
|
|
43
|
+
{ key: "Delete", action: () => calendarElement.deleteSelectedEvents() },
|
|
44
|
+
{ key: "Escape", action: () => calendarElement.escape() },
|
|
45
|
+
{ key: "t", action: () => calendarElement.scrollToToday() },
|
|
46
|
+
],
|
|
47
|
+
calendarElement,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
let workerPromise: Promise<ServiceWorkerRegistration> | undefined;
|
|
38
51
|
|
|
39
52
|
// Register service worker for PWA
|
|
40
53
|
if ("serviceWorker" in navigator) {
|
|
@@ -107,8 +120,30 @@ async function sync(cal: Calendar, options?: { force?: boolean }): Promise<void>
|
|
|
107
120
|
(async () => {
|
|
108
121
|
await calendar.initPromise;
|
|
109
122
|
|
|
110
|
-
//
|
|
123
|
+
// Register sample source
|
|
124
|
+
const sampleSource = new InMemorySource('sample', "Sample Events", '#10b981');
|
|
125
|
+
const sampleCalendar = createInMemoryCalendar(sampleSource);
|
|
126
|
+
loadedCalendars.set(sampleCalendar.id, sampleCalendar);
|
|
127
|
+
activeCalendarStore.registerCalendar(sampleCalendar);
|
|
128
|
+
|
|
129
|
+
const button = document.createElement("button");
|
|
130
|
+
button.className = "toolbar-button";
|
|
131
|
+
button.title = "CalDAV Sources";
|
|
132
|
+
button.innerHTML = "📅";
|
|
133
|
+
button.addEventListener("click", () => {
|
|
134
|
+
showCalDAVConfig();
|
|
135
|
+
});
|
|
136
|
+
button.slot = "toolbar-center";
|
|
137
|
+
calendarElement.appendChild(button);
|
|
138
|
+
|
|
139
|
+
// Create the active calendar selector UI
|
|
140
|
+
createActiveCalendarSelector();
|
|
141
|
+
|
|
142
|
+
// Sync CalDAV sources (this will call updateEnabledCalendars after calendars are registered)
|
|
111
143
|
await syncCalDAV();
|
|
144
|
+
|
|
145
|
+
// Update active calendar color after sync
|
|
146
|
+
updateActiveCalendarColor();
|
|
112
147
|
})();
|
|
113
148
|
|
|
114
149
|
// CalDAV configuration UI
|
|
@@ -219,6 +254,7 @@ async function createCalDAVCalendars(
|
|
|
219
254
|
name: calInfo.displayName,
|
|
220
255
|
color: calInfo.color || source.color,
|
|
221
256
|
enabled: source.enabled,
|
|
257
|
+
locked: (source as { locked?: boolean }).locked,
|
|
222
258
|
sourceId: source.id,
|
|
223
259
|
sourceType: "caldav",
|
|
224
260
|
calendarUrl: calInfo.url,
|
|
@@ -240,14 +276,6 @@ async function createCalDAVCalendars(
|
|
|
240
276
|
return caldavSource.updateEvent(id, { ...updates, calendarId: calInfo.url });
|
|
241
277
|
},
|
|
242
278
|
|
|
243
|
-
async moveEvent(
|
|
244
|
-
id: string,
|
|
245
|
-
newStart: Date,
|
|
246
|
-
newEnd: Date,
|
|
247
|
-
): Promise<CalendarEvent> {
|
|
248
|
-
return caldavSource.updateEvent(id, { start: newStart, end: newEnd, calendarId: calInfo.url });
|
|
249
|
-
},
|
|
250
|
-
|
|
251
279
|
async deleteEvent(id: string): Promise<void> {
|
|
252
280
|
const eventUrl = `${calInfo.url.replace(/\/$/, '')}/${id}.ics`;
|
|
253
281
|
await caldavSource.request(eventUrl, 'DELETE', undefined, {});
|
|
@@ -265,6 +293,7 @@ function createICalCalendar(source: CalendarSource): Calendar {
|
|
|
265
293
|
name: source.name,
|
|
266
294
|
color: source.color,
|
|
267
295
|
enabled: source.enabled,
|
|
296
|
+
locked: (source as { locked?: boolean }).locked,
|
|
268
297
|
sourceId: source.id,
|
|
269
298
|
sourceType: "ical",
|
|
270
299
|
|
|
@@ -272,7 +301,7 @@ function createICalCalendar(source: CalendarSource): Calendar {
|
|
|
272
301
|
const result = await fetchICalEventsWithNotifications(source.credentials, source.color, source.name);
|
|
273
302
|
return result.events;
|
|
274
303
|
},
|
|
275
|
-
// iCal calendars are read-only
|
|
304
|
+
// iCal calendars are read-only by default
|
|
276
305
|
};
|
|
277
306
|
}
|
|
278
307
|
|
|
@@ -297,6 +326,7 @@ function createGoogleCalendar(source: CalendarSource): Calendar {
|
|
|
297
326
|
name: source.name,
|
|
298
327
|
color: source.color,
|
|
299
328
|
enabled: source.enabled,
|
|
329
|
+
locked: (source as { locked?: boolean }).locked,
|
|
300
330
|
sourceId: source.id,
|
|
301
331
|
sourceType: "google",
|
|
302
332
|
calendarUrl: "https://calendar.google.com/calendar/u/0/r?tab=mc",
|
|
@@ -318,14 +348,6 @@ function createGoogleCalendar(source: CalendarSource): Calendar {
|
|
|
318
348
|
return googleSource.updateEvent(id, updates);
|
|
319
349
|
},
|
|
320
350
|
|
|
321
|
-
async moveEvent(
|
|
322
|
-
id: string,
|
|
323
|
-
newStart: Date,
|
|
324
|
-
newEnd: Date,
|
|
325
|
-
): Promise<CalendarEvent> {
|
|
326
|
-
return googleSource.moveEvent(id, newStart, newEnd);
|
|
327
|
-
},
|
|
328
|
-
|
|
329
351
|
async deleteEvent(id: string): Promise<void> {
|
|
330
352
|
return googleSource.deleteEvent(id);
|
|
331
353
|
},
|
|
@@ -351,12 +373,34 @@ function createInhouseCalendar(source: CalendarSource): Calendar {
|
|
|
351
373
|
name: source.name,
|
|
352
374
|
color: source.color,
|
|
353
375
|
enabled: source.enabled,
|
|
376
|
+
locked: (source as { locked?: boolean }).locked,
|
|
354
377
|
sourceId: source.id,
|
|
355
378
|
sourceType: "inhouse",
|
|
356
379
|
|
|
357
380
|
async fetchEvents(): Promise<CalendarEvent[]> {
|
|
358
381
|
return inhouseSource.fetchEvents();
|
|
359
382
|
},
|
|
383
|
+
|
|
384
|
+
async createEvent(
|
|
385
|
+
event: Omit<CalendarEvent, "calendar" | "color">,
|
|
386
|
+
): Promise<CalendarEvent> {
|
|
387
|
+
return inhouseSource.createEvent(event);
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
async updateEvent(
|
|
391
|
+
id: string,
|
|
392
|
+
updates: Partial<CalendarEvent>,
|
|
393
|
+
): Promise<CalendarEvent> {
|
|
394
|
+
return inhouseSource.updateEvent(id, updates);
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
async deleteEvent(id: string): Promise<void> {
|
|
398
|
+
return inhouseSource.deleteEvent(id);
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
async fetchProjects(): Promise<Array<{ id: number; name: string }>> {
|
|
402
|
+
return inhouseSource.fetchProjects();
|
|
403
|
+
},
|
|
360
404
|
};
|
|
361
405
|
}
|
|
362
406
|
|
|
@@ -391,16 +435,6 @@ function createInMemoryCalendar(source: InMemorySource): Calendar {
|
|
|
391
435
|
return source.updateEvent(id, updates);
|
|
392
436
|
},
|
|
393
437
|
|
|
394
|
-
async moveEvent(
|
|
395
|
-
id: string,
|
|
396
|
-
newStart: Date,
|
|
397
|
-
newEnd: Date,
|
|
398
|
-
): Promise<CalendarEvent> {
|
|
399
|
-
if (!source.moveEvent)
|
|
400
|
-
throw new Error("Source does not support moveEvent");
|
|
401
|
-
return source.moveEvent(id, newStart, newEnd);
|
|
402
|
-
},
|
|
403
|
-
|
|
404
438
|
async deleteEvent(id: string): Promise<void> {
|
|
405
439
|
if (!source.deleteEvent)
|
|
406
440
|
throw new Error("Source does not support deleteEvent");
|
|
@@ -508,7 +542,6 @@ async function syncCalDAV(force = false) {
|
|
|
508
542
|
continue;
|
|
509
543
|
}
|
|
510
544
|
|
|
511
|
-
// Inhouse Booking sources have one calendar
|
|
512
545
|
const calendar = createInhouseCalendar(source);
|
|
513
546
|
loadedCalendars.set(calendar.id, calendar);
|
|
514
547
|
activeCalendarStore.registerCalendar(calendar);
|
|
@@ -525,6 +558,7 @@ async function syncCalDAV(force = false) {
|
|
|
525
558
|
|
|
526
559
|
// Update enabled calendars filter
|
|
527
560
|
updateEnabledCalendars();
|
|
561
|
+
updateLockedCalendars();
|
|
528
562
|
|
|
529
563
|
// Update the UI selector and color after sync
|
|
530
564
|
updateActiveCalendarSelector();
|
|
@@ -545,6 +579,75 @@ calendarElement.addEventListener("selection-change", (e) => {
|
|
|
545
579
|
console.log("Selection changed:", e.detail.selectedEvents);
|
|
546
580
|
});
|
|
547
581
|
|
|
582
|
+
function showProjectPickerDialog(
|
|
583
|
+
projects: Array<{ id: number; name: string }>,
|
|
584
|
+
): Promise<number | null> {
|
|
585
|
+
return new Promise((resolve) => {
|
|
586
|
+
let resolved = false;
|
|
587
|
+
const finish = (value: number | null) => {
|
|
588
|
+
if (resolved) return;
|
|
589
|
+
resolved = true;
|
|
590
|
+
resolve(value);
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const dialog = document.createElement("dialog");
|
|
594
|
+
dialog.style.cssText =
|
|
595
|
+
"background:var(--bg-secondary);color:var(--text-primary);border:1px solid var(--grid-color-strong);border-radius:var(--border-radius);padding:16px;min-width:300px;max-width:480px;";
|
|
596
|
+
|
|
597
|
+
const input = document.createElement("input");
|
|
598
|
+
input.type = "text";
|
|
599
|
+
input.placeholder = "Search projects...";
|
|
600
|
+
input.style.cssText =
|
|
601
|
+
"width:100%;padding:6px 8px;margin-bottom:8px;background:var(--bg-input);color:var(--text-primary);border:1px solid var(--grid-color-strong);border-radius:var(--border-radius-sm);box-sizing:border-box;";
|
|
602
|
+
|
|
603
|
+
const list = document.createElement("div");
|
|
604
|
+
list.style.cssText =
|
|
605
|
+
"max-height:320px;overflow-y:auto;display:flex;flex-direction:column;gap:2px;";
|
|
606
|
+
|
|
607
|
+
const renderList = (filter: string) => {
|
|
608
|
+
list.innerHTML = "";
|
|
609
|
+
const filtered = filter
|
|
610
|
+
? projects.filter((p) =>
|
|
611
|
+
p.name.toLowerCase().includes(filter.toLowerCase()),
|
|
612
|
+
)
|
|
613
|
+
: projects;
|
|
614
|
+
for (const project of filtered) {
|
|
615
|
+
const item = document.createElement("button");
|
|
616
|
+
item.textContent = project.name;
|
|
617
|
+
item.style.cssText =
|
|
618
|
+
"text-align:left;padding:6px 8px;background:var(--bg-item);color:var(--text-primary);border:none;border-radius:var(--border-radius-sm);cursor:pointer;";
|
|
619
|
+
item.addEventListener("mouseenter", () => {
|
|
620
|
+
item.style.background = "var(--bg-item-hover)";
|
|
621
|
+
});
|
|
622
|
+
item.addEventListener("mouseleave", () => {
|
|
623
|
+
item.style.background = "var(--bg-item)";
|
|
624
|
+
});
|
|
625
|
+
item.addEventListener("click", () => {
|
|
626
|
+
finish(project.id);
|
|
627
|
+
dialog.close();
|
|
628
|
+
});
|
|
629
|
+
list.appendChild(item);
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
renderList("");
|
|
634
|
+
input.addEventListener("input", () => renderList(input.value));
|
|
635
|
+
|
|
636
|
+
dialog.addEventListener("close", () => {
|
|
637
|
+
dialog.remove();
|
|
638
|
+
finish(null);
|
|
639
|
+
});
|
|
640
|
+
dialog.addEventListener("click", (ev) => {
|
|
641
|
+
if (ev.target === dialog) dialog.close();
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
dialog.append(input, list);
|
|
645
|
+
document.body.appendChild(dialog);
|
|
646
|
+
dialog.showModal();
|
|
647
|
+
input.focus();
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
548
651
|
calendarElement.addEventListener("create-event", async (e) => {
|
|
549
652
|
const { start, end } = e.detail;
|
|
550
653
|
const calendar = getActiveCalendar();
|
|
@@ -558,14 +661,19 @@ calendarElement.addEventListener("create-event", async (e) => {
|
|
|
558
661
|
return;
|
|
559
662
|
}
|
|
560
663
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
664
|
+
let id = `event-${Date.now()}`;
|
|
665
|
+
|
|
666
|
+
if (calendar.sourceType === "inhouse") {
|
|
667
|
+
const inhouseCalendar = calendar as typeof calendar & {
|
|
668
|
+
fetchProjects(): Promise<Array<{ id: number; name: string }>>;
|
|
669
|
+
};
|
|
670
|
+
const projects = await inhouseCalendar.fetchProjects();
|
|
671
|
+
const projectId = await showProjectPickerDialog(projects);
|
|
672
|
+
if (projectId === null) return; // user cancelled
|
|
673
|
+
id = `0:0:${projectId}`;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
await calendar.createEvent({ id, title: "New Event", start, end });
|
|
569
677
|
await sync(calendar, { force: true });
|
|
570
678
|
updateStatusBar();
|
|
571
679
|
});
|
|
@@ -580,7 +688,7 @@ calendarElement.addEventListener("move-event", async (e) => {
|
|
|
580
688
|
return;
|
|
581
689
|
}
|
|
582
690
|
|
|
583
|
-
if (!calendar.
|
|
691
|
+
if (!calendar.updateEvent) {
|
|
584
692
|
queueStatus(`Calendar "${calendar.name}" does not support moving events.`);
|
|
585
693
|
return;
|
|
586
694
|
}
|
|
@@ -596,7 +704,7 @@ calendarElement.addEventListener("move-event", async (e) => {
|
|
|
596
704
|
calendar.name,
|
|
597
705
|
);
|
|
598
706
|
try {
|
|
599
|
-
await calendar.
|
|
707
|
+
await calendar.updateEvent(event.id, { start, end });
|
|
600
708
|
await sync(calendar, { force: true });
|
|
601
709
|
console.log("Move completed on calendar:", calendar.name);
|
|
602
710
|
updateStatusBar();
|
|
@@ -688,10 +796,15 @@ calendarElement.addEventListener("delete-events", async (e) => {
|
|
|
688
796
|
":",
|
|
689
797
|
calendarEvents.map((ev) => ev.id),
|
|
690
798
|
);
|
|
691
|
-
|
|
692
|
-
|
|
799
|
+
try {
|
|
800
|
+
for (const event of calendarEvents) {
|
|
801
|
+
await calendar.deleteEvent(event.id);
|
|
802
|
+
}
|
|
803
|
+
await sync(calendar, { force: true });
|
|
804
|
+
} catch (error) {
|
|
805
|
+
console.error("Failed to delete event:", error);
|
|
806
|
+
queueStatus(`Failed to delete event: ${error instanceof Error ? error.message : String(error)}`);
|
|
693
807
|
}
|
|
694
|
-
await sync(calendar, { force: true });
|
|
695
808
|
}
|
|
696
809
|
console.log("Delete completed");
|
|
697
810
|
updateStatusBar();
|
|
@@ -781,25 +894,6 @@ calendarElement.addEventListener("import-ical", async (e) => {
|
|
|
781
894
|
}
|
|
782
895
|
});
|
|
783
896
|
|
|
784
|
-
// --- In-Memory Source for Testing ---
|
|
785
|
-
let sampleSource: InMemorySource | null = null;
|
|
786
|
-
|
|
787
|
-
(async () => {
|
|
788
|
-
sampleSource = new InMemorySource('sample', "Sample Events", '#10b981');
|
|
789
|
-
|
|
790
|
-
// Register the sample source as a calendar
|
|
791
|
-
const sampleCalendar = createInMemoryCalendar(sampleSource);
|
|
792
|
-
loadedCalendars.set(sampleCalendar.id, sampleCalendar);
|
|
793
|
-
activeCalendarStore.registerCalendar(sampleCalendar);
|
|
794
|
-
|
|
795
|
-
// Create the active calendar selector UI
|
|
796
|
-
createActiveCalendarSelector();
|
|
797
|
-
|
|
798
|
-
// Update enabled calendars and active calendar color
|
|
799
|
-
updateEnabledCalendars();
|
|
800
|
-
updateActiveCalendarColor();
|
|
801
|
-
})();
|
|
802
|
-
|
|
803
897
|
// --- Active Calendar Selector UI ---
|
|
804
898
|
function createActiveCalendarSelector() {
|
|
805
899
|
// Check if selector already exists in the slot
|
|
@@ -819,7 +913,6 @@ function createActiveCalendarSelector() {
|
|
|
819
913
|
`;
|
|
820
914
|
|
|
821
915
|
const label = document.createElement("span");
|
|
822
|
-
label.textContent = "Active:";
|
|
823
916
|
label.style.cssText = `
|
|
824
917
|
color: var(--text-muted, rgba(255, 255, 255, 0.5));
|
|
825
918
|
white-space: nowrap;
|
|
@@ -889,6 +982,23 @@ function updateEnabledCalendars() {
|
|
|
889
982
|
calendar.setEnabledCalendars(enabledCalendarIdentifiers);
|
|
890
983
|
}
|
|
891
984
|
|
|
985
|
+
function updateLockedCalendars() {
|
|
986
|
+
const lockedCalendarIdentifiers = Array.from(loadedCalendars.values())
|
|
987
|
+
.filter(cal => (cal as { locked?: boolean }).locked)
|
|
988
|
+
.flatMap(cal => {
|
|
989
|
+
// For CalDAV, use the calendar URL; for others, use the source ID
|
|
990
|
+
const identifiers = [];
|
|
991
|
+
if (cal.calendarUrl) {
|
|
992
|
+
identifiers.push(cal.calendarUrl);
|
|
993
|
+
}
|
|
994
|
+
if (cal.sourceId) {
|
|
995
|
+
identifiers.push(cal.sourceId);
|
|
996
|
+
}
|
|
997
|
+
return identifiers;
|
|
998
|
+
});
|
|
999
|
+
calendar.setLockedCalendars(lockedCalendarIdentifiers);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
892
1002
|
function getActiveCalendarSelect(): HTMLSelectElement | null {
|
|
893
1003
|
// Look in the light DOM first (where slotted content lives)
|
|
894
1004
|
const selector = document.getElementById("active-calendar-selector");
|